diff --git a/darwin/aerospace.nix b/darwin/aerospace.nix index 4b1e774..269b022 100644 --- a/darwin/aerospace.nix +++ b/darwin/aerospace.nix @@ -2,37 +2,56 @@ pkgs, lib, config, + home-dir, ... }: +let + cfg = config.custom.aerospace; +in { options.custom = with lib; { aerospace = { enable = mkEnableOption "Enable aerospace"; + logFile = mkOption { + type = types.str; + default = "${home-dir}/Library/Logs/aerospace.log"; + description = "Filepath of log output"; + }; }; }; - config = lib.mkIf config.custom.aerospace.enable { + config = lib.mkIf cfg.enable { + launchd.user.agents.aerospace.serviceConfig = { + StandardErrorPath = cfg.logFile; + StandardOutPath = cfg.logFile; + }; + services.aerospace = { - enable = config.custom.aerospace.enable; + enable = true; settings = { - enable-normalization-flatten-containers = true; + enable-normalization-flatten-containers = false; enable-normalization-opposite-orientation-for-nested-containers = true; accordion-padding = 0; on-focused-monitor-changed = [ "move-mouse monitor-lazy-center" ]; default-root-container-layout = "tiles"; default-root-container-orientation = "auto"; + # exec-on-workspace-change = [ + # "/bin/bash" + # "-c" + # "sketchybar --trigger aerospace_workspace_change FOCUSED=$AEROSPACE_FOCUSED_WORKSPACE" + # ]; gaps = { inner = { - horizontal = 0; - vertical = 0; + horizontal = 15; + vertical = 15; }; outer = { - left = 0; - bottom = 0; - top = 0; - right = 0; + left = 10; + bottom = 5; + top = 12; + right = 10; }; }; @@ -47,14 +66,25 @@ in "exec-and-forget osascript ${script}"; + # alt-enter = "exec-and-forget \"open -n -a /Applications/Slack.app\""; + alt-shift-f = "fullscreen"; - alt-p = "layout floating tiling"; + alt-space = "layout floating tiling"; alt-e = "layout tiles horizontal vertical"; - alt-t = "layout accordion horizontal vertical"; + alt-t = "layout h_accordion"; + alt-s = "layout v_accordion"; + alt-v = "split vertical"; + alt-shift-v = "split horizontal"; + # alt-v = "join-with down"; + # alt-shift-v = "join-with right"; alt-h = "focus left"; alt-l = "focus right"; + alt-j = "focus down"; + alt-k = "focus up"; alt-shift-h = "move left"; alt-shift-l = "move right"; + alt-shift-j = "move down"; + alt-shift-k = "move up"; alt-1 = "workspace 1"; alt-2 = "workspace 2"; @@ -69,46 +99,91 @@ alt-shift-1 = [ "move-node-to-workspace 1" - "workspace 1" + # "workspace 1" ]; alt-shift-2 = [ "move-node-to-workspace 2" - "workspace 2" + # "workspace 2" ]; alt-shift-3 = [ "move-node-to-workspace 3" - "workspace 3" + # "workspace 3" ]; alt-shift-4 = [ "move-node-to-workspace 4" - "workspace 4" + # "workspace 4" ]; alt-shift-5 = [ "move-node-to-workspace 5" - "workspace 5" + # "workspace 5" ]; alt-shift-6 = [ "move-node-to-workspace 6" - "workspace 6" + # "workspace 6" ]; alt-shift-7 = [ "move-node-to-workspace 7" - "workspace 7" + # "workspace 7" ]; alt-shift-8 = [ "move-node-to-workspace 8" - "workspace 8" + # "workspace 8" ]; alt-shift-9 = [ "move-node-to-workspace 9" - "workspace 9" + # "workspace 9" ]; alt-shift-0 = [ "move-node-to-workspace 10" - "workspace 10" + # "workspace 10" ]; + alt-tab = "workspace-back-and-forth"; + alt-shift-tab = "move-workspace-to-monitor --wrap-around next"; alt-r = "mode resize"; + alt-shift-semicolon = "mode service"; + }; + }; + + service = { + binding = { + esc = [ + "reload-config" + "mode main" + ]; + r = [ + "flatten-workspace-tree" + "mode main" + ]; + f = [ + "layout floating tiling" + "mode main" + ]; + backspace = [ + "close-all-windows-but-current" + "mode main" + ]; + + alt-shift-h = [ + "join-with left" + "mode main" + ]; + alt-shift-j = [ + "join-with down" + "mode main" + ]; + alt-shift-k = [ + "join-with up" + "mode main" + ]; + alt-shift-l = [ + "join-with right" + "mode main" + ]; + + # down = "volume down"; + # up = "volume up"; + # shift-down = [ "volume set 0" "mode main" ]; }; }; @@ -124,11 +199,40 @@ }; }; + workspace-to-monitor-force-assignment = { + "1" = "main"; + "2" = "main"; + "3" = "main"; + "4" = "main"; + "5" = "main"; + "6" = "main"; + "7" = "main"; + "8" = "main"; + "9" = "main"; + "10" = [ + "secondary" + "main" + ]; + }; + on-window-detected = [ + { + "if".app-id = "com.spotify.client"; + run = [ "move-node-to-workspace 7" ]; + } + { + "if".app-id = "app.zen-browser.zen"; + run = [ "move-node-to-workspace 10" ]; + } { "if".app-id = "com.mitchellh.ghostty"; run = [ "layout tiling" ]; } + { + "if".app-id = "com.tinyspeck.slackmacgap"; + run = [ "move-node-to-workspace 9" ]; + } + ]; }; }; diff --git a/darwin/default.nix b/darwin/default.nix index 565aa46..78304b3 100644 --- a/darwin/default.nix +++ b/darwin/default.nix @@ -5,13 +5,16 @@ pkgs, system, host, + user, ... }: { imports = [ ./aerospace.nix ./homebrew.nix + ./jankyborders.nix ./keyboard.nix + ./sketchybar ]; options.custom = with lib; { @@ -44,15 +47,106 @@ symlinks = mkOption { type = types.attrsOf types.str; default = { }; - description = "Symlinks to create in the format { dest = src;}"; + description = "Symlinks to create in the format { dest = src; }"; }; }; config = { - # networking - # networking.hostName = host; - # networking.hostId = builtins.substring 0 8 (builtins.hashString "md5" config.networking.hostName); - # networking.networkmanager.enable = true; + system.defaults = { + CustomUserPreferences = { + NSGlobalDomain = { + WebKitDeveloperExtras = true; + # AppleHighlightColor = "0.65098 0.85490 0.58431"; + # AppleAccentColor = 1; + }; + "com.apple.finder" = { + DisableAllAnimations = true; + ShowExternalHardDrivesOnDesktop = false; + ShowMountedServersOnDesktop = false; + ShowRemovableMediaOnDesktop = false; + ShowHardDrivesOnDesktop = false; + _FXSortFoldersFirst = true; + # search the current folder by default + FXDefaultSearchScope = "SCcf"; + }; + "com.apple.NetworkBrowser" = { + BrowseAllInterfaces = 1; + }; + "com.apple.DesktopServices" = { + DSDontWriteNetworkStores = true; + }; + # "com.apple.Safari" = { + # AutoOpenSafeDownloads = false; + # IncludeDevelopMenu = true; + # WebKitDeveloperExtrasEnabledPreferenceKey = true; + # "com.apple.Safari.ContentPageGroupIdentifier.WebKit2DeveloperExtrasEnabled" = true; + # }; + # "com.apple.mail" = { + # AddressesIncludeNameOnPasteboard = false; + # }; + "net.sourceforge.skim-app.skim" = { + SKPSConversionCommand = "${pkgs.custom.ps2pdfcrop}/bin/ps2pdfcrop"; + }; + }; + screencapture = { + disable-shadow = true; + location = "/Users/${user}/Screenshots"; + type = "png"; + }; + trackpad = { + Clicking = true; + Dragging = true; + TrackpadRightClick = true; + # TrackpadThreeFingerDrag = true; + }; + dock = { + autohide = true; + expose-animation-duration = 0.0; + mineffect = "scale"; + minimize-to-application = true; + mru-spaces = false; + orientation = "left"; + show-recents = false; + wvous-br-corner = 1; # Disabled + }; + finder = { + AppleShowAllExtensions = true; + FXDefaultSearchScope = "SCcf"; + FXEnableExtensionChangeWarning = false; + FXPreferredViewStyle = "Nlsv"; + AppleShowAllFiles = true; + _FXShowPosixPathInTitle = true; + ShowPathbar = true; + ShowStatusBar = false; + }; + # + # NSGlobalDomain = { + # # AppleMeasurementUnits = "Centimeters"; + # AppleMetricUnits = 1; + # AppleShowAllExtensions = true; + # # AppleTemperatureUnit = "Celsius"; + # # AppleInterfaceStyle = "Dark"; + # AppleInterfaceStyle = null; # -- light mode + # AppleInterfaceStyleSwitchesAutomatically = false; + # InitialKeyRepeat = 20; + # KeyRepeat = 2; + # NSAutomaticCapitalizationEnabled = false; + # NSAutomaticDashSubstitutionEnabled = false; + # NSAutomaticPeriodSubstitutionEnabled = false; + # NSAutomaticQuoteSubstitutionEnabled = false; + # NSAutomaticSpellingCorrectionEnabled = false; + # NSDisableAutomaticTermination = true; + # NSAutomaticWindowAnimationsEnabled = false; + # NSDocumentSaveNewDocumentsToCloud = false; + # NSNavPanelExpandedStateForSaveMode = true; + # NSNavPanelExpandedStateForSaveMode2 = true; + # NSTableViewDefaultSizeMode = 2; + # NSWindowResizeTime = 1.0e-4; + # PMPrintingExpandedStateForPrint = true; + # PMPrintingExpandedStateForPrint2 = true; + # }; + # LaunchServices.LSQuarantine = false; + }; environment.systemPackages = with pkgs; diff --git a/darwin/homebrew.nix b/darwin/homebrew.nix index 56f0735..1949fe7 100644 --- a/darwin/homebrew.nix +++ b/darwin/homebrew.nix @@ -17,6 +17,11 @@ ms-office = mkEnableOption "Enable MS Office"; ms-teams = mkEnableOption "Enable MS Teams"; spotify = mkEnableOption "Enable Spotify"; + raycast = mkEnableOption "Enable Raycast"; + whatsapp = mkEnableOption "Enable Whatsapp"; + vscode = mkEnableOption "Enable VSCode"; + firefox = mkEnableOption "Enable Firefox"; + chromium = mkEnableOption "Enable Chromium"; }; config = lib.mkMerge [ @@ -83,5 +88,30 @@ "microsoft-teams" ]; }) + (lib.mkIf config.custom.brew.raycast { + homebrew.casks = [ + "raycast" + ]; + }) + (lib.mkIf config.custom.brew.whatsapp { + homebrew.casks = [ + "whatsapp" + ]; + }) + (lib.mkIf config.custom.brew.vscode { + homebrew.casks = [ + "visual-studio-code" + ]; + }) + (lib.mkIf config.custom.brew.firefox { + homebrew.casks = [ + "firefox" + ]; + }) + (lib.mkIf config.custom.brew.chromium { + homebrew.casks = [ + "chromium" + ]; + }) ]; } diff --git a/darwin/jankyborders.nix b/darwin/jankyborders.nix new file mode 100644 index 0000000..ff16154 --- /dev/null +++ b/darwin/jankyborders.nix @@ -0,0 +1,33 @@ +{ + pkgs, + lib, + config, + dots, + home-dir, + ... +}: + +let + cfg = config.custom.jankyborders; +in +{ + options.custom = with lib; { + jankyborders = { + enable = mkEnableOption "Enable jankyborders"; + logFile = mkOption { + type = types.str; + default = "${home-dir}/Library/Logs/jankyborders.log"; + description = "Filepath of log output"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.jankyborders = { + enable = true; + active_color = "0xFFA1EFE4"; + inactive_color = "0xFFd3b58d"; + width = 5.0; + }; + }; +} diff --git a/darwin/sketchybar/config/app_icons.lua b/darwin/sketchybar/config/app_icons.lua new file mode 100644 index 0000000..9ddba7c --- /dev/null +++ b/darwin/sketchybar/config/app_icons.lua @@ -0,0 +1,321 @@ +-- these seem to get disentangled to https://github.com/kvndrsslr/sketchybar-app-font/tree/main/mappings +return { + ["Live"] = ":ableton:", + ["Activity Monitor"] = ":numbers:", -- no pre-existing, sadly + ["Adobe Bridge"] = ":adobe_bridge:", + ["Affinity Designer"] = ":affinity_designer:", + ["Affinity Designer 2"] = ":affinity_designer_2:", + ["Affinity Photo"] = ":affinity_photo:", + ["Affinity Photo 2"] = ":affinity_photo_2:", + ["Affinity Publisher"] = ":affinity_publisher:", + ["Affinity Publisher 2"] = ":affinity_publisher_2:", + ["Airmail"] = ":airmail:", + ["Alacritty"] = ":alacritty:", + ["Alfred"] = ":alfred:", + ["Android Messages"] = ":android_messages:", + ["Android Studio"] = ":android_studio:", + ["Anki"] = ":anki:", + ["Anytype"] = ":anytype:", + ["App Eraser"] = ":app_eraser:", + ["App Store"] = ":app_store:", + ["Arc"] = ":arc:", + ["Arduino"] = ":arduino:", + ["Arduino IDE"] = ":arduino:", + ["Atom"] = ":atom:", + ["Audacity"] = ":audacity:", + ["Bambu Studio"] = ":bambu_studio:", + ["MoneyMoney"] = ":bank:", + ["Battle.net"] = ":battle_net:", + ["Bear"] = ":bear:", + ["BetterTouchTool"] = ":bettertouchtool:", + ["Bilibili"] = ":bilibili:", + ["哔哩哔哩"] = ":bilibili:", + ["Bitwarden"] = ":bit_warden:", + ["Blender"] = ":blender:", + ["BluOS Controller"] = ":bluos_controller:", + ["Calibre"] = ":book:", + ["Brave Browser"] = ":brave_browser:", + ["BusyCal"] = ":busycal:", + ["Calculator"] = ":calculator:", + ["Calculette"] = ":calculator:", + ["Calendar"] = ":calendar:", + ["日历"] = ":calendar:", + ["Fantastical"] = ":calendar:", + ["Cron"] = ":calendar:", + ["Amie"] = ":calendar:", + ["Calendrier"] = ":calendar:", + ["Notion Calendar"] = ":calendar:", + ["Caprine"] = ":caprine:", + ["Amazon Chime"] = ":chime:", + ["Citrix Workspace"] = ":citrix:", + ["Citrix Viewer"] = ":citrix:", + ["Claude"] = ":claude:", + ["ClickUp"] = ":click_up:", + ["Code"] = ":code:", + ["Code - Insiders"] = ":code:", + ["Cold Turkey Blocker"] = ":cold_turkey_blocker:", + ["Color Picker"] = ":color_picker:", + ["数码测色计"] = ":color_picker:", + ["Copilot"] = ":copilot:", + ["CotEditor"] = ":coteditor:", + ["Creative Cloud"] = ":creative_cloud:", + ["Cursor"] = ":cursor:", + ["Cypress"] = ":cypress:", + ["DataGrip"] = ":datagrip:", + ["DataSpell"] = ":dataspell:", + ["DaVinci Resolve"] = ":davinciresolve:", + ["Deezer"] = ":deezer:", + ["Default"] = ":default:", + ["CleanMyMac X"] = ":desktop:", + ["DEVONthink 3"] = ":devonthink3:", + ["DingTalk"] = ":dingtalk:", + ["钉钉"] = ":dingtalk:", + ["阿里钉"] = ":dingtalk:", + ["Discord"] = ":discord:", + ["Discord Canary"] = ":discord:", + ["Discord PTB"] = ":discord:", + ["Vesktop"] = ":discord:", + ["Docker"] = ":docker:", + ["Docker Desktop"] = ":docker:", + ["GrandTotal"] = ":dollar:", + ["Receipts"] = ":dollar:", + ["Double Commander"] = ":doublecmd:", + ["Drafts"] = ":drafts:", + ["draw.io"] = ":draw_io:", + ["Dropbox"] = ":dropbox:", + ["Element"] = ":element:", + ["Emacs"] = ":emacs:", + ["Evernote Legacy"] = ":evernote_legacy:", + ["FaceTime"] = ":face_time:", + ["FaceTime 通话"] = ":face_time:", + ["Figma"] = ":figma:", + ["Final Cut Pro"] = ":final_cut_pro:", + ["Finder"] = ":finder:", + ["访达"] = ":finder:", + ["Firefox"] = ":firefox:", + ["Firefox Developer Edition"] = ":firefox_developer_edition:", + ["Firefox Nightly"] = ":firefox_developer_edition:", + ["Folx"] = ":folx:", + ["Fork"] = ":fork:", + ["FreeTube"] = ":freetube:", + ["Fusion"] = ":fusion:", + ["System Preferences"] = ":gear:", + ["System Settings"] = ":gear:", + ["系统设置"] = ":gear:", + ["Réglages Système"] = ":gear:", + ["GitHub Desktop"] = ":git_hub:", + ["Godot"] = ":godot:", + ["GoLand"] = ":goland:", + ["Chromium"] = ":google_chrome:", + ["Google Chrome"] = ":google_chrome:", + ["Google Chrome Canary"] = ":google_chrome:", + ["Grammarly Editor"] = ":grammarly:", + ["Home Assistant"] = ":home_assistant:", + ["Hyper"] = ":hyper:", + ["IntelliJ IDEA"] = ":idea:", + ["IINA"] = ":iina:", + ["Adobe Illustrator"] = ":illustrator:", + ["Illustrator"] = ":illustrator:", + ["Adobe InDesign"] = ":indesign:", + ["InDesign"] = ":indesign:", + ["Inkdrop"] = ":inkdrop:", + ["Inkscape"] = ":inkscape:", + ["Insomnia"] = ":insomnia:", + ["Iris"] = ":iris:", + ["iTerm"] = ":iterm:", + ["iTerm2"] = ":iterm:", + ["Jellyfin Media Player"] = ":jellyfin:", + ["Joplin"] = ":joplin:", + ["카카오톡"] = ":kakaotalk:", + ["KakaoTalk"] = ":kakaotalk:", + ["Kakoune"] = ":kakoune:", + ["KeePassXC"] = ":kee_pass_x_c:", + ["Keyboard Maestro"] = ":keyboard_maestro:", + ["Keynote"] = ":keynote:", + ["Keynote 讲演"] = ":keynote:", + ["kitty"] = ":kitty:", + ["League of Legends"] = ":league_of_legends:", + ["LibreWolf"] = ":libre_wolf:", + ["Adobe Lightroom"] = ":lightroom:", + ["Lightroom Classic"] = ":lightroomclassic:", + ["LINE"] = ":line:", + ["Linear"] = ":linear:", + ["LM Studio"] = ":lm_studio:", + ["LocalSend"] = ":localsend:", + ["Logic Pro"] = ":logicpro:", + ["Logseq"] = ":logseq:", + ["Canary Mail"] = ":mail:", + ["HEY"] = ":mail:", + ["Mail"] = ":mail:", + ["Mailspring"] = ":mail:", + ["MailMate"] = ":mail:", + ["Superhuman"] = ":mail:", + ["Spark"] = ":mail:", + ["邮件"] = ":mail:", + ["MAMP"] = ":mamp:", + ["MAMP PRO"] = ":mamp:", + ["Maps"] = ":maps:", + ["News"] = ":sioyek:", + ["Ghostty"] = ":terminal:", + ["Google Maps"] = ":maps:", + ["Marta"] = ":marta:", + ["Matlab"] = ":matlab:", + ["Mattermost"] = ":mattermost:", + ["Messages"] = ":messages:", + ["信息"] = ":messages:", + ["Nachrichten"] = ":messages:", + ["Messenger"] = ":messenger:", + ["Microsoft Edge"] = ":microsoft_edge:", + ["Microsoft Excel"] = ":microsoft_excel:", + ["Microsoft Outlook"] = ":microsoft_outlook:", + ["Microsoft PowerPoint"] = ":microsoft_power_point:", + ["Microsoft Remote Desktop"] = ":microsoft_remote_desktop:", + ["Microsoft Teams"] = ":microsoft_teams:", + ["Microsoft Teams (work or school)"] = ":microsoft_teams:", + ["Microsoft Word"] = ":microsoft_word:", + ["Min"] = ":min_browser:", + ["Miro"] = ":miro:", + ["MongoDB Compass"] = ":mongodb:", + ["mpv"] = ":mpv:", + ["Mullvad Browser"] = ":mullvad_browser:", + ["Music"] = ":music:", + ["音乐"] = ":music:", + ["Musique"] = ":music:", + ["PWNeovide"] = ":neovide:", + ["Neovide"] = ":neovide:", + ["neovide"] = ":neovide:", + ["Neovim"] = ":neovim:", + ["neovim"] = ":neovim:", + ["nvim"] = ":neovim:", + ["网易云音乐"] = ":netease_music:", + ["Noodl"] = ":noodl:", + ["Noodl Editor"] = ":noodl:", + ["NordVPN"] = ":nord_vpn:", + ["Notability"] = ":notability:", + ["Notes"] = ":notes:", + ["备忘录"] = ":notes:", + ["Notion"] = ":notion:", + ["Nova"] = ":nova:", + ["Numbers"] = ":numbers:", + ["Numbers 表格"] = ":numbers:", + ["Obsidian"] = ":obsidian:", + ["OBS"] = ":obsstudio:", + ["OmniFocus"] = ":omni_focus:", + ["1Password"] = ":one_password:", + ["Open Video Downloader"] = ":open_video_downloader:", + ["ChatGPT"] = ":openai:", + ["OpenVPN Connect"] = ":openvpn_connect:", + ["Opera"] = ":opera:", + ["OrbStack"] = ":orbstack:", + ["OrcaSlicer"] = ":orcaslicer:", + ["Orion"] = ":orion:", + ["Orion RC"] = ":orion:", + ["Pages"] = ":pages:", + ["Pages 文稿"] = ":pages:", + ["Parallels Desktop"] = ":parallels:", + ["Parsec"] = ":parsec:", + ["Preview"] = ":pdf:", + ["预览"] = ":pdf:", + ["Skim"] = ":pdf:", + ["zathura"] = ":pdf:", + ["Aperçu"] = ":pdf:", + ["PDF Expert"] = ":pdf_expert:", + ["Pearcleaner"] = ":pearcleaner:", + ["Phoenix Slides"] = ":phoenix_slides:", + ["Adobe Photoshop"] = ":photoshop:", + ["PhpStorm"] = ":php_storm:", + ["Pi-hole Remote"] = ":pihole:", + ["Pine"] = ":pine:", + ["Plex"] = ":plex:", + ["Plexamp"] = ":plexamp:", + ["Podcasts"] = ":podcasts:", + ["播客"] = ":podcasts:", + ["PomoDone App"] = ":pomodone:", + ["Postman"] = ":postman:", + ["Proton Mail"] = ":proton_mail:", + ["Proton Mail Bridge"] = ":proton_mail:", + ["PrusaSlicer"] = ":prusaslicer:", + ["SuperSlicer"] = ":prusaslicer:", + ["PyCharm"] = ":pycharm:", + ["QQ"] = ":qq:", + ["QQ音乐"] = ":qqmusic:", + ["QQMusic"] = ":qqmusic:", + ["Quantumult X"] = ":quantumult_x:", + ["qutebrowser"] = ":qute_browser:", + ["Raindrop.io"] = ":raindrop_io:", + ["Reeder"] = ":reeder5:", + ["Reminders"] = ":reminders:", + ["提醒事项"] = ":reminders:", + ["Rappels"] = ":reminders:", + ["Replit"] = ":replit:", + ["Rider"] = ":rider:", + ["JetBrains Rider"] = ":rider:", + ["Rio"] = ":rio:", + ["Royal TSX"] = ":royaltsx:", + ["Safari"] = ":safari:", + ["Safari浏览器"] = ":safari:", + ["Safari Technology Preview"] = ":safari:", + ["Sequel Ace"] = ":sequel_ace:", + ["Sequel Pro"] = ":sequel_pro:", + ["Setapp"] = ":setapp:", + ["SF Symbols"] = ":sf_symbols:", + ["Signal"] = ":signal:", + ["sioyek"] = ":sioyek:", + ["Sketch"] = ":sketch:", + ["Skype"] = ":skype:", + ["Slack"] = ":slack:", + ["Spark Desktop"] = ":spark:", + ["Spotify"] = ":spotify:", + ["Spotlight"] = ":spotlight:", + ["Sublime Text"] = ":sublime_text:", + ["Strongbox"] = ":one_password:", + ["superProductivity"] = ":superproductivity:", + ["Tana"] = ":tana:", + ["TeamSpeak 3"] = ":team_speak:", + ["Telegram"] = ":telegram:", + ["Terminal"] = ":terminal:", + ["终端"] = ":terminal:", + ["Typora"] = ":text:", + ["Microsoft To Do"] = ":things:", + ["Things"] = ":things:", + ["Thunderbird"] = ":thunderbird:", + ["TickTick"] = ":tick_tick:", + ["TIDAL"] = ":tidal:", + ["Tiny RDM"] = ":tinyrdm:", + ["Todoist"] = ":todoist:", + ["Toggl Track"] = ":toggl_track:", + ["Tor Browser"] = ":tor_browser:", + ["Tower"] = ":tower:", + ["Transmit"] = ":transmit:", + ["Trello"] = ":trello:", + ["Tweetbot"] = ":twitter:", + ["Twitter"] = ":twitter:", + ["UTM"] = ":utm:", + ["MacVim"] = ":vim:", + ["Vim"] = ":vim:", + ["VimR"] = ":vim:", + ["Vivaldi"] = ":vivaldi:", + ["VLC"] = ":vlc:", + ["VMware Fusion"] = ":vmware_fusion:", + ["VSCodium"] = ":vscodium:", + ["Warp"] = ":warp:", + ["WebStorm"] = ":web_storm:", + ["微信"] = ":wechat:", + ["WeChat"] = ":wechat:", + ["企业微信"] = ":wecom:", + ["WeCom"] = ":wecom:", + ["WezTerm"] = ":wezterm:", + ["WhatsApp"] = ":whats_app:", + ["‎WhatsApp"] = ":whats_app:", + ["Xcode"] = ":xcode:", + ["Yandex Music"] = ":yandex_music:", + ["Yuque"] = ":yuque:", + ["语雀"] = ":yuque:", + ["Zed"] = ":zed:", + ["Zen Browser"] = ":zen_browser:", + ["Zeplin"] = ":zeplin:", + ["zoom.us"] = ":zoom:", + ["Zotero"] = ":zotero:", + ["Zulip"] = ":zulip:", + ["Zen"] = ":zen_browser:", +} diff --git a/darwin/sketchybar/config/bar.lua b/darwin/sketchybar/config/bar.lua new file mode 100644 index 0000000..0ec641c --- /dev/null +++ b/darwin/sketchybar/config/bar.lua @@ -0,0 +1,23 @@ +#!/usr/bin/env lua + +local colors = require("colors") + +local bar_height = 35 + +sbar.bar({ + blur_radius = 20, + border_color = colors.bar.border, + border_width = 2, + color = colors.bar.bg, + corner_radius = 5, + height = bar_height, + margin = 10, + notch_width = 0, + padding_left = 10, + padding_right = 10, + position = "top", + shadow = true, + sticky = true, + topmost = false, + y_offset = 2, +}) diff --git a/darwin/sketchybar/config/colors.lua b/darwin/sketchybar/config/colors.lua new file mode 100644 index 0000000..ee1f5ae --- /dev/null +++ b/darwin/sketchybar/config/colors.lua @@ -0,0 +1,24 @@ +local function with_alpha(color, alpha) + if alpha > 1.0 or alpha < 0.0 then return color end + return (color & 0x00ffffff) | (math.floor(alpha * 255.0) << 24) +end + +return { + black = 0xff181819, + white = 0xffd3b58d, + red = 0xffF92672, + blue = 0xff66D9EF, + grey = 0xff7f8490, + transparent = 0x00000000, + + bar = { + bg = 0xff072626, + border = 0xffFD971F, + }, + popup = { + bg = with_alpha(0xff072626, 0.6), + border = 0xffFD971F, + }, + + with_alpha = with_alpha, +} diff --git a/darwin/sketchybar/config/icons.lua b/darwin/sketchybar/config/icons.lua new file mode 100644 index 0000000..2e52a91 --- /dev/null +++ b/darwin/sketchybar/config/icons.lua @@ -0,0 +1,92 @@ +local settings = require("settings") + +local icons = { + sf_symbols = { + plus = "􀅼", + loading = "􀖇", + apple = "􀣺", + gear = "􀍟", + cpu = "􀫥", + clipboard = "􀉄", + + switch = { + on = "􁏮", + off = "􁏯", + }, + volume = { + _100="􀊩", + _66="􀊧", + _33="􀊥", + _10="􀊡", + _0="􀊣", + }, + battery = { + _100 = "􀛨", + _75 = "􀺸", + _50 = "􀺶", + _25 = "􀛩", + _0 = "􀛪", + charging = "􀢋" + }, + wifi = { + upload = "􀄨", + download = "􀄩", + connected = "􀙇", + disconnected = "􀙈", + router = "􁓤", + }, + media = { + back = "􀊊", + forward = "􀊌", + play_pause = "􀊈", + }, + }, + + -- Alternative NerdFont icons + nerdfont = { + plus = "", + loading = "", + apple = "", + gear = "", + cpu = "", + clipboard = "Missing Icon", + + switch = { + on = "󱨥", + off = "󱨦", + }, + volume = { + _100="", + _66="", + _33="", + _10="", + _0="", + }, + battery = { + _100 = "", + _75 = "", + _50 = "", + _25 = "", + _0 = "", + charging = "" + }, + wifi = { + upload = "", + download = "", + connected = "󰖩", + disconnected = "󰖪", + router = "Missing Icon" + }, + media = { + back = "", + forward = "", + play_pause = "", + }, + }, +} + +if not (settings.icons == "NerdFont") then + return icons.sf_symbols +else + return icons.nerdfont +end diff --git a/darwin/sketchybar/config/init.lua b/darwin/sketchybar/config/init.lua new file mode 100644 index 0000000..358f18c --- /dev/null +++ b/darwin/sketchybar/config/init.lua @@ -0,0 +1,56 @@ +#!/usr/bin/env lua + +local colors = require("colors") +local settings = require("settings") + +sbar.default({ + updates = "when_shown", + icon = { + font = { + family = settings.font.text, + style = "Bold", + size = 13.0, + }, + color = colors.white, + padding_left = settings.paddings, + padding_right = settings.paddings, + background = { image = { corner_radius = 9 } }, + }, + label = { + font = { + family = settings.font.text, + style = "Semibold", + size = 13.0, + }, + color = colors.white, + padding_left = settings.paddings, + padding_right = settings.paddings, + }, + background = { + height = 30, + corner_radius = 12, + border_width = 1, + border_color = colors.bar.border, + image = { + corner_radius = 8, + border_color = colors.grey, + border_width = 1, + }, + }, + popup = { + background = { + border_width = 1, + corner_radius = 8, + border_color = colors.popup.border, + color = colors.popup.bg, + shadow = { drawing = true }, + }, + blur_radius = 20, + }, + padding_left = 5, + padding_right = 5, + scroll_texts = true, +}) + +require("bar") +require("items") diff --git a/darwin/sketchybar/config/items/aerospace.lua b/darwin/sketchybar/config/items/aerospace.lua new file mode 100644 index 0000000..0dd98f0 --- /dev/null +++ b/darwin/sketchybar/config/items/aerospace.lua @@ -0,0 +1,247 @@ +local Promise = require("promise") +local colors = require("colors") +local utils = require("utils") +local settings = require("settings") +local app_icons = require("app_icons") + +function parse_string_to_table(s) + local result = {} + for line in s:gmatch("([^\n]+)") do + table.insert(result, line) + end + return result +end + +local function getAllWorkspaces() + return utils.sbarExecP("aerospace list-workspaces --all --format '%{workspace}%{monitor-appkit-nsscreen-screens-id}%{monitor-id}%{monitor-name}' --json") +end + +local function getVisibleWorkspaces() + return utils.sbarExecP("aerospace list-workspaces --visible --monitor all --format '%{workspace}%{monitor-appkit-nsscreen-screens-id}%{monitor-id}%{monitor-name}' --json") +end + +local function getAllWindows() + return utils.sbarExecP( + "aerospace list-windows --all --format '%{app-name}%{window-title}%{workspace}%{monitor-id}%{monitor-appkit-nsscreen-screens-id}%{monitor-name}' --json" + ) +end + +local function getMonitorId(obj) + if obj["monitor-appkit-nsscreen-screens-id"] then + return obj["monitor-appkit-nsscreen-screens-id"] + end + return obj["monitor-id"] +end + +local spaces = {} +local space_paddings = {} +local brackets = {} +local state = { + workspaces = {}, + updating = false, +} + +function getState() + local newstate = { + workspaces = {}, + } + + for workspaceid, space in pairs(spaces) do + newstate.workspaces[workspaceid] = { + monitor = 0, + active = false, + empty = true, + apps = {}, + appicons = "", + } + end + + return Promise.all({ getAllWorkspaces(), getVisibleWorkspaces(), getAllWindows() }):thenCall(function(values) + local all, visible, apps = values[1], values[2], values[3] + for _, workspace in ipairs(all) do + local workspaceid = workspace["workspace"] + newstate.workspaces[workspaceid]["id"] = workspaceid + newstate.workspaces[workspaceid]["monitor"] = getMonitorId(workspace) + end + + for _, workspace in ipairs(visible) do + local workspaceid = workspace["workspace"] + newstate.workspaces[workspaceid]["active"] = true + end + + for _, window in ipairs(apps) do + local workspaceid = window["workspace"] + local appname = window["app-name"] + newstate.workspaces[workspaceid]["apps"][appname] = true + newstate.workspaces[workspaceid]["empty"] = false + end + + for workspaceid, workspacestate in pairs(newstate.workspaces) do + local appkeys = {} + for app in pairs(workspacestate["apps"]) do + table.insert(appkeys, app) + end + table.sort(appkeys) + if #appkeys > 0 then + for _, app in ipairs(appkeys) do + local lookup = app_icons[app] + local icon = ((lookup == nil) and app_icons["Default"] or lookup) + workspacestate["appicons"] = workspacestate["appicons"] .. " " .. icon + end + else + workspacestate["appicons"] = "" + end + -- print(utils.dump(workspacestate)) + end + + return newstate + end) +end + +local function updateState() + if not state.updating then + state.updating = true + return getState():thenCall(function(newstate) + state.workspaces = newstate.workspaces + state.updating = false + end) + end + return Promise.reject("State is already updating") +end + + +local function highlightSpace(space, space_padding, space_bracket, selected) + space:set({ + drawing = true, + icon = { highlight = selected, }, + label = { highlight = selected }, + -- background = { border_color = selected and colors.white or colors.bg2 } + }) + space_padding:set({ + drawing = true, + }) + if space_bracket then + space_bracket:set({ + -- background = { border_color = selected and colors.grey or colors.bg2 }, + }) + end +end + +local function onActiveSpaceChange(env) + local focused_workspace = env.FOCUSED_WORKSPACE + local last_workspace = env.PREV_WORKSPACE + -- print("aerospace_workspace_change from " .. last_workspace .. " to " .. focused_workspace) + + local space = spaces[focused_workspace] + local space_padding = space_paddings[focused_workspace] + + local prev_space = spaces[last_workspace] + local prev_space_padding = space_paddings[last_workspace] + + sbar.animate("tanh", 10, function() + highlightSpace(space, space_padding, nil, true) + if state.workspaces[last_workspace]["monitor"] == state.workspaces[focused_workspace]["monitor"] then + highlightSpace(prev_space, prev_space_padding, nil, false) + end + end) + + updateState() +end + +local function syncState() + sbar.animate("tanh", 10, function() + for workspaceid, workspacestate in pairs(state.workspaces) do + if not workspacestate["empty"] then + spaces[workspaceid]:set({ + drawing = true, + display = workspacestate["monitor"], + -- label = { + -- string = workspaceid, + -- highlight = workspacestate["active"], + -- }, + -- icon = { + -- string = workspaceid, + -- color = colors.white, + -- highlight = workspacestate["active"], + -- }, + label = { + string = workspacestate["appicons"], + highlight = workspacestate["active"], + }, + icon = { + highlight = workspacestate["active"], + }, + }) + space_paddings[workspaceid]:set({ drawing = true }) + else + -- These should be hidden + spaces[workspaceid]:set({ + drawing = false, + display = workspacestate["monitor"], + label = workspacestate["appicons"], + }) + space_paddings[workspaceid]:set({ drawing = false }) + end + end + end) +end + +local function updateStateAndSync() + return updateState():thenCall(syncState) +end + +function setup() + getAllWorkspaces():thenCall(function(workspaces) + for _, workspace in ipairs(workspaces) do + local workspaceid = workspace["workspace"] + local display = getMonitorId(workspace) + + local space = sbar.add("item", "space." .. workspaceid, { + drawing = false, -- default to not showing the space -- we'll show if it has windows or is activated + updates = "when_shown", + display = display, + icon = { + string = workspaceid, + color = colors.white, + highlight_color = colors.red, + }, + label = { + padding_right = 12, + color = colors.grey, + highlight_color = colors.white, + font = "sketchybar-app-font:Regular:14.0", + y_offset = -1, + -- drawing = false + }, + padding_left = 1, + padding_right = 1, + click_script = "aerospace workspace " .. workspaceid, + }) + + spaces[workspaceid] = space + + local padding = sbar.add("space", "space.padding." .. space.name, { + drawing = false, + updates = "when_shown", + display = display, + script = "", + width = settings.space_paddings, + }) + space_paddings[workspaceid] = padding + end + end) + :thenCall(function() + local space_window_observer = sbar.add("item", { + drawing = false, + updates = true, + }) + + space_window_observer:subscribe("aerospace_workspace_change", onActiveSpaceChange) + space_window_observer:subscribe("space_windows_change", updateStateAndSync) + space_window_observer:subscribe("system_woke", updateStateAndSync) + space_window_observer:subscribe("front_app_switched", updateStateAndSync) + end) + :thenCall(updateStateAndSync) +end + +setup() diff --git a/darwin/sketchybar/config/items/apple.lua b/darwin/sketchybar/config/items/apple.lua new file mode 100644 index 0000000..e48a993 --- /dev/null +++ b/darwin/sketchybar/config/items/apple.lua @@ -0,0 +1,36 @@ +local colors = require("colors") +local icons = require("icons") +local settings = require("settings") + +-- Padding item required because of bracket +sbar.add("item", { width = 5 }) + +local apple = sbar.add("item", { + icon = { + font = { size = 16.0 }, + string = icons.apple, + padding_right = settings.paddings, + padding_left = 0, + }, + label = { drawing = false }, + background = { + color = colors.bg2, + -- border_color = colors.black, + -- border_width = 1 + }, + padding_left = 1, + padding_right = 1, + click_script = "sk-menus -s 0" +}) + +-- Double border for apple using a single item bracket +-- sbar.add("bracket", { apple.name }, { +-- background = { +-- color = colors.transparent, +-- height = 30, +-- -- border_color = colors.grey, +-- } +-- }) + +-- Padding item required because of bracket +sbar.add("item", { width = 7 }) diff --git a/darwin/sketchybar/config/items/battery.lua b/darwin/sketchybar/config/items/battery.lua new file mode 100644 index 0000000..4f9b927 --- /dev/null +++ b/darwin/sketchybar/config/items/battery.lua @@ -0,0 +1,45 @@ +local icons = require("icons") + +local battery = sbar.add("item", { + position = "right", + icon = { + font = { + style = "Regular", + size = 19.0, + } + }, + label = { drawing = false }, + update_freq = 120, +}) + +local function battery_update() + sbar.exec("pmset -g batt", function(batt_info) + local icon = "!" + + if (string.find(batt_info, 'AC Power')) then + icon = icons.battery.charging + else + local found, _, charge = batt_info:find("(%d+)%%") + if found then + charge = tonumber(charge) + end + + if found and charge > 80 then + icon = icons.battery._100 + elseif found and charge > 60 then + icon = icons.battery._75 + elseif found and charge > 40 then + icon = icons.battery._50 + elseif found and charge > 20 then + icon = icons.battery._25 + else + icon = icons.battery._0 + end + end + + battery:set({ icon = icon }) + end) +end + + +battery:subscribe({"routine", "power_source_change", "system_woke"}, battery_update) diff --git a/darwin/sketchybar/config/items/cal.lua b/darwin/sketchybar/config/items/cal.lua new file mode 100644 index 0000000..cd6672d --- /dev/null +++ b/darwin/sketchybar/config/items/cal.lua @@ -0,0 +1,44 @@ +local colors = require("colors") +local settings = require("settings") + +-- Padding item required because of bracket +sbar.add("item", { position = "right", width = settings.group_paddings }) + +local cal = sbar.add("item", { + icon = { + color = colors.white, + padding_left = 0, + font = { size = 13 }, + }, + label = { + color = colors.white, + padding_right = 0, + align = "right", + }, + position = "right", + update_freq = 30, + padding_left = 0, + padding_right = 0, + click_script = "open -n -a Calendar", + -- background = { + -- color = colors.bg1, + -- border_color = colors.bar.border, + -- border_width = 1, + -- }, +}) + +-- Double border for calendar using a single item bracket +-- sbar.add("bracket", { cal.name }, { +-- background = { +-- color = colors.transparent, +-- height = 30, +-- border_color = colors.bar.bg, +-- }, +-- }) + +-- Padding item required because of bracket +sbar.add("item", { position = "right", width = settings.group_paddings }) + +cal:subscribe({ "forced", "routine", "system_woke" }, function(env) + cal:set({ icon = os.date("􀉉 %B %d %a"), label = os.date("􀐫 %H:%M") }) +end) diff --git a/darwin/sketchybar/config/items/cpu.lua b/darwin/sketchybar/config/items/cpu.lua new file mode 100644 index 0000000..748ed29 --- /dev/null +++ b/darwin/sketchybar/config/items/cpu.lua @@ -0,0 +1,69 @@ +local icons = require("icons") +local colors = require("colors") +local settings = require("settings") + +-- Execute the event provider binary which provides the event "cpu_update" for +-- the cpu load data, which is fired every 2.0 seconds. +sbar.exec("killall sk-cpu-load >/dev/null; sk-cpu-load cpu_update 2.0") + +local cpu = sbar.add("graph", "widgets.cpu" , 42, { + position = "right", + graph = { color = colors.blue }, + background = { + height = 22, + color = { alpha = 0 }, + border_color = { alpha = 0 }, + drawing = true, + }, + icon = { string = icons.cpu }, + label = { + string = "cpu ??%", + font = { + family = settings.font.numbers, + style = settings.font.style_map["Bold"], + size = 9.0, + }, + align = "right", + padding_right = 0, + width = 0, + y_offset = 4 + }, + padding_right = settings.paddings + 6 +}) + +cpu:subscribe("cpu_update", function(env) + -- Also available: env.user_load, env.sys_load + local load = tonumber(env.total_load) + cpu:push({ load / 100. }) + + local color = colors.blue + if load > 30 then + if load < 60 then + color = colors.yellow + elseif load < 80 then + color = colors.orange + else + color = colors.red + end + end + + cpu:set({ + graph = { color = color }, + label = "cpu " .. env.total_load .. "%", + }) +end) + +cpu:subscribe("mouse.clicked", function(env) + sbar.exec("open -a 'Activity Monitor'") +end) + +-- Background around the cpu item +sbar.add("bracket", "widgets.cpu.bracket", { cpu.name }, { + background = { color = colors.bg1 } +}) + +-- Background around the cpu item +sbar.add("item", "widgets.cpu.padding", { + position = "right", + width = settings.group_paddings +}) diff --git a/darwin/sketchybar/config/items/front_app.lua b/darwin/sketchybar/config/items/front_app.lua new file mode 100644 index 0000000..f4bacd9 --- /dev/null +++ b/darwin/sketchybar/config/items/front_app.lua @@ -0,0 +1,28 @@ +local settings = require("settings") + +local front_app = sbar.add("item", { + icon = { + drawing = false + }, + label = { + font = { + style = settings.font.style_map["Bold"], + size = 12.0, + } + } +}) + +front_app:subscribe("front_app_switched", function(env) + front_app:set({ + label = { + string = env.INFO:upper() + } + }) + + -- Or equivalently: + -- sbar.set(env.NAME, { + -- label = { + -- string = env.INFO + -- } + -- }) +end) diff --git a/darwin/sketchybar/config/items/init.lua b/darwin/sketchybar/config/items/init.lua new file mode 100644 index 0000000..1ea8912 --- /dev/null +++ b/darwin/sketchybar/config/items/init.lua @@ -0,0 +1,9 @@ +require("items.aerospace") +require("items.apple") +require("items.menu") +require("items.front_app") +require("items.battery") +require("items.cal") +require("items.volume") +require("items.cpu") +require("items.wifi") diff --git a/darwin/sketchybar/config/items/menu.lua b/darwin/sketchybar/config/items/menu.lua new file mode 100644 index 0000000..064334b --- /dev/null +++ b/darwin/sketchybar/config/items/menu.lua @@ -0,0 +1,76 @@ +local colors = require("colors") +local icons = require("icons") +local settings = require("settings") + +local menu_watcher = sbar.add("item", { + drawing = false, + updates = false, +}) +local space_menu_swap = sbar.add("item", { + drawing = false, + updates = true, +}) +sbar.add("event", "swap_menus_and_spaces") + +local max_items = 15 +local menu_items = {} +for i = 1, max_items, 1 do + local menu = sbar.add("item", "menu." .. i, { + padding_left = settings.paddings, + padding_right = settings.paddings, + drawing = false, + icon = { drawing = false }, + label = { + font = { + style = settings.font.style_map[i == 1 and "Heavy" or "Semibold"] + }, + padding_left = 6, + padding_right = 6, + }, + click_script = "sk-menus -s " .. i, + }) + + menu_items[i] = menu +end + +sbar.add("bracket", { '/menu\\..*/' }, { + background = { color = colors.bg1 } +}) + +local menu_padding = sbar.add("item", "menu.padding", { + drawing = false, + width = 5 +}) + +local function update_menus(env) + sbar.exec("sk-menus -l", function(menus) + sbar.set('/menu\\..*/', { drawing = false }) + menu_padding:set({ drawing = true }) + id = 1 + for menu in string.gmatch(menus, '[^\r\n]+') do + if id < max_items then + menu_items[id]:set( { label = menu, drawing = true } ) + else break end + id = id + 1 + end + end) +end + +menu_watcher:subscribe("front_app_switched", update_menus) + +space_menu_swap:subscribe("swap_menus_and_spaces", function(env) + local drawing = menu_items[1]:query().geometry.drawing == "on" + if drawing then + menu_watcher:set( { updates = false }) + sbar.set("/menu\\..*/", { drawing = false }) + sbar.set("/space\\..*/", { drawing = true }) + sbar.set("front_app", { drawing = true }) + else + menu_watcher:set( { updates = true }) + sbar.set("/space\\..*/", { drawing = false }) + sbar.set("front_app", { drawing = false }) + update_menus() + end +end) + +return menu_watcher diff --git a/darwin/sketchybar/config/items/volume.lua b/darwin/sketchybar/config/items/volume.lua new file mode 100644 index 0000000..b338310 --- /dev/null +++ b/darwin/sketchybar/config/items/volume.lua @@ -0,0 +1,79 @@ +local colors = require("colors") +local icons = require("icons") + +local volume_slider = sbar.add("slider", 100, { + position = "right", + updates = true, + label = { drawing = false }, + icon = { drawing = false }, + slider = { + highlight_color = colors.blue, + width = 0, + background = { + height = 6, + corner_radius = 3, + color = colors.bg2, + }, + knob= { + string = "􀀁", + drawing = false, + }, + }, +}) + +local volume_icon = sbar.add("item", { + position = "right", + icon = { + string = icons.volume._100, + width = 0, + align = "left", + color = colors.grey, + font = { + style = "Regular", + size = 14.0, + }, + }, + label = { + width = 25, + align = "left", + font = { + style = "Regular", + size = 14.0, + }, + }, +}) + +volume_slider:subscribe("mouse.clicked", function(env) + sbar.exec("osascript -e 'set volume output volume " .. env["PERCENTAGE"] .. "'") +end) + +volume_slider:subscribe("volume_change", function(env) + local volume = tonumber(env.INFO) + local icon = icons.volume._0 + if volume > 60 then + icon = icons.volume._100 + elseif volume > 30 then + icon = icons.volume._66 + elseif volume > 10 then + icon = icons.volume._33 + elseif volume > 0 then + icon = icons.volume._10 + end + + volume_icon:set({ label = icon }) + volume_slider:set({ slider = { percentage = volume } }) +end) + +local function animate_slider_width(width) + sbar.animate("tanh", 30.0, function() + volume_slider:set({ slider = { width = width }}) + end) +end + +volume_icon:subscribe("mouse.clicked", function() + if tonumber(volume_slider:query().slider.width) > 0 then + animate_slider_width(0) + else + animate_slider_width(100) + end +end) diff --git a/darwin/sketchybar/config/items/wifi.lua b/darwin/sketchybar/config/items/wifi.lua new file mode 100644 index 0000000..bd8f20c --- /dev/null +++ b/darwin/sketchybar/config/items/wifi.lua @@ -0,0 +1,234 @@ +local icons = require("icons") +local colors = require("colors") +local settings = require("settings") + +-- Execute the event provider binary which provides the event "network_update" +-- for the network interface "en0", which is fired every 2.0 seconds. +sbar.exec("killall sk-network-load >/dev/null; sk-network-load en0 network_update 2.0") + +local popup_width = 250 + +local wifi_up = sbar.add("item", "widgets.wifi1", { + position = "right", + padding_left = -5, + width = 0, + icon = { + padding_right = 0, + font = { + style = settings.font.style_map["Bold"], + size = 9.0, + }, + string = icons.wifi.upload, + }, + label = { + font = { + family = settings.font.numbers, + style = settings.font.style_map["Bold"], + size = 9.0, + }, + color = colors.red, + string = "??? Bps", + }, + y_offset = 4, +}) + +local wifi_down = sbar.add("item", "widgets.wifi2", { + position = "right", + padding_left = -5, + icon = { + padding_right = 0, + font = { + style = settings.font.style_map["Bold"], + size = 9.0, + }, + string = icons.wifi.download, + }, + label = { + font = { + family = settings.font.numbers, + style = settings.font.style_map["Bold"], + size = 9.0, + }, + color = colors.blue, + string = "??? Bps", + }, + y_offset = -4, +}) + +local wifi = sbar.add("item", "widgets.wifi.padding", { + position = "right", + label = { drawing = false }, +}) + +-- Background around the item +local wifi_bracket = sbar.add("bracket", "widgets.wifi.bracket", { + wifi.name, + wifi_up.name, + wifi_down.name +}, { + -- background = { color = colors.bar.bg }, + popup = { align = "center", height = 30 } +}) + +local ssid = sbar.add("item", { + position = "popup." .. wifi_bracket.name, + icon = { + font = { + style = settings.font.style_map["Bold"] + }, + string = icons.wifi.router, + }, + width = popup_width, + align = "center", + label = { + font = { + size = 15, + style = settings.font.style_map["Bold"] + }, + max_chars = 18, + string = "????????????", + }, + background = { + height = 2, + color = colors.bar.bg, + y_offset = -15 + } +}) + +local hostname = sbar.add("item", { + position = "popup." .. wifi_bracket.name, + icon = { + align = "left", + string = "Hostname:", + width = popup_width / 2, + }, + label = { + max_chars = 20, + string = "????????????", + width = popup_width / 2, + align = "right", + } +}) + +local ip = sbar.add("item", { + position = "popup." .. wifi_bracket.name, + icon = { + align = "left", + string = "IP:", + width = popup_width / 2, + }, + label = { + string = "???.???.???.???", + width = popup_width / 2, + align = "right", + } +}) + +local mask = sbar.add("item", { + position = "popup." .. wifi_bracket.name, + icon = { + align = "left", + string = "Subnet mask:", + width = popup_width / 2, + }, + label = { + string = "???.???.???.???", + width = popup_width / 2, + align = "right", + } +}) + +local router = sbar.add("item", { + position = "popup." .. wifi_bracket.name, + icon = { + align = "left", + string = "Router:", + width = popup_width / 2, + }, + label = { + string = "???.???.???.???", + width = popup_width / 2, + align = "right", + }, +}) + +sbar.add("item", { position = "right", width = settings.group_paddings }) + +wifi_up:subscribe("network_update", function(env) + local up_color = (env.upload == "000 Bps") and colors.grey or colors.red + local down_color = (env.download == "000 Bps") and colors.grey or colors.blue + wifi_up:set({ + icon = { color = up_color }, + label = { + string = env.upload, + color = up_color + } + }) + wifi_down:set({ + icon = { color = down_color }, + label = { + string = env.download, + color = down_color + } + }) +end) + +wifi:subscribe({"wifi_change", "system_woke"}, function(env) + sbar.exec("ipconfig getifaddr en0", function(ip) + local connected = not (ip == "") + wifi:set({ + icon = { + string = connected and icons.wifi.connected or icons.wifi.disconnected, + color = connected and colors.white or colors.red, + }, + }) + end) +end) + +local function hide_details() + wifi_bracket:set({ popup = { drawing = false } }) +end + +local function toggle_details() + local should_draw = wifi_bracket:query().popup.drawing == "off" + if should_draw then + wifi_bracket:set({ popup = { drawing = true }}) + sbar.exec("networksetup -getcomputername", function(result) + hostname:set({ label = result }) + end) + sbar.exec("ipconfig getifaddr en0", function(result) + ip:set({ label = result }) + end) + sbar.exec("ipconfig getsummary en0 | awk -F ' SSID : ' '/ SSID : / {print $2}'", function(result) + ssid:set({ label = result }) + end) + sbar.exec("networksetup -getinfo Wi-Fi | awk -F 'Subnet mask: ' '/^Subnet mask: / {print $2}'", function(result) + mask:set({ label = result }) + end) + sbar.exec("networksetup -getinfo Wi-Fi | awk -F 'Router: ' '/^Router: / {print $2}'", function(result) + router:set({ label = result }) + end) + else + hide_details() + end +end + +wifi_up:subscribe("mouse.clicked", toggle_details) +wifi_down:subscribe("mouse.clicked", toggle_details) +wifi:subscribe("mouse.clicked", toggle_details) +wifi:subscribe("mouse.exited.global", hide_details) + +local function copy_label_to_clipboard(env) + local label = sbar.query(env.NAME).label.value + sbar.exec("echo \"" .. label .. "\" | pbcopy") + sbar.set(env.NAME, { label = { string = icons.clipboard, align="center" } }) + sbar.delay(1, function() + sbar.set(env.NAME, { label = { string = label, align = "right" } }) + end) +end + +ssid:subscribe("mouse.clicked", copy_label_to_clipboard) +hostname:subscribe("mouse.clicked", copy_label_to_clipboard) +ip:subscribe("mouse.clicked", copy_label_to_clipboard) +mask:subscribe("mouse.clicked", copy_label_to_clipboard) +router:subscribe("mouse.clicked", copy_label_to_clipboard) diff --git a/darwin/sketchybar/config/settings.lua b/darwin/sketchybar/config/settings.lua new file mode 100644 index 0000000..621936c --- /dev/null +++ b/darwin/sketchybar/config/settings.lua @@ -0,0 +1,19 @@ +#!/usr/bin/env lua + +return { + icons = "sf-symbols", + font = { + text = "SF Pro", + numbers = "SF Pro", + style_map = { + ["Regular"] = "Regular", + ["Semibold"] = "Semibold", + ["Bold"] = "Bold", + ["Heavy"] = "Heavy", + ["Black"] = "Black", + }, + }, + paddings = 3, + group_paddings = 0, + space_paddings = 5, +} diff --git a/darwin/sketchybar/config/utils.lua b/darwin/sketchybar/config/utils.lua new file mode 100644 index 0000000..12c3163 --- /dev/null +++ b/darwin/sketchybar/config/utils.lua @@ -0,0 +1,41 @@ +local Promise = require("promise") + +local M = {} + +function M.dump(o) + if type(o) == "table" then + local s = "{" + for k, v in pairs(o) do + if type(k) ~= "number" then + k = '"' .. k .. '"' + end + s = s .. " [" .. k .. "] = " .. M.dump(v) .. "," + end + return s .. "} " + else + return tostring(o) + end +end + +local function onErrorP(reason) + print("Error found: " .. (reason and M.dump(reason) or "unknown")) +end + +-- https://github.com/Tnixc/nix-config/blob/main/home/programs/aerospace-sketchybar/sbar-config-libs/items/aerospaces.lua +function M.sbarExecP(cmd) + return Promise.new(function(resolve, failfunc) + sbar.exec(cmd, function(result, exit_code) + if exit_code ~= 0 then + if failfunc ~= nil then + failfunc(string.format("Exit Code: %s Message: %s", tostring(exit_code), M.dump(result))) + end + else + if resolve ~= nil then + resolve(result) + end + end + end) + end):catch(onErrorP) +end + +return M diff --git a/darwin/sketchybar/default.nix b/darwin/sketchybar/default.nix new file mode 100644 index 0000000..dd7faf8 --- /dev/null +++ b/darwin/sketchybar/default.nix @@ -0,0 +1,88 @@ +{ + pkgs, + lib, + config, + dots, + home-dir, + ... +}: + +let + cfg = config.custom.sketchybar; + sketchybar = lib.getExe pkgs.sketchybar; + lua = pkgs.lua5_4.withPackages ( + ps: with ps; [ + pkgs.custom.sbarlua + pkgs.custom.promise-lua + luafilesystem + ] + ); +in +{ + options.custom = with lib; { + sketchybar = { + enable = mkEnableOption "Enable sketchybar"; + logFile = mkOption { + type = types.str; + default = "${home-dir}/Library/Logs/sketchybar.log"; + description = "Filepath of log output"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.sketchybar = { + enable = true; + extraPackages = with pkgs; [ + custom.sk-utils + # sketchybar-app-font + ]; + }; + + hm.home.shellAliases = { + restart-sketchybar = ''launchctl kickstart -k gui/"$(id -u)"/org.nixos.sketchybar''; + }; + + hm.home.packages = with pkgs; [ + sketchybar-app-font + ]; + + hm.xdg.configFile = { + "sketchybar/sketchybarrc" = { + executable = true; + text = # Lua + '' + #!${lua}/bin/lua + + -- Add the sketchybar module to the package cpath (the module could be + -- installed into the default search path then this would not be needed) + package.path = "${dots}/darwin/sketchybar/config/?.lua;${dots}/darwin/sketchybar/config/?/?.lua;${dots}/darwin/sketchybar/config/?/init.lua;" .. package.path + + sbar = require("sketchybar") + sbar.exec("killall sketchyhelper || sketchyhelper git.felix.sketchyhelper >/dev/null 2>&1 &") + sbar.begin_config() + require("init") + sbar.hotload(true) + sbar.end_config() + sbar.event_loop() + ''; + }; + }; + + services.aerospace.settings.exec-on-workspace-change = lib.mkIf config.custom.aerospace.enable [ + "/bin/bash" + "-c" + "${sketchybar} --trigger aerospace_workspace_change FOCUSED_WORKSPACE=$AEROSPACE_FOCUSED_WORKSPACE PREV_WORKSPACE=$AEROSPACE_PREV_WORKSPACE" + ]; + + launchd.user.agents.sketchybar.serviceConfig = { + StandardErrorPath = cfg.logFile; + StandardOutPath = cfg.logFile; + KeepAlive = lib.mkForce { + PathState = { + "/run/current-system/sw/bin/sketchybar" = true; + }; + }; + }; + }; +} diff --git a/home/default.nix b/home/default.nix index 5f1aacf..cce2d84 100644 --- a/home/default.nix +++ b/home/default.nix @@ -28,6 +28,7 @@ sd ugrep unzip + fastfetch # gpclient openconnect fd diff --git a/home/gui.nix b/home/gui.nix index 1ec65d5..2dbd14a 100644 --- a/home/gui.nix +++ b/home/gui.nix @@ -103,8 +103,6 @@ (lib.mkIf config.custom.gui.darwin.enable { home.packages = with pkgs; [ - raycast - custom.whatsapp-for-mac ]; }) diff --git a/hosts/dango/default.nix b/hosts/dango/default.nix index c78489b..d271c3f 100644 --- a/hosts/dango/default.nix +++ b/hosts/dango/default.nix @@ -24,6 +24,8 @@ custom = { aerospace.enable = true; + sketchybar.enable = true; + jankyborders.enable = true; brew = { zen-browser = true; webex = true; @@ -33,6 +35,11 @@ ghostty = true; ms-office = true; ms-teams = true; + raycast = true; + whatsapp = true; + vscode = true; + firefox = true; + chromium = true; }; }; @@ -43,8 +50,6 @@ default.enable = true; darwin.enable = true; ghostty.enable = true; - firefox.enable = true; - vscode.enable = true; }; }; } diff --git a/hosts/default.nix b/hosts/default.nix index e53d7ad..e4f4d9b 100644 --- a/hosts/default.nix +++ b/hosts/default.nix @@ -33,6 +33,7 @@ let specialArgs = specialArgs // { inherit host user; dots = "/home/${user}/dotfiles"; + home-dir = "/home/${user}"; }; modules = [ @@ -52,7 +53,6 @@ let inherit host user system; system-font = "Consolas"; dots = "/home/${user}/dotfiles"; - home-dir = config.home-manager.users.${user}.home.homeDirectory; }; users.${user} = { @@ -107,6 +107,7 @@ let specialArgs = specialArgs // { inherit host user; dots = "/Users/${user}/dotfiles"; + home-dir = "/Users/${user}"; }; modules = [ diff --git a/packages/default.nix b/packages/default.nix index fd23ac8..ce375bd 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -13,4 +13,8 @@ rec { whatsapp-for-mac = callPackage ./whatsapp-for-mac { }; sf-symbols = callPackage ./sf-symbols { full = true; }; sf-mono = callPackage ./sf-mono { }; + ps2pdfcrop = callPackage ./ps2pdfcrop { }; + sbarlua = callPackage ./sbarlua { }; + sk-utils = callPackage ./sk-utils { }; + promise-lua = callPackage ./promise-lua { }; } diff --git a/packages/promise-lua/default.nix b/packages/promise-lua/default.nix new file mode 100644 index 0000000..a0d2beb --- /dev/null +++ b/packages/promise-lua/default.nix @@ -0,0 +1,30 @@ +# https://github.com/Tnixc/nix-config/blob/main/home/programs/aerospace-sketchybar/flake.nix +{ + clang, + fetchFromGitHub, + fetchurl, + gcc, + readline, + lua5_4, + lua54Packages, + stdenv, + darwin, + lib, + luaPackages, +}: +lua54Packages.buildLuarocksPackage rec { + pname = "promise-lua"; + version = "0.4.1-1"; + name = "${pname}-${version}"; + src = fetchFromGitHub { + owner = "pyericz"; + repo = "promise-lua"; + rev = "20d2ab1bb6aa398fe06ba597c943077a72c22451"; + sha256 = "sha256-LNNLTAAEPIalRSjC6z8QTkhnmwpFAHSlwxuFxb9r2OE="; + }; + knownRockspec = + (fetchurl { + url = "https://luarocks.org/manifests/pyericz/promise-lua-0.4.1-1.rockspec"; + sha256 = "sha256-P/HP015RE/GUfarez/ezMuOflhXYPRTwbPdmz5J6qGE="; + }).outPath; +} diff --git a/packages/ps2pdfcrop/default.nix b/packages/ps2pdfcrop/default.nix new file mode 100644 index 0000000..dd38579 --- /dev/null +++ b/packages/ps2pdfcrop/default.nix @@ -0,0 +1,48 @@ +{ + bash, + nix, + resholve, + ghostscript, + substituteAll, + isNixOS ? false, +}: +resholve.mkDerivation rec { + pname = "ps2pdfcrop"; + + version = "0.0.1"; + + src = substituteAll { + src = ./ps2pdfcrop.sh; + ps2pdf = "${ghostscript}/bin/ps2pdf"; + }; + + dontUnpack = true; + + installPhase = '' + runHook preInstall + + install -Dm755 "$src" "$out/bin/${pname}" + + runHook postInstall + ''; + + solutions = { + ps2pdfcrop = { + scripts = [ "bin/${pname}" ]; + interpreter = "${bash}/bin/bash"; + inputs = [ ghostscript ]; + keep = { + "$PS2PDF" = true; + }; + fake = { + external = [ + # https://github.com/abathur/resholve/issues/29 + ]; + }; + execer = [ + "cannot:${nix}/bin/nix-store" + "cannot:${nix}/bin/nix-collect-garbage" + ]; + }; + }; +} diff --git a/packages/ps2pdfcrop/ps2pdfcrop.sh b/packages/ps2pdfcrop/ps2pdfcrop.sh new file mode 100644 index 0000000..ca8a6b3 --- /dev/null +++ b/packages/ps2pdfcrop/ps2pdfcrop.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +readonly PS2PDF=@ps2pdf@ + +"$PS2PDF" -dEPSCrop -dALLOWPSTRANSPARENCY $@ diff --git a/packages/sbarlua/default.nix b/packages/sbarlua/default.nix new file mode 100644 index 0000000..fe6d945 --- /dev/null +++ b/packages/sbarlua/default.nix @@ -0,0 +1,78 @@ +# https://github.com/khaneliman/khanelinix/blob/7703e485a2c7431f63004321da9e02ff7e06eb0b/packages/sbarlua/default.nix + +# { +# clang, +# fetchFromGitHub, +# gcc, +# readline, +# lua, +# }: +# lua.stdenv.mkDerivation rec { +# pname = "SBarLua"; +# version = "0-unstable-2024-08-12"; +# +# name = "lua${lua.luaversion}-" + pname + "-" + version; +# +# src = fetchFromGitHub { +# owner = "FelixKratz"; +# repo = "SbarLua"; +# rev = "437bd2031da38ccda75827cb7548e7baa4aa9978"; +# hash = "sha256-F0UfNxHM389GhiPQ6/GFbeKQq5EvpiqQdvyf7ygzkPg="; +# }; +# +# nativeBuildInputs = [ +# clang +# gcc +# ]; +# +# buildInputs = [ readline ]; +# +# propagatedBuildInputs = [ lua ]; +# +# makeFlags = [ +# "PREFIX=$(out)" +# "LUA_INC=-I${lua}/include" +# "LUA_LIBDIR=$(out)/lib/lua/${lua.luaversion}" +# "LUA_VERSION=${lua.luaversion}" +# ]; +# +# installPhase = '' +# mkdir -p $out/lib/lua/${lua.luaversion}/ +# cp -r bin/* "$out/lib/lua/${lua.luaversion}/" +# ''; +# } + +# https://github.com/Tnixc/nix-config/blob/main/home/programs/aerospace-sketchybar/flake.nix +{ + clang, + fetchFromGitHub, + gcc, + readline, + lua5_4, + lua54Packages, + stdenv, + darwin, + lib, + luaPackages, +}: +lua54Packages.buildLuaPackage rec { + pname = "sbar"; + version = "0-unstable-2024-08-12"; + name = "lua${lua5_4.luaversion}-" + pname + "-" + version; + src = fetchFromGitHub { + owner = "FelixKratz"; + repo = "SbarLua"; + rev = "437bd2031da38ccda75827cb7548e7baa4aa9978"; + hash = "sha256-F0UfNxHM389GhiPQ6/GFbeKQq5EvpiqQdvyf7ygzkPg="; + }; + installPhase = '' + mkdir -p $out/lib/lua/${lua5_4.luaversion} + cp bin/sketchybar.so $out/lib/lua/${lua5_4.luaversion}/ + ''; + nativeBuildInputs = [ + gcc + readline + clang + stdenv + ] ++ lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ CoreFoundation ]); +} diff --git a/packages/sk-utils/default.nix b/packages/sk-utils/default.nix new file mode 100644 index 0000000..c0da1f3 --- /dev/null +++ b/packages/sk-utils/default.nix @@ -0,0 +1,23 @@ +{ + stdenv, + clang, + gnumake, +}: +stdenv.mkDerivation { + pname = "sk-utils"; + version = "1.0"; + src = ./src; + buildInputs = [ + clang + gnumake + ]; + buildPhase = '' + make + ''; + installPhase = '' + mkdir -p $out/bin + cp event_providers/cpu_load/bin/cpu_load $out/bin/sk-cpu-load + cp event_providers/network_load/bin/network_load $out/bin/sk-network-load + cp menus/bin/menus $out/bin/sk-menus + ''; +} diff --git a/packages/sk-utils/src/event_providers/cpu_load/cpu.h b/packages/sk-utils/src/event_providers/cpu_load/cpu.h new file mode 100644 index 0000000..413f70f --- /dev/null +++ b/packages/sk-utils/src/event_providers/cpu_load/cpu.h @@ -0,0 +1,58 @@ +#include +#include +#include +#include + +struct cpu { + host_t host; + mach_msg_type_number_t count; + host_cpu_load_info_data_t load; + host_cpu_load_info_data_t prev_load; + bool has_prev_load; + + int user_load; + int sys_load; + int total_load; +}; + +static inline void cpu_init(struct cpu* cpu) { + cpu->host = mach_host_self(); + cpu->count = HOST_CPU_LOAD_INFO_COUNT; + cpu->has_prev_load = false; +} + +static inline void cpu_update(struct cpu* cpu) { + kern_return_t error = host_statistics(cpu->host, + HOST_CPU_LOAD_INFO, + (host_info_t)&cpu->load, + &cpu->count ); + + if (error != KERN_SUCCESS) { + printf("Error: Could not read cpu host statistics.\n"); + return; + } + + if (cpu->has_prev_load) { + uint32_t delta_user = cpu->load.cpu_ticks[CPU_STATE_USER] + - cpu->prev_load.cpu_ticks[CPU_STATE_USER]; + + uint32_t delta_system = cpu->load.cpu_ticks[CPU_STATE_SYSTEM] + - cpu->prev_load.cpu_ticks[CPU_STATE_SYSTEM]; + + uint32_t delta_idle = cpu->load.cpu_ticks[CPU_STATE_IDLE] + - cpu->prev_load.cpu_ticks[CPU_STATE_IDLE]; + + cpu->user_load = (double)delta_user / (double)(delta_system + + delta_user + + delta_idle) * 100.0; + + cpu->sys_load = (double)delta_system / (double)(delta_system + + delta_user + + delta_idle) * 100.0; + + cpu->total_load = cpu->user_load + cpu->sys_load; + } + + cpu->prev_load = cpu->load; + cpu->has_prev_load = true; +} diff --git a/packages/sk-utils/src/event_providers/cpu_load/cpu_load.c b/packages/sk-utils/src/event_providers/cpu_load/cpu_load.c new file mode 100644 index 0000000..ee97613 --- /dev/null +++ b/packages/sk-utils/src/event_providers/cpu_load/cpu_load.c @@ -0,0 +1,41 @@ +#include "cpu.h" +#include "../sketchybar.h" + +int main (int argc, char** argv) { + float update_freq; + if (argc < 3 || (sscanf(argv[2], "%f", &update_freq) != 1)) { + printf("Usage: %s \"\" \"\"\n", argv[0]); + exit(1); + } + + alarm(0); + struct cpu cpu; + cpu_init(&cpu); + + // Setup the event in sketchybar + char event_message[512]; + snprintf(event_message, 512, "--add event '%s'", argv[1]); + sketchybar(event_message); + + char trigger_message[512]; + for (;;) { + // Acquire new info + cpu_update(&cpu); + + // Prepare the event message + snprintf(trigger_message, + 512, + "--trigger '%s' user_load='%d' sys_load='%02d' total_load='%02d'", + argv[1], + cpu.user_load, + cpu.sys_load, + cpu.total_load ); + + // Trigger the event + sketchybar(trigger_message); + + // Wait + usleep(update_freq * 1000000); + } + return 0; +} diff --git a/packages/sk-utils/src/event_providers/cpu_load/makefile b/packages/sk-utils/src/event_providers/cpu_load/makefile new file mode 100644 index 0000000..6366a0f --- /dev/null +++ b/packages/sk-utils/src/event_providers/cpu_load/makefile @@ -0,0 +1,5 @@ +bin/cpu_load: cpu_load.c cpu.h ../sketchybar.h | bin + clang -std=c99 -O3 $< -o $@ + +bin: + mkdir bin diff --git a/packages/sk-utils/src/event_providers/makefile b/packages/sk-utils/src/event_providers/makefile new file mode 100644 index 0000000..8c1ca39 --- /dev/null +++ b/packages/sk-utils/src/event_providers/makefile @@ -0,0 +1,3 @@ +all: + (cd cpu_load && $(MAKE)) + (cd network_load && $(MAKE)) diff --git a/packages/sk-utils/src/event_providers/network_load/makefile b/packages/sk-utils/src/event_providers/network_load/makefile new file mode 100644 index 0000000..e464482 --- /dev/null +++ b/packages/sk-utils/src/event_providers/network_load/makefile @@ -0,0 +1,5 @@ +bin/network_load: network_load.c network.h ../sketchybar.h | bin + clang -std=c99 -O3 $< -o $@ + +bin: + mkdir bin diff --git a/packages/sk-utils/src/event_providers/network_load/network.h b/packages/sk-utils/src/event_providers/network_load/network.h new file mode 100644 index 0000000..25175e5 --- /dev/null +++ b/packages/sk-utils/src/event_providers/network_load/network.h @@ -0,0 +1,90 @@ +#include +#include +#include +#include +#include +#include +#include + +static char unit_str[3][6] = { { " Bps" }, { "KBps" }, { "MBps" }, }; + +enum unit { + UNIT_BPS, + UNIT_KBPS, + UNIT_MBPS +}; +struct network { + uint32_t row; + struct ifmibdata data; + struct timeval tv_nm1, tv_n, tv_delta; + + int up; + int down; + enum unit up_unit, down_unit; +}; + +static inline void ifdata(uint32_t net_row, struct ifmibdata* data) { + static size_t size = sizeof(struct ifmibdata); + static int32_t data_option[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_IFDATA, 0, IFDATA_GENERAL }; + data_option[4] = net_row; + sysctl(data_option, 6, data, &size, NULL, 0); +} + +static inline void network_init(struct network* net, char* ifname) { + memset(net, 0, sizeof(struct network)); + + static int count_option[] = { CTL_NET, PF_LINK, NETLINK_GENERIC, IFMIB_SYSTEM, IFMIB_IFCOUNT }; + uint32_t interface_count = 0; + size_t size = sizeof(uint32_t); + sysctl(count_option, 5, &interface_count, &size, NULL, 0); + + for (int i = 0; i < interface_count; i++) { + ifdata(i, &net->data); + if (strcmp(net->data.ifmd_name, ifname) == 0) { + net->row = i; + break; + } + } +} + +static inline void network_update(struct network* net) { + gettimeofday(&net->tv_n, NULL); + timersub(&net->tv_n, &net->tv_nm1, &net->tv_delta); + net->tv_nm1 = net->tv_n; + + uint64_t ibytes_nm1 = net->data.ifmd_data.ifi_ibytes; + uint64_t obytes_nm1 = net->data.ifmd_data.ifi_obytes; + ifdata(net->row, &net->data); + + double time_scale = (net->tv_delta.tv_sec + 1e-6*net->tv_delta.tv_usec); + if (time_scale < 1e-6 || time_scale > 1e2) return; + double delta_ibytes = (double)(net->data.ifmd_data.ifi_ibytes - ibytes_nm1) + / time_scale; + double delta_obytes = (double)(net->data.ifmd_data.ifi_obytes - obytes_nm1) + / time_scale; + + double exponent_ibytes = log10(delta_ibytes); + double exponent_obytes = log10(delta_obytes); + + if (exponent_ibytes < 3) { + net->down_unit = UNIT_BPS; + net->down = delta_ibytes; + } else if (exponent_ibytes < 6) { + net->down_unit = UNIT_KBPS; + net->down = delta_ibytes / 1000.0; + } else if (exponent_ibytes < 9) { + net->down_unit = UNIT_MBPS; + net->down = delta_ibytes / 1000000.0; + } + + if (exponent_obytes < 3) { + net->up_unit = UNIT_BPS; + net->up = delta_obytes; + } else if (exponent_obytes < 6) { + net->up_unit = UNIT_KBPS; + net->up = delta_obytes / 1000.0; + } else if (exponent_obytes < 9) { + net->up_unit = UNIT_MBPS; + net->up = delta_obytes / 1000000.0; + } +} diff --git a/packages/sk-utils/src/event_providers/network_load/network_load.c b/packages/sk-utils/src/event_providers/network_load/network_load.c new file mode 100644 index 0000000..06afe95 --- /dev/null +++ b/packages/sk-utils/src/event_providers/network_load/network_load.c @@ -0,0 +1,42 @@ +#include +#include "network.h" +#include "../sketchybar.h" + +int main (int argc, char** argv) { + float update_freq; + if (argc < 4 || (sscanf(argv[3], "%f", &update_freq) != 1)) { + printf("Usage: %s \"\" \"\" \"\"\n", argv[0]); + exit(1); + } + + alarm(0); + // Setup the event in sketchybar + char event_message[512]; + snprintf(event_message, 512, "--add event '%s'", argv[2]); + sketchybar(event_message); + + struct network network; + network_init(&network, argv[1]); + char trigger_message[512]; + for (;;) { + // Acquire new info + network_update(&network); + + // Prepare the event message + snprintf(trigger_message, + 512, + "--trigger '%s' upload='%03d%s' download='%03d%s'", + argv[2], + network.up, + unit_str[network.up_unit], + network.down, + unit_str[network.down_unit]); + + // Trigger the event + sketchybar(trigger_message); + + // Wait + usleep(update_freq * 1000000); + } + return 0; +} diff --git a/packages/sk-utils/src/event_providers/sketchybar.h b/packages/sk-utils/src/event_providers/sketchybar.h new file mode 100644 index 0000000..72bf06a --- /dev/null +++ b/packages/sk-utils/src/event_providers/sketchybar.h @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +typedef char* env; + +#define MACH_HANDLER(name) void name(env env) +typedef MACH_HANDLER(mach_handler); + +struct mach_message { + mach_msg_header_t header; + mach_msg_size_t msgh_descriptor_count; + mach_msg_ool_descriptor_t descriptor; +}; + +struct mach_buffer { + struct mach_message message; + mach_msg_trailer_t trailer; +}; + +static mach_port_t g_mach_port = 0; + +static inline mach_port_t mach_get_bs_port() { + mach_port_name_t task = mach_task_self(); + + mach_port_t bs_port; + if (task_get_special_port(task, + TASK_BOOTSTRAP_PORT, + &bs_port ) != KERN_SUCCESS) { + return 0; + } + + char* name = getenv("BAR_NAME"); + if (!name) name = "sketchybar"; + uint32_t lookup_len = 16 + strlen(name); + + char buffer[lookup_len]; + snprintf(buffer, lookup_len, "git.felix.%s", name); + + mach_port_t port; + if (bootstrap_look_up(bs_port, buffer, &port) != KERN_SUCCESS) return 0; + return port; +} + +static inline bool mach_send_message(mach_port_t port, char* message, uint32_t len) { + if (!message || !port) { + return false; + } + + struct mach_message msg = { 0 }; + msg.header.msgh_remote_port = port; + msg.header.msgh_local_port = 0; + msg.header.msgh_id = 0; + msg.header.msgh_bits = MACH_MSGH_BITS_SET(MACH_MSG_TYPE_COPY_SEND, + MACH_MSG_TYPE_MAKE_SEND, + 0, + MACH_MSGH_BITS_COMPLEX ); + + msg.header.msgh_size = sizeof(struct mach_message); + msg.msgh_descriptor_count = 1; + msg.descriptor.address = message; + msg.descriptor.size = len * sizeof(char); + msg.descriptor.copy = MACH_MSG_VIRTUAL_COPY; + msg.descriptor.deallocate = false; + msg.descriptor.type = MACH_MSG_OOL_DESCRIPTOR; + + kern_return_t err = mach_msg(&msg.header, + MACH_SEND_MSG, + sizeof(struct mach_message), + 0, + MACH_PORT_NULL, + MACH_MSG_TIMEOUT_NONE, + MACH_PORT_NULL ); + + return err == KERN_SUCCESS; +} + +static inline uint32_t format_message(char* message, char* formatted_message) { + // This is not actually robust, switch to stack based messaging. + char outer_quote = 0; + uint32_t caret = 0; + uint32_t message_length = strlen(message) + 1; + for (int i = 0; i < message_length; ++i) { + if (message[i] == '"' || message[i] == '\'') { + if (outer_quote && outer_quote == message[i]) outer_quote = 0; + else if (!outer_quote) outer_quote = message[i]; + continue; + } + formatted_message[caret] = message[i]; + if (message[i] == ' ' && !outer_quote) formatted_message[caret] = '\0'; + caret++; + } + + if (caret > 0 && formatted_message[caret] == '\0' + && formatted_message[caret - 1] == '\0') { + caret--; + } + formatted_message[caret] = '\0'; + return caret + 1; +} + +static inline void sketchybar(char* message) { + char formatted_message[strlen(message) + 2]; + uint32_t length = format_message(message, formatted_message); + if (!length) return; + + if (!g_mach_port) g_mach_port = mach_get_bs_port(); + if (!mach_send_message(g_mach_port, formatted_message, length)) { + g_mach_port = mach_get_bs_port(); + if (!mach_send_message(g_mach_port, formatted_message, length)) { + // No sketchybar instance running, exit. + exit(0); + } + } +} diff --git a/packages/sk-utils/src/makefile b/packages/sk-utils/src/makefile new file mode 100644 index 0000000..3246108 --- /dev/null +++ b/packages/sk-utils/src/makefile @@ -0,0 +1,3 @@ +all: + (cd event_providers && $(MAKE)) >/dev/null + (cd menus && $(MAKE)) >/dev/null diff --git a/packages/sk-utils/src/menus/makefile b/packages/sk-utils/src/menus/makefile new file mode 100644 index 0000000..0cb454e --- /dev/null +++ b/packages/sk-utils/src/menus/makefile @@ -0,0 +1,5 @@ +bin/menus: menus.c | bin + clang -std=c99 -O3 -F/System/Library/PrivateFrameworks/ -framework Carbon -framework SkyLight $< -o $@ + +bin: + mkdir bin diff --git a/packages/sk-utils/src/menus/menus.c b/packages/sk-utils/src/menus/menus.c new file mode 100644 index 0000000..2e77822 --- /dev/null +++ b/packages/sk-utils/src/menus/menus.c @@ -0,0 +1,248 @@ +#include + +void ax_init() { + const void *keys[] = { kAXTrustedCheckOptionPrompt }; + const void *values[] = { kCFBooleanTrue }; + + CFDictionaryRef options; + options = CFDictionaryCreate(kCFAllocatorDefault, + keys, + values, + sizeof(keys) / sizeof(*keys), + &kCFCopyStringDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks ); + + bool trusted = AXIsProcessTrustedWithOptions(options); + CFRelease(options); + if (!trusted) exit(1); +} + +void ax_perform_click(AXUIElementRef element) { + if (!element) return; + AXUIElementPerformAction(element, kAXCancelAction); + usleep(150000); + AXUIElementPerformAction(element, kAXPressAction); +} + +CFStringRef ax_get_title(AXUIElementRef element) { + CFTypeRef title = NULL; + AXError error = AXUIElementCopyAttributeValue(element, + kAXTitleAttribute, + &title ); + + if (error != kAXErrorSuccess) return NULL; + return title; +} + +void ax_select_menu_option(AXUIElementRef app, int id) { + AXUIElementRef menubars_ref = NULL; + CFArrayRef children_ref = NULL; + + AXError error = AXUIElementCopyAttributeValue(app, + kAXMenuBarAttribute, + (CFTypeRef*)&menubars_ref); + if (error == kAXErrorSuccess) { + error = AXUIElementCopyAttributeValue(menubars_ref, + kAXVisibleChildrenAttribute, + (CFTypeRef*)&children_ref ); + + if (error == kAXErrorSuccess) { + uint32_t count = CFArrayGetCount(children_ref); + if (id < count) { + AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, id); + ax_perform_click(item); + } + if (children_ref) CFRelease(children_ref); + } + if (menubars_ref) CFRelease(menubars_ref); + } +} + +void ax_print_menu_options(AXUIElementRef app) { + AXUIElementRef menubars_ref = NULL; + CFTypeRef menubar = NULL; + CFArrayRef children_ref = NULL; + + AXError error = AXUIElementCopyAttributeValue(app, + kAXMenuBarAttribute, + (CFTypeRef*)&menubars_ref); + if (error == kAXErrorSuccess) { + error = AXUIElementCopyAttributeValue(menubars_ref, + kAXVisibleChildrenAttribute, + (CFTypeRef*)&children_ref ); + + if (error == kAXErrorSuccess) { + uint32_t count = CFArrayGetCount(children_ref); + + for (int i = 1; i < count; i++) { + AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i); + CFTypeRef title = ax_get_title(item); + + if (title) { + uint32_t buffer_len = 2*CFStringGetLength(title); + char buffer[2*CFStringGetLength(title)]; + CFStringGetCString(title, buffer, buffer_len, kCFStringEncodingUTF8); + printf("%s\n", buffer); + CFRelease(title); + } + } + } + if (menubars_ref) CFRelease(menubars_ref); + if (children_ref) CFRelease(children_ref); + } +} + +AXUIElementRef ax_get_extra_menu_item(char* alias) { + pid_t pid = 0; + CGRect bounds = CGRectNull; + CFArrayRef window_list = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, + kCGNullWindowID ); + char owner_buffer[256]; + char name_buffer[256]; + char buffer[512]; + int window_count = CFArrayGetCount(window_list); + for (int i = 0; i < window_count; ++i) { + CFDictionaryRef dictionary = CFArrayGetValueAtIndex(window_list, i); + if (!dictionary) continue; + + CFStringRef owner_ref = CFDictionaryGetValue(dictionary, + kCGWindowOwnerName); + + CFNumberRef owner_pid_ref = CFDictionaryGetValue(dictionary, + kCGWindowOwnerPID); + + CFStringRef name_ref = CFDictionaryGetValue(dictionary, kCGWindowName); + CFNumberRef layer_ref = CFDictionaryGetValue(dictionary, kCGWindowLayer); + CFDictionaryRef bounds_ref = CFDictionaryGetValue(dictionary, + kCGWindowBounds); + + if (!name_ref || !owner_ref || !owner_pid_ref || !layer_ref || !bounds_ref) + continue; + + long long int layer = 0; + CFNumberGetValue(layer_ref, CFNumberGetType(layer_ref), &layer); + uint64_t owner_pid = 0; + CFNumberGetValue(owner_pid_ref, + CFNumberGetType(owner_pid_ref), + &owner_pid ); + + if (layer != 0x19) continue; + bounds = CGRectNull; + if (!CGRectMakeWithDictionaryRepresentation(bounds_ref, &bounds)) continue; + CFStringGetCString(owner_ref, + owner_buffer, + sizeof(owner_buffer), + kCFStringEncodingUTF8); + + CFStringGetCString(name_ref, + name_buffer, + sizeof(name_buffer), + kCFStringEncodingUTF8); + snprintf(buffer, sizeof(buffer), "%s,%s", owner_buffer, name_buffer); + + if (strcmp(buffer, alias) == 0) { + pid = owner_pid; + break; + } + } + CFRelease(window_list); + if (!pid) return NULL; + + AXUIElementRef app = AXUIElementCreateApplication(pid); + if (!app) return NULL; + AXUIElementRef result = NULL; + CFTypeRef extras = NULL; + CFArrayRef children_ref = NULL; + AXError error = AXUIElementCopyAttributeValue(app, + kAXExtrasMenuBarAttribute, + &extras ); + if (error == kAXErrorSuccess) { + error = AXUIElementCopyAttributeValue(extras, + kAXVisibleChildrenAttribute, + (CFTypeRef*)&children_ref ); + + if (error == kAXErrorSuccess) { + uint32_t count = CFArrayGetCount(children_ref); + for (uint32_t i = 0; i < count; i++) { + AXUIElementRef item = CFArrayGetValueAtIndex(children_ref, i); + CFTypeRef position_ref = NULL; + CFTypeRef size_ref = NULL; + AXUIElementCopyAttributeValue(item, kAXPositionAttribute, + &position_ref ); + AXUIElementCopyAttributeValue(item, kAXSizeAttribute, + &size_ref ); + if (!position_ref || !size_ref) continue; + + CGPoint position = CGPointZero; + AXValueGetValue(position_ref, kAXValueCGPointType, &position); + CGSize size = CGSizeZero; + AXValueGetValue(size_ref, kAXValueCGSizeType, &size); + CFRelease(position_ref); + CFRelease(size_ref); + // The offset is exactly 8 on macOS Sonoma... + // printf("%f %f\n", position.x, bounds.origin.x); + if (error == kAXErrorSuccess + && fabs(position.x - bounds.origin.x) <= 10) { + result = item; + break; + } + } + } + } + + CFRelease(app); + return result; +} + +extern int SLSMainConnectionID(); +extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled); +extern void SLSSetMenuBarVisibilityOverrideOnDisplay(int cid, int did, bool enabled); +extern void SLSSetMenuBarInsetAndAlpha(int cid, double u1, double u2, float alpha); +void ax_select_menu_extra(char* alias) { + AXUIElementRef item = ax_get_extra_menu_item(alias); + if (!item) return; + SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0); + SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, true); + SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 0.0); + ax_perform_click(item); + SLSSetMenuBarVisibilityOverrideOnDisplay(SLSMainConnectionID(), 0, false); + SLSSetMenuBarInsetAndAlpha(SLSMainConnectionID(), 0, 1, 1.0); + CFRelease(item); +} + +extern void _SLPSGetFrontProcess(ProcessSerialNumber* psn); +extern void SLSGetConnectionIDForPSN(int cid, ProcessSerialNumber* psn, int* cid_out); +extern void SLSConnectionGetPID(int cid, pid_t* pid_out); +AXUIElementRef ax_get_front_app() { + ProcessSerialNumber psn; + _SLPSGetFrontProcess(&psn); + int target_cid; + SLSGetConnectionIDForPSN(SLSMainConnectionID(), &psn, &target_cid); + + pid_t pid; + SLSConnectionGetPID(target_cid, &pid); + return AXUIElementCreateApplication(pid); +} + +int main (int argc, char **argv) { + if (argc == 1) { + printf("Usage: %s [-l | -s id/alias ]\n", argv[0]); + exit(0); + } + ax_init(); + if (strcmp(argv[1], "-l") == 0) { + AXUIElementRef app = ax_get_front_app(); + if (!app) return 1; + ax_print_menu_options(app); + CFRelease(app); + } else if (argc == 3 && strcmp(argv[1], "-s") == 0) { + int id = 0; + if (sscanf(argv[2], "%d", &id) == 1) { + AXUIElementRef app = ax_get_front_app(); + if (!app) return 1; + ax_select_menu_option(app, id); + CFRelease(app); + } else ax_select_menu_extra(argv[2]); + } + return 0; +}