├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── bar.lua ├── colors.lua ├── default.lua ├── helpers ├── .DS_Store ├── .gitignore ├── app_icons.lua ├── default_font.lua ├── event_providers │ ├── .DS_Store │ ├── apple_menu │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── Sources │ │ │ └── AppleMenu │ │ │ │ ├── Components │ │ │ │ ├── AgendaCard.swift │ │ │ │ ├── AgendaSection.swift │ │ │ │ ├── CalendarCard.swift │ │ │ │ ├── CalendarSection.swift │ │ │ │ ├── Card.swift │ │ │ │ ├── MediaPlayerCard.swift │ │ │ │ ├── ProfileCard.swift │ │ │ │ ├── SystemControlsCard.swift │ │ │ │ └── TimeDisplay.swift │ │ │ │ ├── Utils │ │ │ │ └── Formatters.swift │ │ │ │ └── main.swift │ │ └── makefile │ ├── cpu_load │ │ ├── cpu.h │ │ ├── cpu_load.c │ │ └── makefile │ ├── hdd_load │ │ ├── hdd.h │ │ ├── hdd_load │ │ ├── hdd_load.c │ │ └── makefile │ ├── makefile │ ├── memory_load │ │ ├── makefile │ │ ├── memory.h │ │ └── memory_load.c │ ├── network_load │ │ ├── makefile │ │ ├── network.h │ │ └── network_load.c │ └── sketchybar.h ├── init.lua ├── install.sh ├── makefile └── menus │ ├── makefile │ └── menus.c ├── icons.lua ├── init.lua ├── items ├── aerospace.lua ├── apple.lua ├── calendar.lua ├── front_app.lua ├── init.lua ├── media.lua ├── menus.lua ├── profile.lua ├── spaces.lua └── widgets │ ├── battery.lua │ ├── init.lua │ ├── metrics.lua │ ├── notifications.lua │ ├── volume.lua │ └── wifi.lua ├── reload.js ├── settings.lua ├── sketchybarrc └── terminal /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsec/sketchybar/9d95690eacda47ab029c633670545dfbb6b678f1/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | /helpers/event_providers/apple_menu/.env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 SketchyBar Lua Configuration 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 SketchyBar Lua Configuration 2 | 3 | A modern, feature-rich configuration for [SketchyBar](https://felixkratz.github.io/SketchyBar) using the Lua plugin system. This configuration provides a clean, informative, and customizable menu bar experience for macOS. 4 | 5 | ## ✨ Features 6 | 7 | - 🎨 **Modern Design** 8 | - Clean and minimal aesthetic with blur effects 9 | - Fully customizable colors and transparency 10 | - Rounded corners with dynamic borders 11 | - Consistent spacing and padding system 12 | 13 | - 📊 **System Monitoring** 14 | - Real-time CPU usage tracking 15 | - Memory utilization metrics 16 | - Network traffic monitoring (up/down) 17 | - Battery status with charging indicators 18 | - Disk usage tracking 19 | 20 | - 🎵 **Media Controls** 21 | - Current track information 22 | - Dynamic album artwork display 23 | - Media player controls 24 | - Support for multiple players: 25 | - Spotify 26 | - Music 27 | - Brave Browser 28 | 29 | - 🔔 **Smart Notifications** 30 | - Homebrew updates counter 31 | - Mail notifications 32 | - Message indicators 33 | - System alerts 34 | - Volume and audio device controls 35 | 36 | - 🖥️ **Workspace Management** 37 | - Dynamic space indicators 38 | - Active application tracking 39 | - Custom application icons 40 | - Window management integration 41 | - Space labels and navigation 42 | 43 | ## 🛠️ Prerequisites 44 | 45 | - macOS 46 | - [Homebrew](https://brew.sh) 47 | - [Lua](https://www.lua.org) 48 | - [SketchyBar](https://felixkratz.github.io/SketchyBar) 49 | - [SbarLua](https://github.com/FelixKratz/SbarLua) 50 | 51 | ## 📦 Key Components 52 | 53 | ### Core Files 54 | - `sketchybarrc` - Main entry point (Lua) 55 | - `init.lua` - Initial configuration and module loading 56 | - `bar.lua` - Bar appearance and behavior settings 57 | - `colors.lua` - Color scheme definitions 58 | - `settings.lua` - General configuration settings 59 | - `icons.lua` - Icon definitions (SF Symbols/NerdFont) 60 | 61 | ### Modules 62 | - **System Widgets** - CPU, Memory, Battery, Network monitoring 63 | - **Media Controls** - Music player integration and controls 64 | - **Space Management** - Workspace organization and navigation 65 | - **Application Tracking** - Active window and application monitoring 66 | - **Notification Center** - System and application notifications 67 | 68 | ## 🎨 Customization 69 | 70 | The configuration is highly modular and customizable through: 71 | - Color schemes 72 | - Font selections 73 | - Icon sets (SF Symbols or NerdFont) 74 | - Layout adjustments 75 | - Widget behavior 76 | - Event triggers 77 | 78 | ## 🔧 Event System 79 | 80 | Built-in C-based event providers for: 81 | - CPU monitoring 82 | - Memory usage 83 | - Network traffic 84 | - Disk usage 85 | - Weather information 86 | 87 | ## 📚 Additional Resources 88 | 89 | - [SketchyBar Documentation](https://felixkratz.github.io/SketchyBar/config/getting-started) 90 | - [Lua Documentation](https://www.lua.org/docs.html) 91 | - [SbarLua Wiki](https://github.com/FelixKratz/SbarLua/wiki) 92 | 93 | ## 🙏 Credits 94 | 95 | - [SketchyBar](https://felixkratz.github.io/SketchyBar) by Felix Kratz 96 | - [SbarLua](https://github.com/FelixKratz/SbarLua) by Felix Kratz 97 | 98 | ## 📝 License 99 | 100 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. -------------------------------------------------------------------------------- /bar.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local settings = require("settings") 3 | 4 | -- Equivalent to the --bar domain 5 | sbar.bar({ 6 | sticky = true, 7 | position = "top", 8 | height = settings.bar.height, 9 | margin=15, 10 | color = colors.bar.bg, 11 | border_color=colors.bar.border, 12 | border_width=0, 13 | padding_right=10, 14 | padding_left=10, 15 | corner_radius=8, 16 | blur_radius=6, 17 | y_offset=settings.y_offset 18 | }) 19 | -------------------------------------------------------------------------------- /colors.lua: -------------------------------------------------------------------------------- 1 | return { 2 | black = 0xff232a2d, 3 | white = 0xffe2e2e3, 4 | red = 0xffe57474, 5 | green = 0xff8ccf7e, 6 | blue = 0xff67b0e8, 7 | yellow = 0xffe5c76b, 8 | orange = 0xffd07360, 9 | magenta = 0xffc47fd5, 10 | grey = 0xff7f8490, 11 | teal = 0xff6cbfbf, 12 | transparent = 0x00000000, 13 | 14 | bar = { 15 | bg = 0xFF111825, 16 | border = 0xff2c2e34, 17 | }, 18 | popup = { 19 | bg = 0xF2232634, 20 | border = 0xff7f8490, 21 | card = 0xff232634, 22 | }, 23 | spaces = { 24 | active = 0xff474b54, 25 | inactive = 0xff474b54, 26 | }, 27 | bg1 = 0xff282f3b, 28 | bg2 = 0xff414559, 29 | 30 | with_alpha = function(color, alpha) 31 | if alpha > 1.0 or alpha < 0.0 then return color end 32 | return (color & 0x00ffffff) | (math.floor(alpha * 255.0) << 24) 33 | end, 34 | } 35 | -------------------------------------------------------------------------------- /default.lua: -------------------------------------------------------------------------------- 1 | local settings = require("settings") 2 | local colors = require("colors") 3 | 4 | -- Equivalent to the --default domain 5 | sbar.default({ 6 | updates = "when_shown", 7 | icon = { 8 | font = { 9 | family = settings.font.text, 10 | style = settings.font.style_map["Bold"], 11 | size = 14.0 12 | }, 13 | color = colors.white, 14 | padding_left = settings.paddings, 15 | padding_right = settings.paddings, 16 | background = { image = { corner_radius = 9 } }, 17 | }, 18 | label = { 19 | font = { 20 | family = settings.font.text, 21 | style = settings.font.style_map["Semibold"], 22 | size = 13.0 23 | }, 24 | color = colors.white, 25 | padding_left = settings.paddings, 26 | padding_right = settings.paddings, 27 | }, 28 | background = { 29 | height = 25, 30 | corner_radius = 5, 31 | border_width = 0, 32 | border_color = colors.bg2, 33 | image = { 34 | corner_radius = 9, 35 | border_color = colors.grey, 36 | border_width = 1 37 | } 38 | }, 39 | popup = { 40 | background = { 41 | border_width = 2, 42 | corner_radius = 9, 43 | border_color = colors.popup.border, 44 | color = colors.popup.bg, 45 | shadow = { drawing = true }, 46 | }, 47 | blur_radius = 50, 48 | }, 49 | padding_left = 5, 50 | padding_right = 5, 51 | scroll_texts = true, 52 | }) 53 | -------------------------------------------------------------------------------- /helpers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsec/sketchybar/9d95690eacda47ab029c633670545dfbb6b678f1/helpers/.DS_Store -------------------------------------------------------------------------------- /helpers/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | -------------------------------------------------------------------------------- /helpers/app_icons.lua: -------------------------------------------------------------------------------- 1 | return { 2 | ["Typora"] = ":text:", 3 | ["Orion"] = ":orion:", 4 | ["Orion RC"] = ":orion:", 5 | ["Grammarly Editor"] = ":grammarly:", 6 | ["kitty"] = ":kitty:", 7 | ["ClickUp"] = ":click_up:", 8 | ["Iris"] = ":iris:", 9 | ["PomoDone App"] = ":pomodone:", 10 | ["qutebrowser"] = ":qute_browser:", 11 | ["Raindrop.io"] = ":raindrop_io:", 12 | ["Airmail"] = ":airmail:", 13 | ["Affinity Publisher 2"] = ":affinity_publisher_2:", 14 | ["Calendar"] = ":calendar:", 15 | ["日历"] = ":calendar:", 16 | ["Fantastical"] = ":calendar:", 17 | ["Cron"] = ":calendar:", 18 | ["Amie"] = ":calendar:", 19 | ["Figma"] = ":figma:", 20 | ["Element"] = ":element:", 21 | ["Signal"] = ":signal:", 22 | ["Mattermost"] = ":mattermost:", 23 | ["Caprine"] = ":caprine:", 24 | ["Microsoft To Do"] = ":things:", 25 | ["Things"] = ":things:", 26 | ["Godot"] = ":godot:", 27 | ["Android Messages"] = ":android_messages:", 28 | ["Zed"] = ":zed:", 29 | ["Anytype"] = ":anytype:", 30 | ["TeamSpeak 3"] = ":team_speak:", 31 | ["LibreWolf"] = ":libre_wolf:", 32 | ["Spotlight"] = ":spotlight:", 33 | ["微信"] = ":wechat:", 34 | ["Dropbox"] = ":dropbox:", 35 | ["Transmit"] = ":transmit:", 36 | ["TickTick"] = ":tick_tick:", 37 | ["Parallels Desktop"] = ":parallels:", 38 | ["Audacity"] = ":audacity:", 39 | ["Rider"] = ":rider:", 40 | ["JetBrains Rider"] = ":rider:", 41 | ["DEVONthink 3"] = ":devonthink3:", 42 | ["Docker"] = ":docker:", 43 | ["Docker Desktop"] = ":docker:", 44 | ["Matlab"] = ":matlab:", 45 | ["VLC"] = ":vlc:", 46 | ["Alacritty"] = ":alacritty:", 47 | ["Pages"] = ":pages:", 48 | ["Pages 文稿"] = ":pages:", 49 | ["Bear"] = ":bear:", 50 | ["Pine"] = ":pine:", 51 | ["Affinity Designer 2"] = ":affinity_designer_2:", 52 | ["Keyboard Maestro"] = ":keyboard_maestro:", 53 | ["Joplin"] = ":joplin:", 54 | ["mpv"] = ":mpv:", 55 | ["zoom.us"] = ":zoom:", 56 | ["Affinity Photo 2"] = ":affinity_photo_2:", 57 | ["Music"] = ":music:", 58 | ["音乐"] = ":music:", 59 | ["League of Legends"] = ":league_of_legends:", 60 | ["Tor Browser"] = ":tor_browser:", 61 | ["Hyper"] = ":hyper:", 62 | ["‎WhatsApp"] = ":whats_app:", 63 | ["카카오톡"] = ":kakaotalk:", 64 | ["Discord"] = ":discord:", 65 | ["Discord Canary"] = ":discord:", 66 | ["Discord PTB"] = ":discord:", 67 | ["Neovide"] = ":vim:", 68 | ["MacVim"] = ":vim:", 69 | ["Vim"] = ":vim:", 70 | ["VimR"] = ":vim:", 71 | ["Keynote"] = ":keynote:", 72 | ["Keynote 讲演"] = ":keynote:", 73 | ["iTerm"] = ":iterm:", 74 | ["iTerm2"] = ":iterm:", 75 | ["IntelliJ IDEA"] = ":idea:", 76 | ["Finder"] = ":finder:", 77 | ["访达"] = ":finder:", 78 | ["Xcode"] = ":xcode:", 79 | ["GoLand"] = ":goland:", 80 | ["Android Studio"] = ":android_studio:", 81 | ["MoneyMoney"] = ":bank:", 82 | ["Spotify"] = ":spotify:", 83 | ["KeePassXC"] = ":kee_pass_x_c:", 84 | ["Alfred"] = ":alfred:", 85 | ["Color Picker"] = ":color_picker:", 86 | ["数码测色计"] = ":color_picker:", 87 | ["Microsoft Word"] = ":microsoft_word:", 88 | ["Microsoft PowerPoint"] = ":microsoft_power_point:", 89 | ["Notes"] = ":notes:", 90 | ["备忘录"] = ":notes:", 91 | ["Microsoft Edge"] = ":microsoft_edge:", 92 | ["Sublime Text"] = ":sublime_text:", 93 | ["Sequel Ace"] = ":sequel_ace:", 94 | ["Folx"] = ":folx:", 95 | ["DingTalk"] = ":dingtalk:", 96 | ["钉钉"] = ":dingtalk:", 97 | ["阿里钉"] = ":dingtalk:", 98 | ["WebStorm"] = ":web_storm:", 99 | ["Sequel Pro"] = ":sequel_pro:", 100 | ["Skype"] = ":skype:", 101 | ["网易云音乐"] = ":netease_music:", 102 | ["PyCharm"] = ":pycharm:", 103 | ["Canary Mail"] = ":mail:", 104 | ["HEY"] = ":mail:", 105 | ["Mail"] = ":mail:", 106 | ["Mailspring"] = ":mail:", 107 | ["MailMate"] = ":mail:", 108 | ["邮件"] = ":mail:", 109 | ["Default"] = ":default:", 110 | ["App Store"] = ":app_store:", 111 | ["Calibre"] = ":book:", 112 | ["Todoist"] = ":todoist:", 113 | ["Emacs"] = ":emacs:", 114 | ["Messenger"] = ":messenger:", 115 | ["Tower"] = ":tower:", 116 | ["VSCodium"] = ":vscodium:", 117 | ["Drafts"] = ":drafts:", 118 | ["Cypress"] = ":cypress:", 119 | ["GitHub Desktop"] = ":git_hub:", 120 | ["Telegram"] = ":telegram:", 121 | ["Firefox Developer Edition"] = ":firefox_developer_edition:", 122 | ["Firefox Nightly"] = ":firefox_developer_edition:", 123 | ["Min"] = ":min_browser:", 124 | ["Sketch"] = ":sketch:", 125 | ["Affinity Photo"] = ":affinity_photo:", 126 | ["MAMP"] = ":mamp:", 127 | ["MAMP PRO"] = ":mamp:", 128 | ["Insomnia"] = ":insomnia:", 129 | ["Bitwarden"] = ":bit_warden:", 130 | ["Warp"] = ":warp:", 131 | ["System Preferences"] = ":gear:", 132 | ["System Settings"] = ":gear:", 133 | ["系统设置"] = ":gear:", 134 | ["Affinity Designer"] = ":affinity_designer:", 135 | ["Live"] = ":ableton:", 136 | ["Arc"] = ":arc:", 137 | ["Chromium"] = ":google_chrome:", 138 | ["Google Chrome"] = ":google_chrome:", 139 | ["Google Chrome Canary"] = ":google_chrome:", 140 | ["Jellyfin Media Player"] = ":jellyfin:", 141 | ["Zulip"] = ":zulip:", 142 | ["1Password"] = ":one_password:", 143 | ["FaceTime"] = ":face_time:", 144 | ["FaceTime 通话"] = ":face_time:", 145 | ["Citrix Workspace"] = ":citrix:", 146 | ["Citrix Viewer"] = ":citrix:", 147 | ["Logseq"] = ":logseq:", 148 | ["Reeder"] = ":reeder5:", 149 | ["Code"] = ":code:", 150 | ["Code - Insiders"] = ":code:", 151 | ["Notion"] = ":notion:", 152 | ["Final Cut Pro"] = ":final_cut_pro:", 153 | ["Zotero"] = ":zotero:", 154 | ["Safari"] = ":safari:", 155 | ["Safari浏览器"] = ":safari:", 156 | ["Safari Technology Preview"] = ":safari:", 157 | ["Blender"] = ":blender:", 158 | ["Affinity Publisher"] = ":affinity_publisher:", 159 | ["Spark Desktop"] = ":spark:", 160 | ["Zeplin"] = ":zeplin:", 161 | ["Replit"] = ":replit:", 162 | ["Podcasts"] = ":podcasts:", 163 | ["播客"] = ":podcasts:", 164 | ["NordVPN"] = ":nord_vpn:", 165 | ["Notability"] = ":notability:", 166 | ["Numbers"] = ":numbers:", 167 | ["Numbers 表格"] = ":numbers:", 168 | ["Nova"] = ":nova:", 169 | ["Microsoft Excel"] = ":microsoft_excel:", 170 | ["Trello"] = ":trello:", 171 | ["Pi-hole Remote"] = ":pihole:", 172 | ["Linear"] = ":linear:", 173 | ["CleanMyMac X"] = ":desktop:", 174 | ["GrandTotal"] = ":dollar:", 175 | ["Receipts"] = ":dollar:", 176 | ["Evernote Legacy"] = ":evernote_legacy:", 177 | ["OmniFocus"] = ":omni_focus:", 178 | ["Terminal"] = ":terminal:", 179 | ["终端"] = ":terminal:", 180 | ["Atom"] = ":atom:", 181 | ["Kakoune"] = ":kakoune:", 182 | ["Reminders"] = ":reminders:", 183 | ["提醒事项"] = ":reminders:", 184 | ["Tana"] = ":tana:", 185 | ["OBS"] = ":obsstudio:", 186 | ["VMware Fusion"] = ":vmware_fusion:", 187 | ["Tweetbot"] = ":twitter:", 188 | ["Twitter"] = ":twitter:", 189 | ["Microsoft Teams"] = ":microsoft_teams:", 190 | ["Yuque"] = ":yuque:", 191 | ["语雀"] = ":yuque:", 192 | ["Slack"] = ":slack:", 193 | ["Vivaldi"] = ":vivaldi:", 194 | ["Setapp"] = ":setapp:", 195 | ["TIDAL"] = ":tidal:", 196 | ["Miro"] = ":miro:", 197 | ["Messages"] = ":messages:", 198 | ["信息"] = ":messages:", 199 | ["Nachrichten"] = ":messages:", 200 | ["Brave Browser"] = ":brave_browser:", 201 | ["Brave"] = ":brave_browser:", 202 | ["Preview"] = ":pdf:", 203 | ["预览"] = ":pdf:", 204 | ["Skim"] = ":pdf:", 205 | ["zathura"] = ":pdf:", 206 | ["Obsidian"] = ":obsidian:", 207 | ["Thunderbird"] = ":thunderbird:", 208 | ["Firefox"] = ":firefox:", 209 | ["WezTerm"] = ":wezterm:", 210 | ["default"] = ":default:", 211 | } -------------------------------------------------------------------------------- /helpers/default_font.lua: -------------------------------------------------------------------------------- 1 | return { 2 | text = "SF Pro", -- Used for text 3 | numbers = "SF Mono", -- Used for numbers 4 | 5 | -- Unified font style map 6 | style_map = { 7 | ["Regular"] = "Regular", 8 | ["Semibold"] = "Semibold", 9 | ["Bold"] = "Bold", 10 | ["Heavy"] = "Heavy", 11 | ["Black"] = "Black", 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /helpers/event_providers/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsec/sketchybar/9d95690eacda47ab029c633670545dfbb6b678f1/helpers/event_providers/.DS_Store -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots/**/*.png 66 | fastlane/test_output 67 | 68 | # Code Injection 69 | # 70 | # After new code Injection tools there's a generated folder /iOSInjectionProject 71 | # https://github.com/johnno1962/injectionforxcode 72 | iOSInjectionProject/ 73 | 74 | # macOS 75 | .DS_Store 76 | .AppleDouble 77 | .LSOverride 78 | 79 | # Icon must end with two \r 80 | Icon 81 | 82 | # Thumbnails 83 | ._* 84 | 85 | # Files that might appear in the root of a volume 86 | .DocumentRevisions-V100 87 | .fseventsd 88 | .Spotlight-V100 89 | .TemporaryItems 90 | .Trashes 91 | .VolumeIcon.icns 92 | .com.apple.timemachine.donotpresent 93 | 94 | # Directories potentially created on remote AFP share 95 | .AppleDB 96 | .AppleDesktop 97 | Network Trash Folder 98 | Temporary Items 99 | .apdisk -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "AppleMenu", 6 | platforms: [ 7 | .macOS(.v12) 8 | ], 9 | products: [ 10 | .executable(name: "AppleMenu", targets: ["AppleMenu"]) 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .executableTarget( 15 | name: "AppleMenu", 16 | dependencies: [], 17 | path: "Sources/AppleMenu" 18 | ) 19 | ] 20 | ) -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/AgendaCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AgendaCard: View { 4 | @State private var tasks = [ 5 | "Yet another agenda file", 6 | "TODO insert important task here^^", 7 | "Pending", 8 | "TODO バイデン・ブラスト!", 9 | "TODO 日本語学習", 10 | "TODO RUSTプログラミング言語を習う", 11 | "TODO 数学を学習", 12 | "TODO finish [the pragmatic programmer] book", 13 | "TODO get the bootloader to work" 14 | ] 15 | 16 | let showHeader: Bool 17 | 18 | public init(showHeader: Bool = true) { 19 | self.showHeader = showHeader 20 | } 21 | 22 | public var body: some View { 23 | Card { 24 | VStack(alignment: .leading, spacing: 12) { 25 | VStack(alignment: .leading, spacing: 8) { 26 | ForEach(tasks, id: \.self) { task in 27 | Text(task) 28 | .foregroundColor(task.hasPrefix("TODO") ? Color(red: 0.8, green: 0.8, blue: 0.6) : .gray) 29 | .font(.system(size: 12)) 30 | } 31 | } 32 | } 33 | .padding(12) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/AgendaSection.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct AgendaSection: View { 4 | public init() {} 5 | 6 | public var body: some View { 7 | VStack(spacing: 12) { 8 | // Agenda card with header 9 | Card { 10 | VStack(spacing: 12) { 11 | // Agenda header 12 | HStack { 13 | Image(systemName: "list.bullet") 14 | .foregroundColor(.gray) 15 | Text("Agenda") 16 | .foregroundColor(.gray) 17 | .font(.system(size: 14)) 18 | } 19 | .frame(maxWidth: .infinity, alignment: .leading) 20 | 21 | // Agenda content 22 | AgendaCard(showHeader: false) 23 | } 24 | .padding(12) 25 | } 26 | 27 | // System controls as separate card 28 | SystemControlsCard() 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/CalendarCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct CalendarCard: View { 4 | let calendar = Calendar.current 5 | @State private var selectedDate = Date() 6 | let showHeader: Bool 7 | 8 | public init(showHeader: Bool = true) { 9 | self.showHeader = showHeader 10 | } 11 | 12 | public var body: some View { 13 | Card { 14 | VStack(alignment: .leading, spacing: 12) { 15 | // Calendar grid 16 | LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 8) { 17 | // Weekday headers 18 | ForEach(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], id: \.self) { day in 19 | Text(day) 20 | .foregroundColor(.gray) 21 | .font(.system(size: 12)) 22 | } 23 | 24 | // Date cells 25 | ForEach(getDaysInMonth(), id: \.self) { date in 26 | if let date = date { 27 | Text("\(calendar.component(.day, from: date))") 28 | .foregroundColor(.white) 29 | .font(.system(size: 12)) 30 | .frame(maxWidth: .infinity) 31 | } else { 32 | Text("") 33 | .frame(maxWidth: .infinity) 34 | } 35 | } 36 | } 37 | } 38 | .padding(12) 39 | } 40 | } 41 | 42 | private func getDaysInMonth() -> [Date?] { 43 | var days: [Date?] = [] 44 | let range = calendar.range(of: .day, in: .month, for: selectedDate)! 45 | 46 | // Add empty cells for days before the first of the month 47 | let firstDayOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: selectedDate))! 48 | let weekday = calendar.component(.weekday, from: firstDayOfMonth) 49 | for _ in 1..: View { 9 | let content: Content 10 | let style: CardStyle 11 | var padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16) 12 | var cornerRadius: CGFloat = 16 13 | var borderWidth: CGFloat = 1 14 | 15 | public init( 16 | style: CardStyle = .primary, 17 | padding: EdgeInsets = EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16), 18 | cornerRadius: CGFloat = 16, 19 | borderWidth: CGFloat = 1, 20 | @ViewBuilder content: () -> Content 21 | ) { 22 | self.content = content() 23 | self.style = style 24 | self.padding = padding 25 | self.cornerRadius = cornerRadius 26 | self.borderWidth = borderWidth 27 | } 28 | 29 | private var backgroundColor: Color { 30 | switch style { 31 | case .primary: 32 | return Color(red: 27/255.0, green: 33/255.0, blue: 40/255.0) // #1B2128 33 | case .secondary: 34 | return Color(red: 17/255.0, green: 17/255.0, blue: 27/255.0) // Matches window background 35 | } 36 | } 37 | 38 | private var borderColor: Color { 39 | switch style { 40 | case .primary: 41 | return Color(red: 32/255.0, green: 38/255.0, blue: 45/255.0) 42 | case .secondary: 43 | return Color(red: 28/255.0, green: 28/255.0, blue: 38/255.0) 44 | } 45 | } 46 | 47 | public var body: some View { 48 | ZStack { 49 | // Base background 50 | RoundedRectangle(cornerRadius: cornerRadius) 51 | .fill(backgroundColor) 52 | 53 | // Gradient overlay 54 | RoundedRectangle(cornerRadius: cornerRadius) 55 | .fill( 56 | LinearGradient( 57 | colors: [ 58 | Color.white.opacity(0.1), 59 | Color.white.opacity(0.05), 60 | Color.clear 61 | ], 62 | startPoint: .top, 63 | endPoint: .bottom 64 | ) 65 | ) 66 | 67 | // Border 68 | RoundedRectangle(cornerRadius: cornerRadius) 69 | .strokeBorder(borderColor, lineWidth: borderWidth) 70 | 71 | // Content 72 | content 73 | .padding(padding) 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/MediaPlayerCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct MediaPlayerCard: View { 4 | public init() {} 5 | 6 | public var body: some View { 7 | Card(style: .secondary) { 8 | HStack(spacing: 12) { 9 | // Album art 10 | Image(nsImage: NSImage(named: "albumArt") ?? NSImage()) 11 | .resizable() 12 | .frame(width: 48, height: 48) 13 | .cornerRadius(6) 14 | 15 | VStack(alignment: .leading, spacing: 4) { 16 | Text("Little Dark Age") 17 | .foregroundColor(.white) 18 | .font(.system(size: 14, weight: .medium)) 19 | 20 | Text("ISTERIF") 21 | .foregroundColor(.gray) 22 | .font(.system(size: 12)) 23 | } 24 | 25 | Spacer() 26 | 27 | // Playback controls 28 | HStack(spacing: 12) { 29 | Button(action: {}) { 30 | Image(systemName: "backward.fill") 31 | .foregroundColor(.white) 32 | .font(.system(size: 12)) 33 | } 34 | .buttonStyle(PlainButtonStyle()) 35 | 36 | Button(action: {}) { 37 | Image(systemName: "play.fill") 38 | .foregroundColor(.white) 39 | .font(.system(size: 12)) 40 | } 41 | .buttonStyle(PlainButtonStyle()) 42 | 43 | Button(action: {}) { 44 | Image(systemName: "forward.fill") 45 | .foregroundColor(.white) 46 | .font(.system(size: 12)) 47 | } 48 | .buttonStyle(PlainButtonStyle()) 49 | } 50 | } 51 | .padding(12) 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/ProfileCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct ProfileCard: View { 4 | let username: String 5 | let uptime: String 6 | 7 | public init(username: String, uptime: String) { 8 | self.username = username 9 | self.uptime = uptime 10 | } 11 | 12 | public var body: some View { 13 | Card { 14 | VStack(alignment: .leading, spacing: 8) { 15 | Text(username) 16 | .font(.system(size: 24, weight: .medium)) 17 | .foregroundColor(.white) 18 | Text(uptime) 19 | .font(.system(size: 14)) 20 | .foregroundColor(.gray) 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/SystemControlsCard.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct SystemControlsCard: View { 4 | public init() {} 5 | 6 | public var body: some View { 7 | Card(style: .secondary) { 8 | HStack(spacing: 16) { 9 | // Power 10 | Button(action: {}) { 11 | Image(systemName: "power") 12 | .foregroundColor(.white) 13 | .font(.system(size: 12)) 14 | } 15 | .buttonStyle(PlainButtonStyle()) 16 | 17 | // Moon 18 | Button(action: {}) { 19 | Image(systemName: "moon.fill") 20 | .foregroundColor(.white) 21 | .font(.system(size: 12)) 22 | } 23 | .buttonStyle(PlainButtonStyle()) 24 | 25 | // WiFi 26 | Button(action: {}) { 27 | Image(systemName: "wifi") 28 | .foregroundColor(.white) 29 | .font(.system(size: 12)) 30 | } 31 | .buttonStyle(PlainButtonStyle()) 32 | 33 | // Bluetooth 34 | Button(action: {}) { 35 | Image(systemName: "bluetooth") 36 | .foregroundColor(.white) 37 | .font(.system(size: 12)) 38 | } 39 | .buttonStyle(PlainButtonStyle()) 40 | 41 | // Desktop 42 | Button(action: {}) { 43 | Image(systemName: "display") 44 | .foregroundColor(.white) 45 | .font(.system(size: 12)) 46 | } 47 | .buttonStyle(PlainButtonStyle()) 48 | 49 | // Layers 50 | Button(action: {}) { 51 | Image(systemName: "square.3.stack.3d") 52 | .foregroundColor(.white) 53 | .font(.system(size: 12)) 54 | } 55 | .buttonStyle(PlainButtonStyle()) 56 | } 57 | .padding(12) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Components/TimeDisplay.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct TimeDisplay: View { 4 | @State private var currentTime = Date() 5 | private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 6 | 7 | private let timeFormatter: DateFormatter = { 8 | let formatter = DateFormatter() 9 | formatter.dateFormat = "HH:mm" 10 | return formatter 11 | }() 12 | 13 | public init() {} 14 | 15 | public var body: some View { 16 | Text(timeFormatter.string(from: currentTime)) 17 | .foregroundColor(.gray) 18 | .font(.system(size: 14)) 19 | .onReceive(timer) { input in 20 | currentTime = input 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/Utils/Formatters.swift: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/Sources/AppleMenu/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import UserNotifications 4 | 5 | struct ContentView: View { 6 | @State private var username = "paulknight" 7 | @State private var uptime = "up 13 hours" 8 | 9 | var body: some View { 10 | ZStack { 11 | // Background color - darker theme 12 | Color(red: 17/255.0, green: 17/255.0, blue: 27/255.0) 13 | .edgesIgnoringSafeArea(.all) 14 | 15 | VStack(spacing: 16) { 16 | // Top row with profile and time 17 | HStack { 18 | // Left side - Profile 19 | HStack(spacing: 8) { 20 | Image(systemName: "person.circle.fill") 21 | .foregroundColor(.gray) 22 | Text(username) 23 | .foregroundColor(.gray) 24 | .font(.system(size: 14)) 25 | Text("|") 26 | .foregroundColor(.gray) 27 | .font(.system(size: 14)) 28 | Text(uptime) 29 | .foregroundColor(.gray) 30 | .font(.system(size: 14)) 31 | } 32 | 33 | Spacer() 34 | 35 | // Right side - Time 36 | TimeDisplay() 37 | } 38 | .padding(.horizontal, 16) 39 | .padding(.vertical, 8) 40 | 41 | // Main content 42 | HStack(spacing: 12) { 43 | // Left section 44 | CalendarSection() 45 | .frame(maxWidth: .infinity) 46 | 47 | // Right section 48 | AgendaSection() 49 | .frame(maxWidth: .infinity) 50 | } 51 | .padding(.horizontal, 16) 52 | } 53 | .padding(.vertical, 16) 54 | } 55 | .frame(width: 800, height: 500) 56 | } 57 | } 58 | 59 | class AppDelegate: NSObject, NSApplicationDelegate { 60 | var window: NSWindow? 61 | var eventMonitor: Any? 62 | 63 | func applicationDidFinishLaunching(_ notification: Notification) { 64 | // Create the SwiftUI view that provides the window contents 65 | let contentView = ContentView() 66 | 67 | // Get the main screen's frame 68 | if let screen = NSScreen.main { 69 | let screenFrame = screen.frame 70 | let windowWidth: CGFloat = 800 71 | let windowHeight: CGFloat = 500 72 | 73 | // Calculate center position 74 | let x = (screenFrame.width - windowWidth) / 2 75 | let y = (screenFrame.height - windowHeight) / 2 76 | 77 | // Create the window and set the content view 78 | window = NSWindow( 79 | contentRect: NSRect(x: x, y: y, width: windowWidth, height: windowHeight), 80 | styleMask: [.borderless], 81 | backing: .buffered, 82 | defer: false 83 | ) 84 | 85 | window?.isReleasedWhenClosed = false 86 | window?.level = .floating 87 | window?.backgroundColor = NSColor(red: 0x0D/255.0, green: 0x11/255.0, blue: 0x16/255.0, alpha: 1.0) 88 | 89 | // Add corner radius to the window itself 90 | window?.hasShadow = false 91 | if let windowFrame = window?.contentView?.superview { 92 | windowFrame.wantsLayer = true 93 | windowFrame.layer?.cornerRadius = 20 94 | windowFrame.layer?.masksToBounds = true 95 | } 96 | 97 | window?.contentView = NSHostingView(rootView: contentView) 98 | window?.makeKeyAndOrderFront(nil) 99 | 100 | if let contentView = window?.contentView { 101 | contentView.wantsLayer = true 102 | contentView.layer?.cornerRadius = 20 103 | contentView.layer?.masksToBounds = true 104 | } 105 | 106 | // Set up event monitor for clicks outside the window 107 | eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] event in 108 | if let window = self?.window { 109 | let windowFrame = window.frame 110 | let clickLocation = NSPoint(x: event.locationInWindow.x, y: screen.frame.height - event.locationInWindow.y) 111 | 112 | if !NSPointInRect(clickLocation, windowFrame) { 113 | NSApp.terminate(nil) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | func applicationWillTerminate(_ notification: Notification) { 121 | // Clean up event monitor 122 | if let monitor = eventMonitor { 123 | NSEvent.removeMonitor(monitor) 124 | } 125 | } 126 | } 127 | 128 | // Initialize and run the application 129 | let app = NSApplication.shared 130 | let delegate = AppDelegate() 131 | app.delegate = delegate 132 | app.run() -------------------------------------------------------------------------------- /helpers/event_providers/apple_menu/makefile: -------------------------------------------------------------------------------- 1 | BINARY_NAME = apple_menu 2 | FRAMEWORKS = -framework Cocoa -framework SwiftUI -framework MediaPlayer -framework CoreAudio 3 | 4 | bin/$(BINARY_NAME): apple_menu.swift | bin 5 | swiftc $< $(FRAMEWORKS) -o $@ 6 | 7 | bin: 8 | mkdir -p bin 9 | 10 | clean: 11 | rm -rf bin 12 | 13 | .PHONY: clean -------------------------------------------------------------------------------- /helpers/event_providers/cpu_load/cpu.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | struct cpu { 7 | host_t host; 8 | mach_msg_type_number_t count; 9 | host_cpu_load_info_data_t load; 10 | host_cpu_load_info_data_t prev_load; 11 | bool has_prev_load; 12 | 13 | int user_load; 14 | int sys_load; 15 | int total_load; 16 | }; 17 | 18 | static inline void cpu_init(struct cpu* cpu) { 19 | cpu->host = mach_host_self(); 20 | cpu->count = HOST_CPU_LOAD_INFO_COUNT; 21 | cpu->has_prev_load = false; 22 | } 23 | 24 | static inline void cpu_update(struct cpu* cpu) { 25 | kern_return_t error = host_statistics(cpu->host, 26 | HOST_CPU_LOAD_INFO, 27 | (host_info_t)&cpu->load, 28 | &cpu->count ); 29 | 30 | if (error != KERN_SUCCESS) { 31 | printf("Error: Could not read cpu host statistics.\n"); 32 | return; 33 | } 34 | 35 | if (cpu->has_prev_load) { 36 | uint32_t delta_user = cpu->load.cpu_ticks[CPU_STATE_USER] 37 | - cpu->prev_load.cpu_ticks[CPU_STATE_USER]; 38 | 39 | uint32_t delta_system = cpu->load.cpu_ticks[CPU_STATE_SYSTEM] 40 | - cpu->prev_load.cpu_ticks[CPU_STATE_SYSTEM]; 41 | 42 | uint32_t delta_idle = cpu->load.cpu_ticks[CPU_STATE_IDLE] 43 | - cpu->prev_load.cpu_ticks[CPU_STATE_IDLE]; 44 | 45 | cpu->user_load = (double)delta_user / (double)(delta_system 46 | + delta_user 47 | + delta_idle) * 100.0; 48 | 49 | cpu->sys_load = (double)delta_system / (double)(delta_system 50 | + delta_user 51 | + delta_idle) * 100.0; 52 | 53 | cpu->total_load = cpu->user_load + cpu->sys_load; 54 | } 55 | 56 | cpu->prev_load = cpu->load; 57 | cpu->has_prev_load = true; 58 | } 59 | -------------------------------------------------------------------------------- /helpers/event_providers/cpu_load/cpu_load.c: -------------------------------------------------------------------------------- 1 | #include "cpu.h" 2 | #include "../sketchybar.h" 3 | 4 | int main (int argc, char** argv) { 5 | // Redirect stdout and stderr to /dev/null 6 | freopen("/dev/null", "w", stdout); 7 | freopen("/dev/null", "w", stderr); 8 | 9 | float update_freq; 10 | if (argc < 3 || (sscanf(argv[2], "%f", &update_freq) != 1)) { 11 | exit(1); // Removed printf since output is redirected 12 | } 13 | 14 | alarm(0); 15 | struct cpu cpu; 16 | cpu_init(&cpu); 17 | 18 | // Setup the event in sketchybar 19 | char event_message[512]; 20 | snprintf(event_message, 512, "--add event '%s'", argv[1]); 21 | sketchybar(event_message); 22 | 23 | char trigger_message[512]; 24 | for (;;) { 25 | // Acquire new info 26 | cpu_update(&cpu); 27 | 28 | // Prepare the event message 29 | snprintf(trigger_message, 30 | 512, 31 | "--trigger '%s' user_load='%d' sys_load='%02d' total_load='%02d'", 32 | argv[1], 33 | cpu.user_load, 34 | cpu.sys_load, 35 | cpu.total_load ); 36 | 37 | // Trigger the event 38 | sketchybar(trigger_message); 39 | 40 | // Wait 41 | usleep(update_freq * 1000000); 42 | } 43 | return 0; 44 | } 45 | -------------------------------------------------------------------------------- /helpers/event_providers/cpu_load/makefile: -------------------------------------------------------------------------------- 1 | bin/cpu_load: cpu_load.c cpu.h ../sketchybar.h | bin 2 | clang -std=c99 -O3 $< -o $@ 3 | 4 | bin: 5 | mkdir bin 6 | -------------------------------------------------------------------------------- /helpers/event_providers/hdd_load/hdd.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | struct disk_info { 7 | unsigned long total_space; // Total disk space in GB 8 | unsigned long free_space; // Free disk space in GB 9 | unsigned long used_space; // Used disk space in GB 10 | int percent_used; // Percentage of used space 11 | int percent_remaining; // Percentage of free space remaining 12 | }; 13 | 14 | static inline void disk_init(struct disk_info* disk) { 15 | // Initialize disk information 16 | disk->total_space = 0; 17 | disk->free_space = 0; 18 | disk->used_space = 0; 19 | disk->percent_used = 0; 20 | disk->percent_remaining = 0; 21 | } 22 | 23 | static inline bool disk_update(struct disk_info* disk, const char* path) { 24 | struct statvfs stat; 25 | if (statvfs(path, &stat) != 0) { 26 | printf("Error: Could not read disk statistics for %s\n", path); 27 | return false; 28 | } 29 | 30 | // Total space in bytes 31 | unsigned long total_bytes = stat.f_blocks * stat.f_frsize; 32 | // Free space in bytes 33 | unsigned long free_bytes = stat.f_bfree * stat.f_frsize; 34 | 35 | // Convert to GB 36 | disk->total_space = total_bytes / (1024 * 1024 * 1024); 37 | disk->free_space = free_bytes / (1024 * 1024 * 1024); 38 | disk->used_space = disk->total_space - disk->free_space; 39 | 40 | // Calculate percentage of used and remaining space 41 | disk->percent_used = (disk->total_space > 0) ? ((disk->used_space * 100) / disk->total_space) : 0; 42 | disk->percent_remaining = (disk->total_space > 0) ? ((disk->free_space * 100) / disk->total_space) : 0; 43 | 44 | return true; 45 | } 46 | -------------------------------------------------------------------------------- /helpers/event_providers/hdd_load/hdd_load: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/logicsec/sketchybar/9d95690eacda47ab029c633670545dfbb6b678f1/helpers/event_providers/hdd_load/hdd_load -------------------------------------------------------------------------------- /helpers/event_providers/hdd_load/hdd_load.c: -------------------------------------------------------------------------------- 1 | #include "hdd.h" 2 | #include "../sketchybar.h" 3 | #include 4 | 5 | int main (int argc, char** argv) { 6 | // Comment out stdout/stderr redirection for debugging 7 | // freopen("/dev/null", "w", stdout); 8 | // freopen("/dev/null", "w", stderr); 9 | 10 | float update_freq; 11 | if (argc < 3 || (sscanf(argv[2], "%f", &update_freq) != 1)) { 12 | printf("Usage: %s \"\" \"\"\n", argv[0]); 13 | exit(1); 14 | } 15 | 16 | alarm(0); 17 | struct disk_info root_disk; 18 | struct disk_info external_disk; 19 | disk_init(&root_disk); 20 | disk_init(&external_disk); 21 | 22 | // Setup the event in sketchybar 23 | char event_message[512]; 24 | snprintf(event_message, 512, "--add event '%s'", argv[1]); 25 | sketchybar(event_message); 26 | 27 | char trigger_message[1024]; 28 | for (;;) { 29 | // Acquire new disk info for both drives 30 | disk_update(&root_disk, "/"); 31 | disk_update(&external_disk, "/Volumes/ExternalDrive"); 32 | 33 | // Calculate combined free space in TB (with one decimal place) 34 | double total_free_tb = (root_disk.free_space + external_disk.free_space) / 1024.0; 35 | 36 | // Prepare the event message with available space in TB 37 | snprintf(trigger_message, 1024, 38 | "--trigger '%s' available='%.1fT'", 39 | argv[1], 40 | total_free_tb); 41 | 42 | // Trigger the event 43 | sketchybar(trigger_message); 44 | 45 | // Wait 46 | usleep(update_freq * 1000000); 47 | } 48 | return 0; 49 | } 50 | -------------------------------------------------------------------------------- /helpers/event_providers/hdd_load/makefile: -------------------------------------------------------------------------------- 1 | bin/hdd_load: hdd_load.c hdd.h ../sketchybar.h | bin 2 | clang -std=c99 -O3 $< -o $@ 3 | 4 | bin: 5 | mkdir -p bin 6 | -------------------------------------------------------------------------------- /helpers/event_providers/makefile: -------------------------------------------------------------------------------- 1 | all: 2 | (cd cpu_load && $(MAKE)) 3 | (cd memory_load && $(MAKE)) 4 | (cd hdd_load && $(MAKE)) 5 | (cd network_load && $(MAKE)) 6 | (cd media_player && $(MAKE)) 7 | (cd apple_menu && $(MAKE)) -------------------------------------------------------------------------------- /helpers/event_providers/memory_load/makefile: -------------------------------------------------------------------------------- 1 | bin/memory_load: memory_load.c memory.h ../sketchybar.h | bin 2 | clang -std=c99 -O3 $< -o $@ 3 | 4 | bin: 5 | mkdir -p bin 6 | -------------------------------------------------------------------------------- /helpers/event_providers/memory_load/memory.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | struct memory { 8 | host_t host; 9 | mach_msg_type_number_t count; 10 | vm_statistics_data_t vm_info; 11 | vm_statistics_data_t prev_vm_info; 12 | bool has_prev_info; 13 | 14 | int used_memory; 15 | int free_memory; 16 | int total_memory; 17 | int memory_load_percentage; 18 | }; 19 | 20 | static inline void memory_init(struct memory* mem) { 21 | mem->host = mach_host_self(); 22 | mem->count = HOST_VM_INFO_COUNT; 23 | mem->has_prev_info = false; 24 | } 25 | 26 | static inline void memory_update(struct memory* mem) { 27 | kern_return_t error = host_statistics(mem->host, 28 | HOST_VM_INFO, 29 | (host_info_t)&mem->vm_info, 30 | &mem->count); 31 | 32 | if (error != KERN_SUCCESS) { 33 | printf("Error: Could not read memory host statistics.\n"); 34 | return; 35 | } 36 | 37 | // Get total physical memory 38 | int mib[2] = {CTL_HW, HW_MEMSIZE}; 39 | uint64_t total_memory_bytes = 0; 40 | size_t length = sizeof(total_memory_bytes); 41 | if (sysctl(mib, 2, &total_memory_bytes, &length, NULL, 0) == -1) { 42 | printf("Error: Could not get total memory.\n"); 43 | return; 44 | } 45 | 46 | // Convert to MB 47 | mem->total_memory = total_memory_bytes / (1024 * 1024); 48 | 49 | // Calculate used memory (active + wired) 50 | uint64_t used_bytes = (mem->vm_info.active_count + mem->vm_info.wire_count) * ((uint64_t)vm_page_size); 51 | mem->used_memory = used_bytes / (1024 * 1024); 52 | 53 | mem->free_memory = mem->total_memory - mem->used_memory; 54 | 55 | // Calculate percentage based on used vs total 56 | mem->memory_load_percentage = (int)((double)mem->used_memory / (double)mem->total_memory * 100.0 + 0.5); 57 | } 58 | -------------------------------------------------------------------------------- /helpers/event_providers/memory_load/memory_load.c: -------------------------------------------------------------------------------- 1 | #include "memory.h" 2 | #include "../sketchybar.h" 3 | 4 | int main (int argc, char** argv) { 5 | // Redirect stdout and stderr to /dev/null 6 | freopen("/dev/null", "w", stdout); 7 | freopen("/dev/null", "w", stderr); 8 | float update_freq; 9 | if (argc < 3 || (sscanf(argv[2], "%f", &update_freq) != 1)) { 10 | printf("Usage: %s \"\" \"\"\n", argv[0]); 11 | exit(1); 12 | } 13 | 14 | alarm(0); 15 | struct memory mem; 16 | memory_init(&mem); 17 | 18 | // Setup the event in sketchybar 19 | char event_message[512]; 20 | snprintf(event_message, 512, "--add event '%s'", argv[1]); 21 | sketchybar(event_message); 22 | 23 | char trigger_message[512]; 24 | for (;;) { 25 | // Acquire new memory info 26 | memory_update(&mem); 27 | 28 | // Prepare the event message 29 | snprintf(trigger_message, 30 | 512, 31 | "--trigger '%s' used_memory='%dMB' free_memory='%dMB' memory_load='%02d%%'", 32 | argv[1], 33 | mem.used_memory, 34 | mem.free_memory, 35 | mem.memory_load_percentage); 36 | 37 | // Trigger the event 38 | sketchybar(trigger_message); 39 | 40 | // Debugging output 41 | printf("Trigger message: %s\n", trigger_message); // Debugging 42 | 43 | // Wait 44 | usleep(update_freq * 1000000); 45 | } 46 | return 0; 47 | } 48 | -------------------------------------------------------------------------------- /helpers/event_providers/network_load/makefile: -------------------------------------------------------------------------------- 1 | bin/network_load: network_load.c network.h ../sketchybar.h | bin 2 | clang -std=c99 -O3 $< -o $@ 3 | 4 | bin: 5 | mkdir bin 6 | -------------------------------------------------------------------------------- /helpers/event_providers/network_load/network.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | static char unit_str[3][6] = { { " Bps" }, { "KBps" }, { "MBps" }, }; 10 | 11 | enum unit { 12 | UNIT_BPS, 13 | UNIT_KBPS, 14 | UNIT_MBPS 15 | }; 16 | struct network { 17 | uint32_t row; 18 | struct ifmibdata data; 19 | struct timeval tv_nm1, tv_n, tv_delta; 20 | 21 | int up; 22 | int down; 23 | enum unit up_unit, down_unit; 24 | }; 25 | 26 | static inline void ifdata(uint32_t net_row, struct ifmibdata* data) { 27 | static size_t size = sizeof(struct ifmibdata); 28 | static int32_t data_option[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_IFDATA, 0, IFDATA_GENERAL }; 29 | data_option[4] = net_row; 30 | sysctl(data_option, 6, data, &size, NULL, 0); 31 | } 32 | 33 | static inline void network_init(struct network* net, char* ifname) { 34 | memset(net, 0, sizeof(struct network)); 35 | 36 | static int count_option[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_SYSTEM, IFMIB_IFCOUNT }; 37 | uint32_t interface_count = 0; 38 | size_t size = sizeof(uint32_t); 39 | sysctl(count_option, 5, &interface_count, &size, NULL, 0); 40 | 41 | for (int i = 0; i < interface_count; i++) { 42 | ifdata(i, &net->data); 43 | if (strcmp(net->data.ifmd_name, ifname) == 0) { 44 | net->row = i; 45 | break; 46 | } 47 | } 48 | } 49 | 50 | static inline void network_update(struct network* net) { 51 | gettimeofday(&net->tv_n, NULL); 52 | timersub(&net->tv_n, &net->tv_nm1, &net->tv_delta); 53 | net->tv_nm1 = net->tv_n; 54 | 55 | uint64_t ibytes_nm1 = net->data.ifmd_data.ifi_ibytes; 56 | uint64_t obytes_nm1 = net->data.ifmd_data.ifi_obytes; 57 | ifdata(net->row, &net->data); 58 | 59 | double time_scale = (net->tv_delta.tv_sec + 1e-6*net->tv_delta.tv_usec); 60 | if (time_scale < 1e-6 || time_scale > 1e2) return; 61 | double delta_ibytes = (double)(net->data.ifmd_data.ifi_ibytes - ibytes_nm1) 62 | / time_scale; 63 | double delta_obytes = (double)(net->data.ifmd_data.ifi_obytes - obytes_nm1) 64 | / time_scale; 65 | 66 | double exponent_ibytes = log10(delta_ibytes); 67 | double exponent_obytes = log10(delta_obytes); 68 | 69 | if (exponent_ibytes < 3) { 70 | net->down_unit = UNIT_BPS; 71 | net->down = delta_ibytes; 72 | } else if (exponent_ibytes < 6) { 73 | net->down_unit = UNIT_KBPS; 74 | net->down = delta_ibytes / 1000.0; 75 | } else if (exponent_ibytes < 9) { 76 | net->down_unit = UNIT_MBPS; 77 | net->down = delta_ibytes / 1000000.0; 78 | } 79 | 80 | if (exponent_obytes < 3) { 81 | net->up_unit = UNIT_BPS; 82 | net->up = delta_obytes; 83 | } else if (exponent_obytes < 6) { 84 | net->up_unit = UNIT_KBPS; 85 | net->up = delta_obytes / 1000.0; 86 | } else if (exponent_obytes < 9) { 87 | net->up_unit = UNIT_MBPS; 88 | net->up = delta_obytes / 1000000.0; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /helpers/event_providers/network_load/network_load.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "network.h" 3 | #include "../sketchybar.h" 4 | 5 | int main (int argc, char** argv) { 6 | float update_freq; 7 | if (argc < 4 || (sscanf(argv[3], "%f", &update_freq) != 1)) { 8 | printf("Usage: %s \"\" \"\" \"\"\n", argv[0]); 9 | exit(1); 10 | } 11 | 12 | alarm(0); 13 | // Setup the event in sketchybar 14 | char event_message[512]; 15 | snprintf(event_message, 512, "--add event '%s'", argv[2]); 16 | sketchybar(event_message); 17 | 18 | struct network network; 19 | network_init(&network, argv[1]); 20 | char trigger_message[512]; 21 | for (;;) { 22 | // Acquire new info 23 | network_update(&network); 24 | 25 | // Prepare the event message 26 | snprintf(trigger_message, 27 | 512, 28 | "--trigger '%s' upload='%03d%s' download='%03d%s'", 29 | argv[2], 30 | network.up, 31 | unit_str[network.up_unit], 32 | network.down, 33 | unit_str[network.down_unit]); 34 | 35 | // Trigger the event 36 | sketchybar(trigger_message); 37 | 38 | // Wait 39 | usleep(update_freq * 1000000); 40 | } 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /helpers/event_providers/sketchybar.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | typedef char* env; 13 | 14 | #define MACH_HANDLER(name) void name(env env) 15 | typedef MACH_HANDLER(mach_handler); 16 | 17 | struct mach_message { 18 | mach_msg_header_t header; 19 | mach_msg_size_t msgh_descriptor_count; 20 | mach_msg_ool_descriptor_t descriptor; 21 | }; 22 | 23 | struct mach_buffer { 24 | struct mach_message message; 25 | mach_msg_trailer_t trailer; 26 | }; 27 | 28 | static mach_port_t g_mach_port = 0; 29 | 30 | static inline mach_port_t mach_get_bs_port() { 31 | mach_port_name_t task = mach_task_self(); 32 | 33 | mach_port_t bs_port; 34 | if (task_get_special_port(task, 35 | TASK_BOOTSTRAP_PORT, 36 | &bs_port ) != KERN_SUCCESS) { 37 | return 0; 38 | } 39 | 40 | char* name = getenv("BAR_NAME"); 41 | if (!name) name = "sketchybar"; 42 | uint32_t lookup_len = 16 + strlen(name); 43 | 44 | char buffer[lookup_len]; 45 | snprintf(buffer, lookup_len, "git.felix.%s", name); 46 | 47 | mach_port_t port; 48 | if (bootstrap_look_up(bs_port, buffer, &port) != KERN_SUCCESS) return 0; 49 | return port; 50 | } 51 | 52 | static inline bool mach_send_message(mach_port_t port, char* message, uint32_t len) { 53 | if (!message || !port) { 54 | return false; 55 | } 56 | 57 | struct mach_message msg = { 0 }; 58 | msg.header.msgh_remote_port = port; 59 | msg.header.msgh_local_port = 0; 60 | msg.header.msgh_id = 0; 61 | msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, 62 | MACH_MSG_TYPE_MAKE_SEND, 63 | 0, 64 | MACH_MSGH_BITS_COMPLEX ); 65 | 66 | msg.header.msgh_size = sizeof(struct mach_message); 67 | msg.msgh_descriptor_count = 1; 68 | msg.descriptor.address = message; 69 | msg.descriptor.size = len * sizeof(char); 70 | msg.descriptor.copy = MACH_MSG_VIRTUAL_COPY; 71 | msg.descriptor.deallocate = false; 72 | msg.descriptor.type = MACH_MSG_OOL_DESCRIPTOR; 73 | 74 | kern_return_t err = mach_msg(&msg.header, 75 | MACH_SEND_MSG, 76 | sizeof(struct mach_message), 77 | 0, 78 | MACH_PORT_NULL, 79 | MACH_MSG_TIMEOUT_NONE, 80 | MACH_PORT_NULL ); 81 | 82 | return err == KERN_SUCCESS; 83 | } 84 | 85 | static inline uint32_t format_message(char* message, char* formatted_message) { 86 | // This is not actually robust, switch to stack based messaging. 87 | char outer_quote = 0; 88 | uint32_t caret = 0; 89 | uint32_t message_length = strlen(message) + 1; 90 | for (int i = 0; i < message_length; ++i) { 91 | if (message[i] == '"' || message[i] == '\'') { 92 | if (outer_quote && outer_quote == message[i]) outer_quote = 0; 93 | else if (!outer_quote) outer_quote = message[i]; 94 | continue; 95 | } 96 | formatted_message[caret] = message[i]; 97 | if (message[i] == ' ' && !outer_quote) formatted_message[caret] = '\0'; 98 | caret++; 99 | } 100 | 101 | if (caret > 0 && formatted_message[caret] == '\0' 102 | && formatted_message[caret - 1] == '\0') { 103 | caret--; 104 | } 105 | formatted_message[caret] = '\0'; 106 | return caret + 1; 107 | } 108 | 109 | static inline void sketchybar(char* message) { 110 | char formatted_message[strlen(message) + 2]; 111 | uint32_t length = format_message(message, formatted_message); 112 | if (!length) return; 113 | 114 | if (!g_mach_port) g_mach_port = mach_get_bs_port(); 115 | if (!mach_send_message(g_mach_port, formatted_message, length)) { 116 | g_mach_port = mach_get_bs_port(); 117 | if (!mach_send_message(g_mach_port, formatted_message, length)) { 118 | // No sketchybar instance running, exit. 119 | exit(0); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /helpers/init.lua: -------------------------------------------------------------------------------- 1 | -- Add the sketchybar module to the package cpath 2 | package.cpath = package.cpath .. ";/Users/" .. os.getenv("USER") .. "/.local/share/sketchybar_lua/?.so" 3 | 4 | os.execute("(cd helpers && make)") 5 | -------------------------------------------------------------------------------- /helpers/install.sh: -------------------------------------------------------------------------------- 1 | # Packages 2 | brew install lua 3 | brew install switchaudio-osx 4 | brew install nowplaying-cli 5 | 6 | brew tap FelixKratz/formulae 7 | brew install sketchybar 8 | 9 | # Fonts 10 | brew install --cask sf-symbols 11 | brew install --cask homebrew/cask-fonts/font-sf-mono 12 | brew install --cask homebrew/cask-fonts/font-sf-pro 13 | 14 | curl -L https://github.com/kvndrsslr/sketchybar-app-font/releases/download/v2.0.5/sketchybar-app-font.ttf -o $HOME/Library/Fonts/sketchybar-app-font.ttf 15 | 16 | # SbarLua 17 | (git clone https://github.com/FelixKratz/SbarLua.git /tmp/SbarLua && cd /tmp/SbarLua/ && make install && rm -rf /tmp/SbarLua/) 18 | -------------------------------------------------------------------------------- /helpers/makefile: -------------------------------------------------------------------------------- 1 | all: 2 | (cd event_providers && $(MAKE)) >/dev/null 3 | (cd menus && $(MAKE)) >/dev/null 4 | -------------------------------------------------------------------------------- /helpers/menus/makefile: -------------------------------------------------------------------------------- 1 | bin/menus: menus.c | bin 2 | clang -std=c99 -O3 -F/System/Library/PrivateFrameworks/ -framework Carbon -framework SkyLight $< -o $@ 3 | 4 | bin: 5 | mkdir bin 6 | -------------------------------------------------------------------------------- /helpers/menus/menus.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | void ax_init() { 4 | const void *keys[] = { kAXTrustedCheckOptionPrompt }; 5 | const void *values[] = { kCFBooleanTrue }; 6 | 7 | CFDictionaryRef options; 8 | options = CFDictionaryCreate(kCFAllocatorDefault, 9 | keys, 10 | values, 11 | sizeof(keys) / sizeof(*keys), 12 | &kCFCopyStringDictionaryKeyCallBacks, 13 | &kCFTypeDictionaryValueCallBacks ); 14 | 15 | bool trusted = AXIsProcessTrustedWithOptions(options); 16 | CFRelease(options); 17 | if (!trusted) exit(1); 18 | } 19 | 20 | void ax_perform_click(AXUIElementRef element) { 21 | if (!element) return; 22 | AXUIElementPerformAction(element, kAXCancelAction); 23 | usleep(150000); 24 | AXUIElementPerformAction(element, kAXPressAction); 25 | } 26 | 27 | CFStringRef ax_get_title(AXUIElementRef element) { 28 | CFTypeRef title = NULL; 29 | AXError error = AXUIElementCopyAttributeValue(element, 30 | kAXTitleAttribute, 31 | &title ); 32 | 33 | if (error != kAXErrorSuccess) return NULL; 34 | return title; 35 | } 36 | 37 | void ax_select_menu_option(AXUIElementRef app, int id) { 38 | AXUIElementRef menubars_ref = NULL; 39 | CFArrayRef children_ref = NULL; 40 | 41 | AXError error = AXUIElementCopyAttributeValue(app, 42 | kAXMenuBarAttribute, 43 | (CFTypeRef*)&menubars_ref); 44 | if (error == kAXErrorSuccess) { 45 | error = AXUIElementCopyAttributeValue(menubars_ref, 46 | kAXVisibleChildrenAttribute, 47 | (CFTypeRef*)&children_ref ); 48 | 49 | if (error == kAXErrorSuccess) { 50 | uint32_t count = CFArrayGetCount(children_ref); 51 | if (id < count) { 52 | AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, id); 53 | ax_perform_click(item); 54 | } 55 | if (children_ref) CFRelease(children_ref); 56 | } 57 | if (menubars_ref) CFRelease(menubars_ref); 58 | } 59 | } 60 | 61 | void ax_print_menu_options(AXUIElementRef app) { 62 | AXUIElementRef menubars_ref = NULL; 63 | CFTypeRef menubar = NULL; 64 | CFArrayRef children_ref = NULL; 65 | 66 | AXError error = AXUIElementCopyAttributeValue(app, 67 | kAXMenuBarAttribute, 68 | (CFTypeRef*)&menubars_ref); 69 | if (error == kAXErrorSuccess) { 70 | error = AXUIElementCopyAttributeValue(menubars_ref, 71 | kAXVisibleChildrenAttribute, 72 | (CFTypeRef*)&children_ref ); 73 | 74 | if (error == kAXErrorSuccess) { 75 | uint32_t count = CFArrayGetCount(children_ref); 76 | 77 | for (int i = 1; i < count; i++) { 78 | AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i); 79 | CFTypeRef title = ax_get_title(item); 80 | 81 | if (title) { 82 | uint32_t buffer_len = 2*CFStringGetLength(title); 83 | char buffer[2*CFStringGetLength(title)]; 84 | CFStringGetCString(title, buffer, buffer_len, kCFStringEncodingUTF8); 85 | printf("%s\n", buffer); 86 | CFRelease(title); 87 | } 88 | } 89 | } 90 | if (menubars_ref) CFRelease(menubars_ref); 91 | if (children_ref) CFRelease(children_ref); 92 | } 93 | } 94 | 95 | AXUIElementRef ax_get_extra_menu_item(char* alias) { 96 | pid_t pid = 0; 97 | CGRect bounds = CGRectNull; 98 | CFArrayRef window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, 99 | kCGNullWindowID ); 100 | char owner_buffer[256]; 101 | char name_buffer[256]; 102 | char buffer[512]; 103 | int window_count = CFArrayGetCount(window_list); 104 | for (int i = 0; i < window_count; ++i) { 105 | CFDictionaryRef dictionary = CFArrayGetValueAtIndex(window_list, i); 106 | if (!dictionary) continue; 107 | 108 | CFStringRef owner_ref = CFDictionaryGetValue(dictionary, 109 | kCGWindowOwnerName); 110 | 111 | CFNumberRef owner_pid_ref = CFDictionaryGetValue(dictionary, 112 | kCGWindowOwnerPID); 113 | 114 | CFStringRef name_ref = CFDictionaryGetValue(dictionary, kCGWindowName); 115 | CFNumberRef layer_ref = CFDictionaryGetValue(dictionary, kCGWindowLayer); 116 | CFDictionaryRef bounds_ref = CFDictionaryGetValue(dictionary, 117 | kCGWindowBounds); 118 | 119 | if (!name_ref || !owner_ref || !owner_pid_ref || !layer_ref || !bounds_ref) 120 | continue; 121 | 122 | long long int layer = 0; 123 | CFNumberGetValue(layer_ref, CFNumberGetType(layer_ref), &layer); 124 | uint64_t owner_pid = 0; 125 | CFNumberGetValue(owner_pid_ref, 126 | CFNumberGetType(owner_pid_ref), 127 | &owner_pid ); 128 | 129 | if (layer != 0x19) continue; 130 | bounds = CGRectNull; 131 | if (!CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) continue; 132 | CFStringGetCString(owner_ref, 133 | owner_buffer, 134 | sizeof(owner_buffer), 135 | kCFStringEncodingUTF8); 136 | 137 | CFStringGetCString(name_ref, 138 | name_buffer, 139 | sizeof(name_buffer), 140 | kCFStringEncodingUTF8); 141 | snprintf(buffer, sizeof(buffer), "%s,%s", owner_buffer, name_buffer); 142 | 143 | if (strcmp(buffer, alias) == 0) { 144 | pid = owner_pid; 145 | break; 146 | } 147 | } 148 | CFRelease(window_list); 149 | if (!pid) return NULL; 150 | 151 | AXUIElementRef app = AXUIElementCreateApplication(pid); 152 | if (!app) return NULL; 153 | AXUIElementRef result = NULL; 154 | CFTypeRef extras = NULL; 155 | CFArrayRef children_ref = NULL; 156 | AXError error = AXUIElementCopyAttributeValue(app, 157 | kAXExtrasMenuBarAttribute, 158 | &extras ); 159 | if (error == kAXErrorSuccess) { 160 | error = AXUIElementCopyAttributeValue(extras, 161 | kAXVisibleChildrenAttribute, 162 | (CFTypeRef*)&children_ref ); 163 | 164 | if (error == kAXErrorSuccess) { 165 | uint32_t count = CFArrayGetCount(children_ref); 166 | for (uint32_t i = 0; i < count; i++) { 167 | AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i); 168 | CFTypeRef position_ref = NULL; 169 | CFTypeRef size_ref = NULL; 170 | AXUIElementCopyAttributeValue(item, kAXPositionAttribute, 171 | &position_ref ); 172 | AXUIElementCopyAttributeValue(item, kAXSizeAttribute, 173 | &size_ref ); 174 | if (!position_ref || !size_ref) continue; 175 | 176 | CGPoint position = CGPointZero; 177 | AXValueGetValue(position_ref, kAXValueCGPointType, &position); 178 | CGSize size = CGSizeZero; 179 | AXValueGetValue(size_ref, kAXValueCGSizeType, &size); 180 | CFRelease(position_ref); 181 | CFRelease(size_ref); 182 | // The offset is exactly 8 on macOS Sonoma... 183 | // printf("%f %f\n", position.x, bounds.origin.x); 184 | if (error == kAXErrorSuccess 185 | && fabs(position.x - bounds.origin.x) <= 10) { 186 | result = item; 187 | break; 188 | } 189 | } 190 | } 191 | } 192 | 193 | CFRelease(app); 194 | return result; 195 | } 196 | 197 | extern int SLSMainConnectionID(); 198 | extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled); 199 | extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled); 200 | extern void SLSSetMenuBarInsetAndAlpha(int cid, double u1, double u2, float alpha); 201 | void ax_select_menu_extra(char* alias) { 202 | AXUIElementRef item = ax_get_extra_menu_item(alias); 203 | if (!item) return; 204 | SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0); 205 | SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, true); 206 | SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0); 207 | ax_perform_click(item); 208 | SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, false); 209 | SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 1.0); 210 | CFRelease(item); 211 | } 212 | 213 | extern void _SLPSGetFrontProcess(ProcessSerialNumber* psn); 214 | extern void SLSGetConnectionIDForPSN(int cid, ProcessSerialNumber* psn, int* cid_out); 215 | extern void SLSConnectionGetPID(int cid, pid_t* pid_out); 216 | AXUIElementRef ax_get_front_app() { 217 | ProcessSerialNumber psn; 218 | _SLPSGetFrontProcess(&psn); 219 | int target_cid; 220 | SLSGetConnectionIDForPSN(SLSMainConnectionID(), &psn, &target_cid); 221 | 222 | pid_t pid; 223 | SLSConnectionGetPID(target_cid, &pid); 224 | return AXUIElementCreateApplication(pid); 225 | } 226 | 227 | int main (int argc, char **argv) { 228 | if (argc == 1) { 229 | printf("Usage: %s [-l | -s id/alias ]\n", argv[0]); 230 | exit(0); 231 | } 232 | ax_init(); 233 | if (strcmp(argv[1], "-l") == 0) { 234 | AXUIElementRef app = ax_get_front_app(); 235 | if (!app) return 1; 236 | ax_print_menu_options(app); 237 | CFRelease(app); 238 | } else if (argc == 3 && strcmp(argv[1], "-s") == 0) { 239 | int id = 0; 240 | if (sscanf(argv[2], "%d", &id) == 1) { 241 | AXUIElementRef app = ax_get_front_app(); 242 | if (!app) return 1; 243 | ax_select_menu_option(app, id); 244 | CFRelease(app); 245 | } else ax_select_menu_extra(argv[2]); 246 | } 247 | return 0; 248 | } 249 | -------------------------------------------------------------------------------- /icons.lua: -------------------------------------------------------------------------------- 1 | local settings = require("settings") 2 | 3 | local icons = { 4 | sf_symbols = { 5 | plus = "􀅼", 6 | loading = "􀖇", 7 | apple = "􂮢", 8 | gear = "􀍟", 9 | cpu = "􀫥", 10 | memory = "􀫦", 11 | hdd = "􀥾", 12 | brew = "􀐚", 13 | mail = "􀍕", 14 | messages = "􀌤", 15 | clipboard = "􀉄", 16 | 17 | switch = { 18 | on = "􁏮", 19 | off = "􁏯", 20 | }, 21 | volume = { 22 | _100="􀊩", 23 | _66="􀊧", 24 | _33="􀊥", 25 | _10="􀊡", 26 | _0="􀊣", 27 | }, 28 | battery = { 29 | _100 = "􀛨", 30 | _75 = "􀺸", 31 | _50 = "􀺶", 32 | _25 = "􀛩", 33 | _0 = "􀛪", 34 | charging = "􀢋" 35 | }, 36 | wifi = { 37 | upload = "􀄨", 38 | download = "􀄩", 39 | connected = "􀙇", 40 | disconnected = "􀙈", 41 | router = "􁓤", 42 | }, 43 | media = { 44 | back = "􀊊", 45 | forward = "􀊌", 46 | play_pause = "􀊈", 47 | }, 48 | }, 49 | 50 | -- Alternative NerdFont icons 51 | nerdfont = { 52 | plus = "", 53 | loading = "", 54 | apple = "􀝶", 55 | gear = "", 56 | cpu = "", 57 | clipboard = "Missing Icon", 58 | 59 | switch = { 60 | on = "󱨥", 61 | off = "󱨦", 62 | }, 63 | volume = { 64 | _100="", 65 | _66="", 66 | _33="", 67 | _10="", 68 | _0="", 69 | }, 70 | battery = { 71 | _100 = "", 72 | _75 = "", 73 | _50 = "", 74 | _25 = "", 75 | _0 = "", 76 | charging = "" 77 | }, 78 | wifi = { 79 | upload = "", 80 | download = "", 81 | connected = "󰖩", 82 | disconnected = "󰖪", 83 | router = "Missing Icon" 84 | }, 85 | media = { 86 | back = "", 87 | forward = "", 88 | play_pause = "", 89 | }, 90 | }, 91 | } 92 | 93 | if not (settings.icons == "NerdFont") then 94 | return icons.sf_symbols 95 | else 96 | return icons.nerdfont 97 | end 98 | -------------------------------------------------------------------------------- /init.lua: -------------------------------------------------------------------------------- 1 | -- Require the sketchybar module 2 | sbar = require("sketchybar") 3 | 4 | -- Set the bar name, if you are using another bar instance than sketchybar 5 | -- sbar.set_bar_name("bottom_bar") 6 | 7 | -- Bundle the entire initial configuration into a single message to sketchybar 8 | sbar.begin_config() 9 | require("bar") 10 | require("default") 11 | require("items") 12 | sbar.end_config() 13 | 14 | -- Run the event loop of the sketchybar module (without this there will be no 15 | -- callback functions executed in the lua module) 16 | sbar.event_loop() 17 | -------------------------------------------------------------------------------- /items/aerospace.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local icons = require("icons") 3 | local settings = require("settings") 4 | local app_icons = require("helpers.app_icons") 5 | 6 | local item_order = "" 7 | local spaces_by_name = {} 8 | 9 | local function getAppIcon(app_name) 10 | return app_icons[app_name] or app_icons["default"] 11 | end 12 | 13 | -- Simple JSON array parser for our specific case 14 | local function parse_workspace_json(json_str) 15 | if not json_str then return {} end 16 | if type(json_str) == "table" then return json_str end 17 | 18 | local spaces = {} 19 | -- Remove brackets and split by commas 20 | local items = json_str:gsub("^%[", ""):gsub("%]$", ""):gsub("%s+", "") 21 | for item in items:gmatch("{[^}]+}") do 22 | local workspace = item:match('"workspace"%s*:%s*"([^"]+)"') 23 | local monitor_id = item:match('"monitor%-id"%s*:%s*(%d+)') 24 | if workspace and monitor_id then 25 | table.insert(spaces, { 26 | workspace = workspace, 27 | ["monitor-id"] = tonumber(monitor_id) 28 | }) 29 | end 30 | end 31 | return spaces 32 | end 33 | 34 | local function get_workspace_icon(workspace_name) 35 | -- Extract just the workspace name without any prefix 36 | local name = workspace_name:match("[^/]+$") or workspace_name 37 | return workspace_icons[name] or "󰆮" 38 | end 39 | 40 | sbar.exec("aerospace list-workspaces --all --format '%{workspace}%{monitor-id}' --json", function(spaces_json) 41 | local spaces = parse_workspace_json(spaces_json) 42 | 43 | -- Get all visible workspaces 44 | sbar.exec("aerospace list-workspaces --monitor all --visible", function(visible_workspaces) 45 | -- Create a set of visible workspaces for quick lookup 46 | local visible_set = {} 47 | for workspace in visible_workspaces:gmatch("[^\r\n]+") do 48 | visible_set[workspace] = true 49 | end 50 | 51 | -- Group spaces by monitor 52 | local monitors = {} 53 | for _, space_info in ipairs(spaces) do 54 | local monitor_id = space_info["monitor-id"] 55 | monitors[monitor_id] = monitors[monitor_id] or {} 56 | table.insert(monitors[monitor_id], space_info.workspace) 57 | end 58 | 59 | for monitor_id, monitor_spaces in pairs(monitors) do 60 | for _, space_name in ipairs(monitor_spaces) do 61 | local space = sbar.add("item", "space." .. space_name, { 62 | icon = { 63 | drawing = false, 64 | }, 65 | label = { 66 | drawing = true, 67 | string = space_name:match("[^/]+$") or space_name, 68 | color = visible_set[space_name] and colors.black or colors.white, 69 | font = { 70 | style = settings.font.style_map["SemiBold"], 71 | size = 12.0, 72 | }, 73 | padding_right = 10, 74 | padding_left = 10 75 | }, 76 | padding_right = 1, 77 | padding_left = 1, 78 | background = { 79 | color = visible_set[space_name] and colors.spaces.active or colors.bg1, 80 | border_width = 0, 81 | height = 26, 82 | }, 83 | associated_display = monitor_id 84 | }) 85 | 86 | local space_bracket = sbar.add("bracket", { space.name }, { 87 | background = { 88 | color = colors.transparent, 89 | border_color = colors.bg2, 90 | height = 28, 91 | border_width = 0 92 | } 93 | }) 94 | 95 | -- Padding space 96 | local space_padding = sbar.add("item", "space.padding." .. space_name, { 97 | script = "", 98 | width = settings.group_paddings, 99 | associated_display = monitor_id 100 | }) 101 | 102 | space:subscribe("aerospace_workspace_change", function(env) 103 | -- Get current visible workspaces after change 104 | sbar.exec("aerospace list-workspaces --monitor all --visible", function(visible_workspaces) 105 | local visible_set = {} 106 | for workspace in visible_workspaces:gmatch("[^\r\n]+") do 107 | visible_set[workspace] = true 108 | end 109 | 110 | space:set({ 111 | icon = { color = colors.white }, 112 | label = { 113 | drawing = true, 114 | color = visible_set[space_name] and colors.black or colors.white, 115 | }, 116 | background = { color = visible_set[space_name] and colors.spaces.active or colors.bg1, } 117 | }) 118 | end) 119 | end) 120 | 121 | space:subscribe("mouse.clicked", function() 122 | sbar.exec("aerospace workspace " .. space_name) 123 | end) 124 | 125 | item_order = item_order .. " " .. space.name .. " " .. space_padding.name 126 | end 127 | end 128 | sbar.exec("sketchybar --reorder " .. item_order .. " front_app menus") 129 | end) 130 | end) -------------------------------------------------------------------------------- /items/apple.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | 4 | -- Convert color to hex string 5 | local function to_hex(color) 6 | -- Assuming color is in format 0xAARRGGBB 7 | return string.format("%08x", color) 8 | end 9 | 10 | -- Create the Apple menu item with a specific name 11 | local apple = sbar.add("item", "apple.logo", { 12 | position = "left", 13 | icon = { 14 | string = icons.apple, 15 | font = { 16 | family = "SF Pro", 17 | style = "SemiBold", 18 | size = 15.0 19 | }, 20 | color = colors.red, 21 | padding_left = 8, 22 | padding_right = 8, 23 | }, 24 | label = { drawing = false }, 25 | }) 26 | 27 | -- Track menu visibility 28 | local menu_visible = false 29 | local menu_process = nil 30 | 31 | -- Functions to handle menu visibility 32 | local function show_menu() 33 | if not menu_visible then 34 | -- Kill any existing menu process 35 | if menu_process then 36 | sbar.exec("pkill -f apple_menu") 37 | menu_process = nil 38 | end 39 | 40 | -- Start new menu process 41 | sbar.exec("~/.config/sketchybar/helpers/event_providers/apple_menu/bin/apple_menu app=menu &") 42 | menu_visible = true 43 | end 44 | end 45 | 46 | local function hide_menu() 47 | if menu_visible then 48 | sbar.exec("pkill -f apple_menu") 49 | menu_visible = false 50 | menu_process = nil 51 | end 52 | end 53 | 54 | -- Toggle menu on click only 55 | apple:subscribe("mouse.clicked", function(env) 56 | if menu_visible then 57 | hide_menu() 58 | else 59 | show_menu() 60 | end 61 | end) 62 | 63 | -- Add mouse.exited to hide menu when mouse leaves 64 | -- apple:subscribe("mouse.exited", function(env) 65 | -- -- Add a small delay to allow clicking inside the menu 66 | -- sbar.delay(0.5, function() 67 | -- hide_menu() 68 | -- end) 69 | -- end) 70 | 71 | -- Add this to prevent window from closing when clicking inside it 72 | apple:subscribe("mouse.clicked.inside", function(env) 73 | return 74 | end) -------------------------------------------------------------------------------- /items/calendar.lua: -------------------------------------------------------------------------------- 1 | local settings = require("settings") 2 | local colors = require("colors") 3 | 4 | -- Convert color to hex string 5 | local function to_hex(color) 6 | -- Assuming color is in format 0xAARRGGBB 7 | return string.format("%08x", color) 8 | end 9 | 10 | -- Padding item required because of bracket 11 | -- sbar.add("item", { position = "right", width = settings.group_paddings }) 12 | 13 | local time = sbar.add("item", "time", { 14 | icon = { 15 | drawing = false, 16 | color = colors.white, 17 | padding_left = 8, 18 | font = { 19 | style = settings.font.style_map["Black"], 20 | size = 12.0, 21 | }, 22 | }, 23 | label = { 24 | color = colors.red, 25 | padding_right = 12, 26 | padding_left = 12, 27 | align = "right", 28 | font = { family = settings.font.numbers }, 29 | }, 30 | position = "right", 31 | update_freq = 30, 32 | padding_left = 1, 33 | padding_right = 1, 34 | background = { 35 | color = colors.transparent, 36 | border_width = 0, 37 | corner_radius = 5, 38 | padding_right = 0 39 | }, 40 | }) 41 | 42 | local date = sbar.add("item", "date", { 43 | icon = { 44 | drawing = false, 45 | color = colors.white, 46 | padding_left = 8, 47 | font = { 48 | style = settings.font.style_map["Black"], 49 | size = 12.0, 50 | }, 51 | }, 52 | label = { 53 | color = colors.white, 54 | padding_right = 1, 55 | padding_left = 12, 56 | align = "right", 57 | font = { family = settings.font.numbers }, 58 | }, 59 | position = "right", 60 | update_freq = 30, 61 | padding_left = 1, 62 | padding_right = 1, 63 | background = { 64 | color = colors.transparent, 65 | border_width = 0, 66 | corner_radius = 0, 67 | padding_right = 0 68 | }, 69 | }) 70 | 71 | sbar.add("bracket", "datetime", { date.name, time.name }, { 72 | background = { 73 | color = colors.transparent 74 | }, 75 | padding_right = 0 76 | }) 77 | 78 | -- -- Padding item required because of bracket 79 | -- sbar.add("item", { position = "right", width = 2 }) 80 | 81 | -- Subscribe to update the time and date 82 | date:subscribe({ "forced", "routine", "system_woke" }, function(env) 83 | date:set({ label = os.date("%a %b %d") }) 84 | end) 85 | 86 | time:subscribe({ "forced", "routine", "system_woke" }, function(env) 87 | time:set({ label = os.date("%H:%M") }) 88 | end) 89 | 90 | -- Track menu visibility 91 | local menu_visible = false 92 | 93 | -- Functions to handle menu visibility 94 | local function toggle_menu() 95 | if menu_visible then 96 | menu_visible = false 97 | else 98 | sbar.exec("~/.config/sketchybar/helpers/event_providers/apple_menu/bin/apple_menu app=date") 99 | menu_visible = true 100 | end 101 | end 102 | 103 | -- Add click handlers for both time and date 104 | time:subscribe("mouse.clicked", function(env) 105 | toggle_menu() 106 | end) 107 | 108 | date:subscribe("mouse.clicked", function(env) 109 | toggle_menu() 110 | end) 111 | 112 | time:subscribe("mouse.entered", function(env) 113 | toggle_menu() 114 | end) 115 | 116 | date:subscribe("mouse.entered", function(env) 117 | toggle_menu() 118 | end) 119 | 120 | -- Handle window closing when clicking outside 121 | time:subscribe("mouse.clicked.outside", function(env) 122 | if menu_visible then 123 | sbar.exec("pkill -SIGUSR1 apple_menu") 124 | menu_visible = false 125 | end 126 | end) 127 | 128 | date:subscribe("mouse.clicked.outside", function(env) 129 | if menu_visible then 130 | sbar.exec("pkill -SIGUSR1 apple_menu") 131 | menu_visible = false 132 | end 133 | end) 134 | 135 | -- Prevent window from closing when clicking inside 136 | time:subscribe("mouse.clicked.inside", function(env) 137 | return 138 | end) 139 | 140 | date:subscribe("mouse.clicked.inside", function(env) 141 | return 142 | end) -------------------------------------------------------------------------------- /items/front_app.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local settings = require("settings") 3 | local app_icons = require("helpers.app_icons") 4 | 5 | local space_id = sbar.exec("aerospace list-workspaces --focused") 6 | 7 | -- Create the front app item 8 | local front_app = sbar.add("item", "front_app", { 9 | label = { 10 | drawing = true, 11 | color = colors.white, 12 | font = { 13 | family = settings.font.text, 14 | style = settings.font.style_map["Semibold"], 15 | size = 12, 16 | }, 17 | padding_right = 10, 18 | }, 19 | icon = { 20 | background = { 21 | drawing = true, 22 | image = { 23 | scale = 0.75, 24 | padding_right = settings.paddings, 25 | padding_left = settings.paddings 26 | } 27 | } 28 | }, 29 | background = { 30 | color = colors.bg1, 31 | border_width = 0, 32 | height = 26, 33 | }, 34 | updates = true, 35 | }) 36 | 37 | -- Event: Front app switched 38 | front_app:subscribe("front_app_switched", function(env) 39 | sbar.exec("aerospace list-windows --focused --format '%{monitor-id}'", function(monitor_id, exit_code) 40 | front_app:set({ 41 | icon = { 42 | background = { 43 | image = "app." .. env.INFO, 44 | }, 45 | }, 46 | label = { 47 | drawing = true, 48 | string = env.INFO, 49 | }, 50 | display = monitor_id 51 | }) 52 | end) 53 | end) 54 | 55 | return front_app -------------------------------------------------------------------------------- /items/init.lua: -------------------------------------------------------------------------------- 1 | -- Left items 2 | require("items.apple") 3 | -- require("items.spaces") 4 | require("items.front_app") 5 | require("items.menus") 6 | require("items.aerospace") 7 | 8 | -- Right items 9 | -- require("items.profile") 10 | require("items.calendar") 11 | require("items.widgets") 12 | -- require("items.media") 13 | -------------------------------------------------------------------------------- /items/media.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | 4 | local whitelist = { 5 | ["Spotify"] = true, 6 | ["Brave Browser"] = true, 7 | } 8 | 9 | local media_cover = sbar.add("item", "media.cover", { 10 | position = "right", 11 | background = { 12 | image = { 13 | string = "media.artwork", 14 | scale = 0.75 15 | }, 16 | color = colors.transparent, 17 | }, 18 | label = { drawing = false }, 19 | icon = { drawing = false }, 20 | drawing = false, 21 | updates = true, 22 | name = "media.cover", 23 | }) 24 | 25 | local media_artist = sbar.add("item", { 26 | position = "right", 27 | drawing = false, 28 | padding_left = 3, 29 | padding_right = 0, 30 | width = 0, 31 | icon = { drawing = false }, 32 | label = { 33 | width = 0, 34 | font = { size = 9 }, 35 | color = colors.with_alpha(colors.white, 0.6), 36 | max_chars = 18, 37 | y_offset = 6, 38 | }, 39 | }) 40 | 41 | local media_title = sbar.add("item", { 42 | position = "right", 43 | drawing = false, 44 | padding_left = 3, 45 | padding_right = 0, 46 | icon = { drawing = false }, 47 | label = { 48 | font = { size = 11 }, 49 | width = 0, 50 | max_chars = 16, 51 | y_offset = -5, 52 | }, 53 | }) 54 | 55 | local interrupt = 0 56 | local function animate_detail(detail) 57 | -- Don't show details if player is visible 58 | if player_visible then return end 59 | 60 | if (not detail) then interrupt = interrupt - 1 end 61 | if interrupt > 0 and (not detail) then return end 62 | 63 | sbar.animate("tanh", 30, function() 64 | media_artist:set({ label = { width = detail and "dynamic" or 0 } }) 65 | media_title:set({ label = { width = detail and "dynamic" or 0 } }) 66 | end) 67 | end 68 | 69 | media_cover:subscribe("media_change", function(env) 70 | if whitelist[env.INFO.app] then 71 | local drawing = (env.INFO.artist ~= "" or env.INFO.title ~= "") 72 | media_artist:set({ drawing = drawing, label = env.INFO.artist, }) 73 | media_title:set({ drawing = drawing, label = env.INFO.title, }) 74 | media_cover:set({ 75 | drawing = drawing, 76 | background = { 77 | image = { 78 | string = "media.artwork", 79 | scale = 0.75 80 | } 81 | } 82 | }) 83 | 84 | -- Only show details if player isn't visible 85 | if drawing and not player_visible then 86 | animate_detail(true) 87 | interrupt = interrupt + 1 88 | sbar.delay(5, animate_detail) 89 | end 90 | end 91 | end) 92 | 93 | local player_visible = false 94 | 95 | -- Add a function to handle player visibility 96 | local function show_player() 97 | if not player_visible then 98 | sbar.exec("~/.config/sketchybar/helpers/event_providers/media_player/bin/media_player") 99 | player_visible = true 100 | end 101 | end 102 | 103 | local function hide_player() 104 | if player_visible then 105 | sbar.exec("pkill -SIGUSR1 media_player") 106 | player_visible = false 107 | end 108 | end 109 | 110 | media_cover:subscribe("mouse.entered", function(env) 111 | interrupt = interrupt + 1 112 | animate_detail(true) 113 | show_player() 114 | end) 115 | 116 | media_cover:subscribe("mouse.exited", function(env) 117 | animate_detail(false) 118 | end) 119 | 120 | -- Keep click handler for closing 121 | media_cover:subscribe("mouse.clicked", function(env) 122 | if player_visible then 123 | hide_player() 124 | else 125 | show_player() 126 | end 127 | end) 128 | 129 | -- Add this to prevent window from closing when clicking inside it 130 | media_cover:subscribe("mouse.clicked.inside", function(env) 131 | return 132 | end) 133 | 134 | -- Add this to track when the player closes itself 135 | media_cover:subscribe("mouse.exited.global", function(env) 136 | player_visible = false 137 | end) 138 | -------------------------------------------------------------------------------- /items/menus.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local icons = require("icons") 3 | local settings = require("settings") 4 | local app_icons = require("helpers.app_icons") 5 | 6 | local function getAppIcon(app_name) 7 | return app_icons[app_name] or app_icons["default"] 8 | end 9 | 10 | local menu_watcher = sbar.add("item", { 11 | drawing = false, 12 | updates = false, 13 | }) 14 | 15 | local space_menu_swap = sbar.add("item", { 16 | drawing = false, 17 | updates = true, 18 | }) 19 | 20 | sbar.add("event", "swap_menus_and_spaces") 21 | 22 | local max_items = 15 23 | local menu_items = {} 24 | for i = 1, max_items do 25 | local menu = sbar.add("item", "menu." .. i, { 26 | padding_left = settings.paddings, 27 | padding_right = settings.paddings, 28 | drawing = false, 29 | icon = { drawing = false }, 30 | label = { 31 | padding_left = 6, 32 | padding_right = 6, 33 | }, 34 | click_script = "$CONFIG_DIR/helpers/menus/bin/menus -s " .. i, 35 | }) 36 | 37 | menu_items[i] = menu 38 | end 39 | 40 | sbar.add("bracket", { '/menu\\..*/' }, { 41 | background = { color = colors.bg1 } 42 | }) 43 | 44 | local menu_padding = sbar.add("item", "menu.padding", { 45 | drawing = false, 46 | width = 5 47 | }) 48 | 49 | local function update_menus(space_id) 50 | sbar.exec("$CONFIG_DIR/helpers/menus/bin/menus -l", function(menus) 51 | sbar.set('/menu\\..*/', { drawing = false }) 52 | menu_padding:set({ drawing = true }) 53 | 54 | local id = 1 55 | 56 | for menu in string.gmatch(menus, '[^\r\n]+') do 57 | local label = "" 58 | local icon = nil 59 | local icon_line = "" 60 | 61 | if id == 1 then 62 | menu_items[id]:set({ 63 | icon = { 64 | drawing = false, 65 | padding_left = 10, 66 | font = "sketchybar-app-font:Regular:12.0", 67 | string = getAppIcon(menu) 68 | }, 69 | label = { 70 | drawing = true, 71 | string = menu, 72 | color = colors.white, 73 | font = { 74 | style = settings.font.style_map["Bold"], 75 | size = 12.0, 76 | }, 77 | }, 78 | drawing = true, 79 | space = space_id, 80 | }) 81 | else 82 | label = menu 83 | if id <= max_items then 84 | menu_items[id]:set({ 85 | label = { 86 | string = label, 87 | color = colors.quicksilver, 88 | font = { 89 | style = settings.font.style_map["SemiBold"], 90 | size = 12.0, 91 | } 92 | }, 93 | drawing = true, 94 | space = space_id, 95 | }) 96 | else 97 | break 98 | end 99 | end 100 | 101 | id = id + 1 102 | end 103 | end) 104 | end 105 | 106 | 107 | menu_watcher:subscribe("front_app_switched", function() 108 | sbar.exec("yabai -m query --windows --window | jq -r '.space'", function(space_id, exit_code) 109 | update_menus(space_id) 110 | sbar.set("/menu\\..*/", { drawing = false }) -- Clear previous menu state 111 | sbar.set("/menu\\..*/", { drawing = true }) -- Show menus for the new space 112 | end) 113 | end) 114 | 115 | space_menu_swap:subscribe("swap_menus_and_spaces", function(env) 116 | local drawing = menu_items[1]:query().geometry.drawing == "on" 117 | if drawing then 118 | menu_watcher:set({ updates = false }) 119 | sbar.set("/menu\\..*/", { drawing = false }) 120 | else 121 | menu_watcher:set({ updates = true }) 122 | sbar.exec("yabai -m query --windows --window | jq -r '.space'", function(space_id, exit_code) 123 | update_menus(space_id) -- Update menus based on the active space 124 | sbar.set("/menu\\..*/", { drawing = true }) -- Show updated menus 125 | end) 126 | end 127 | end) 128 | 129 | return menu_watcher 130 | -------------------------------------------------------------------------------- /items/profile.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | local settings = require("settings") 4 | 5 | local current_user = os.getenv("USER") 6 | local profile_pic = string.format("/Users/%s/Pictures/profile.jpg", current_user) 7 | 8 | local profile = sbar.add("item", "widgets.profile", { 9 | position = "right", 10 | background = { 11 | image = { 12 | string = profile_pic, 13 | corner_radius = 8, 14 | scale = 0.12, 15 | drawing = true, 16 | }, 17 | drawing = true, 18 | padding_left = 15, 19 | padding_right = 5, 20 | }, 21 | icon = { 22 | drawing = false, 23 | }, 24 | label = { 25 | drawing = false, 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /items/spaces.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local icons = require("icons") 3 | local settings = require("settings") 4 | local app_icons = require("helpers.app_icons") 5 | 6 | local spaces = {} 7 | 8 | for i = 1, 10, 1 do 9 | local space = sbar.add("space", "space." .. i, { 10 | space = i, 11 | icon = { 12 | drawing = false, 13 | }, 14 | label = { 15 | padding_left = 2, 16 | padding_right = 0, 17 | color = colors.grey, 18 | highlight_color = colors.white, 19 | }, 20 | padding_right = 1, 21 | padding_left = 1, 22 | background = { 23 | color = colors.spaces.inactive, 24 | border_width = 0, 25 | border_color = colors.black, 26 | }, 27 | popup = { background = { border_width = 0, border_color = colors.black } } 28 | }) 29 | 30 | spaces[i] = space 31 | 32 | -- Single item bracket for space items to achieve double border on highlight 33 | local space_bracket = sbar.add("bracket", { space.name }, { 34 | background = { 35 | color = colors.transparent, 36 | border_color = colors.bg2, 37 | border_width = 0 38 | } 39 | }) 40 | 41 | -- Padding space 42 | sbar.add("space", "space.padding." .. i, { 43 | space = i, 44 | script = "", 45 | width = settings.group_paddings, 46 | }) 47 | 48 | local space_popup = sbar.add("item", { 49 | position = "popup." .. space.name, 50 | padding_left = 5, 51 | padding_right = 0, 52 | background = { 53 | drawing = true, 54 | image = { 55 | corner_radius = 9, 56 | scale = 0.2 57 | } 58 | } 59 | }) 60 | 61 | space:subscribe("space_change", function(env) 62 | local selected = env.SELECTED == "true" 63 | space:set({ 64 | icon = { highlight = selected }, 65 | label = { highlight = selected }, 66 | background = { 67 | color = selected and colors.spaces.active or colors.spaces.inactive, 68 | border_color = selected and colors.spaces.active or colors.black 69 | }, 70 | width = selected and 30 or 30 71 | }) 72 | space_bracket:set({ 73 | background = { color = colors.transparent, border_color = selected and colors.spaces.active or colors.bg2 } 74 | }) 75 | end) 76 | 77 | space:subscribe("mouse.clicked", function(env) 78 | if env.BUTTON == "other" then 79 | space_popup:set({ background = { image = "space." .. env.SID } }) 80 | space:set({ popup = { drawing = "toggle" } }) 81 | else 82 | local op = (env.BUTTON == "right") and "--destroy" or "--focus" 83 | sbar.exec("yabai -m space " .. op .. " " .. env.SID) 84 | end 85 | end) 86 | 87 | space:subscribe("mouse.entered", function(env) 88 | space_popup:set({ background = { image = "space." .. env.SID } }) 89 | space:set({ popup = { drawing = "toggle" } }) 90 | end) 91 | 92 | space:subscribe("mouse.exited", function(_) 93 | space:set({ popup = { drawing = false } }) 94 | end) 95 | 96 | end 97 | 98 | local space_window_observer = sbar.add("item", { 99 | drawing = false, 100 | updates = true, 101 | }) -------------------------------------------------------------------------------- /items/widgets/battery.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | local settings = require("settings") 4 | 5 | local battery = sbar.add("item", "widgets.battery", { 6 | position = "right", 7 | icon = { 8 | font = { 9 | style = settings.font.style_map["Regular"], 10 | } 11 | }, 12 | label = { font = { family = settings.font.numbers } }, 13 | background = { 14 | color = colors.transparent, 15 | border_width = 0, 16 | corner_radius=5, 17 | height=25 18 | }, 19 | update_freq = 180, 20 | popup = { align = "center" } 21 | }) 22 | 23 | local remaining_time = sbar.add("item", { 24 | position = "popup." .. battery.name, 25 | icon = { 26 | string = "Time remaining:", 27 | width = 100, 28 | align = "left" 29 | }, 30 | label = { 31 | string = "??:??h", 32 | width = 100, 33 | align = "right" 34 | }, 35 | }) 36 | 37 | 38 | battery:subscribe({"routine", "power_source_change", "system_woke"}, function() 39 | sbar.exec("pmset -g batt", function(batt_info) 40 | local icon = "!" 41 | local label = "?" 42 | 43 | local found, _, charge = batt_info:find("(%d+)%%") 44 | if found then 45 | charge = tonumber(charge) 46 | label = charge .. "%" 47 | end 48 | 49 | local color = colors.teal 50 | local charging, _, _ = batt_info:find("AC Power") 51 | 52 | if charging then 53 | icon = icons.battery.charging 54 | else 55 | if found and charge > 80 then 56 | icon = icons.battery._100 57 | elseif found and charge > 60 then 58 | icon = icons.battery._75 59 | elseif found and charge > 40 then 60 | icon = icons.battery._50 61 | elseif found and charge > 20 then 62 | icon = icons.battery._25 63 | color = colors.orange 64 | else 65 | icon = icons.battery._0 66 | color = colors.red 67 | end 68 | end 69 | 70 | local lead = "" 71 | if found and charge < 10 then 72 | lead = "0" 73 | end 74 | 75 | battery:set({ 76 | icon = { 77 | string = icon, 78 | color = color 79 | }, 80 | label = { string = lead .. label }, 81 | }) 82 | end) 83 | end) 84 | 85 | battery:subscribe("mouse.clicked", function(env) 86 | local drawing = battery:query().popup.drawing 87 | battery:set( { popup = { drawing = "toggle" } }) 88 | 89 | if drawing == "off" then 90 | sbar.exec("pmset -g batt", function(batt_info) 91 | local found, _, remaining = batt_info:find(" (%d+:%d+) remaining") 92 | local label = found and remaining .. "h" or "No estimate" 93 | remaining_time:set( { label = label }) 94 | end) 95 | end 96 | end) 97 | 98 | sbar.add("bracket", "widgets.battery.bracket", { battery.name }, { 99 | background = { color = colors.bg1 } 100 | }) 101 | 102 | sbar.add("item", "widgets.battery.padding", { 103 | position = "right", 104 | width = settings.group_paddings 105 | }) 106 | -------------------------------------------------------------------------------- /items/widgets/init.lua: -------------------------------------------------------------------------------- 1 | -- require("items.widgets.notifications") 2 | -- require("items.widgets.battery") 3 | require("items.widgets.volume") 4 | -- require("items.widgets.wifi") 5 | require("items.widgets.metrics") 6 | -------------------------------------------------------------------------------- /items/widgets/metrics.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | local settings = require("settings") 4 | 5 | sbar.exec("killall cpu_load >/dev/null; $CONFIG_DIR/helpers/event_providers/cpu_load/bin/cpu_load cpu_update 2.0") 6 | sbar.exec("killall memory_load >/dev/null; $CONFIG_DIR/helpers/event_providers/memory_load/bin/memory_load memory_update 2.0") 7 | sbar.exec("killall hdd_load >/dev/null; $CONFIG_DIR/helpers/event_providers/hdd_load/bin/hdd_load hdd_update 2.0") 8 | 9 | local hdd = sbar.add("item", "widgets.hdd" , 52, { 10 | position = "right", 11 | background = { 12 | height = 22, 13 | color = { alpha = 0 }, 14 | border_width = 0, 15 | drawing = true, 16 | }, 17 | icon = { 18 | string = icons.hdd, 19 | color = colors.yellow 20 | }, 21 | label = { 22 | string = "??T", 23 | font = { 24 | family = settings.font.numbers, 25 | style = settings.font.style_map["Bold"], 26 | }, 27 | color = colors.yellow, 28 | align = "right", 29 | padding_right = 0, 30 | }, 31 | padding_right = settings.paddings + 6 32 | }) 33 | 34 | local memory = sbar.add("item", "widgets.memory" , 42, { 35 | position = "right", 36 | background = { 37 | height = 22, 38 | color = { alpha = 0 }, 39 | border_width = 0, 40 | drawing = true, 41 | }, 42 | icon = { 43 | string = icons.memory, 44 | color = colors.teal 45 | }, 46 | label = { 47 | string = "??%", 48 | font = { 49 | family = settings.font.numbers, 50 | style = settings.font.style_map["Bold"], 51 | }, 52 | color = colors.teal, 53 | align = "right", 54 | padding_right = 0, 55 | }, 56 | padding_right = settings.paddings + 6 57 | }) 58 | 59 | local cpu = sbar.add("item", "widgets.cpu" , 42, { 60 | position = "right", 61 | background = { 62 | height = 22, 63 | color = { alpha = 0 }, 64 | border_width = 0, 65 | drawing = true, 66 | }, 67 | icon = { 68 | string = icons.cpu, 69 | color = colors.red 70 | }, 71 | label = { 72 | string = "??%", 73 | font = { 74 | family = settings.font.numbers, 75 | style = settings.font.style_map["Bold"], 76 | }, 77 | color = colors.red, 78 | align = "right", 79 | padding_right = 0, 80 | }, 81 | padding_right = settings.paddings + 6 82 | }) 83 | 84 | cpu:subscribe("cpu_update", function(env) 85 | -- Also available: env.user_load, env.sys_load 86 | cpu:set({ 87 | label = env.total_load .. "%", 88 | }) 89 | end) 90 | 91 | memory:subscribe("memory_update", function(env) 92 | memory:set({ 93 | label = env.memory_load, 94 | }) 95 | end) 96 | 97 | hdd:subscribe("hdd_update", function(env) 98 | hdd:set({ 99 | label = env.available, 100 | }) 101 | end) 102 | 103 | cpu:subscribe("mouse.clicked", function(env) 104 | sbar.exec("open -a 'Activity Monitor'") 105 | end) 106 | 107 | memory:subscribe("mouse.clicked", function(env) 108 | sbar.exec("open -a 'Activity Monitor'") 109 | end) 110 | 111 | -- Background around the cpu item 112 | sbar.add("bracket", "widgets.metrics.bracket", { cpu.name, memory.name, hdd.name }, { 113 | background = { color = colors.transparent } 114 | }) 115 | 116 | -- Background around the cpu item 117 | sbar.add("item", "widgets.cpu.padding", { 118 | position = "right", 119 | width = settings.group_paddings 120 | }) 121 | -------------------------------------------------------------------------------- /items/widgets/notifications.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | local settings = require("settings") 4 | 5 | local brew = sbar.add("item", "widgets.brew", 42, { 6 | position = "right", 7 | background = { 8 | height = 22, 9 | color = { alpha = 0 }, 10 | border_width = 0, 11 | drawing = true, 12 | }, 13 | icon = { 14 | string = icons.brew, 15 | color = colors.green 16 | }, 17 | label = { 18 | string = "0", 19 | font = { 20 | family = settings.font.numbers, 21 | style = settings.font.style_map["Bold"], 22 | }, 23 | align = "right", 24 | padding_right = 0, 25 | }, 26 | update_freq = 5, 27 | padding_right = settings.paddings + 6 28 | }) 29 | 30 | local mail = sbar.add("item", "widgets.mail", 42, { 31 | position = "right", 32 | background = { 33 | height = 22, 34 | color = { alpha = 0 }, 35 | border_width = 0, 36 | drawing = true, 37 | }, 38 | icon = { 39 | string = icons.mail, 40 | color = colors.green 41 | }, 42 | label = { 43 | string = "0", 44 | font = { 45 | family = settings.font.numbers, 46 | style = settings.font.style_map["Bold"], 47 | }, 48 | align = "right", 49 | padding_right = 0, 50 | }, 51 | update_freq = 5, 52 | padding_right = settings.paddings 53 | }) 54 | 55 | local messages = sbar.add("item", "widgets.messages", 42, { 56 | position = "right", 57 | background = { 58 | height = 22, 59 | color = { alpha = 0 }, 60 | border_width = 0, 61 | drawing = true, 62 | }, 63 | icon = { 64 | string = icons.messages, 65 | color = colors.green 66 | }, 67 | label = { 68 | string = "0", 69 | font = { 70 | family = settings.font.numbers, 71 | style = settings.font.style_map["Bold"], 72 | }, 73 | align = "right", 74 | padding_right = 0, 75 | }, 76 | update_freq = 5, 77 | padding_right = settings.paddings 78 | }) 79 | 80 | sbar.add("bracket", "widgets.notifications.bracket", { brew.name, mail.name, messages.name }, { 81 | background = { color = colors.bg1 } 82 | }) 83 | 84 | sbar.add("item", "widgets.notifications.padding", { 85 | position = "right", 86 | width = settings.group_paddings 87 | }) 88 | 89 | -- Function to update brew count 90 | local function update_brew_count() 91 | sbar.exec("brew outdated | wc -l | tr -d ' '", function(count) 92 | local brew_count = tonumber(count) or 0 93 | local color 94 | local label_padding 95 | 96 | -- Color logic based on outdated package count 97 | if brew_count >= 30 then 98 | color = colors.red 99 | label_padding = 1 100 | elseif brew_count >= 10 then 101 | color = colors.yellow 102 | label_padding = 1 103 | elseif brew_count >= 1 then 104 | color = colors.blue 105 | label_padding = 1 106 | else 107 | color = colors.green 108 | label_padding = 0 109 | end 110 | 111 | brew:set({ 112 | label = { 113 | string = tostring(brew_count) 114 | }, 115 | icon = { 116 | color = color 117 | } 118 | }) 119 | end) 120 | end 121 | 122 | -- Function to update mail count 123 | local function update_mail_count() 124 | sbar.exec([[ 125 | osascript -e 'tell application "Mail" to count of (messages of inbox whose read status is false)' 126 | ]], function(count) 127 | local mail_count = tonumber(count) or 0 128 | local color 129 | local label_padding 130 | 131 | -- Color logic based on unread email count 132 | if mail_count >= 30 then 133 | color = colors.red 134 | label_padding = 1 135 | elseif mail_count >= 10 then 136 | color = colors.yellow 137 | label_padding = 1 138 | elseif mail_count >= 1 then 139 | color = colors.blue 140 | label_padding = 1 141 | else 142 | color = colors.green 143 | label_padding = 0 144 | end 145 | 146 | mail:set({ 147 | label = { 148 | string = tostring(mail_count) 149 | }, 150 | icon = { 151 | color = color 152 | } 153 | }) 154 | end) 155 | end 156 | 157 | -- Function to update messages count 158 | local function update_messages_count() 159 | local db_path = os.getenv("HOME") .. "/Library/Messages/chat.db" 160 | local cmd = string.format("sqlite3 %s \"SELECT COUNT(*) FROM message JOIN chat_message_join ON chat_message_join.message_id = message.ROWID JOIN chat ON chat.ROWID = chat_message_join.chat_id WHERE message.is_from_me = 0 AND message.is_read = 0\"", db_path) 161 | 162 | sbar.exec(cmd, function(count) 163 | local message_count = tonumber(count) or 0 164 | local icon_color, label_color, label_padding 165 | 166 | -- Color logic based on unread count 167 | if message_count >= 30 then 168 | icon_color = colors.red 169 | label_color = colors.white 170 | label_padding = 1 171 | elseif message_count >= 10 then 172 | icon_color = colors.yellow 173 | label_color = colors.white 174 | label_padding = 1 175 | elseif message_count >= 1 then 176 | icon_color = colors.blue 177 | label_color = colors.white 178 | label_padding = 1 179 | else 180 | icon_color = colors.green 181 | label_color = colors.white 182 | label_padding = 0 183 | end 184 | 185 | messages:set({ 186 | label = { 187 | string = tostring(message_count) 188 | }, 189 | icon = { 190 | color = icon_color 191 | } 192 | }) 193 | end) 194 | end 195 | 196 | -- Subscribe to update events 197 | sbar.subscribe("brew_update", update_brew_count) 198 | sbar.subscribe("mail_check", update_mail_count) 199 | sbar.subscribe("messages_check", update_messages_count) 200 | 201 | -- Add these new event subscriptions 202 | brew:subscribe({ "forced", "routine", "system_woke" }, update_brew_count) 203 | mail:subscribe({ "forced", "routine", "system_woke" }, update_mail_count) 204 | messages:subscribe({ "forced", "routine", "system_woke" }, update_messages_count) 205 | 206 | brew:subscribe("mouse.clicked", function(env) 207 | if env.modifier == "cmd" then -- Command + Click to upgrade 208 | sbar.exec("brew upgrade", function() 209 | update_brew_count() -- Update the count after upgrade 210 | sbar.exec([[ 211 | osascript -e 'display notification "All packages have been upgraded" with title "Brew Upgrade Complete"' 212 | ]]) 213 | end) 214 | else -- Normal click just updates the list 215 | sbar.exec("brew update", function() 216 | update_brew_count() 217 | sbar.exec([[ 218 | osascript -e 'display notification "Brew package list has been updated" with title "Brew Update Complete"' 219 | ]]) 220 | end) 221 | end 222 | end) 223 | 224 | mail:subscribe("mouse.clicked", function(env) 225 | sbar.exec("open -a Mail") 226 | end) 227 | 228 | messages:subscribe("mouse.clicked", function(env) 229 | sbar.exec("open -a Messages") 230 | end) 231 | 232 | -- Initial count fetch 233 | update_brew_count() 234 | update_mail_count() 235 | update_messages_count() 236 | -------------------------------------------------------------------------------- /items/widgets/volume.lua: -------------------------------------------------------------------------------- 1 | local colors = require("colors") 2 | local icons = require("icons") 3 | local settings = require("settings") 4 | 5 | local popup_width = 250 6 | 7 | local volume_percent = sbar.add("item", "widgets.volume1", { 8 | position = "right", 9 | icon = { drawing = false }, 10 | label = { 11 | string = "??%", 12 | padding_left = -1, 13 | font = { family = settings.font.numbers }, 14 | color = colors.magenta 15 | }, 16 | }) 17 | 18 | local volume_icon = sbar.add("item", "widgets.volume2", { 19 | position = "right", 20 | padding_right = -1, 21 | icon = { 22 | string = icons.volume._100, 23 | width = 0, 24 | align = "left", 25 | color = colors.grey, 26 | font = { 27 | style = settings.font.style_map["Regular"], 28 | }, 29 | }, 30 | label = { 31 | width = 25, 32 | align = "left", 33 | font = { 34 | style = settings.font.style_map["Regular"], 35 | size = 14.0, 36 | }, 37 | color = colors.magenta 38 | }, 39 | }) 40 | 41 | local volume_bracket = sbar.add("bracket", "widgets.volume.bracket", { 42 | volume_icon.name, 43 | volume_percent.name 44 | }, { 45 | background = { color = colors.transparent }, 46 | popup = { align = "center" } 47 | }) 48 | 49 | -- sbar.add("item", "widgets.volume.padding", { 50 | -- position = "right", 51 | -- width = settings.group_paddings 52 | -- }) 53 | 54 | local volume_slider = sbar.add("slider", popup_width, { 55 | position = "popup." .. volume_bracket.name, 56 | slider = { 57 | highlight_color = colors.blue, 58 | background = { 59 | height = 6, 60 | corner_radius = 3, 61 | color = colors.bg2, 62 | }, 63 | knob= { 64 | string = "􀀁", 65 | drawing = true, 66 | }, 67 | }, 68 | background = { color = colors.transparent, height = 2, y_offset = -20 }, 69 | click_script = 'osascript -e "set volume output volume $PERCENTAGE"' 70 | }) 71 | 72 | volume_percent:subscribe("volume_change", function(env) 73 | local volume = tonumber(env.INFO) 74 | local icon = icons.volume._0 75 | if volume > 60 then 76 | icon = icons.volume._100 77 | elseif volume > 30 then 78 | icon = icons.volume._66 79 | elseif volume > 10 then 80 | icon = icons.volume._33 81 | elseif volume > 0 then 82 | icon = icons.volume._10 83 | end 84 | 85 | local lead = "" 86 | if volume < 10 then 87 | lead = "0" 88 | end 89 | 90 | volume_icon:set({ label = icon }) 91 | volume_percent:set({ label = lead .. volume .. "%" }) 92 | volume_slider:set({ slider = { percentage = volume } }) 93 | end) 94 | 95 | local function volume_collapse_details() 96 | local drawing = volume_bracket:query().popup.drawing == "on" 97 | if not drawing then return end 98 | volume_bracket:set({ popup = { drawing = false } }) 99 | sbar.remove('/volume.device\\.*/') 100 | end 101 | 102 | local current_audio_device = "None" 103 | local function volume_toggle_details(env) 104 | if env.BUTTON == "right" then 105 | sbar.exec("open /System/Library/PreferencePanes/Sound.prefpane") 106 | return 107 | end 108 | 109 | local should_draw = volume_bracket:query().popup.drawing == "off" 110 | if should_draw then 111 | volume_bracket:set({ popup = { drawing = true } }) 112 | sbar.exec("SwitchAudioSource -t output -c", function(result) 113 | current_audio_device = result:sub(1, -2) 114 | sbar.exec("SwitchAudioSource -a -t output", function(available) 115 | current = current_audio_device 116 | local color = colors.grey 117 | local counter = 0 118 | 119 | for device in string.gmatch(available, '[^\r\n]+') do 120 | local color = colors.grey 121 | if current == device then 122 | color = colors.white 123 | end 124 | sbar.add("item", "volume.device." .. counter, { 125 | position = "popup." .. volume_bracket.name, 126 | width = popup_width, 127 | align = "center", 128 | label = { string = device, color = color }, 129 | click_script = 'SwitchAudioSource -s "' .. device .. '" && sketchybar --set /volume.device\\.*/ label.color=' .. colors.grey .. ' --set $NAME label.color=' .. colors.white 130 | 131 | }) 132 | counter = counter + 1 133 | end 134 | end) 135 | end) 136 | else 137 | volume_collapse_details() 138 | end 139 | end 140 | 141 | local function volume_scroll(env) 142 | local delta = env.SCROLL_DELTA 143 | sbar.exec('osascript -e "set volume output volume (output volume of (get volume settings) + ' .. delta .. ')"') 144 | end 145 | 146 | volume_icon:subscribe("mouse.clicked", volume_toggle_details) 147 | volume_icon:subscribe("mouse.scrolled", volume_scroll) 148 | volume_percent:subscribe("mouse.clicked", volume_toggle_details) 149 | volume_percent:subscribe("mouse.exited.global", volume_collapse_details) 150 | volume_percent:subscribe("mouse.scrolled", volume_scroll) 151 | 152 | -------------------------------------------------------------------------------- /items/widgets/wifi.lua: -------------------------------------------------------------------------------- 1 | local icons = require("icons") 2 | local colors = require("colors") 3 | local settings = require("settings") 4 | 5 | -- Execute the event provider binary which provides the event "network_update" 6 | -- for the network interface "en0", which is fired every 2.0 seconds. 7 | sbar.exec("killall network_load >/dev/null; $CONFIG_DIR/helpers/event_providers/network_load/bin/network_load en0 network_update 2.0") 8 | 9 | local popup_width = 250 10 | 11 | local wifi_up = sbar.add("item", "widgets.wifi1", { 12 | position = "right", 13 | padding_left = -5, 14 | width = 0, 15 | icon = { 16 | padding_right = 0, 17 | font = { 18 | style = settings.font.style_map["Bold"], 19 | size = 9.0, 20 | }, 21 | string = icons.wifi.upload, 22 | }, 23 | label = { 24 | font = { 25 | family = settings.font.numbers, 26 | style = settings.font.style_map["Bold"], 27 | size = 9.0, 28 | }, 29 | color = colors.red, 30 | string = "??? Bps", 31 | }, 32 | y_offset = 4, 33 | }) 34 | 35 | local wifi_down = sbar.add("item", "widgets.wifi2", { 36 | position = "right", 37 | padding_left = -5, 38 | icon = { 39 | padding_right = 0, 40 | font = { 41 | style = settings.font.style_map["Bold"], 42 | size = 9.0, 43 | }, 44 | string = icons.wifi.download, 45 | }, 46 | label = { 47 | font = { 48 | family = settings.font.numbers, 49 | style = settings.font.style_map["Bold"], 50 | size = 9.0, 51 | }, 52 | color = colors.blue, 53 | string = "??? Bps", 54 | }, 55 | y_offset = -4, 56 | }) 57 | 58 | local wifi = sbar.add("item", "widgets.wifi.padding", { 59 | position = "right", 60 | label = { drawing = false }, 61 | }) 62 | 63 | -- Background around the item 64 | local wifi_bracket = sbar.add("bracket", "widgets.wifi.bracket", { 65 | wifi.name, 66 | wifi_up.name, 67 | wifi_down.name 68 | }, { 69 | background = { color = colors.bg1 }, 70 | popup = { align = "center", height = 30 } 71 | }) 72 | 73 | local ssid = sbar.add("item", { 74 | position = "popup." .. wifi_bracket.name, 75 | icon = { 76 | font = { 77 | style = settings.font.style_map["Bold"] 78 | }, 79 | string = icons.wifi.router, 80 | }, 81 | width = popup_width, 82 | align = "center", 83 | label = { 84 | font = { 85 | size = 15, 86 | style = settings.font.style_map["Bold"] 87 | }, 88 | max_chars = 18, 89 | string = "????????????", 90 | }, 91 | background = { 92 | height = 2, 93 | color = colors.grey, 94 | y_offset = -15 95 | } 96 | }) 97 | 98 | local hostname = sbar.add("item", { 99 | position = "popup." .. wifi_bracket.name, 100 | icon = { 101 | align = "left", 102 | string = "Hostname:", 103 | width = popup_width / 2, 104 | }, 105 | label = { 106 | max_chars = 20, 107 | string = "????????????", 108 | width = popup_width / 2, 109 | align = "right", 110 | } 111 | }) 112 | 113 | local ip = sbar.add("item", { 114 | position = "popup." .. wifi_bracket.name, 115 | icon = { 116 | align = "left", 117 | string = "IP:", 118 | width = popup_width / 2, 119 | }, 120 | label = { 121 | string = "???.???.???.???", 122 | width = popup_width / 2, 123 | align = "right", 124 | } 125 | }) 126 | 127 | local mask = sbar.add("item", { 128 | position = "popup." .. wifi_bracket.name, 129 | icon = { 130 | align = "left", 131 | string = "Subnet mask:", 132 | width = popup_width / 2, 133 | }, 134 | label = { 135 | string = "???.???.???.???", 136 | width = popup_width / 2, 137 | align = "right", 138 | } 139 | }) 140 | 141 | local router = sbar.add("item", { 142 | position = "popup." .. wifi_bracket.name, 143 | icon = { 144 | align = "left", 145 | string = "Router:", 146 | width = popup_width / 2, 147 | }, 148 | label = { 149 | string = "???.???.???.???", 150 | width = popup_width / 2, 151 | align = "right", 152 | }, 153 | }) 154 | 155 | sbar.add("item", { position = "right", width = settings.group_paddings }) 156 | 157 | wifi_up:subscribe("network_update", function(env) 158 | local up_color = (env.upload == "000 Bps") and colors.grey or colors.red 159 | local down_color = (env.download == "000 Bps") and colors.grey or colors.blue 160 | wifi_up:set({ 161 | icon = { color = up_color }, 162 | label = { 163 | string = env.upload, 164 | color = up_color 165 | } 166 | }) 167 | wifi_down:set({ 168 | icon = { color = down_color }, 169 | label = { 170 | string = env.download, 171 | color = down_color 172 | } 173 | }) 174 | end) 175 | 176 | wifi:subscribe({"wifi_change", "system_woke"}, function(env) 177 | sbar.exec("ipconfig getifaddr en0", function(ip) 178 | local connected = not (ip == "") 179 | wifi:set({ 180 | icon = { 181 | string = connected and icons.wifi.connected or icons.wifi.disconnected, 182 | color = connected and colors.white or colors.red, 183 | }, 184 | }) 185 | end) 186 | end) 187 | 188 | local function hide_details() 189 | wifi_bracket:set({ popup = { drawing = false } }) 190 | end 191 | 192 | local function toggle_details() 193 | local should_draw = wifi_bracket:query().popup.drawing == "off" 194 | if should_draw then 195 | wifi_bracket:set({ popup = { drawing = true }}) 196 | sbar.exec("networksetup -getcomputername", function(result) 197 | hostname:set({ label = result }) 198 | end) 199 | sbar.exec("ipconfig getifaddr en0", function(result) 200 | ip:set({ label = result }) 201 | end) 202 | sbar.exec("ipconfig getsummary en0 | awk -F ' SSID : ' '/ SSID : / {print $2}'", function(result) 203 | ssid:set({ label = result }) 204 | end) 205 | sbar.exec("networksetup -getinfo Wi-Fi | awk -F 'Subnet mask: ' '/^Subnet mask: / {print $2}'", function(result) 206 | mask:set({ label = result }) 207 | end) 208 | sbar.exec("networksetup -getinfo Wi-Fi | awk -F 'Router: ' '/^Router: / {print $2}'", function(result) 209 | router:set({ label = result }) 210 | end) 211 | else 212 | hide_details() 213 | end 214 | end 215 | 216 | wifi_up:subscribe("mouse.clicked", toggle_details) 217 | wifi_down:subscribe("mouse.clicked", toggle_details) 218 | wifi:subscribe("mouse.clicked", toggle_details) 219 | wifi:subscribe("mouse.exited.global", hide_details) 220 | 221 | local function copy_label_to_clipboard(env) 222 | local label = sbar.query(env.NAME).label.value 223 | sbar.exec("echo \"" .. label .. "\" | pbcopy") 224 | sbar.set(env.NAME, { label = { string = icons.clipboard, align="center" } }) 225 | sbar.delay(1, function() 226 | sbar.set(env.NAME, { label = { string = label, align = "right" } }) 227 | end) 228 | end 229 | 230 | ssid:subscribe("mouse.clicked", copy_label_to_clipboard) 231 | hostname:subscribe("mouse.clicked", copy_label_to_clipboard) 232 | ip:subscribe("mouse.clicked", copy_label_to_clipboard) 233 | mask:subscribe("mouse.clicked", copy_label_to_clipboard) 234 | router:subscribe("mouse.clicked", copy_label_to_clipboard) 235 | -------------------------------------------------------------------------------- /reload.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | 3 | exec('brew services restart sketchybar', (error, stdout, stderr) => { 4 | if (error) { 5 | console.error(`Error: ${error.message}`); 6 | return; 7 | } 8 | if (stderr) { 9 | console.error(`stderr: ${stderr}`); 10 | return; 11 | } 12 | console.log(`stdout: ${stdout}`); 13 | }); 14 | -------------------------------------------------------------------------------- /settings.lua: -------------------------------------------------------------------------------- 1 | return { 2 | display = 1, 3 | paddings = 5, 4 | group_paddings = 5, 5 | corner_radius = 8, 6 | bar = { 7 | height = 40, 8 | }, 9 | y_offset = 5, 10 | 11 | -- Apple Menu Config 12 | app = { 13 | offset = { 14 | y = 60, 15 | x = 5, 16 | }, 17 | corner_radius = 2, 18 | font = { 19 | text = { 20 | family = "FiraMono Nerd Font", 21 | size = 14.0 22 | }, 23 | numbers = { 24 | family = "FiraMono Nerd Font", 25 | size = 14.0 26 | }, 27 | icons = "SF Pro Text", -- Used for icons (or NerdFont) 28 | style_map = { 29 | ["Regular"] = "Regular", 30 | ["Semibold"] = "Medium", 31 | ["Bold"] = "Bold", 32 | ["Heavy"] = "Bold", 33 | ["Black"] = "ExtraBold" 34 | }, 35 | overrides = { 36 | TimeView = { 37 | family = "FiraMono Nerd Font", 38 | size = 2.0 39 | } 40 | } 41 | } 42 | }, 43 | icons = "sf-symbols", -- Options: "sf-symbols", "nerdfont" 44 | animated_icons = false, -- Set to true if you want to use animated icons 45 | 46 | font = { 47 | text = "FiraMono Nerd Font", -- Used for text 48 | numbers = "FiraMono Nerd Font", -- Used for numbers 49 | icons = "SF Pro Text", -- Used for icons (or NerdFont) 50 | style_map = { 51 | ["Regular"] = "Regular", 52 | ["Semibold"] = "Medium", 53 | ["Bold"] = "Bold", 54 | ["Heavy"] = "Bold", 55 | ["Black"] = "ExtraBold" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sketchybarrc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | -- Load the sketchybar-package and prepare the helper binaries 4 | require("helpers") 5 | require("init") 6 | 7 | -- Enable hot reloading 8 | sbar.exec("sketchybar --hotload true") 9 | -------------------------------------------------------------------------------- /terminal: -------------------------------------------------------------------------------- 1 | git checkout -b backup-branch 13d2b97190c04f548b01201ad977601e70e4ab5f 2 | 3 | # First ensure you're on the backup branch 4 | git checkout backup-branch 5 | 6 | # Force push this branch as the new main 7 | git push origin backup-branch:main -f 8 | 9 | git fetch origin 10 | git checkout main 11 | git reset --hard origin/main --------------------------------------------------------------------------------