├── SwiftBar ├── Resources │ ├── SwiftBarMAS.xcconfig │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── mac_128.png │ │ │ ├── mac_16.png │ │ │ ├── mac_256.png │ │ │ ├── mac_32.png │ │ │ ├── mac_512.png │ │ │ ├── mac_16@2x.png │ │ │ ├── mac_32@2x.png │ │ │ ├── mac_128@2x.png │ │ │ ├── mac_256@2x.png │ │ │ ├── mac_512@2x.png │ │ │ └── Contents.json │ │ └── AccentColor.colorset │ │ │ └── Contents.json │ ├── Credits.rtf │ ├── SwiftBar MAS.entitlements │ ├── SwiftBar.entitlements │ ├── Info.plist │ └── Localization │ │ ├── zh-Hans.lproj │ │ └── Localizable.strings │ │ ├── de.lproj │ │ └── Localizable.strings │ │ ├── en.lproj │ │ └── Localizable.strings │ │ ├── ru.lproj │ │ └── Localizable.strings │ │ ├── nl.lproj │ │ └── Localizable.strings │ │ ├── hr.lproj │ │ └── Localizable.strings │ │ ├── es.lproj │ │ └── Localizable.strings │ │ └── Localizable.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── main.swift ├── Utility │ ├── Notification+Extension.swift │ ├── SystemNotificationName.swift │ ├── DirectoryObserver.swift │ ├── NSFont+Offset.swift │ ├── PluginUtilities.swift │ ├── NSImage.swift │ ├── String+Escaped.swift │ ├── Environment.swift │ ├── NSMutableAttributedString+SFSymbols.swift │ ├── URL+Extension.swift │ ├── String+ANSIColor.swift │ ├── ShortcutsManager.swift │ ├── LaunchAtLogin.swift │ ├── NSColor.swift │ └── RunScript.swift ├── Log.swift ├── UI │ ├── Helpers │ │ ├── Binding+Extension.swift │ │ ├── AnimatableWindow.swift │ │ ├── URLTextView.swift │ │ ├── EnumPicker.swift │ │ ├── ImageLoader.swift │ │ └── ImageView.swift │ ├── Preferences │ │ ├── AdvancedPreferencesView.swift │ │ ├── GeneralPreferencesView.swift │ │ ├── AboutSettingsView.swift │ │ ├── PreferencesView.swift │ │ ├── PluginsPreferencesView.swift │ │ └── PluginDetailsView.swift │ ├── PluginErrorView.swift │ ├── Plugin Repository │ │ ├── PluginRepositoryAPI.swift │ │ ├── PluginRepository.swift │ │ └── PluginRepositoryView.swift │ ├── Debug │ │ └── DebugView.swift │ ├── WebView.swift │ └── AboutPluginView.swift ├── Intents │ ├── GetPluginsIntentHandler.swift │ ├── EnablePluginIntentHandler.swift │ ├── DisablePluginIntentHandler.swift │ ├── ReloadPluginIntentHandler.swift │ └── SetEphemeralPluginIntentHandler.swift ├── AppDelegate+Intents.swift ├── Plugin │ ├── PluginDebugInfo.swift │ ├── EphemeralPlugin.swift │ ├── ShortcutPlugin.swift │ ├── Plugin.swift │ └── ExecutablePlugin.swift ├── AppDelegate+Menu.swift ├── AppDelegate+Toolbar.swift └── PreferencesStore.swift ├── Resources ├── logo.png └── Plugin Repository.jpg ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── SwiftBar.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── SwiftBar MAS.xcscheme │ └── SwiftBar.xcscheme ├── LICENSE ├── CLAUDE.md ├── .gitignore ├── README-PACKAGED-PLUGINS.md └── SWIFTBAR_CODE_REVIEW_REPORT.md /SwiftBar/Resources/SwiftBarMAS.xcconfig: -------------------------------------------------------------------------------- 1 | MAC_APP_STORE = YES 2 | -------------------------------------------------------------------------------- /Resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/Resources/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [swiftbar] 4 | -------------------------------------------------------------------------------- /Resources/Plugin Repository.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/Resources/Plugin Repository.jpg -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftBar/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_128.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_16.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_256.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_32.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_512.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_16@2x.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_32@2x.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_128@2x.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_256@2x.png -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swiftbar/SwiftBar/HEAD/SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/mac_512@2x.png -------------------------------------------------------------------------------- /SwiftBar/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let delegate = AppDelegate() 4 | NSApplication.shared.delegate = delegate 5 | 6 | _ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) 7 | -------------------------------------------------------------------------------- /SwiftBar/Utility/Notification+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Notification.Name { 4 | static let repositoirySearchUpdate = Notification.Name("RepositoirySearchUpdate") 5 | } 6 | -------------------------------------------------------------------------------- /SwiftBar.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /SwiftBar/Utility/SystemNotificationName.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum SystemNotificationName { 4 | public static let url = "url" 5 | public static let command = "command" 6 | public static let pluginID = "pluginID" 7 | } 8 | -------------------------------------------------------------------------------- /SwiftBar/Log.swift: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | private let subsystem = "com.ameba.SwiftBar" 4 | 5 | enum Log { 6 | static let plugin = OSLog(subsystem: subsystem, category: "Plugin") 7 | static let repository = OSLog(subsystem: subsystem, category: "Plugin Repository") 8 | } 9 | -------------------------------------------------------------------------------- /SwiftBar.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/Binding+Extension.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Binding { 4 | func onUpdate(_ closure: @escaping () -> Void) -> Binding { 5 | Binding(get: { 6 | wrappedValue 7 | }, set: { newValue in 8 | wrappedValue = newValue 9 | closure() 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/AnimatableWindow.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Foundation 3 | 4 | class AnimatableWindow: NSWindow { 5 | var lastContentSize: CGSize = .zero 6 | 7 | override func setContentSize(_ size: NSSize) { 8 | if lastContentSize == size { return } 9 | lastContentSize = size 10 | animator().setFrame(NSRect(origin: frame.origin, size: size), display: true, animate: false) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SwiftBar/Intents/GetPluginsIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | public class GetPluginsIntentHandler: NSObject, GetPluginsIntentHandling { 4 | @available(macOS 11.0, *) 5 | public func handle(intent _: GetPluginsIntent, completion: @escaping (GetPluginsIntentResponse) -> Void) { 6 | let plugins = delegate.pluginManager.plugins.map { SKPlugin(identifier: $0.id, display: $0.name) } 7 | completion(GetPluginsIntentResponse.success(plugins: plugins)) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\ansi\ansicpg1252\cocoartf2577 2 | \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fswiss\fcharset0 Helvetica;} 3 | {\colortbl;\red255\green255\blue255;} 4 | {\*\expandedcolortbl;;} 5 | \margl1440\margr1440\vieww9000\viewh8400\viewkind0 6 | \pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\partightenfactor0 7 | 8 | \f0\fs24 \cf0 Website: {\field{\*\fldinst{HYPERLINK "https://swiftbar.app"}}{\fldrslt https://swiftbar.app}}} -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/URLTextView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct URLTextView: View { 4 | var text: String 5 | var url: URL 6 | var sfSymbol: String? 7 | var body: some View { 8 | if #available(OSX 11.0, *), let sfSymbol { 9 | Image(systemName: sfSymbol) 10 | .colorMultiply(.blue) 11 | .onTapGesture { 12 | NSWorkspace.shared.open(url) 13 | } 14 | } else { 15 | Text(text) 16 | .font(.headline) 17 | .underline() 18 | .onTapGesture { 19 | NSWorkspace.shared.open(url) 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /SwiftBar/AppDelegate+Intents.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Intents 3 | 4 | extension AppDelegate { 5 | @available(macOS 11.0, *) 6 | func application(_: NSApplication, handlerFor intent: INIntent) -> Any? { 7 | switch intent { 8 | case is GetPluginsIntent: 9 | GetPluginsIntentHandler() 10 | case is EnablePluginIntent: 11 | EnablePluginIntentHandler() 12 | case is DisablePluginIntent: 13 | DisablePluginIntentHandler() 14 | case is ReloadPluginIntent: 15 | ReloadPluginIntentHandler() 16 | case is SetEphemeralPluginIntent: 17 | SetEphemeralPluginIntentHandler() 18 | default: 19 | nil 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/EnumPicker.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct EnumPicker: View { 4 | @Binding public var selected: T 5 | public var title: String? 6 | 7 | public let mapping: (T) -> V 8 | 9 | public var body: some View { 10 | Picker(selection: $selected, label: Text(title ?? "")) { 11 | ForEach(Array(T.allCases), id: \.self) { 12 | mapping($0).tag($0) 13 | } 14 | } 15 | } 16 | } 17 | 18 | public extension EnumPicker where T: RawRepresentable, T.RawValue == String, V == Text { 19 | init(selected: Binding, title: String? = nil) { 20 | self.init(selected: selected, title: title) { 21 | Text($0.rawValue) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Environment:** 24 | - macOS version: 25 | - SwiftBar version: 26 | 27 | **Plugin Example:** 28 | Sample plugin to reproduce the issue, link or code. 29 | 30 | **Additional Context:** 31 | 32 | - [ ] I don't run Bartender/Dozer/etc. or tested the issue without it running 33 | -------------------------------------------------------------------------------- /SwiftBar/Utility/DirectoryObserver.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if !MAC_APP_STORE 4 | 5 | class DirectoryObserver { 6 | private let fileDescriptor: CInt 7 | private let source: DispatchSourceProtocol 8 | public let url: URL 9 | 10 | deinit { 11 | self.source.cancel() 12 | close(fileDescriptor) 13 | } 14 | 15 | init(url: URL, block: @escaping () -> Void) { 16 | self.url = url 17 | fileDescriptor = open(url.path, O_EVTONLY) 18 | source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fileDescriptor, eventMask: .all, queue: DispatchQueue.global()) 19 | source.setEventHandler { 20 | block() 21 | } 22 | source.resume() 23 | } 24 | } 25 | #endif 26 | -------------------------------------------------------------------------------- /SwiftBar/Resources/SwiftBar MAS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.automation.apple-events 8 | 9 | com.apple.security.files.user-selected.read-write 10 | 11 | com.apple.security.temporary-exception.apple-events:before:10.8 12 | com.apple.Terminal 13 | com.apple.security.temporary-exception.files.home-relative-path.read-onlycom.apple.security.temporary-exception.files.home-relative-path.read-only 14 | 15 | /bin 16 | /etc/profile 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /SwiftBar/Resources/SwiftBar.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.automation.apple-events 6 | 7 | com.apple.security.personal-information.addressbook 8 | 9 | com.apple.security.personal-information.calendars 10 | 11 | com.apple.security.temporary-exception.apple-events:before:10.8 12 | com.apple.Terminal 13 | com.apple.security.temporary-exception.files.home-relative-path.read-onlycom.apple.security.temporary-exception.files.home-relative-path.read-only 14 | 15 | /usr/bin 16 | /bin 17 | /etc/profile 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/ImageLoader.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Combine 3 | import Foundation 4 | 5 | class ImageLoader: ObservableObject { 6 | @Published var imageData: Data? 7 | 8 | init(url: URL?) { 9 | guard let url else { return } 10 | let cache = URLCache.shared 11 | let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.returnCacheDataElseLoad, timeoutInterval: 60.0) 12 | if let data = cache.cachedResponse(for: request)?.data { 13 | imageData = data 14 | } else { 15 | URLSession.shared.dataTask(with: request, completionHandler: { data, response, _ in 16 | if let data, let response { 17 | let cachedData = CachedURLResponse(response: response, data: data) 18 | cache.storeCachedResponse(cachedData, for: request) 19 | DispatchQueue.main.async { 20 | self.imageData = data 21 | } 22 | } 23 | }).resume() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SwiftBar/Utility/NSFont+Offset.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSFont { 4 | var menuBarOffset: CGFloat { 5 | // Adjusted values to better center text vertically 6 | // Reduced offsets to fix alignment issues in SwiftBar 2.0 7 | switch pointSize { 8 | case 0 ..< 2: 9 | 1.5 10 | case 2 ..< 5: 11 | 1.0 12 | case 5 ..< 8: 13 | 0.5 14 | case 8 ..< 10: 15 | 0.5 16 | case 10 ..< 13: 17 | 0 18 | case 13 ..< 15: 19 | 0 20 | case 15 ..< 17: 21 | 0 22 | case 17 ..< 20: 23 | -0.5 24 | case 20 ..< 22: 25 | -1.0 26 | case 22 ..< 24: 27 | -1.5 28 | case 24 ..< 26: 29 | -2.0 30 | case 26 ..< 28: 31 | -2.5 32 | default: 33 | 0 34 | } 35 | } 36 | 37 | var twoLineMenuBarOffset: CGFloat { 38 | // Adjust offset for 2-line content (move up slightly to center better) 39 | menuBarOffset - (pointSize * 0.15) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ameba Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SwiftBar/UI/Helpers/ImageView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ImageView: View { 4 | @ObservedObject var imageLoader: ImageLoader 5 | var image: NSImage? { 6 | guard let data = imageLoader.imageData else { return nil } 7 | return NSImage(data: data) 8 | } 9 | 10 | let width: CGFloat? 11 | let height: CGFloat? 12 | let fallbackView: AnyView 13 | 14 | /// Create an view that shows an image loaded from the URL. The fallbackView will be shown instead when the URL is nil, can't load, or is still loading. 15 | init(withURL url: URL?, width: CGFloat? = nil, height: CGFloat? = nil, fallbackView: AnyView = AnyView(EmptyView())) { 16 | self.width = width 17 | self.height = height 18 | self.fallbackView = fallbackView 19 | imageLoader = ImageLoader(url: url) 20 | } 21 | 22 | var body: some View { 23 | if let image { 24 | Image(nsImage: image) 25 | .resizable() 26 | .aspectRatio(contentMode: .fit) 27 | .frame(width: width, height: height) 28 | } else { 29 | fallbackView 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/AdvancedPreferencesView.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import SwiftUI 3 | 4 | struct AdvancedPreferencesView: View { 5 | @EnvironmentObject var preferences: PreferencesStore 6 | @State private var launchAtLogin = true 7 | 8 | var body: some View { 9 | Preferences.Container(contentWidth: 350) { 10 | Preferences.Section(title: "\(Localizable.Preferences.Terminal.localized):", verticalAlignment: .top) { 11 | EnumPicker(selected: $preferences.terminal, title: "") 12 | .frame(width: 120.0) 13 | } 14 | Preferences.Section(title: "\(Localizable.Preferences.Shell.localized):", bottomDivider: true) { 15 | EnumPicker(selected: $preferences.shell, title: "") 16 | .frame(width: 120.0) 17 | } 18 | Preferences.Section(title: "\(Localizable.Preferences.HideSwiftBarIcon.localized):", verticalAlignment: .top) { 19 | Toggle("", isOn: $preferences.swiftBarIconIsHidden) 20 | } 21 | Preferences.Section(title: "\(Localizable.Preferences.StealthMode.localized):", verticalAlignment: .top) { 22 | Toggle("", isOn: $preferences.stealthMode) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SwiftBar/Utility/PluginUtilities.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | func parseRefreshInterval(intervalStr: String, baseUpdateinterval: Double) -> Double? { 4 | guard let interval = Double(intervalStr.filter("0123456789.".contains)) else { return nil } 5 | var updateInterval: Double = baseUpdateinterval 6 | 7 | if intervalStr.hasSuffix("s") { 8 | updateInterval = interval 9 | if intervalStr.hasSuffix("ms") { 10 | updateInterval = interval / 1000 11 | } 12 | } 13 | if intervalStr.hasSuffix("m") { 14 | updateInterval = interval * 60 15 | } 16 | if intervalStr.hasSuffix("h") { 17 | updateInterval = interval * 60 * 60 18 | } 19 | if intervalStr.hasSuffix("d") { 20 | updateInterval = interval * 60 * 60 * 24 21 | } 22 | 23 | return updateInterval 24 | } 25 | 26 | final class RunPluginOperation: Operation { 27 | weak var plugin: T? 28 | 29 | init(plugin: T) { 30 | self.plugin = plugin 31 | super.init() 32 | } 33 | 34 | override func main() { 35 | guard !isCancelled else { return } 36 | plugin?.content = plugin?.invoke() 37 | (plugin as? ExecutablePlugin)?.enableTimer() 38 | (plugin as? ShortcutPlugin)?.enableTimer() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SwiftBar/Plugin/PluginDebugInfo.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class PluginDebugInfo: ObservableObject { 4 | enum EventType: String { 5 | case ContentUpdate 6 | case ContentUpdateError 7 | case PluginRefresh 8 | case Environment 9 | case PluginMetadata 10 | } 11 | 12 | struct Event { 13 | let type: EventType 14 | let value: String 15 | 16 | var eventString: String { 17 | "\(type): \(value)" 18 | } 19 | } 20 | 21 | @Published var events: [Date: Event] = [:] 22 | 23 | func addEvent(type: EventType, value: String) { 24 | guard PreferencesStore.shared.pluginDebugMode else { 25 | return 26 | } 27 | var newValue = value 28 | DispatchQueue.main.async { [weak self] in 29 | if type == .PluginRefresh { 30 | newValue = """ 31 | 32 | ================================== 33 | \(newValue) 34 | ================================== 35 | 36 | """ 37 | } 38 | self?.events[Date()] = Event(type: type, value: newValue) 39 | } 40 | } 41 | 42 | func clear() { 43 | DispatchQueue.main.async { [weak self] in 44 | self?.events.removeAll() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # SwiftBar Development Guide 2 | 3 | ## Build Commands 4 | - Open project: `open SwiftBar/SwiftBar.xcodeproj` 5 | - Build: Press "Play" in Xcode 6 | - Test: Run unit tests through Xcode's Test Navigator 7 | - Debug: Enable Plugin Debug Mode with `defaults write com.ameba.SwiftBar PluginDebugMode -bool YES` 8 | 9 | ## Code Style Guidelines 10 | - **Imports**: Group by standard libraries first, then third-party libraries 11 | - **Naming**: Use descriptive camelCase variables, PascalCase for types 12 | - **Types**: Swift strong typing with proper optionals handling 13 | - **Error Handling**: Use do/catch blocks, proper error propagation 14 | - **File Organization**: Keep related functionality in dedicated files 15 | - **UI**: Use SwiftUI for new UI components when possible 16 | - **Comments**: Document public APIs and complex logic 17 | - **Dependencies**: SwiftBar uses HotKey, LaunchAtLogin, Preferences, Sparkle, SwiftCron 18 | 19 | ## Terminal Support 20 | SwiftBar supports running scripts in these terminals: 21 | - macOS Terminal.app 22 | - iTerm2 23 | - Ghostty 24 | 25 | ## Environment Variables 26 | SWIFTBAR_VERSION, SWIFTBAR_BUILD, SWIFTBAR_PLUGINS_PATH, SWIFTBAR_PLUGIN_PATH, 27 | SWIFTBAR_PLUGIN_CACHE_PATH, SWIFTBAR_PLUGIN_DATA_PATH, SWIFTBAR_PLUGIN_REFRESH_REASON, 28 | OS_APPEARANCE, OS_VERSION_MAJOR, OS_VERSION_MINOR, OS_VERSION_PATCH -------------------------------------------------------------------------------- /SwiftBar/Intents/EnablePluginIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | public class EnablePluginIntentHandler: NSObject, EnablePluginIntentHandling { 4 | @available(macOS 11.0, *) 5 | public func handle(intent: EnablePluginIntent, completion: @escaping (EnablePluginIntentResponse) -> Void) { 6 | guard let pluginID = intent.plugin?.identifier, 7 | let plugin = delegate.pluginManager.plugins.first(where: { $0.id == pluginID }) 8 | else { 9 | completion(EnablePluginIntentResponse(code: .failure, userActivity: nil)) 10 | return 11 | } 12 | delegate.pluginManager.enablePlugin(plugin: plugin) 13 | completion(EnablePluginIntentResponse(code: .success, userActivity: nil)) 14 | } 15 | 16 | @available(macOS 11.0, *) 17 | public func resolvePlugin(for intent: EnablePluginIntent, with completion: @escaping (SKPluginResolutionResult) -> Void) { 18 | guard let plugin = intent.plugin else { 19 | completion(.needsValue()) 20 | return 21 | } 22 | completion(.success(with: plugin)) 23 | } 24 | 25 | @available(macOS 11.0, *) 26 | public func providePluginOptionsCollection(for _: EnablePluginIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { 27 | let plugins = delegate.pluginManager.plugins.map { SKPlugin(identifier: $0.id, display: $0.name) } 28 | completion(INObjectCollection(items: plugins), nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftBar/Intents/DisablePluginIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | public class DisablePluginIntentHandler: NSObject, DisablePluginIntentHandling { 4 | @available(macOS 11.0, *) 5 | public func handle(intent: DisablePluginIntent, completion: @escaping (DisablePluginIntentResponse) -> Void) { 6 | guard let pluginID = intent.plugin?.identifier, 7 | let plugin = delegate.pluginManager.plugins.first(where: { $0.id == pluginID }) 8 | else { 9 | completion(DisablePluginIntentResponse(code: .failure, userActivity: nil)) 10 | return 11 | } 12 | delegate.pluginManager.disablePlugin(plugin: plugin) 13 | completion(DisablePluginIntentResponse(code: .success, userActivity: nil)) 14 | } 15 | 16 | @available(macOS 11.0, *) 17 | public func resolvePlugin(for intent: DisablePluginIntent, with completion: @escaping (SKPluginResolutionResult) -> Void) { 18 | guard let plugin = intent.plugin else { 19 | completion(.needsValue()) 20 | return 21 | } 22 | completion(.success(with: plugin)) 23 | } 24 | 25 | @available(macOS 11.0, *) 26 | public func providePluginOptionsCollection(for _: DisablePluginIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { 27 | let plugins = delegate.pluginManager.plugins.map { SKPlugin(identifier: $0.id, display: $0.name) } 28 | completion(INObjectCollection(items: plugins), nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SwiftBar/Intents/ReloadPluginIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | public class ReloadPluginIntentHandler: NSObject, ReloadPluginIntentHandling { 4 | @available(macOS 11.0, *) 5 | public func handle(intent: ReloadPluginIntent, completion: @escaping (ReloadPluginIntentResponse) -> Void) { 6 | guard let pluginID = intent.plugin?.identifier, 7 | let plugin = delegate.pluginManager.plugins.first(where: { $0.id == pluginID }) 8 | else { 9 | completion(ReloadPluginIntentResponse(code: .failure, userActivity: nil)) 10 | return 11 | } 12 | delegate.pluginManager.menuBarItems[plugin.id]?.dimOnManualRefresh() 13 | plugin.refresh(reason: .Shortcut) 14 | completion(ReloadPluginIntentResponse(code: .success, userActivity: nil)) 15 | } 16 | 17 | @available(macOS 11.0, *) 18 | public func resolvePlugin(for intent: ReloadPluginIntent, with completion: @escaping (SKPluginResolutionResult) -> Void) { 19 | guard let plugin = intent.plugin else { 20 | completion(.needsValue()) 21 | return 22 | } 23 | completion(.success(with: plugin)) 24 | } 25 | 26 | @available(macOS 11.0, *) 27 | public func providePluginOptionsCollection(for _: ReloadPluginIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { 28 | let plugins = delegate.pluginManager.plugins.map { SKPlugin(identifier: $0.id, display: $0.name) } 29 | completion(INObjectCollection(items: plugins), nil) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftBar/Intents/SetEphemeralPluginIntentHandler.swift: -------------------------------------------------------------------------------- 1 | import Intents 2 | 3 | @available(macOS 11.0, *) 4 | public class SetEphemeralPluginIntentHandler: NSObject, SetEphemeralPluginIntentHandling { 5 | @MainActor 6 | public func handle(intent: SetEphemeralPluginIntent) async -> SetEphemeralPluginIntentResponse { 7 | guard let id = intent.name, let content = intent.content, let exitAfter = intent.exitAfter else { 8 | return SetEphemeralPluginIntentResponse() 9 | } 10 | delegate.pluginManager.setEphemeralPlugin(pluginId: id, content: content, exitAfter: Double(truncating: exitAfter)) 11 | return SetEphemeralPluginIntentResponse() 12 | } 13 | 14 | public func resolveName(for intent: SetEphemeralPluginIntent) async -> INStringResolutionResult { 15 | guard let name = intent.name, !name.isEmpty else { 16 | return INStringResolutionResult.needsValue() 17 | } 18 | return INStringResolutionResult.success(with: name) 19 | } 20 | 21 | public func resolveContent(for intent: SetEphemeralPluginIntent) async -> INStringResolutionResult { 22 | guard let content = intent.content, !content.isEmpty else { 23 | return INStringResolutionResult.needsValue() 24 | } 25 | return INStringResolutionResult.success(with: content) 26 | } 27 | 28 | public func resolveExitAfter(for intent: SetEphemeralPluginIntent) async -> INTimeIntervalResolutionResult { 29 | INTimeIntervalResolutionResult.success(with: TimeInterval(truncating: intent.exitAfter ?? 0)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SwiftBar/Utility/NSImage.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSImage { 4 | static func createImage(from base64: String?, isTemplate: Bool) -> NSImage? { 5 | guard let base64, let data = Data(base64Encoded: base64, options: .ignoreUnknownCharacters) else { return nil } 6 | let image = NSImage(data: data) 7 | image?.isTemplate = isTemplate 8 | return image 9 | } 10 | 11 | func resizedCopy(w: CGFloat, h: CGFloat) -> NSImage { 12 | let destSize = NSMakeSize(w, h) 13 | let newImage = NSImage(size: destSize) 14 | 15 | newImage.lockFocus() 16 | 17 | draw(in: NSRect(origin: .zero, size: destSize), 18 | from: NSRect(origin: .zero, size: size), 19 | operation: .copy, 20 | fraction: CGFloat(1)) 21 | 22 | newImage.unlockFocus() 23 | 24 | guard let data = newImage.tiffRepresentation, 25 | let result = NSImage(data: data) 26 | else { return NSImage() } 27 | result.isTemplate = isTemplate 28 | return result 29 | } 30 | 31 | func tintedImage(color: NSColor?) -> NSImage { 32 | guard isTemplate else { return self } 33 | guard let color, let newImage = copy() as? NSImage else { return self } 34 | 35 | newImage.lockFocus() 36 | 37 | color.set() 38 | 39 | let imageRect = NSRect(origin: .zero, size: newImage.size) 40 | imageRect.fill(using: .sourceAtop) 41 | 42 | newImage.unlockFocus() 43 | newImage.isTemplate = false 44 | 45 | return newImage 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mac_16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "mac_16@2x.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "mac_32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "mac_32@2x.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "mac_128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "mac_128@2x.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "mac_256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "mac_256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "mac_512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "mac_512@2x.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SwiftBar.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "00d86dc916f446cbdad9679b84ebcf616ce2940c273d68994fc85aa694a5481e", 3 | "pins" : [ 4 | { 5 | "identity" : "hotkey", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swiftbar/HotKey", 8 | "state" : { 9 | "revision" : "c13662730cb5bc28de4a799854bbb018a90649bf", 10 | "version" : "0.1.3" 11 | } 12 | }, 13 | { 14 | "identity" : "launchatlogin", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/sindresorhus/LaunchAtLogin", 17 | "state" : { 18 | "revision" : "7ad6331f9c38953eb1ce8737758e18f7607e984a", 19 | "version" : "5.0.0" 20 | } 21 | }, 22 | { 23 | "identity" : "preferences", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/sindresorhus/Preferences", 26 | "state" : { 27 | "revision" : "2651cd144615009242c994b087508fef99e9275c", 28 | "version" : "2.6.0" 29 | } 30 | }, 31 | { 32 | "identity" : "sparkle", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/sparkle-project/Sparkle", 35 | "state" : { 36 | "revision" : "7907f058bcef1132c9b4af6c049cac598330a5f9", 37 | "version" : "2.4.1" 38 | } 39 | }, 40 | { 41 | "identity" : "swifcron", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/MihaelIsaev/SwifCron", 44 | "state" : { 45 | "revision" : "51d388da749b002522261e6fe4171acf93ee1b74", 46 | "version" : "2.0.0" 47 | } 48 | } 49 | ], 50 | "version" : 3 51 | } 52 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/GeneralPreferencesView.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import SwiftUI 3 | 4 | struct GeneralPreferencesView: View { 5 | @EnvironmentObject var preferences: PreferencesStore 6 | @State private var launchAtLogin = true 7 | 8 | var body: some View { 9 | Preferences.Container(contentWidth: 350) { 10 | Preferences.Section(title: "\(Localizable.Preferences.LaunchAtLogin.localized):") { 11 | ModernLaunchAtLogin.Toggle { 12 | Text(Localizable.Preferences.LaunchAtLogin.localized) 13 | } 14 | } 15 | Preferences.Section(title: "\(Localizable.Preferences.MenuBarItem.localized):", verticalAlignment: .top) { 16 | Toggle(Localizable.Preferences.DimOnManualRefresh.localized, isOn: $preferences.dimOnManualRefresh) 17 | } 18 | 19 | Preferences.Section(title: "\(Localizable.Preferences.PluginsFolder.localized):", verticalAlignment: .top) { 20 | Button(Localizable.Preferences.ChangePath.localized) { 21 | AppShared.changePluginFolder() 22 | } 23 | Text(preferences.pluginDirectoryPath ?? Localizable.Preferences.PathIsNone.localized) 24 | .preferenceDescription() 25 | Spacer() 26 | } 27 | Preferences.Section(title: "\(Localizable.Preferences.UpdateLabel.localized):", verticalAlignment: .top) { 28 | HStack { 29 | Button(Localizable.Preferences.CheckForUpdates.localized) { 30 | AppShared.checkForUpdates() 31 | } 32 | } 33 | Toggle(Localizable.Preferences.IncludeBetaUpdates.localized, isOn: $preferences.includeBetaUpdates) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /SwiftBar/Utility/String+Escaped.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | func escaped() -> Self { 5 | guard contains(" ") else { return self } 6 | return "'\(self)'" 7 | } 8 | } 9 | 10 | extension String { 11 | func getURL() -> URL? { 12 | if let url = URL(string: self) { 13 | return url 14 | } 15 | 16 | var characterSet = CharacterSet.urlHostAllowed 17 | characterSet.formUnion(.urlPathAllowed) 18 | if let str = addingPercentEncoding(withAllowedCharacters: characterSet) { 19 | return URL(string: str) 20 | } 21 | 22 | return nil 23 | } 24 | } 25 | 26 | extension String { 27 | var URLEncoded: String { 28 | let unreservedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~/:" 29 | let unreservedCharsSet = CharacterSet(charactersIn: unreservedChars) 30 | let encodedString = addingPercentEncoding(withAllowedCharacters: unreservedCharsSet)! 31 | return encodedString 32 | } 33 | } 34 | 35 | extension String { 36 | var isEnclosedInQuotes: Bool { 37 | hasPrefix("'") && hasSuffix("'") 38 | } 39 | 40 | var needsShellQuoting: Bool { 41 | let specialCharacters = " \t\n\"'`$\\|&;()<>[]*?{}!^~#%" 42 | let shellOperators = ["&&", "||", ";", "|", "<", ">"] 43 | 44 | // Check if the string is exactly a logical operator 45 | if shellOperators.contains(self) { 46 | return false 47 | } 48 | 49 | return rangeOfCharacter(from: CharacterSet(charactersIn: specialCharacters)) != nil 50 | } 51 | 52 | func quoteIfNeeded() -> String { 53 | guard needsShellQuoting else { return self } 54 | return isEnclosedInQuotes ? self : "\'\(replacingOccurrences(of: "'", with: "'\\''"))\'" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /SwiftBar/UI/PluginErrorView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PluginErrorView: View { 4 | var plugin: Plugin 5 | var lastUpdateDate: String { 6 | let date = plugin.lastUpdated ?? Date.distantPast 7 | let formatter = DateFormatter() 8 | formatter.dateStyle = .medium 9 | formatter.timeStyle = .medium 10 | return formatter.string(from: date) 11 | } 12 | 13 | var body: some View { 14 | ScrollView(showsIndicators: true) { 15 | Form { 16 | LabelView(label: "Plugin:", value: plugin.name) 17 | LabelView(label: "File:", value: plugin.file) 18 | LabelView(label: "Runned at:", value: lastUpdateDate) 19 | LabelView(label: "Error:", value: errorMessage()) 20 | LabelView(label: "Script Output:", value: errorOutput()) 21 | }.padding() 22 | .frame(width: 500) 23 | } 24 | } 25 | 26 | func errorMessage() -> String { 27 | switch plugin.type { 28 | case .Executable, .Streamable: 29 | (plugin.error as? ShellOutError)?.message ?? "none" 30 | case .Shortcut: 31 | (plugin.error as? RunShortcutError)?.message ?? "none" 32 | case .Ephemeral: 33 | "none" 34 | } 35 | } 36 | 37 | func errorOutput() -> String { 38 | switch plugin.type { 39 | case .Executable, .Streamable: 40 | (plugin.error as? ShellOutError)?.output ?? "none" 41 | case .Shortcut, .Ephemeral: 42 | "none" 43 | } 44 | } 45 | } 46 | 47 | struct PluginErrorView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | PluginErrorView(plugin: ExecutablePlugin(fileURL: URL(string: "/Users/melonamin/Downloads/bitbar-scripts-copy/mounted.5s.sh")!)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/AboutSettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AboutSettingsView: View { 4 | var body: some View { 5 | VStack { 6 | HStack { 7 | Image(nsImage: NSImage(named: "AppIcon")!) 8 | .resizable() 9 | .renderingMode(.original) 10 | .frame(width: 90, height: 90, alignment: .leading) 11 | 12 | VStack(alignment: .leading) { 13 | if #available(macOS 11.0, *) { 14 | Text("SwiftBar") 15 | .font(.title3) 16 | .bold() 17 | } 18 | Text("Version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "") (\(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""))") 19 | .font(.subheadline) 20 | Text("Copyright ©2020-2025 Ameba Labs. All rights reserved.") 21 | .font(.footnote) 22 | .padding(.top, 10) 23 | } 24 | } 25 | Spacer() 26 | Divider() 27 | HStack { 28 | Spacer() 29 | Button("Visit our Website", action: { 30 | NSWorkspace.shared.open(URL(string: "https://swiftbar.app")!) 31 | }) 32 | Button("Contact Us", action: { 33 | NSWorkspace.shared.open(URL(string: "mailto:info@swiftbar.app")!) 34 | }) 35 | }.padding(.top, 10) 36 | .padding(.bottom, 10) 37 | }.padding(.trailing, 20) 38 | .padding(.bottom, 10) 39 | .frame(width: 410, height: 160) 40 | } 41 | } 42 | 43 | struct AboutSettingsView_Previews: PreviewProvider { 44 | static var previews: some View { 45 | AboutSettingsView() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /SwiftBar/AppDelegate+Menu.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class AppMenu: NSMenu { 4 | private lazy var applicationName = ProcessInfo.processInfo.processName 5 | let preferencesItem = NSMenuItem(title: Localizable.MenuBar.Preferences.localized, action: #selector(openPreferences), keyEquivalent: ",") 6 | let sendFeedbackItem = NSMenuItem(title: Localizable.MenuBar.SendFeedback.localized, action: #selector(sendFeedback), keyEquivalent: "") 7 | let aboutSwiftbarItem = NSMenuItem(title: Localizable.MenuBar.AboutPlugin.localized, action: #selector(aboutSwiftBar), keyEquivalent: "") 8 | let quitItem = NSMenuItem(title: Localizable.App.Quit.localized, action: #selector(quit), keyEquivalent: "q") 9 | override init(title: String) { 10 | super.init(title: title) 11 | let menuItemOne = NSMenuItem() 12 | menuItemOne.submenu = NSMenu(title: "menuItemOne") 13 | menuItemOne.submenu?.items = [aboutSwiftbarItem, NSMenuItem.separator(), sendFeedbackItem, preferencesItem, NSMenuItem.separator(), quitItem] 14 | for item in [aboutSwiftbarItem, preferencesItem, sendFeedbackItem, quitItem] { 15 | item.target = self 16 | } 17 | items = [menuItemOne] 18 | } 19 | 20 | required init(coder: NSCoder) { 21 | super.init(coder: coder) 22 | } 23 | 24 | @objc func openPreferences() { 25 | AppShared.openPreferences() 26 | } 27 | 28 | @objc func sendFeedback() { 29 | NSWorkspace.shared.open(URL(string: "https://github.com/swiftbar/SwiftBar/issues")!) 30 | } 31 | 32 | @objc func aboutSwiftBar() { 33 | AppShared.showAbout() 34 | } 35 | 36 | @objc func quit() { 37 | // Ensure the app is in regular activation policy before quitting 38 | // This fixes an issue where CMD+Q would hide the dock instead of quitting 39 | NSApp.setActivationPolicy(.regular) 40 | NSApp.terminate(self) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SwiftBar/Utility/Environment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Environment { 4 | static let shared = Environment() 5 | 6 | enum Variables: String { 7 | case swiftBar = "SWIFTBAR" 8 | case swiftBarVersion = "SWIFTBAR_VERSION" 9 | case swiftBarBuild = "SWIFTBAR_BUILD" 10 | case swiftBarPluginsPath = "SWIFTBAR_PLUGINS_PATH" 11 | case swiftBarPluginPath = "SWIFTBAR_PLUGIN_PATH" 12 | case swiftBarPluginCachePath = "SWIFTBAR_PLUGIN_CACHE_PATH" 13 | case swiftBarPluginDataPath = "SWIFTBAR_PLUGIN_DATA_PATH" 14 | case swiftBarPluginRefreshReason = "SWIFTBAR_PLUGIN_REFRESH_REASON" 15 | case swiftBarLaunchTime = "SWIFTBAR_LAUNCH_TIME" 16 | case osVersionMajor = "OS_VERSION_MAJOR" 17 | case osVersionMinor = "OS_VERSION_MINOR" 18 | case osVersionPatch = "OS_VERSION_PATCH" 19 | case osAppearance = "OS_APPEARANCE" 20 | case osLastSleepTime = "OS_LAST_SLEEP_TIME" 21 | case osLastWakeTime = "OS_LAST_WAKE_TIME" 22 | } 23 | 24 | private var dateFormatter: ISO8601DateFormatter = { 25 | let formatter = ISO8601DateFormatter() 26 | formatter.timeZone = TimeZone.current 27 | return formatter 28 | }() 29 | 30 | var userLoginShell = "/bin/zsh" 31 | 32 | private var systemEnv: [Variables: String] = [ 33 | .swiftBar: "1", 34 | .swiftBarVersion: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "", 35 | .swiftBarBuild: Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "", 36 | .swiftBarPluginsPath: PreferencesStore.shared.pluginDirectoryPath ?? "", 37 | .osVersionMajor: String(ProcessInfo.processInfo.operatingSystemVersion.majorVersion), 38 | .osVersionMinor: String(ProcessInfo.processInfo.operatingSystemVersion.minorVersion), 39 | .osVersionPatch: String(ProcessInfo.processInfo.operatingSystemVersion.patchVersion), 40 | ] 41 | 42 | var systemEnvStr: [String: String] { 43 | Dictionary(uniqueKeysWithValues: 44 | systemEnv.map { key, value in (key.rawValue, value) }) 45 | } 46 | 47 | init() { 48 | systemEnv[.swiftBarLaunchTime] = dateFormatter.string(from: NSDate.now) 49 | } 50 | 51 | func updateSleepTime(date: Date) { 52 | systemEnv[.osLastSleepTime] = dateFormatter.string(from: date) 53 | } 54 | 55 | func updateWakeTime(date: Date) { 56 | systemEnv[.osLastWakeTime] = dateFormatter.string(from: date) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /SwiftBar/UI/Plugin Repository/PluginRepositoryAPI.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | 4 | struct Agent { 5 | let session = URLSession.shared 6 | 7 | struct Response { 8 | let value: T 9 | let response: URLResponse 10 | } 11 | 12 | func run(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher, Error> { 13 | let cache = URLCache.shared 14 | if request.cachePolicy != .reloadIgnoringCacheData, let cachedResponse = cache.cachedResponse(for: request) { 15 | return Just(cachedResponse) 16 | .tryMap { result -> Response in 17 | let value = try decoder.decode(T.self, from: result.data) 18 | return Response(value: value, response: result.response) 19 | } 20 | .receive(on: DispatchQueue.main) 21 | .eraseToAnyPublisher() 22 | } 23 | 24 | return URLSession.shared 25 | .dataTaskPublisher(for: request) 26 | .tryMap { result -> Response in 27 | let value = try decoder.decode(T.self, from: result.data) 28 | let cachedData = CachedURLResponse(response: result.response, data: result.data) 29 | cache.storeCachedResponse(cachedData, for: request) 30 | return Response(value: value, response: result.response) 31 | } 32 | .receive(on: DispatchQueue.main) 33 | .eraseToAnyPublisher() 34 | } 35 | } 36 | 37 | enum PluginRepositoryAPI { 38 | static let agent = Agent() 39 | static func base() -> URL { 40 | PreferencesStore.shared.pluginRepositoryURL 41 | } 42 | } 43 | 44 | extension PluginRepositoryAPI { 45 | static func categories(ignoreCache: Bool = false) -> AnyPublisher { 46 | run(URLRequest(url: base().appendingPathComponent("categories.json"), 47 | cachePolicy: ignoreCache ? .reloadIgnoringCacheData : .returnCacheDataElseLoad)) 48 | } 49 | 50 | static func plugins(category: String, ignoreCache: Bool = false) -> AnyPublisher { 51 | run(URLRequest(url: base().appendingPathComponent("\(category)/plugins.json"), 52 | cachePolicy: ignoreCache ? .reloadIgnoringCacheData : .returnCacheDataElseLoad)) 53 | } 54 | 55 | static func run(_ request: URLRequest) -> AnyPublisher { 56 | agent.run(request) 57 | .map(\.value) 58 | .eraseToAnyPublisher() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Viewer 26 | CFBundleURLSchemes 27 | 28 | swiftbar 29 | 30 | 31 | 32 | CFBundleVersion 33 | $(CURRENT_PROJECT_VERSION) 34 | INIntentsSupported 35 | 36 | GetPluginsIntent 37 | EnablePluginIntent 38 | DisablePluginIntent 39 | ReloadPluginIntent 40 | SetEphemeralPluginIntent 41 | 42 | LSApplicationCategoryType 43 | public.app-category.utilities 44 | LSMinimumSystemVersion 45 | $(MACOSX_DEPLOYMENT_TARGET) 46 | LSUIElement 47 | 48 | NSAppTransportSecurity 49 | 50 | NSAllowsArbitraryLoads 51 | 52 | 53 | NSAppleEventsUsageDescription 54 | Allow SwiftBar to run plugin in Terminal 55 | NSCalendarsUsageDescription 56 | Allow SwiftBar to access your Calendar. This is required by one of your plugins. 57 | NSHumanReadableCopyright 58 | Copyright ©2020-2025 Ameba Labs. All rights reserved. 59 | NSPrincipalClass 60 | NSApplication 61 | NSRemindersUsageDescription 62 | Allow SwiftBar to access your Reminders. This is required by one of your plugins. 63 | NSUserActivityTypes 64 | 65 | DisablePluginIntent 66 | EnablePluginIntent 67 | GetPluginsIntent 68 | ReloadPluginIntent 69 | SetEphemeralPluginIntent 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /SwiftBar/UI/Debug/DebugView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct DebugView: View { 4 | let plugin: Plugin 5 | let sharedEnv = Environment.shared 6 | @ObservedObject var debugInfo: PluginDebugInfo 7 | var debugText: String { 8 | String(debugInfo.events.sorted(by: { $0.key < $1.key }).map { "\n🕐 \($0.key) \($0.value.eventString)" } 9 | .joined(separator: "\n") 10 | .prefix(100_000)) 11 | } 12 | 13 | var body: some View { 14 | VStack { 15 | HStack { 16 | Text(plugin.name) 17 | .font(.headline) 18 | Text("(\(plugin.file))") 19 | .font(.caption) 20 | Spacer() 21 | if #available(OSX 11.0, *) { 22 | Button(action: { 23 | AppShared.openPluginFolder(path: plugin.file) 24 | }) { 25 | Image(systemName: "folder") 26 | }.padding() 27 | } 28 | } 29 | ZStack { 30 | RoundedRectangle(cornerRadius: 10) 31 | .foregroundColor(.black) 32 | ScrollView(showsIndicators: false) { 33 | Text(debugText) 34 | .foregroundColor(.white) 35 | }.padding() 36 | .contextMenu(ContextMenu(menuItems: { 37 | Button("Copy", action: { 38 | let pasteboard = NSPasteboard.general 39 | pasteboard.declareTypes([.string], owner: nil) 40 | pasteboard.setString(debugText, forType: .string) 41 | }) 42 | Button("Clear", action: { 43 | debugInfo.clear() 44 | }) 45 | })) 46 | } 47 | HStack { 48 | Spacer() 49 | Button("Refresh Plugin", action: { 50 | plugin.refresh(reason: .DebugView) 51 | }) 52 | 53 | Button("Print SwiftBar ENV", action: { 54 | let envs = plugin.env 55 | let swiftbarEnv = sharedEnv.systemEnvStr.merging(envs) { current, _ in current } 56 | let debugString = swiftbarEnv.map { "\($0.key) = \($0.value)" }.sorted().joined(separator: "\n") 57 | debugInfo.addEvent(type: .Environment, value: "\n\(debugString)") 58 | }) 59 | Button("Print Plugin Metadata", action: { 60 | debugInfo.addEvent(type: .PluginMetadata, value: "\n\(plugin.metadata?.genereteMetadataString() ?? "")") 61 | }) 62 | } 63 | }.padding() 64 | .frame(minWidth: 500, minHeight: 500) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SwiftBar/Utility/NSMutableAttributedString+SFSymbols.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSMutableAttributedString { 4 | func symbolize(font: NSFont, colors: [NSColor], sfsize: CGFloat?) { 5 | guard #available(OSX 11.0, *) else { 6 | return 7 | } 8 | let regex = ":[a-z,0-9,.]*:" 9 | var resultRanges = [NSRange]() 10 | let currentString = string 11 | do { 12 | let regex = try NSRegularExpression(pattern: regex) 13 | let results = regex.matches(in: currentString, 14 | range: NSRange(currentString.startIndex..., in: currentString)) 15 | for result in results { 16 | resultRanges.append(result.range) 17 | } 18 | 19 | } catch { 20 | print("invalid regex: \(error.localizedDescription)") 21 | return 22 | } 23 | 24 | var index = resultRanges.count - 1 25 | for range in resultRanges.reversed() { 26 | let imageName = (currentString as NSString).substring(with: range) 27 | let clearedImageName = getImageName(from: imageName) 28 | 29 | let imageConfig = NSImage.SymbolConfiguration(pointSize: sfsize ?? font.pointSize, weight: .regular) 30 | guard let image = NSImage(systemSymbolName: clearedImageName, accessibilityDescription: nil)?.withSymbolConfiguration(imageConfig) else { continue } 31 | let tintColor: NSColor? = if index >= colors.count { 32 | colors.last 33 | } else { 34 | colors[index] 35 | } 36 | let attachment = NSTextAttachment.centeredImage(with: image.tintedImage(color: tintColor), and: font) 37 | 38 | let attrWithAttachment = NSAttributedString(attachment: attachment) 39 | 40 | replaceCharacters(in: range, with: "") 41 | insert(attrWithAttachment, at: range.lowerBound) 42 | index -= 1 43 | } 44 | } 45 | 46 | private func getImageName(from s: String) -> String { 47 | guard s.count > 2 else { 48 | return s 49 | } 50 | return String(s.dropFirst().dropLast()) 51 | } 52 | } 53 | 54 | extension NSTextAttachment { 55 | static func centeredImage(with image: NSImage, and font: NSFont) -> NSTextAttachment { 56 | let imageAttachment = NSTextAttachment() 57 | imageAttachment.bounds = CGRect(x: 0, y: (font.capHeight - image.size.height).rounded() / 2, width: image.size.width, height: image.size.height) 58 | imageAttachment.attachmentCell = ImageAttachmentCell(imageCell: image) 59 | return imageAttachment 60 | } 61 | } 62 | 63 | class ImageAttachmentCell: NSTextAttachmentCell { 64 | override func cellBaselineOffset() -> NSPoint { 65 | var baseline = super.cellBaselineOffset() 66 | baseline.y = baseline.y - 3 - (image!.size.height - 16) / 2 67 | return baseline 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /SwiftBar/UI/WebView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import WebKit 3 | 4 | struct WebView: NSViewRepresentable { 5 | let request: URLRequest 6 | let zoomFactor: CGFloat 7 | 8 | init(request: URLRequest, zoomFactor: CGFloat = 1.0) { 9 | self.request = request 10 | self.zoomFactor = zoomFactor 11 | } 12 | 13 | func makeNSView(context: Context) -> WKWebView { 14 | let webView = WKWebView() 15 | webView.navigationDelegate = context.coordinator 16 | return webView 17 | } 18 | 19 | func updateNSView(_ webView: WKWebView, context _: Context) { 20 | webView.load(request) 21 | } 22 | 23 | func makeCoordinator() -> Coordinator { 24 | Coordinator(self) 25 | } 26 | 27 | class Coordinator: NSObject, WKNavigationDelegate { 28 | var parent: WebView 29 | 30 | init(_ parent: WebView) { 31 | self.parent = parent 32 | } 33 | 34 | func webView(_ webView: WKWebView, didFinish _: WKNavigation!) { 35 | if parent.zoomFactor != 1.0 { 36 | applyZoom(to: webView, scale: parent.zoomFactor) 37 | } 38 | } 39 | 40 | private func applyZoom(to webView: WKWebView, scale: CGFloat) { 41 | let zoomScript = """ 42 | (function() { 43 | document.body.style.transformOrigin = 'top left'; 44 | document.body.style.transform = 'scale(\(scale))'; 45 | document.body.style.width = '\(100 / scale)%'; 46 | document.documentElement.style.overflow = 'auto'; 47 | })(); 48 | """ 49 | webView.evaluateJavaScript(zoomScript, completionHandler: nil) 50 | } 51 | } 52 | } 53 | 54 | struct WebPanelView: View { 55 | let request: URLRequest 56 | let name: String 57 | let zoomFactor: CGFloat 58 | 59 | init(request: URLRequest, name: String, zoomFactor: CGFloat = 1.0) { 60 | self.request = request 61 | self.name = name 62 | self.zoomFactor = zoomFactor 63 | } 64 | 65 | // This property lets us detect if we're in a detached window 66 | @State private var isDetachedWindow: Bool = false 67 | 68 | var body: some View { 69 | VStack(spacing: 0) { 70 | if !isDetachedWindow { 71 | ZStack { 72 | if #available(macOS 12.0, *) { 73 | Rectangle().fill(.bar) 74 | } else { 75 | Rectangle().fill(.background) 76 | } 77 | HStack { 78 | Spacer() 79 | Text("SwiftBar: \(name)") 80 | .font(.headline) 81 | .lineLimit(1) 82 | 83 | Spacer() 84 | } 85 | .padding(.horizontal, 12) 86 | } 87 | .frame(height: 28) 88 | .padding(.top, 4) 89 | } 90 | 91 | WebView(request: request, zoomFactor: zoomFactor) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /SwiftBar.xcodeproj/xcshareddata/xcschemes/SwiftBar MAS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /SwiftBar/Plugin/EphemeralPlugin.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import os 4 | 5 | class EphemeralPlugin: Plugin { 6 | var id: PluginID 7 | let type: PluginType = .Ephemeral 8 | var name: String { 9 | "Ephemeral: \(id)" 10 | } 11 | 12 | let file: String = "none" 13 | var refreshEnv: [String: String] = [:] 14 | 15 | var updateInterval: Double = 60 * 60 * 24 * 100 { 16 | didSet { 17 | cancellable.forEach { $0.cancel() } 18 | cancellable.removeAll() 19 | 20 | guard updateInterval != 0 else { return } 21 | updateTimerPublisher 22 | .autoconnect() 23 | .receive(on: RunLoop.main) 24 | .sink(receiveValue: { [weak self] _ in 25 | self?.terminate() 26 | }).store(in: &cancellable) 27 | } 28 | } 29 | 30 | private var _metadata: PluginMetadata? 31 | private let metadataQueue = DispatchQueue(label: "com.ameba.SwiftBar.EphemeralPlugin.metadata", attributes: .concurrent) 32 | 33 | var metadata: PluginMetadata? { 34 | get { 35 | metadataQueue.sync { _metadata } 36 | } 37 | set { 38 | metadataQueue.async(flags: .barrier) { [weak self] in 39 | self?._metadata = newValue 40 | } 41 | } 42 | } 43 | 44 | var lastUpdated: Date? 45 | var lastState: PluginState 46 | var lastRefreshReason: PluginRefreshReason = .FirstLaunch 47 | var contentUpdatePublisher = PassthroughSubject() 48 | var operation: RunPluginOperation? 49 | 50 | var content: String? = "..." { 51 | didSet { 52 | guard content != oldValue else { return } 53 | lastUpdated = Date() 54 | contentUpdatePublisher.send(content) 55 | } 56 | } 57 | 58 | var error: Error? 59 | var debugInfo = PluginDebugInfo() 60 | 61 | lazy var invokeQueue: OperationQueue = delegate.pluginManager.pluginInvokeQueue 62 | 63 | var updateTimerPublisher: Timer.TimerPublisher { 64 | Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .default) 65 | } 66 | 67 | var cronTimer: Timer? 68 | 69 | var cancellable: Set = [] 70 | 71 | let prefs = PreferencesStore.shared 72 | 73 | init(id: PluginID, content: String, exitAfter: Double) { 74 | self.id = id 75 | lastState = .Success 76 | os_log("Initialized ephemeral plugin\n%{public}@", log: Log.plugin, description) 77 | refresh(reason: .FirstLaunch) 78 | self.content = content 79 | lastUpdated = Date() 80 | if exitAfter != 0 { 81 | updateInterval = exitAfter 82 | updateTimerPublisher 83 | .autoconnect() 84 | .receive(on: invokeQueue) 85 | .sink(receiveValue: { [weak self] _ in 86 | self?.terminate() 87 | }).store(in: &cancellable) 88 | } 89 | } 90 | 91 | func disable() {} 92 | 93 | func terminate() { 94 | delegate.pluginManager.setEphemeralPlugin(pluginId: id, content: "") 95 | } 96 | 97 | func enable() {} 98 | 99 | func start() {} 100 | 101 | func refresh(reason _: PluginRefreshReason) {} 102 | 103 | func invoke() -> String? { 104 | nil 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /SwiftBar/Utility/URL+Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension URL { 4 | var queryParameters: [String: String]? { 5 | guard 6 | let components = URLComponents(url: self, resolvingAgainstBaseURL: true), 7 | let queryItems = components.queryItems else { return nil } 8 | return queryItems.reduce(into: [String: String]()) { result, item in 9 | result[item.name] = item.value 10 | } 11 | } 12 | } 13 | 14 | // https://stackoverflow.com/questions/38343186/write-extend-file-attributes-swift-example 15 | extension URL { 16 | func extendedAttribute(forName name: String) throws -> Data { 17 | let data = try withUnsafeFileSystemRepresentation { fileSystemPath -> Data in 18 | 19 | // Determine attribute size: 20 | let length = getxattr(fileSystemPath, name, nil, 0, 0, 0) 21 | guard length >= 0 else { throw URL.posixError(errno) } 22 | 23 | // Create buffer with required size: 24 | var data = Data(count: length) 25 | 26 | // Retrieve attribute: 27 | let result = data.withUnsafeMutableBytes { [count = data.count] in 28 | getxattr(fileSystemPath, name, $0.baseAddress, count, 0, 0) 29 | } 30 | guard result >= 0 else { throw URL.posixError(errno) } 31 | return data 32 | } 33 | return data 34 | } 35 | 36 | /// Set extended attribute. 37 | func setExtendedAttribute(data: Data, forName name: String) throws { 38 | try withUnsafeFileSystemRepresentation { fileSystemPath in 39 | let result = data.withUnsafeBytes { 40 | setxattr(fileSystemPath, name, $0.baseAddress, data.count, 0, 0) 41 | } 42 | guard result >= 0 else { throw URL.posixError(errno) } 43 | } 44 | } 45 | 46 | /// Remove extended attribute. 47 | func removeExtendedAttribute(forName name: String) throws { 48 | try withUnsafeFileSystemRepresentation { fileSystemPath in 49 | let result = removexattr(fileSystemPath, name, 0) 50 | guard result >= 0 else { throw URL.posixError(errno) } 51 | } 52 | } 53 | 54 | /// Get list of all extended attributes. 55 | func listExtendedAttributes() throws -> [String] { 56 | let list = try withUnsafeFileSystemRepresentation { fileSystemPath -> [String] in 57 | let length = listxattr(fileSystemPath, nil, 0, 0) 58 | guard length >= 0 else { throw URL.posixError(errno) } 59 | 60 | // Create buffer with required size: 61 | var namebuf = [CChar](repeating: 0, count: length) 62 | 63 | // Retrieve attribute list: 64 | let result = listxattr(fileSystemPath, &namebuf, namebuf.count, 0) 65 | guard result >= 0 else { throw URL.posixError(errno) } 66 | 67 | // Extract attribute names: 68 | let list = namebuf.split(separator: 0).compactMap { 69 | $0.withUnsafeBufferPointer { 70 | $0.withMemoryRebound(to: UInt8.self) { 71 | String(bytes: $0, encoding: .utf8) 72 | } 73 | } 74 | } 75 | return list 76 | } 77 | return list 78 | } 79 | 80 | /// Helper function to create an NSError from a Unix errno. 81 | private static func posixError(_ err: Int32) -> NSError { 82 | NSError(domain: NSPOSIXErrorDomain, code: Int(err), 83 | userInfo: [NSLocalizedDescriptionKey: String(cString: strerror(err))]) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/PreferencesView.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import SwiftUI 3 | 4 | extension Preferences.PaneIdentifier { 5 | static let general = Self("general") 6 | static let plugins = Self("plugins") 7 | static let shortcutPlugins = Self("shortcutPlugins") 8 | static let advanced = Self("advanced") 9 | static let about = Self("about") 10 | 11 | var image: NSImage { 12 | switch self { 13 | case .general: 14 | if #available(OSX 11.0, *) { 15 | NSImage(systemSymbolName: "gear", accessibilityDescription: nil)! 16 | } else { 17 | NSImage(named: "AppIcon")! 18 | } 19 | case .plugins: 20 | if #available(OSX 11.0, *) { 21 | NSImage(systemSymbolName: "curlybraces", accessibilityDescription: nil)! 22 | } else { 23 | NSImage(named: "AppIcon")! 24 | } 25 | case .shortcutPlugins: 26 | if #available(OSX 11.0, *) { 27 | NSImage(systemSymbolName: "flowchart", accessibilityDescription: nil)! 28 | } else { 29 | NSImage(named: "AppIcon")! 30 | } 31 | case .advanced: 32 | if #available(OSX 11.0, *) { 33 | NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)! 34 | } else { 35 | NSImage(named: "AppIcon")! 36 | } 37 | case .about: 38 | if #available(OSX 11.0, *) { 39 | NSImage(systemSymbolName: "info", accessibilityDescription: nil)! 40 | } else { 41 | NSImage(named: "AppIcon")! 42 | } 43 | default: 44 | NSImage(named: "AppIcon")! 45 | } 46 | } 47 | } 48 | 49 | var preferencePanes: [PreferencePaneConvertible] = { 50 | var panes: [PreferencePaneConvertible] = [ 51 | Preferences.Pane( 52 | identifier: .general, 53 | title: Localizable.Preferences.General.localized, 54 | toolbarIcon: Preferences.PaneIdentifier.general.image 55 | ) { GeneralPreferencesView().environmentObject(PreferencesStore.shared) }, 56 | Preferences.Pane( 57 | identifier: .plugins, 58 | title: Localizable.Preferences.Plugins.localized, 59 | toolbarIcon: Preferences.PaneIdentifier.plugins.image 60 | ) { PluginsPreferencesView(pluginManager: PluginManager.shared).environmentObject(PreferencesStore.shared) }, 61 | ] 62 | 63 | if #available(macOS 12, *) { 64 | panes.append( 65 | Preferences.Pane( 66 | identifier: .shortcutPlugins, 67 | title: Localizable.Preferences.ShortcutPlugins.localized, 68 | toolbarIcon: Preferences.PaneIdentifier.shortcutPlugins.image 69 | ) { ShortcutPluginsPreferencesView(pluginManager: PluginManager.shared).environmentObject(PreferencesStore.shared) } 70 | ) 71 | } 72 | 73 | panes.append( 74 | Preferences.Pane( 75 | identifier: .advanced, 76 | title: Localizable.Preferences.Advanced.localized, 77 | toolbarIcon: Preferences.PaneIdentifier.advanced.image 78 | ) { AdvancedPreferencesView().environmentObject(PreferencesStore.shared) } 79 | ) 80 | 81 | panes.append( 82 | Preferences.Pane( 83 | identifier: .about, 84 | title: Localizable.Preferences.About.localized, 85 | toolbarIcon: Preferences.PaneIdentifier.about.image 86 | ) { AboutSettingsView() } 87 | ) 88 | 89 | return panes 90 | }() 91 | -------------------------------------------------------------------------------- /SwiftBar.xcodeproj/xcshareddata/xcschemes/SwiftBar.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "更新中…"; 4 | "MB_LAST_UPDATED" = "上一次更新"; 5 | "MB_ABOUT_SWIFT_BAR" = "关于"; 6 | "MB_ABOUT_PLUGIN" = "关于"; 7 | "MB_RUN_IN_TERMINAL" = "从命令行运行…"; 8 | "MB_DISABLE_PLUGIN" = "停用插件"; 9 | "MB_DDEBUG_PLUGIN" = "调试插件"; 10 | "MB_TERMINATE_EPH" = "终止插件"; 11 | "MB_PREFERENCES" = "偏好设置…"; 12 | "MB_REFRESH_ALL" = "刷新全部插件"; 13 | "MB_ENABLE_ALL" = "开启全部插件"; 14 | "MB_DISABLE_ALL" = "停用全部插件"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "打开插件目录…"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "修改插件目录…"; 17 | "MB_GET_PLUGINS" = "获取插件..."; 18 | "MB_SEND_FEEDBACK" = "发送反馈..."; 19 | "MB_SHOW_ERROR" = "展示错误信息"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "偏好设置"; 23 | "PF_GENERAL" = "通用"; 24 | "PF_ADVANCED" = "高级"; 25 | "PF_PLUGINS" = "插件"; 26 | "PF_SHORTCUT_PLUGINS" = "快捷指令插件"; 27 | "PF_ABOUT" = "关于"; 28 | "PF_PLUGINS_FOLDER" = "插件目录"; 29 | "PR_LAUNCH_AT_LOGIN" = "登录时启动"; 30 | "PF_PATH" = "路径"; 31 | "PF_PATH_IS_NONE" = "无"; 32 | "PF_CHANGE_PATH" = "修改..."; 33 | "PF_SHELL" = "命令行解释器"; 34 | "PF_TERMINAL" = "终端"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "隐藏 SwiftBar 图标"; 36 | "PF_STEALTH_MODE" = "在菜单栏隐藏 SwiftBar"; 37 | "PF_CHECK_FOR_UPDATE" = "更新"; 38 | "PF_CHECK_FOR_UPDATES" = "检查更新"; 39 | "PR_INCLUDE_BETA_UPDATES" = "包括预发布版本"; 40 | "PR_SHARE_CRASH_REPORTS" = "分享崩溃报告"; 41 | "PF_NO_PLUGINS_MESSAGE" = "插件目录是空的"; 42 | "PF_ENABLE_ALL" = "开启所有"; 43 | "PF_PLUGINS_FOOTNOTE" = "菜单栏中会显示已启用的插件。"; 44 | "PF_MENUBAR_ITEM" = "菜单栏项目"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "手动刷新时变暗"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "名称"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "快捷指令"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "刷新"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "删除"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "你确定要删除 '' 吗?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "添加"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "启用\禁用菜单栏项目"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "刷新菜单栏项目"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "添加到快捷指令插件"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "名称"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "文件夹"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "快捷指令"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "刷新快捷指令列表"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "在 快捷指令.app 中打开"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "新建快捷指令"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "刷新时间"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "类别"; 65 | "PR_PLUGIN_REPOSITORY" = "插件仓库"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "正在刷新数据…"; 67 | "PR_DEPENDENCIES" = "依赖"; 68 | "PR_PLUGIN_SOURCE" = "源码"; 69 | "PR_ABOUT_PLUGIN" = "关于"; 70 | "PR_AUTORH_PREPOSITION" = "作者"; 71 | "PR_INSTALL_STATUS_INSTALL" = "安装"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "已安装"; 73 | "PR_INSTALL_STATUS_FAILED" = "失败"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "下载中…"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "选择插件目录"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "不能使用此目录作为 SwiftBar 的插件目录"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "选择另一个位置"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "哇!看起来你选择了一个包含很多文件的文件夹,请选择另一个。如果有插件依赖的 node_modules 文件夹,请将其隐藏。"; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "设置 SwiftBar 插件位置"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "选择一个目录来储存插件仓库"; 84 | "APP_QUIT" = "退出 SwiftBar"; 85 | "OK" = "确定"; 86 | "CANCEL" = "取消"; 87 | 88 | // Plugin Repository Categories 89 | "CAT_AWS" = "亚马逊云服务(AWS)"; 90 | "CAT_CRYPTOCURRENCY" = "加密货币"; 91 | "CAT_DEV" = "开发"; 92 | "CAT_E-COMMERCE" = "网购"; 93 | "CAT_EMAIL" = "邮箱"; 94 | "CAT_ENVIRONMENT" = "环境"; 95 | "CAT_FINANCE" = "金融"; 96 | "CAT_GAMES" = "游戏"; 97 | "CAT_LIFESTYLE" = "生活"; 98 | "CAT_MESSENGER" = "信息"; 99 | "CAT_MUSIC" = "音乐"; 100 | "CAT_NETWORK" = "网络"; 101 | "CAT_POLITICS" = "政治"; 102 | "CAT_SCIENCE" = "科学"; 103 | "CAT_SPORTS" = "体育"; 104 | "CAT_SYSTEM" = "系统"; 105 | "CAT_TIME" = "时间"; 106 | "CAT_TOOLS" = "工具"; 107 | "CAT_TRAVEL" = "旅行"; 108 | "CAT_TUTORIAL" = "指南"; 109 | "CAT_WEATHER" = "天气"; 110 | "CAT_WEB" = "第三方"; 111 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/PluginsPreferencesView.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import SwiftUI 3 | 4 | struct PluginsPreferencesView: View { 5 | @ObservedObject var pluginManager: PluginManager 6 | 7 | var body: some View { 8 | VStack { 9 | if pluginManager.plugins.isEmpty { 10 | Text(Localizable.Preferences.NoPluginsMessage.localized) 11 | .font(.largeTitle) 12 | .padding(.bottom, 50) 13 | } else { 14 | PluginsView(plugin: pluginManager.plugins.first!, plugins: pluginManager.plugins.filter { $0.type == .Streamable || $0.type == .Executable }) 15 | } 16 | }.frame(width: 750, height: 400) 17 | } 18 | } 19 | 20 | struct PluginsView: View { 21 | @State var plugin: Plugin 22 | 23 | var plugins: [Plugin] 24 | 25 | var body: some View { 26 | PluginPreferencesSplitView(master: { 27 | SidebarView(plugins: plugins, selectedPlugin: $plugin) 28 | }, detail: { 29 | PluginDetailsView(md: plugin.metadata ?? .empty(), plugin: plugin) 30 | }) 31 | } 32 | } 33 | 34 | struct SidebarView: View { 35 | var plugins: [Plugin] 36 | @Binding var selectedPlugin: Plugin 37 | var body: some View { 38 | List { 39 | ForEach(plugins, id: \.id) { plugin in 40 | PluginRowView(plugin: plugin, selected: selectedPlugin.id == plugin.id) 41 | .onTapGesture { 42 | selectedPlugin = plugin 43 | print(plugin.id) 44 | } 45 | .listRowBackground(Group { 46 | if selectedPlugin.id == plugin.id { 47 | Color(NSColor.selectedContentBackgroundColor).mask(RoundedRectangle(cornerRadius: 5, style: .continuous)) 48 | } else { Color.clear } 49 | }) 50 | } 51 | }.listStyle(SidebarListStyle()) 52 | .frame(minWidth: 200) 53 | } 54 | } 55 | 56 | struct PluginRowView: View { 57 | @State private var enabled: Bool = false 58 | var label: String { 59 | guard let name = plugin.metadata?.name, !name.isEmpty else { 60 | return plugin.name 61 | } 62 | return name 63 | } 64 | 65 | let plugin: Plugin 66 | var selected: Bool = false 67 | var body: some View { 68 | HStack(alignment: .center) { 69 | Toggle("", isOn: $enabled.onUpdate(updatePluginStatus)) 70 | 71 | if selected { 72 | Text(label) 73 | .foregroundColor(Color.white) 74 | } else { 75 | Text(label) 76 | } 77 | }.onAppear { 78 | enabled = plugin.enabled 79 | }.padding(5) 80 | } 81 | 82 | private func updatePluginStatus() { 83 | enabled ? delegate.pluginManager.enablePlugin(plugin: plugin) : 84 | delegate.pluginManager.disablePlugin(plugin: plugin) 85 | } 86 | } 87 | 88 | struct PluginPreferencesSplitView: View { 89 | var master: Master 90 | var detail: Detail 91 | 92 | init(@ViewBuilder master: () -> Master, @ViewBuilder detail: () -> Detail) { 93 | self.master = master() 94 | self.detail = detail() 95 | } 96 | 97 | var body: some View { 98 | let viewControllers = [NSHostingController(rootView: master), NSHostingController(rootView: detail)] 99 | return SplitViewController(viewControllers: viewControllers) 100 | } 101 | } 102 | 103 | struct SplitViewController: NSViewControllerRepresentable { 104 | typealias NSViewControllerType = NSSplitViewController 105 | 106 | var viewControllers: [NSViewController] 107 | 108 | func makeNSViewController(context _: Context) -> NSSplitViewController { 109 | NSSplitViewController() 110 | } 111 | 112 | func updateNSViewController(_ splitController: NSSplitViewController, context _: Context) { 113 | splitController.children = viewControllers 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /SwiftBar/Utility/String+ANSIColor.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | private var ANSIForeground: [Int: NSColor] = [ 4 | // foreground 5 | 39: .labelColor, 6 | 30: .black, 7 | 31: .systemRed, 8 | 32: .systemGreen, 9 | 33: .systemYellow, 10 | 34: .systemBlue, 11 | 35: .magenta, 12 | 36: .cyan, 13 | 37: .white, 14 | 90: .darkGray, 15 | 91: .red, 16 | 92: .green, 17 | 93: .yellow, 18 | 94: .blue, 19 | 95: .magenta, 20 | 96: .cyan, 21 | 97: .white, 22 | ] 23 | 24 | private var ANSIBackground: [Int: NSColor] = [ 25 | // background 26 | 40: .black, 27 | 41: .systemRed, 28 | 42: .systemGreen, 29 | 43: .systemYellow, 30 | 44: .systemBlue, 31 | 45: .magenta, 32 | 46: .cyan, 33 | 47: .systemGray, 34 | 49: .textBackgroundColor, 35 | 100: .darkGray, 36 | 101: .red, 37 | 102: .green, 38 | 103: .yellow, 39 | 104: .blue, 40 | 105: .magenta, 41 | 106: .cyan, 42 | 107: .white, 43 | ] 44 | 45 | extension String { 46 | func colorizedWithANSIColor() -> NSMutableAttributedString { 47 | let out = NSMutableAttributedString() 48 | var attributes: [NSAttributedString.Key: Any] = [:] 49 | let parts = replacingOccurrences(of: "\\e", with: "\u{1B}") 50 | .components(separatedBy: "\u{1B}[") 51 | out.append(NSAttributedString(string: parts.first ?? "")) 52 | 53 | for part in parts[1...] { 54 | guard part.count > 0 else { continue } 55 | 56 | let sequence = part.components(separatedBy: "m") 57 | var text = sequence.last ?? "" 58 | 59 | guard sequence.count >= 2 else { 60 | out.append(NSAttributedString(string: text, attributes: attributes)) 61 | continue 62 | } 63 | 64 | text = sequence[1...].joined(separator: "m") 65 | attributes.attributesForANSICodes(codes: sequence[0]) 66 | 67 | out.append(NSAttributedString(string: text, attributes: attributes)) 68 | } 69 | 70 | return out 71 | } 72 | } 73 | 74 | extension [NSAttributedString.Key: Any] { 75 | mutating func attributesForANSICodes(codes: String) { 76 | var color256 = false 77 | var foreground = false 78 | let font = self[.font] 79 | 80 | for codeString in codes.components(separatedBy: ";") { 81 | guard var code = Int(codeString) else { continue } 82 | if color256 { 83 | color256 = false 84 | if let color = NSColor.colorForAnsi256ColorIndex(index: code) { 85 | self[foreground ? .foregroundColor : .backgroundColor] = color 86 | foreground = false 87 | continue 88 | } 89 | 90 | if code >= 8, code < 16 { 91 | code -= 8 92 | } 93 | code += foreground ? 30 : 40 94 | } else if code == 5 { 95 | color256 = true 96 | continue 97 | } 98 | 99 | if code == 0 { 100 | removeAll() 101 | self[.font] = font 102 | continue 103 | } 104 | 105 | if code == 1 { 106 | self[.font] = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize) 107 | continue 108 | } 109 | 110 | if code == 38 { 111 | foreground = true 112 | continue 113 | } 114 | if code == 39 { 115 | removeValue(forKey: .foregroundColor) 116 | continue 117 | } 118 | if code == 48 { 119 | foreground = false 120 | continue 121 | } 122 | if code == 49 { 123 | removeValue(forKey: .backgroundColor) 124 | continue 125 | } 126 | if let color = ANSIForeground[code] { 127 | self[.foregroundColor] = color 128 | continue 129 | } 130 | if let color = ANSIBackground[code] { 131 | self[.backgroundColor] = color 132 | continue 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Updating..."; 4 | "MB_LAST_UPDATED" = "Updated"; 5 | "MB_ABOUT_SWIFT_BAR" = "About"; 6 | "MB_ABOUT_PLUGIN" = "About"; 7 | "MB_RUN_IN_TERMINAL" = "Run in Terminal..."; 8 | "MB_DISABLE_PLUGIN" = "Disable Plugin"; 9 | "MB_DDEBUG_PLUGIN" = "Debug Plugin"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Preferences..."; 12 | "MB_REFRESH_ALL" = "Refresh All"; 13 | "MB_ENABLE_ALL" = "Enable All"; 14 | "MB_DISABLE_ALL" = "Disable All"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Open Plugin Folder..."; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Change Plugin Folder..."; 17 | "MB_GET_PLUGINS" = "Get Plugins..."; 18 | "MB_SEND_FEEDBACK" = "Send Feedback..."; 19 | "MB_SHOW_ERROR" = "Show Error"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Preferences"; 23 | "PF_GENERAL" = "General"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Plugins"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Plugin Folder"; 29 | "PR_LAUNCH_AT_LOGIN" = "Launch at Login"; 30 | "PF_PATH" = "Path"; 31 | "PF_PATH_IS_NONE" = "None"; 32 | "PF_CHANGE_PATH" = "Change..."; 33 | "PF_SHELL" = "Shell"; 34 | "PF_TERMINAL" = "Terminal"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Hide SwiftBar Icon"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Update"; 38 | "PF_CHECK_FOR_UPDATES" = "Check for updates"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Enthalten Vorabversionen"; 40 | "PR_SHARE_CRASH_REPORTS" = "Fehlerberichte teilen"; 41 | "PF_NO_PLUGINS_MESSAGE" = "Plugins folder is empty"; 42 | "PF_ENABLE_ALL" = "Enable All"; 43 | "PF_PLUGINS_FOOTNOTE" = "Enabled plugins appear in the menu bar."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Category"; 65 | "PR_PLUGIN_REPOSITORY" = "Plugin Repository"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Refreshing repository data..."; 67 | "PR_DEPENDENCIES" = "Dependencies"; 68 | "PR_PLUGIN_SOURCE" = "Source"; 69 | "PR_ABOUT_PLUGIN" = "About"; 70 | "PR_AUTORH_PREPOSITION" = "by"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Install"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Installed"; 73 | "PR_INSTALL_STATUS_FAILED" = "Failed"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Downloading"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Choose plugin folder"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "Can't use this folder as SwiftBar plugins location"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Choose New Location"; 81 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Set SwiftBar Plugins Location"; 82 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Select a folder to store the plugins repository"; 83 | "APP_QUIT" = "Quit SwiftBar"; 84 | "OK" = "Ok"; 85 | "CANCEL" = "Cancel"; 86 | 87 | // Plugin Repository Categories 88 | "CAT_AWS" = "AWS"; 89 | "CAT_CRYPTOCURRENCY" = "Cryptocurrency"; 90 | "CAT_DEV" = "Dev"; 91 | "CAT_E-COMMERCE" = "E-Commerce"; 92 | "CAT_EMAIL" = "Email"; 93 | "CAT_ENVIRONMENT" = "Environment"; 94 | "CAT_FINANCE" = "Finance"; 95 | "CAT_GAMES" = "Games"; 96 | "CAT_LIFESTYLE" = "Lifestyle"; 97 | "CAT_MESSENGER" = "Messenger"; 98 | "CAT_MUSIC" = "Music"; 99 | "CAT_NETWORK" = "Network"; 100 | "CAT_POLITICS" = "Politics"; 101 | "CAT_SCIENCE" = "Science"; 102 | "CAT_SPORTS" = "Sports"; 103 | "CAT_SYSTEM" = "System"; 104 | "CAT_TIME" = "Time"; 105 | "CAT_TOOLS" = "Tools"; 106 | "CAT_TRAVEL" = "Travel"; 107 | "CAT_TUTORIAL" = "Tutorial"; 108 | "CAT_WEATHER" = "Weather"; 109 | "CAT_WEB" = "Web"; 110 | -------------------------------------------------------------------------------- /SwiftBar/AppDelegate+Toolbar.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | extension NSToolbarItem.Identifier { 4 | static let sendFeedback = NSToolbarItem.Identifier(rawValue: "sendFeedback") 5 | static let search = NSToolbarItem.Identifier(rawValue: "search") 6 | static let refresh = NSToolbarItem.Identifier(rawValue: "refresh") 7 | } 8 | 9 | extension NSToolbar { 10 | static let repositoryToolbar: NSToolbar = { 11 | let toolbar = NSToolbar(identifier: "RepositoryToolbar") 12 | toolbar.displayMode = .iconOnly 13 | return toolbar 14 | }() 15 | } 16 | 17 | extension AppDelegate: NSToolbarDelegate { 18 | func setupToolbar() { 19 | NSToolbar.repositoryToolbar.delegate = self 20 | if #available(OSX 11.0, *) { 21 | repositoryToolbarSearchItem = NSSearchToolbarItem(itemIdentifier: .search) 22 | guard let searchField = (repositoryToolbarSearchItem as? NSSearchToolbarItem)?.searchField else { return } 23 | searchField.delegate = self 24 | } 25 | } 26 | 27 | func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { 28 | [.toggleSidebar, .flexibleSpace, .search, .sendFeedback, .refresh] 29 | } 30 | 31 | func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] { 32 | [.toggleSidebar, .flexibleSpace, .search, .sendFeedback, .refresh] 33 | } 34 | 35 | func toolbar(_: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar _: Bool) -> NSToolbarItem? { 36 | switch itemIdentifier { 37 | case .sendFeedback: 38 | var button = if #available(OSX 11.0, *) { 39 | NSButton(image: NSImage(systemSymbolName: "ant", accessibilityDescription: "")!, target: nil, action: #selector(sendFeedback)) 40 | } else { 41 | NSButton(title: "Feedback", target: nil, action: #selector(sendFeedback)) 42 | } 43 | button.bezelStyle = .texturedRounded 44 | return customToolbarItem(itemIdentifier: .sendFeedback, label: Localizable.MenuBar.SendFeedback.localized, 45 | paletteLabel: Localizable.MenuBar.SendFeedback.localized, toolTip: "", itemContent: button) 46 | case .search: 47 | return repositoryToolbarSearchItem 48 | case .refresh: 49 | var button = if #available(OSX 11.0, *) { 50 | NSButton(image: NSImage(systemSymbolName: "arrow.counterclockwise.circle", accessibilityDescription: "")!, target: nil, action: #selector(refresh)) 51 | } else { 52 | NSButton(title: "Refresh", target: nil, action: #selector(refresh)) 53 | } 54 | button.bezelStyle = .texturedRounded 55 | return customToolbarItem(itemIdentifier: .refresh, label: Localizable.MenuBar.GetPlugins.localized, 56 | paletteLabel: Localizable.MenuBar.GetPlugins.localized, toolTip: "", itemContent: button) 57 | default: 58 | return nil 59 | } 60 | } 61 | 62 | @objc func sendFeedback() { 63 | NSWorkspace.shared.open(URL(string: "https://github.com/matryer/bitbar-plugins/issues")!) 64 | } 65 | 66 | @objc func refresh() { 67 | AppShared.refreshRepositoryData() 68 | } 69 | 70 | func customToolbarItem( 71 | itemIdentifier: NSToolbarItem.Identifier, 72 | label: String, 73 | paletteLabel: String, 74 | toolTip: String, 75 | itemContent: NSButton 76 | ) -> NSToolbarItem? { 77 | let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier) 78 | 79 | toolbarItem.label = label 80 | toolbarItem.paletteLabel = paletteLabel 81 | toolbarItem.toolTip = toolTip 82 | toolbarItem.view = itemContent 83 | 84 | let menuItem = NSMenuItem() 85 | menuItem.submenu = nil 86 | menuItem.title = label 87 | toolbarItem.menuFormRepresentation = menuItem 88 | 89 | return toolbarItem 90 | } 91 | } 92 | 93 | extension AppDelegate: NSSearchFieldDelegate { 94 | func controlTextDidChange(_ obj: Notification) { 95 | if #available(OSX 11.0, *) { 96 | guard let searchField = (repositoryToolbarSearchItem as? NSSearchToolbarItem)?.searchField, 97 | obj.object as? NSSearchField === searchField 98 | else { return } 99 | let searchString = searchField.stringValue 100 | NotificationCenter.default.post(name: .repositoirySearchUpdate, object: nil, userInfo: ["query": searchString]) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /SwiftBar/Utility/ShortcutsManager.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Combine 3 | import Foundation 4 | import ScriptingBridge 5 | 6 | public struct RunShortcutError: Swift.Error { 7 | public enum FailReason { 8 | case NoPermissions 9 | case ShortcutNotFound 10 | case NoShortcutOutput 11 | case CantParseShortcutOutput 12 | } 13 | 14 | public let errorReason: FailReason 15 | public var message: String 16 | } 17 | 18 | @objc protocol ShortcutsEvents { 19 | @objc optional var shortcuts: SBElementArray { get } 20 | } 21 | 22 | @objc protocol Shortcut { 23 | @objc optional var name: String { get } 24 | @objc optional func run(withInput: Any?) -> Any? 25 | } 26 | 27 | extension SBApplication: ShortcutsEvents {} 28 | extension SBObject: Shortcut {} 29 | 30 | public class ShortcutsManager: ObservableObject { 31 | static let shared = ShortcutsManager() 32 | var task: Process? 33 | var shortcutsURL = URL(fileURLWithPath: "/usr/bin/shortcuts") 34 | var shellURL = URL(fileURLWithPath: "/bin/zsh") 35 | var prefs = PreferencesStore.shared 36 | var cancellable: AnyCancellable? 37 | 38 | @Published public var shortcuts: [String] = [] 39 | @Published public var folders: [String] = [] 40 | 41 | lazy var shortcutInputPath: URL = { 42 | let directory = NSTemporaryDirectory() 43 | return NSURL.fileURL(withPathComponents: [directory, "shortcutInput"])! 44 | }() 45 | 46 | public init() { 47 | if #available(macOS 12, *) { 48 | getShortcutsFolders() 49 | getShortcuts() 50 | cancellable = prefs.$shortcutsFolder.receive(on: RunLoop.main).sink { [weak self] folder in 51 | self?.getShortcuts(folder: folder) 52 | } 53 | } 54 | } 55 | 56 | public func getShortcuts(folder: String? = nil) { 57 | task = Process() 58 | task?.executableURL = shortcutsURL 59 | task?.arguments = ["list", "-f", "\(folder ?? prefs.shortcutsFolder)"] 60 | 61 | let pipe = Pipe() 62 | task?.standardOutput = pipe 63 | task?.launch() 64 | task?.waitUntilExit() 65 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 66 | let output = String(data: data, encoding: .utf8) ?? "" 67 | 68 | shortcuts = output.components(separatedBy: .newlines).sorted() 69 | } 70 | 71 | public func getShortcutsFolders() { 72 | task = Process() 73 | task?.executableURL = shortcutsURL 74 | task?.arguments = ["list", "--folders"] 75 | 76 | let pipe = Pipe() 77 | task?.standardOutput = pipe 78 | task?.launch() 79 | task?.waitUntilExit() 80 | let data = pipe.fileHandleForReading.readDataToEndOfFile() 81 | let output = String(data: data, encoding: .utf8) ?? "" 82 | folders = output.components(separatedBy: .newlines).sorted() 83 | } 84 | 85 | public func runShortcut(shortcut: String, input: Any? = nil) throws -> String { 86 | guard let app: ShortcutsEvents? = SBApplication(bundleIdentifier: "com.apple.shortcuts.events") else { 87 | throw RunShortcutError(errorReason: .NoPermissions, message: "Can't access Shortcuts.app, please verify the permissions") 88 | } 89 | guard let shortcut = app?.shortcuts?.object(withName: shortcut) as? Shortcut else { 90 | throw RunShortcutError(errorReason: .ShortcutNotFound, message: "Can't find shortcut named \(shortcut).") 91 | } 92 | 93 | let res = shortcut.run?(withInput: input) 94 | guard let res else { 95 | throw RunShortcutError(errorReason: .NoShortcutOutput, message: "Shortcut \(shortcut) didn't produced output.") 96 | } 97 | guard let out = (res as? [String])?.first else { 98 | throw RunShortcutError(errorReason: .CantParseShortcutOutput, message: "Shortcut \(shortcut) produced unparsable result - \(res)") 99 | } 100 | 101 | return out 102 | } 103 | 104 | public func viewCurrentShortcut(shortcut: String) { 105 | var components = URLComponents() 106 | components.scheme = "shortcuts" 107 | components.host = "open-shortcut" 108 | components.queryItems = [ 109 | URLQueryItem(name: "name", value: shortcut), 110 | ] 111 | if let url = components.url { 112 | NSWorkspace.shared.open(url) 113 | } 114 | } 115 | 116 | public func createShortcut() { 117 | NSWorkspace.shared.open(URL(string: "shortcuts://create-shortcut")!) 118 | } 119 | 120 | public func refresh() { 121 | getShortcutsFolders() 122 | getShortcuts() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Updating…"; 4 | "MB_LAST_UPDATED" = "Updated"; 5 | "MB_ABOUT_SWIFT_BAR" = "About"; 6 | "MB_ABOUT_PLUGIN" = "About"; 7 | "MB_RUN_IN_TERMINAL" = "Run in Terminal…"; 8 | "MB_DISABLE_PLUGIN" = "Disable Plugin"; 9 | "MB_DDEBUG_PLUGIN" = "Debug Plugin"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Preferences…"; 12 | "MB_REFRESH_ALL" = "Refresh All"; 13 | "MB_ENABLE_ALL" = "Enable All"; 14 | "MB_DISABLE_ALL" = "Disable All"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Open Plugin Folder…"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Change Plugin Folder…"; 17 | "MB_GET_PLUGINS" = "Get Plugins..."; 18 | "MB_SEND_FEEDBACK" = "Send Feedback..."; 19 | "MB_SHOW_ERROR" = "Show Error"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Preferences"; 23 | "PF_GENERAL" = "General"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Code Plugins"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Plugin Folder"; 29 | "PR_LAUNCH_AT_LOGIN" = "Launch at Login"; 30 | "PF_PATH" = "Path"; 31 | "PF_PATH_IS_NONE" = "None"; 32 | "PF_CHANGE_PATH" = "Change..."; 33 | "PF_SHELL" = "Shell"; 34 | "PF_TERMINAL" = "Terminal"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Hide SwiftBar Icon"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Update"; 38 | "PF_CHECK_FOR_UPDATES" = "Check for Updates"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Include Pre-Release Versions"; 40 | "PR_SHARE_CRASH_REPORTS" = "Share Crash Reports"; 41 | "PF_NO_PLUGINS_MESSAGE" = "Plugins folder is empty"; 42 | "PF_ENABLE_ALL" = "Enable All"; 43 | "PF_PLUGINS_FOOTNOTE" = "Enabled plugins appear in the menu bar."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Category"; 65 | "PR_PLUGIN_REPOSITORY" = "Plugin Repository"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Refreshing repository data…"; 67 | "PR_DEPENDENCIES" = "Dependencies"; 68 | "PR_PLUGIN_SOURCE" = "Source"; 69 | "PR_ABOUT_PLUGIN" = "About"; 70 | "PR_AUTORH_PREPOSITION" = "by"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Install"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Installed"; 73 | "PR_INSTALL_STATUS_FAILED" = "Failed"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Downloading"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Choose plugin folder"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "Can't use this folder as SwiftBar plugins location"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Choose New Location"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "WOW! Looks like you chose a folder with a LOT OF FILES, please choose another one. If you have node_modules folder that you need for a plugin, please make it hidden."; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Set SwiftBar Plugins Location"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Select a folder to store the plugins repository"; 84 | "APP_QUIT" = "Quit SwiftBar"; 85 | "OK" = "OK"; 86 | "CANCEL" = "Cancel"; 87 | 88 | // Plugin Repository Categories 89 | "CAT_AWS" = "AWS"; 90 | "CAT_CRYPTOCURRENCY" = "Cryptocurrency"; 91 | "CAT_DEV" = "Dev"; 92 | "CAT_E-COMMERCE" = "E-Commerce"; 93 | "CAT_EMAIL" = "Email"; 94 | "CAT_ENVIRONMENT" = "Environment"; 95 | "CAT_FINANCE" = "Finance"; 96 | "CAT_GAMES" = "Games"; 97 | "CAT_LIFESTYLE" = "Lifestyle"; 98 | "CAT_MESSENGER" = "Messenger"; 99 | "CAT_MUSIC" = "Music"; 100 | "CAT_NETWORK" = "Network"; 101 | "CAT_POLITICS" = "Politics"; 102 | "CAT_SCIENCE" = "Science"; 103 | "CAT_SPORTS" = "Sports"; 104 | "CAT_SYSTEM" = "System"; 105 | "CAT_TIME" = "Time"; 106 | "CAT_TOOLS" = "Tools"; 107 | "CAT_TRAVEL" = "Travel"; 108 | "CAT_TUTORIAL" = "Tutorial"; 109 | "CAT_WEATHER" = "Weather"; 110 | "CAT_WEB" = "Web"; 111 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Обновление…"; 4 | "MB_LAST_UPDATED" = "Обновлено"; 5 | "MB_ABOUT_SWIFT_BAR" = "О программе"; 6 | "MB_ABOUT_PLUGIN" = "О плагине"; 7 | "MB_RUN_IN_TERMINAL" = "Запустить в терминале…"; 8 | "MB_DISABLE_PLUGIN" = "Отключить плагин"; 9 | "MB_DDEBUG_PLUGIN" = "Отладка плагина"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Настройки…"; 12 | "MB_REFRESH_ALL" = "Обновить все"; 13 | "MB_ENABLE_ALL" = "Включить все"; 14 | "MB_DISABLE_ALL" = "Отключить все"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Открыть папку с плагином…"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Изменить папку плагина…"; 17 | "MB_GET_PLUGINS" = "Загрузить плагины..."; 18 | "MB_SEND_FEEDBACK" = "Отправить фидбек..."; 19 | "MB_SHOW_ERROR" = "Показать ошибку"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Настройки"; 23 | "PF_GENERAL" = "Общие"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Плагины"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Папка с плагином"; 29 | "PR_LAUNCH_AT_LOGIN" = "Запускать при входе в систему"; 30 | "PF_PATH" = "Путь"; 31 | "PF_PATH_IS_NONE" = "Путь не указан"; 32 | "PF_CHANGE_PATH" = "Изменить..."; 33 | "PF_SHELL" = "Шелл"; 34 | "PF_TERMINAL" = "Терминал"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Скрыть иконку SwiftBar"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Обновить"; 38 | "PF_CHECK_FOR_UPDATES" = "Проверить обновления"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Включая бета версии"; 40 | "PR_SHARE_CRASH_REPORTS" = "Поделиться отчётами о сбоях"; 41 | "PF_NO_PLUGINS_MESSAGE" = "Папка плагина пуста"; 42 | "PF_ENABLE_ALL" = "Включить все"; 43 | "PF_PLUGINS_FOOTNOTE" = "Включенные плагины видны в панели меню."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Категория"; 65 | "PR_PLUGIN_REPOSITORY" = "Репозиторий плагина"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Данные репозитория обновляются…"; 67 | "PR_DEPENDENCIES" = "Зависимости"; 68 | "PR_PLUGIN_SOURCE" = "Источник"; 69 | "PR_ABOUT_PLUGIN" = "О плагине"; 70 | "PR_AUTORH_PREPOSITION" = "автор:"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Установить"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Установлен"; 73 | "PR_INSTALL_STATUS_FAILED" = "Ошибка"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Загружается"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Выбрать папку с плагинами"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "Эту папку нельзя использовать как папку с плагинами"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Выбрать другую папку"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "Ого! В этой папке реально дофига файлов! Если вам реально нужна папка node_modules в папке с плагином, скройте ее."; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Установить папку с плагинами SwiftBar"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Выбрать папку для хранения репозитория с плагинами"; 84 | "APP_QUIT" = "Выход"; 85 | "OK" = "OK"; 86 | "CANCEL" = "Отмена"; 87 | 88 | // Plugin Repository Categories 89 | "CAT_AWS" = "AWS"; 90 | "CAT_CRYPTOCURRENCY" = "Криптовалюты"; 91 | "CAT_DEV" = "Разработка"; 92 | "CAT_E-COMMERCE" = "Электронная коммерция"; 93 | "CAT_EMAIL" = "Email"; 94 | "CAT_ENVIRONMENT" = "Окружающая среда"; 95 | "CAT_FINANCE" = "Финансы"; 96 | "CAT_GAMES" = "Игры"; 97 | "CAT_LIFESTYLE" = "Лайфстайл"; 98 | "CAT_MESSENGER" = "Мессенджеры"; 99 | "CAT_MUSIC" = "Музыка"; 100 | "CAT_NETWORK" = "Сети"; 101 | "CAT_POLITICS" = "Политика"; 102 | "CAT_SCIENCE" = "Наука"; 103 | "CAT_SPORTS" = "Спорт"; 104 | "CAT_SYSTEM" = "Система"; 105 | "CAT_TIME" = "Время"; 106 | "CAT_TOOLS" = "Инструменты"; 107 | "CAT_TRAVEL" = "Путешествия"; 108 | "CAT_TUTORIAL" = "Туториалы"; 109 | "CAT_WEATHER" = "Погода"; 110 | "CAT_WEB" = "Web"; 111 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/nl.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Bijwerken…"; 4 | "MB_LAST_UPDATED" = "Bijgewerkt"; 5 | "MB_ABOUT_SWIFT_BAR" = "Over"; 6 | "MB_ABOUT_PLUGIN" = "Over"; 7 | "MB_RUN_IN_TERMINAL" = "Voer uit in Terminal"; 8 | "MB_DISABLE_PLUGIN" = "Schakel plug-in uit"; 9 | "MB_DDEBUG_PLUGIN" = "Debug plug-in"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Voorkeuren…"; 12 | "MB_REFRESH_ALL" = "Ververs alle plug-ins"; 13 | "MB_ENABLE_ALL" = "Schakel alle in"; 14 | "MB_DISABLE_ALL" = "Schakel alle uit"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Open map voor plug-ins"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Wijzig map voor plug-ins…"; 17 | "MB_GET_PLUGINS" = "Plug-ins verkrijgen…"; 18 | "MB_SEND_FEEDBACK" = "Stuur feedback…"; 19 | "MB_SHOW_ERROR" = "Toon foutmelding"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Voorkeuren"; 23 | "PF_GENERAL" = "Algemeen"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Plug-ins"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Map voor plug-ins"; 29 | "PR_LAUNCH_AT_LOGIN" = "Opstarten bij inloggen"; 30 | "PF_PATH" = "Pad"; 31 | "PF_PATH_IS_NONE" = "Geen"; 32 | "PF_CHANGE_PATH" = "Wijzig…"; 33 | "PF_SHELL" = "Shell"; 34 | "PF_TERMINAL" = "Shell-applicatie"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Verberg SwiftBar-icoon"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Updates"; 38 | "PF_CHECK_FOR_UPDATES" = "Controleer op updates…"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Pre-releaseversies Opnemen"; 40 | "PR_SHARE_CRASH_REPORTS" = "Crashrapporten delen"; 41 | "PF_NO_PLUGINS_MESSAGE" = "Plug-insmap is leeg"; 42 | "PF_ENABLE_ALL" = "Alle plug-ins inschakelen"; 43 | "PF_PLUGINS_FOOTNOTE" = "Ingeschakelde plug-ins verschijnen in de menubalk."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Categorie"; 65 | "PR_PLUGIN_REPOSITORY" = "Plug-inverzameling"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Verversen verzamelingsgegevens…"; 67 | "PR_DEPENDENCIES" = "Afhankelijkheden"; 68 | "PR_PLUGIN_SOURCE" = "Bron"; 69 | "PR_ABOUT_PLUGIN" = "Over"; 70 | "PR_AUTORH_PREPOSITION" = "door"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Installeer"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Geïnstalleerd"; 73 | "PR_INSTALL_STATUS_FAILED" = "Mislukt"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Downloaden…"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Kies map voor plug-ins"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "Deze map kan niet gebruikt worden voor SwiftBar plug-ins"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Kies een andere map"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "Wauw! De map die je hebt gekozen bevat heel veel bestanden. Kies een andere map, alsjeblieft. Als je een map genaamd 'node_modules' hebt die vereist is voor een plug-in, verberg die dan."; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Kies een locatie voor SwiftBar plug-ins"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Selecteer een map voor de plug-inverzameling"; 84 | "APP_QUIT" = "Stop SwiftBar"; 85 | "OK" = "OK"; 86 | "CANCEL" = "Annuleer"; 87 | 88 | // Plugin Repository Categories 89 | "CAT_AWS" = "AWS"; 90 | "CAT_CRYPTOCURRENCY" = "Cryptovaluta"; 91 | "CAT_DEV" = "Ontwikkelaar"; 92 | "CAT_E-COMMERCE" = "E-commerce"; 93 | "CAT_EMAIL" = "E-mail"; 94 | "CAT_ENVIRONMENT" = "Natuur"; 95 | "CAT_FINANCE" = "Financiën"; 96 | "CAT_GAMES" = "Games"; 97 | "CAT_LIFESTYLE" = "Lifestyle"; 98 | "CAT_MESSENGER" = "Messenger"; 99 | "CAT_MUSIC" = "Muziek"; 100 | "CAT_NETWORK" = "Netwerk"; 101 | "CAT_POLITICS" = "Politiek"; 102 | "CAT_SCIENCE" = "Wetenschap"; 103 | "CAT_SPORTS" = "Sport"; 104 | "CAT_SYSTEM" = "Systeem"; 105 | "CAT_TIME" = "Tijd"; 106 | "CAT_TOOLS" = "Tools"; 107 | "CAT_TRAVEL" = "Reizen"; 108 | "CAT_TUTORIAL" = "Tutorial"; 109 | "CAT_WEATHER" = "Weer"; 110 | "CAT_WEB" = "Web"; 111 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/hr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Aktualiziranje …"; 4 | "MB_LAST_UPDATED" = "Aktualizirano"; 5 | "MB_ABOUT_SWIFT_BAR" = "Informacije"; 6 | "MB_ABOUT_PLUGIN" = "Informacije"; 7 | "MB_RUN_IN_TERMINAL" = "Pokreni u Terminalu …"; 8 | "MB_DISABLE_PLUGIN" = "Deaktiviraj dodatak"; 9 | "MB_DDEBUG_PLUGIN" = "Otklanjanje grešaka dodatka"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Postavke …"; 12 | "MB_REFRESH_ALL" = "Osvježi sve"; 13 | "MB_ENABLE_ALL" = "Aktiviraj sve"; 14 | "MB_DISABLE_ALL" = "Dektiviraj sve"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Otvori mapu dodataka …"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Promijeni mapu dodataka …"; 17 | "MB_GET_PLUGINS" = "Preuzmi dodatak …"; 18 | "MB_SEND_FEEDBACK" = "Pošalji povratne informacije …"; 19 | "MB_SHOW_ERROR" = "Prikaži grešku"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Postavke"; 23 | "PF_GENERAL" = "Opće"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Dodatci"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Mapa dodataka"; 29 | "PR_LAUNCH_AT_LOGIN" = "Pokreni nakon prijave"; 30 | "PF_PATH" = "Staza"; 31 | "PF_PATH_IS_NONE" = "Ništa"; 32 | "PF_CHANGE_PATH" = "Promijeni …"; 33 | "PF_SHELL" = "Naredbeni redak"; 34 | "PF_TERMINAL" = "Terminal"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Sakrij ikonu SwiftBara"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Aktualiziraj"; 38 | "PF_CHECK_FOR_UPDATES" = "Traži nove verzije"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Uključuju Verzije Prije Izdanja"; 40 | "PR_SHARE_CRASH_REPORTS" = "Podijeli izvještaje o padovima"; 41 | "PF_NO_PLUGINS_MESSAGE" = "Mapa dodataka je prazna"; 42 | "PF_ENABLE_ALL" = "Aktiviraj sve"; 43 | "PF_PLUGINS_FOOTNOTE" = "Aktivirani dodaci će se pojaviti u traci izbornika."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Kategorija"; 65 | "PR_PLUGIN_REPOSITORY" = "Repozitorij dodataka"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Aktualiziranje podataka repozitorija …"; 67 | "PR_DEPENDENCIES" = "Ovisnosti"; 68 | "PR_PLUGIN_SOURCE" = "Izvor"; 69 | "PR_ABOUT_PLUGIN" = "Informacije"; 70 | "PR_AUTORH_PREPOSITION" = "od"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Instaliraj"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Instalirano"; 73 | "PR_INSTALL_STATUS_FAILED" = "Neuspjelo"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Preuzimanje"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Odaberi mapu dodataka"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "Ova se mapa ne može koristiti kao mjesto za SwiftBar dodatke"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Odaberi novo mjesto"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "AJME! Odabrao/la si mapu s PUNO DATOTEKA. Odaberi jednu drugu mapu. Ako imaš mapu node_modules koja ti treba za dodatak, sakrij ju."; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Odredi mjesto za SwiftBar dodatke"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Odaberi mapu za spremanje repozitorija dodataka"; 84 | "APP_QUIT" = "Zatvori SwiftBar"; 85 | "OK" = "U redu"; 86 | "CANCEL" = "Odustani"; 87 | 88 | 89 | // Plugin Repository Categories 90 | "CAT_AWS" = "AWS"; 91 | "CAT_CRYPTOCURRENCY" = "Kriptovaluta"; 92 | "CAT_DEV" = "Razvoj"; 93 | "CAT_E-COMMERCE" = "E-trgovina"; 94 | "CAT_EMAIL" = "E-mail"; 95 | "CAT_ENVIRONMENT" = "Okruženje"; 96 | "CAT_FINANCE" = "Financije"; 97 | "CAT_GAMES" = "Igre"; 98 | "CAT_LIFESTYLE" = "Životni stil"; 99 | "CAT_MESSENGER" = "Poruke"; 100 | "CAT_MUSIC" = "Glazba"; 101 | "CAT_NETWORK" = "Mreža"; 102 | "CAT_POLITICS" = "Politika"; 103 | "CAT_SCIENCE" = "Znanost"; 104 | "CAT_SPORTS" = "Sport"; 105 | "CAT_SYSTEM" = "Sustav"; 106 | "CAT_TIME" = "Vrijeme"; 107 | "CAT_TOOLS" = "Alati"; 108 | "CAT_TRAVEL" = "Putovanje"; 109 | "CAT_TUTORIAL" = "Vježbe"; 110 | "CAT_WEATHER" = "Vremenska prognoza"; 111 | "CAT_WEB" = "Internet"; 112 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | //Menubar strings 2 | "MB_SWIFT_BAR" = "SwiftBar"; 3 | "MB_UPDATING_MENU" = "Actualizando…"; 4 | "MB_LAST_UPDATED" = "Actualizado"; 5 | "MB_ABOUT_SWIFT_BAR" = "Acerca"; 6 | "MB_ABOUT_PLUGIN" = "Acerca"; 7 | "MB_RUN_IN_TERMINAL" = "Ejecutar en la Terminal…"; 8 | "MB_DISABLE_PLUGIN" = "Deshabilitar Complemento"; 9 | "MB_DDEBUG_PLUGIN" = "Depurar Complemento"; 10 | "MB_TERMINATE_EPH" = "Terminate Plugin"; 11 | "MB_PREFERENCES" = "Preferencias…"; 12 | "MB_REFRESH_ALL" = "Actualizar Todo"; 13 | "MB_ENABLE_ALL" = "Habilitar Todo"; 14 | "MB_DISABLE_ALL" = "Deshabilitar Todo"; 15 | "MB_OPEN_PLUGINS_FOLDER" = "Abrir Carpeta de Complementos…"; 16 | "MB_CHANGE_PLUGINS_FOLDER" = "Cambiar Carpeta de Complementos…"; 17 | "MB_GET_PLUGINS" = "Obtener Complementos..."; 18 | "MB_SEND_FEEDBACK" = "Enviar Comentarios..."; 19 | "MB_SHOW_ERROR" = "Mostrar Error"; 20 | 21 | //Preferences strings 22 | "PF_PREFERENCES" = "Preferencias"; 23 | "PF_GENERAL" = "General"; 24 | "PF_ADVANCED" = "Advanced"; 25 | "PF_PLUGINS" = "Complementos"; 26 | "PF_SHORTCUT_PLUGINS" = "Shortcut Plugins"; 27 | "PF_ABOUT" = "About"; 28 | "PF_PLUGINS_FOLDER" = "Carpeta de Complementos"; 29 | "PR_LAUNCH_AT_LOGIN" = "Lanzar al Iniciar Sesión"; 30 | "PF_PATH" = "Ruta"; 31 | "PF_PATH_IS_NONE" = "Ninguno"; 32 | "PF_CHANGE_PATH" = "Cambiar..."; 33 | "PF_SHELL" = "Shell"; 34 | "PF_TERMINAL" = "Terminal"; 35 | "PF_HIDE_SWIFTBAR_ICON" = "Ocultar Ícono SwiftBar"; 36 | "PF_STEALTH_MODE" = "Hide SwiftBar in the menu bar"; 37 | "PF_CHECK_FOR_UPDATE" = "Actualizar"; 38 | "PF_CHECK_FOR_UPDATES" = "Verificar Actualizaciones"; 39 | "PR_INCLUDE_BETA_UPDATES" = "Incluir Versiones Beta"; 40 | "PR_SHARE_CRASH_REPORTS" = "Compartir informes de fallos"; 41 | "PF_NO_PLUGINS_MESSAGE" = "La carpeta de los complementos está vacía"; 42 | "PF_ENABLE_ALL" = "Habilitar todo"; 43 | "PF_PLUGINS_FOOTNOTE" = "Los complementos habilitados aparecen en la barra de menú."; 44 | "PF_MENUBAR_ITEM" = "Menu Bar Item"; 45 | "PF_DIM_ON_MANUAL_REFRESH" = "Dim on Manual Refresh"; 46 | "PF_SHORTCUTS_COLUMN_NAME" = "Name"; 47 | "PF_SHORTCUTS_COLUMN_SHORTCUT" = "Shortcut"; 48 | "PF_SHORTCUTS_COLUMN_REFRESH" = "Refresh"; 49 | "PF_SHORTCUTS_DELETE_BUTTON" = "Delete"; 50 | "PF_SHORTCUTS_DELETE_CONFIRMATION" = "Are you sure you want to delete ''?"; 51 | "PF_SHORTCUTS_ADD_BUTTON" = "Add"; 52 | "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" = "Enable\Disable menu bar item"; 53 | "PF_SHORTCUTS_COLUMN_REFRESH_HELP" = "Refresh menu bar item"; 54 | "PF_ADD_SHORTCUT_PLUGIN_HEADER" = "Add Shortcut Plugin"; 55 | "PF_ADD_SHORTCUT_PLUGIN_NAME" = "Name"; 56 | "PF_ADD_SHORTCUT_PLUGIN_FOLDER" = "Folder"; 57 | "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" = "Shortcut"; 58 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" = "Refresh Shortcuts list"; 59 | "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" = "Open in Shortcuts.app"; 60 | "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" = "Create new Shortcut"; 61 | "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" = "Refresh Interval"; 62 | 63 | //Plugin Repository strings 64 | "PR_CATEGORY" = "Categoría"; 65 | "PR_PLUGIN_REPOSITORY" = "Repositorio del Complemento"; 66 | "PR_REFRESHING_DATA_MESSAGE" = "Actualizando datos del repositorio…"; 67 | "PR_DEPENDENCIES" = "Dependencias"; 68 | "PR_PLUGIN_SOURCE" = "Fuente"; 69 | "PR_ABOUT_PLUGIN" = "Acerca"; 70 | "PR_AUTORH_PREPOSITION" = "por"; 71 | "PR_INSTALL_STATUS_INSTALL" = "Instalar"; 72 | "PR_INSTALL_STATUS_INSTALLED" = "Instalado"; 73 | "PR_INSTALL_STATUS_FAILED" = "Falló"; 74 | "PR_INSTALL_STATUS_DOWNLOADING" = "Descargando"; 75 | 76 | 77 | //App strings 78 | "APP_CHOOSE_PLUGIN_FOLDER_TITLE" = "Elegir carpeta de complementos"; 79 | "APP_FOLDER_NOT_ALLOWED_MESSAGE" = "No se puede usar esta carpeta como ubicación de los complementos de SwiftBar"; 80 | "APP_FOLDER_NOT_ALLOWED_ACTION" = "Elegir nueva ubicación"; 81 | "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" = "GUAU! Tal parece que la carpeta elegida posee DEMASIADO ARCHIVOS, por favor elegir otra. Si un complemento necesita de una carpeta node_modules, hágala oculta."; 82 | "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" = "Establecer la ubicación de los complementos de SwiftBar"; 83 | "APP_CHOOSE_PLUGIN_FOLDER_INFO" = "Seleccionar una carpeta para guardar los repositorios de los complementos"; 84 | "APP_QUIT" = "Salir de SwiftBar"; 85 | "OK" = "OK"; 86 | "CANCEL" = "Cancelar"; 87 | 88 | // Plugin Repository Categories 89 | "CAT_AWS" = "AWS"; 90 | "CAT_CRYPTOCURRENCY" = "Criptomonedas"; 91 | "CAT_DEV" = "Dev"; 92 | "CAT_E-COMMERCE" = "Comercio Electrónico"; 93 | "CAT_EMAIL" = "Correo Electrónico"; 94 | "CAT_ENVIRONMENT" = "Medioambiente"; 95 | "CAT_FINANCE" = "Finanzas"; 96 | "CAT_GAMES" = "Juegos"; 97 | "CAT_LIFESTYLE" = "Estilo de Vida"; 98 | "CAT_MESSENGER" = "Messenger"; 99 | "CAT_MUSIC" = "Música"; 100 | "CAT_NETWORK" = "Red"; 101 | "CAT_POLITICS" = "Política"; 102 | "CAT_SCIENCE" = "Ciencias"; 103 | "CAT_SPORTS" = "Deportes"; 104 | "CAT_SYSTEM" = "System"; 105 | "CAT_TIME" = "Time"; 106 | "CAT_TOOLS" = "Herramientas"; 107 | "CAT_TRAVEL" = "Viajes"; 108 | "CAT_TUTORIAL" = "Tutorial"; 109 | "CAT_WEATHER" = "Clima"; 110 | "CAT_WEB" = "Web"; 111 | -------------------------------------------------------------------------------- /SwiftBar/Utility/LaunchAtLogin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchAtLogin.swift 3 | // SwiftBar 4 | // 5 | // Modern implementation of LaunchAtLogin using ServiceManagement API 6 | // Compatible with macOS 13.0+ including macOS Sequoia 7 | // 8 | // Based on https://github.com/sindresorhus/LaunchAtLogin-Modern 9 | // 10 | 11 | import SwiftUI 12 | import ServiceManagement 13 | import os.log 14 | 15 | public enum ModernLaunchAtLogin { 16 | private static let logger = Logger(subsystem: "com.ameba.SwiftBar", category: "LaunchAtLogin") 17 | public static let observable = Observable() 18 | 19 | /** 20 | Toggle "launch at login" for your app or check whether it's enabled. 21 | */ 22 | public static var isEnabled: Bool { 23 | get { 24 | if #available(macOS 13.0, *) { 25 | return SMAppService.mainApp.status == .enabled 26 | } else { 27 | // Fallback for older macOS versions 28 | return false 29 | } 30 | } 31 | set { 32 | observable.objectWillChange.send() 33 | 34 | if #available(macOS 13.0, *) { 35 | do { 36 | if newValue { 37 | if SMAppService.mainApp.status == .enabled { 38 | try? SMAppService.mainApp.unregister() 39 | } 40 | 41 | try SMAppService.mainApp.register() 42 | } else { 43 | try SMAppService.mainApp.unregister() 44 | } 45 | } catch { 46 | logger.error("Failed to \(newValue ? "enable" : "disable") launch at login: \(error.localizedDescription)") 47 | } 48 | } else { 49 | logger.warning("Launch at login requires macOS 13.0 or later") 50 | } 51 | } 52 | } 53 | 54 | /** 55 | Whether the app was launched at login. 56 | 57 | - Important: This property must only be checked in `NSApplicationDelegate#applicationDidFinishLaunching`. 58 | */ 59 | public static var wasLaunchedAtLogin: Bool { 60 | let event = NSAppleEventManager.shared().currentAppleEvent 61 | return event?.eventID == kAEOpenApplication 62 | && event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem 63 | } 64 | } 65 | 66 | extension ModernLaunchAtLogin { 67 | public final class Observable: ObservableObject { 68 | public var isEnabled: Bool { 69 | get { ModernLaunchAtLogin.isEnabled } 70 | set { 71 | ModernLaunchAtLogin.isEnabled = newValue 72 | } 73 | } 74 | } 75 | } 76 | 77 | extension ModernLaunchAtLogin { 78 | /** 79 | This package comes with a `ModernLaunchAtLogin.Toggle` view which is like the built-in `Toggle` but with a predefined binding and label. Clicking the view toggles "launch at login" for your app. 80 | 81 | ``` 82 | struct ContentView: View { 83 | var body: some View { 84 | ModernLaunchAtLogin.Toggle() 85 | } 86 | } 87 | ``` 88 | 89 | The default label is `"Launch at login"`, but it can be overridden for localization and other needs: 90 | 91 | ``` 92 | struct ContentView: View { 93 | var body: some View { 94 | ModernLaunchAtLogin.Toggle { 95 | Text("Launch at login") 96 | } 97 | } 98 | } 99 | ``` 100 | */ 101 | public struct Toggle: View { 102 | @ObservedObject private var launchAtLogin = ModernLaunchAtLogin.observable 103 | private let label: Label 104 | 105 | /** 106 | Creates a toggle that displays a custom label. 107 | 108 | - Parameters: 109 | - label: A view that describes the purpose of the toggle. 110 | */ 111 | public init(@ViewBuilder label: () -> Label) { 112 | self.label = label() 113 | } 114 | 115 | public var body: some View { 116 | if #available(macOS 13.0, *) { 117 | SwiftUI.Toggle(isOn: $launchAtLogin.isEnabled) { label } 118 | } else { 119 | SwiftUI.Toggle(isOn: .constant(false)) { label } 120 | .disabled(true) 121 | .help("Launch at login requires macOS 13.0 or later") 122 | } 123 | } 124 | } 125 | } 126 | 127 | extension ModernLaunchAtLogin.Toggle { 128 | /** 129 | Creates a toggle that generates its label from a localized string key. 130 | 131 | This initializer creates a ``Text`` view on your behalf with the provided `titleKey`. 132 | 133 | - Parameters: 134 | - titleKey: The key for the toggle's localized title, that describes the purpose of the toggle. 135 | */ 136 | public init(_ titleKey: LocalizedStringKey) { 137 | label = Text(titleKey) 138 | } 139 | 140 | /** 141 | Creates a toggle that generates its label from a string. 142 | 143 | This initializer creates a `Text` view on your behalf with the provided `title`. 144 | 145 | - Parameters: 146 | - title: A string that describes the purpose of the toggle. 147 | */ 148 | public init(_ title: some StringProtocol) { 149 | label = Text(title) 150 | } 151 | 152 | /** 153 | Creates a toggle with the default title of `Launch at login`. 154 | */ 155 | public init() { 156 | self.init("Launch at login") 157 | } 158 | } -------------------------------------------------------------------------------- /SwiftBar/Plugin/ShortcutPlugin.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import os 4 | 5 | class PersistentShortcutPlugin: Codable, Identifiable { 6 | var id: PluginID 7 | var name: String 8 | var shortcut: String 9 | var repeatString: String 10 | var cronString: String 11 | 12 | init(id: PluginID, name: String, shortcut: String, repeatString: String, cronString: String) { 13 | self.id = id 14 | self.name = name 15 | self.shortcut = shortcut 16 | self.repeatString = repeatString 17 | self.cronString = cronString 18 | } 19 | } 20 | 21 | class ShortcutPlugin: Plugin, Identifiable, ObservableObject { 22 | var id: PluginID 23 | var type: PluginType = .Shortcut 24 | var name: String 25 | var file: String = "none" 26 | private var _metadata: PluginMetadata? 27 | private let metadataQueue = DispatchQueue(label: "com.ameba.SwiftBar.ShortcutPlugin.metadata", attributes: .concurrent) 28 | 29 | var metadata: PluginMetadata? { 30 | get { 31 | metadataQueue.sync { _metadata } 32 | } 33 | set { 34 | metadataQueue.async(flags: .barrier) { [weak self] in 35 | self?._metadata = newValue 36 | } 37 | } 38 | } 39 | 40 | var contentUpdatePublisher = PassthroughSubject() 41 | var updateInterval: Double = 60 * 60 * 24 * 100 42 | var lastUpdated: Date? 43 | var lastState: PluginState 44 | var lastRefreshReason: PluginRefreshReason = .FirstLaunch 45 | var shortcut: String 46 | var repeatString: String 47 | var cronString: String 48 | var refreshEnv: [String: String] = [:] 49 | @Published var enabled: Bool = true 50 | var operation: RunPluginOperation? 51 | 52 | var content: String? = "..." { 53 | didSet { 54 | contentUpdatePublisher.send(content) 55 | } 56 | } 57 | 58 | var error: Error? 59 | var debugInfo = PluginDebugInfo() 60 | 61 | lazy var invokeQueue: OperationQueue = delegate.pluginManager.pluginInvokeQueue 62 | 63 | var updateTimerPublisher: Timer.TimerPublisher { 64 | Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .default) 65 | } 66 | 67 | var cancellable: Set = [] 68 | 69 | let shortcutsManager = ShortcutsManager.shared 70 | 71 | var persistentPlugin: PersistentShortcutPlugin { 72 | PersistentShortcutPlugin(id: id, name: name, shortcut: shortcut, repeatString: repeatString, cronString: cronString) 73 | } 74 | 75 | init(_ persistentItem: PersistentShortcutPlugin) { 76 | id = persistentItem.id 77 | name = persistentItem.name 78 | shortcut = persistentItem.shortcut 79 | repeatString = persistentItem.repeatString 80 | cronString = persistentItem.cronString 81 | lastState = .Loading 82 | updateInterval = parseRefreshInterval(intervalStr: repeatString, baseUpdateinterval: updateInterval) ?? updateInterval 83 | enabled = !prefs.disabledPlugins.contains(id) 84 | os_log("Initialized Shortcut plugin\n%{public}@", log: Log.plugin, description) 85 | refresh(reason: .FirstLaunch) 86 | } 87 | 88 | func enableTimer() { 89 | guard cancellable.isEmpty else { return } 90 | updateTimerPublisher 91 | .autoconnect() 92 | .receive(on: invokeQueue) 93 | .sink(receiveValue: { [weak self] _ in 94 | self?.lastRefreshReason = .Schedule 95 | self?.invokeQueue.addOperation(RunPluginOperation(plugin: self!)) 96 | }).store(in: &cancellable) 97 | } 98 | 99 | func disableTimer() { 100 | cancellable.forEach { $0.cancel() } 101 | cancellable.removeAll() 102 | } 103 | 104 | func refresh(reason: PluginRefreshReason) { 105 | guard enabled else { 106 | os_log("Skipping refresh for disabled plugin\n%{public}@", log: Log.plugin, description) 107 | return 108 | } 109 | os_log("Requesting manual refresh for plugin\n%{public}@", log: Log.plugin, description) 110 | debugInfo.addEvent(type: .PluginRefresh, value: "Requesting manual refresh") 111 | disableTimer() 112 | operation?.cancel() 113 | 114 | lastRefreshReason = reason 115 | operation = RunPluginOperation(plugin: self) 116 | invokeQueue.addOperation(operation!) 117 | } 118 | 119 | func enable() { 120 | prefs.disabledPlugins.removeAll(where: { $0 == id }) 121 | refresh(reason: .FirstLaunch) 122 | } 123 | 124 | func disable() { 125 | lastState = .Disabled 126 | disableTimer() 127 | prefs.disabledPlugins.append(id) 128 | } 129 | 130 | func start() { 131 | refresh(reason: .FirstLaunch) 132 | } 133 | 134 | func terminate() { 135 | disableTimer() 136 | } 137 | 138 | func invoke() -> String? { 139 | lastUpdated = Date() 140 | do { 141 | let out = try shortcutsManager.runShortcut(shortcut: shortcut) 142 | error = nil 143 | lastState = .Success 144 | os_log("Successfully executed Shortcut plugin: %{public}@", log: Log.plugin, "\(name)(\(shortcut))") 145 | debugInfo.addEvent(type: .ContentUpdate, value: out) 146 | return out 147 | } catch { 148 | guard let error = error as? RunShortcutError else { return nil } 149 | os_log("Failed to execute Shortcut plugin: %{public}@\n%{public}@", log: Log.plugin, type: .error, "\(name)(\(shortcut))", error.message) 150 | self.error = error 151 | debugInfo.addEvent(type: .ContentUpdateError, value: error.message) 152 | lastState = .Failed 153 | } 154 | return nil 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /SwiftBar/UI/AboutPluginView.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | struct AboutPluginView: View { 5 | let md: PluginMetadata 6 | var body: some View { 7 | ScrollView(showsIndicators: true) { 8 | VStack(alignment: .leading, spacing: 8) { 9 | if !md.name.isEmpty { 10 | VStack { 11 | Text(md.name) 12 | .font(.largeTitle) 13 | .fixedSize() 14 | if !md.version.isEmpty { 15 | Text(md.version) 16 | .font(.footnote) 17 | } 18 | }.padding(.bottom) 19 | } 20 | 21 | if !md.desc.isEmpty { 22 | LabelView(label: "Description:", value: md.desc) 23 | } 24 | 25 | Divider() 26 | 27 | if let previewImageURL = md.previewImageURL { 28 | ImageView(withURL: previewImageURL, width: 350, height: 200) 29 | .padding(.bottom, 8) 30 | } 31 | 32 | if !md.author.isEmpty { 33 | LabelView(label: "Author:", value: md.author) 34 | } 35 | 36 | if !md.github.isEmpty { 37 | LabelView(label: "GitHub:", value: md.github, url: URL(string: "https://github.com/\(md.github.replacingOccurrences(of: "@", with: ""))")) 38 | } 39 | 40 | if !md.dependencies.isEmpty { 41 | let dependencies = md.dependencies.filter { !$0.isEmpty }.joined(separator: ", ") 42 | if !dependencies.isEmpty { 43 | LabelView(label: "Dependencies:", value: dependencies) 44 | } 45 | } 46 | 47 | if let about = md.aboutURL { 48 | LabelView(label: "About:", value: about.absoluteString, url: about) 49 | } 50 | 51 | // Display plugin variables if they exist 52 | if !md.environment.isEmpty { 53 | Divider().padding(.vertical, 4) 54 | Text("Variables:").font(.headline).padding(.top, 4) 55 | 56 | ForEach(Array(md.environment.keys.sorted()), id: \.self) { key in 57 | if let value = md.environment[key] { 58 | LabelView(label: key + ":", value: value) 59 | } 60 | } 61 | } 62 | 63 | // Display additional plugin settings 64 | if md.type != .Executable || md.runInBash == false || md.refreshOnOpen || md.persistentWebView { 65 | Divider().padding(.vertical, 4) 66 | Text("Settings:").font(.headline).padding(.top, 4) 67 | 68 | if md.type != .Executable { 69 | LabelView(label: "Type:", value: md.type.rawValue) 70 | } 71 | 72 | if !md.schedule.isEmpty { 73 | LabelView(label: "Schedule:", value: md.schedule) 74 | } 75 | 76 | if md.runInBash == false { 77 | LabelView(label: "Run in Bash:", value: "false") 78 | } 79 | 80 | if md.refreshOnOpen { 81 | LabelView(label: "Refresh on Open:", value: "true") 82 | } 83 | 84 | if md.persistentWebView { 85 | LabelView(label: "Persistent WebView:", value: "true") 86 | } 87 | } 88 | 89 | if !md.dropTypes.isEmpty { 90 | let dropTypes = md.dropTypes.filter { !$0.isEmpty }.joined(separator: ", ") 91 | if !dropTypes.isEmpty { 92 | Divider().padding(.vertical, 4) 93 | LabelView(label: "Drop Types:", value: dropTypes) 94 | } 95 | } 96 | } 97 | .padding() 98 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 99 | } 100 | } 101 | } 102 | 103 | struct LabelView: View { 104 | let label: String 105 | let value: String 106 | var url: URL? 107 | var body: some View { 108 | HStack { 109 | Text(label) 110 | .font(.caption) 111 | .foregroundColor(.secondary) 112 | if let url { 113 | Text(value) 114 | .fixedSize(horizontal: false, vertical: true) 115 | .font(.subheadline) 116 | .foregroundColor(.blue) 117 | .onTapGesture { 118 | NSWorkspace.shared.open(url) 119 | } 120 | } else { 121 | Text(value) 122 | .font(.subheadline) 123 | .fixedSize(horizontal: false, vertical: true) 124 | } 125 | Spacer() 126 | } 127 | } 128 | } 129 | 130 | struct AboutPluginView_Previews: PreviewProvider { 131 | static var previews: some View { 132 | AboutPluginView(md: 133 | PluginMetadata(name: "Super Plugin", 134 | version: "1.0", 135 | author: "SwiftBar", 136 | github: "@melonamin", 137 | desc: "This plugin is so cool you can't imagine your life before it!", 138 | previewImageURL: URL(string: "https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg"), 139 | dependencies: ["ruby", "aws"], 140 | aboutURL: URL(string: "https://github.com/swiftbar"))) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /SwiftBar/Resources/Localization/Localizable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Localizable { 4 | enum App: String { 5 | case ChoosePluginFolderTitle = "APP_CHOOSE_PLUGIN_FOLDER_TITLE" 6 | case FolderNotAllowedMessage = "APP_FOLDER_NOT_ALLOWED_MESSAGE" 7 | case FolderHasToManyFilesMessage = "APP_FOLDER_HAS_TOO_MANY_FILES_MESSAGE" 8 | case FolderNotAllowedAction = "APP_FOLDER_NOT_ALLOWED_ACTION" 9 | case ChoosePluginFolderMessage = "APP_CHOOSE_PLUGIN_FOLDER_MESSAGE" 10 | case ChoosePluginFolderInfo = "APP_CHOOSE_PLUGIN_FOLDER_INFO" 11 | case OKButton = "OK" 12 | case CancelButton = "CANCEL" 13 | case Quit = "APP_QUIT" 14 | } 15 | 16 | enum MenuBar: String { 17 | case SwiftBar = "MB_SWIFT_BAR" 18 | case UpdatingMenu = "MB_UPDATING_MENU" 19 | case LastUpdated = "MB_LAST_UPDATED" 20 | case AboutSwiftBar = "MB_ABOUT_SWIFT_BAR" 21 | case AboutPlugin = "MB_ABOUT_PLUGIN" 22 | case RunInTerminal = "MB_RUN_IN_TERMINAL" 23 | case DisablePlugin = "MB_DISABLE_PLUGIN" 24 | case DebugPlugin = "MB_DDEBUG_PLUGIN" 25 | case Preferences = "MB_PREFERENCES" 26 | case RefreshAll = "MB_REFRESH_ALL" 27 | case EnableAll = "MB_ENABLE_ALL" 28 | case DisableAll = "MB_DISABLE_ALL" 29 | case OpenPluginsFolder = "MB_OPEN_PLUGINS_FOLDER" 30 | case ChangePluginsFolder = "MB_CHANGE_PLUGINS_FOLDER" 31 | case GetPlugins = "MB_GET_PLUGINS" 32 | case SendFeedback = "MB_SEND_FEEDBACK" 33 | case ShowError = "MB_SHOW_ERROR" 34 | case TerminateEphemeralPlugin = "MB_TERMINATE_EPH" 35 | } 36 | 37 | enum Preferences: String { 38 | case Preferences = "PF_PREFERENCES" 39 | case General = "PF_GENERAL" 40 | case Advanced = "PF_ADVANCED" 41 | case Plugins = "PF_PLUGINS" 42 | case ShortcutPlugins = "PF_SHORTCUT_PLUGINS" 43 | case About = "PF_ABOUT" 44 | case PluginsFolder = "PF_PLUGINS_FOLDER" 45 | case Path = "PF_PATH" 46 | case PathIsNone = "PF_PATH_IS_NONE" 47 | case ChangePath = "PF_CHANGE_PATH" 48 | case Terminal = "PF_TERMINAL" 49 | case Shell = "PF_SHELL" 50 | case LaunchAtLogin = "PR_LAUNCH_AT_LOGIN" 51 | case IncludeBetaUpdates = "PR_INCLUDE_BETA_UPDATES" 52 | case ShareCrashReports = "PR_SHARE_CRASH_REPORTS" 53 | case HideSwiftBarIcon = "PF_HIDE_SWIFTBAR_ICON" 54 | case StealthMode = "PF_STEALTH_MODE" 55 | case UpdateLabel = "PF_CHECK_FOR_UPDATE" 56 | case CheckForUpdates = "PF_CHECK_FOR_UPDATES" 57 | case NoPluginsMessage = "PF_NO_PLUGINS_MESSAGE" 58 | case EnableAll = "PF_ENABLE_ALL" 59 | case PluginsFootnote = "PF_PLUGINS_FOOTNOTE" 60 | case MenuBarItem = "PF_MENUBAR_ITEM" 61 | case DimOnManualRefresh = "PF_DIM_ON_MANUAL_REFRESH" 62 | case ShortcutsColumnName = "PF_SHORTCUTS_COLUMN_NAME" 63 | case ShortcutsColumnShortcut = "PF_SHORTCUTS_COLUMN_SHORTCUT" 64 | case ShortcutsColumnRefresh = "PF_SHORTCUTS_COLUMN_REFRESH" 65 | case ShortcutsDeleteButton = "PF_SHORTCUTS_DELETE_BUTTON" 66 | case ShortcutsDeleteConfirmation = "PF_SHORTCUTS_DELETE_CONFIRMATION" 67 | case ShortcutsAddButton = "PF_SHORTCUTS_ADD_BUTTON" 68 | case ShortcutsColumnToggleHelp = "PF_SHORTCUTS_COLUMN_TOGGLE_HELP" 69 | case ShortcutsColumnRefreshHelp = "PF_SHORTCUTS_COLUMN_REFRESH_HELP" 70 | case AddShortcutPluginHeader = "PF_ADD_SHORTCUT_PLUGIN_HEADER" 71 | case AddShortcutPluginName = "PF_ADD_SHORTCUT_PLUGIN_NAME" 72 | case AddShortcutPluginFolder = "PF_ADD_SHORTCUT_PLUGIN_FOLDER" 73 | case AddShortcutPluginShortcut = "PF_ADD_SHORTCUT_PLUGIN_SHORTCUT" 74 | case AddShortcutPluginRefreshHelp = "PF_ADD_SHORTCUT_PLUGIN_REFRESH_HELP" 75 | case AddShortcutPluginOpenHelp = "PF_ADD_SHORTCUT_PLUGIN_OPEN_HELP" 76 | case AddShortcutPluginNewHelp = "PF_ADD_SHORTCUT_PLUGIN_NEW_HELP" 77 | case AddShortcutPluginRefreshInterval = "PF_ADD_SHORTCUT_PLUGIN_REFRESH_INTERVAL" 78 | } 79 | 80 | enum PluginRepository: String { 81 | case Category = "PR_CATEGORY" 82 | case PluginRepository = "PR_PLUGIN_REPOSITORY" 83 | case RefreshingDataMessage = "PR_REFRESHING_DATA_MESSAGE" 84 | case Dependencies = "PR_DEPENDENCIES" 85 | case PluginSource = "PR_PLUGIN_SOURCE" 86 | case AboutPlugin = "PR_ABOUT_PLUGIN" 87 | case AuthorPreposition = "PR_AUTORH_PREPOSITION" 88 | case InstallStatusInstall = "PR_INSTALL_STATUS_INSTALL" 89 | case InstallStatusInstalled = "PR_INSTALL_STATUS_INSTALLED" 90 | case InstallStatusFailed = "PR_INSTALL_STATUS_FAILED" 91 | case InstallStatusDownloading = "PR_INSTALL_STATUS_DOWNLOADING" 92 | } 93 | 94 | enum Categories: String { 95 | case aws = "CAT_AWS" 96 | case cryptocurrency = "CAT_CRYPTOCURRENCY" 97 | case dev = "CAT_DEV" 98 | case ecommerce = "CAT_E-COMMERCE" 99 | case email = "CAT_EMAIL" 100 | case environment = "CAT_ENVIRONMENT" 101 | case finance = "CAT_FINANCE" 102 | case games = "CAT_GAMES" 103 | case lifestyle = "CAT_LIFESTYLE" 104 | case messenger = "CAT_MESSENGER" 105 | case music = "CAT_MUSIC" 106 | case network = "CAT_NETWORK" 107 | case politics = "CAT_POLITICS" 108 | case science = "CAT_SCIENCE" 109 | case sports = "CAT_SPORTS" 110 | case system = "CAT_SYSTEM" 111 | case time = "CAT_TIME" 112 | case tools = "CAT_TOOLS" 113 | case travel = "CAT_TRAVEL" 114 | case tutorial = "CAT_TUTORIAL" 115 | case weather = "CAT_WEATHER" 116 | case web = "CAT_WEB" 117 | } 118 | } 119 | 120 | extension RawRepresentable where RawValue == String { 121 | var localized: String { 122 | NSLocalizedString(rawValue, comment: "") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README-PACKAGED-PLUGINS.md: -------------------------------------------------------------------------------- 1 | # SwiftBar Packaged Plugins 2 | 3 | SwiftBar now supports packaged plugins, which allow you to organize related files into a single plugin package rather than a single script file. 4 | 5 | ## Creating a Packaged Plugin 6 | 7 | A packaged plugin is a directory with the `.swiftbar` extension containing multiple files, including a main script file named `plugin.*`. Here's how to create one: 8 | 9 | 1. Create a directory with the `.swiftbar` extension (e.g., `weather.swiftbar`) 10 | 2. Inside this directory, create a main script file named `plugin.sh` (or `plugin.py`, `plugin.js`, etc.) 11 | 3. Add any additional resources your plugin needs (libraries, helper scripts, assets, etc.) 12 | 4. Make sure your main `plugin.*` script is executable 13 | 14 | ## Example Structure 15 | 16 | ``` 17 | weather.swiftbar/ 18 | ├── Contents/ 19 | │ ├── Info.plist # Bundle information (makes macOS treat it as a bundle) 20 | │ ├── Resources/ # Bundle resources 21 | │ │ └── plugin-icon.icns # Custom icon for the plugin (optional) 22 | ├── plugin.sh # Main entry point (REQUIRED, must be executable) 23 | ├── lib/ # Supporting library scripts 24 | │ ├── weather_api.sh 25 | │ └── formatting.sh 26 | ├── assets/ # Images and other assets 27 | │ └── icons/ 28 | │ ├── sunny.png 29 | │ └── cloudy.png 30 | └── config.json # Configuration files 31 | ``` 32 | 33 | ### Bundle Structure (Optional) 34 | 35 | To make your packaged plugin appear as a bundle in Finder with a custom icon: 36 | 37 | 1. Create a `Contents` directory and add an `Info.plist` file: 38 | 39 | ```xml 40 | 41 | 42 | 43 | 44 | CFBundleIdentifier 45 | com.yourname.swiftbar.weather 46 | CFBundleName 47 | Weather 48 | CFBundlePackageType 49 | BNDL 50 | CFBundleVersion 51 | 1.0 52 | CFBundleShortVersionString 53 | 1.0 54 | LSTypeIsPackage 55 | 56 | 57 | 58 | ``` 59 | 60 | 2. Optionally add a custom icon in `Contents/Resources/plugin-icon.icns` 61 | 62 | ## Writing the Main Script 63 | 64 | Your main script (`plugin.sh`) should source or import any required libraries: 65 | 66 | ```bash 67 | #!/bin/bash 68 | 69 | # Source supporting libraries 70 | source "$PACKAGE_LIB_DIR/weather_api.sh" 71 | source "$PACKAGE_LIB_DIR/formatting.sh" 72 | 73 | # Use functions from the libraries 74 | get_weather_data 75 | format_output 76 | 77 | echo "☀️ 72°F" 78 | echo "---" 79 | echo "Humidity: 45%" 80 | echo "Wind: 5 mph NW" 81 | ``` 82 | 83 | ## Environment Variables 84 | 85 | In packaged plugins, SwiftBar provides these additional environment variables: 86 | 87 | | Variable | Description | 88 | |----------|-------------| 89 | | `PACKAGE_DIR` | Full path to the package directory | 90 | | `PACKAGE_LIB_DIR` | Full path to the `lib` directory inside the package | 91 | | `PACKAGE_BIN_DIR` | Full path to the `bin` directory inside the package | 92 | | `PACKAGE_ASSETS_DIR` | Full path to the `assets` directory inside the package | 93 | | `PACKAGE_RESOURCES_DIR` | Full path to the `resources` directory inside the package | 94 | 95 | Use these variables to reference files within your package, ensuring your plugin works regardless of where it's installed. 96 | 97 | ## Important Notes 98 | 99 | 1. **Plugin Entry Point**: The main script must be named `plugin.*` with any extension (e.g., `plugin.sh`, `plugin.py`, etc.) 100 | 2. **Package Extension**: The package directory must have the `.swiftbar` extension 101 | 3. **Working Directory**: When your main script runs, the working directory is set to the package directory, so relative paths will work 102 | 4. **Executable Permission**: Make sure your main script has executable permissions: `chmod +x plugin.sh` 103 | 104 | ## Command Line Example 105 | 106 | ```bash 107 | # Create a packaged plugin 108 | mkdir -p weather.swiftbar/lib 109 | mkdir -p weather.swiftbar/assets 110 | mkdir -p weather.swiftbar/Contents/Resources 111 | 112 | # Create the Info.plist to make it display as a bundle 113 | cat > weather.swiftbar/Contents/Info.plist << 'EOF' 114 | 115 | 116 | 117 | 118 | CFBundleIdentifier 119 | com.example.swiftbar.weather 120 | CFBundleName 121 | Weather 122 | CFBundlePackageType 123 | BNDL 124 | CFBundleVersion 125 | 1.0 126 | CFBundleShortVersionString 127 | 1.0 128 | LSTypeIsPackage 129 | 130 | 131 | 132 | EOF 133 | 134 | # Create the main script 135 | cat > weather.swiftbar/plugin.sh << 'EOF' 136 | #!/bin/bash 137 | 138 | # Use the built-in environment variables to source libraries 139 | source "$PACKAGE_LIB_DIR/weather_api.sh" 140 | 141 | # Get weather data using functions from the library 142 | get_weather_data 143 | 144 | # Output to menu bar 145 | echo "☀️ 72°F" 146 | echo "---" 147 | echo "Humidity: 45%" 148 | echo "Wind: 5 mph NW" 149 | EOF 150 | 151 | # Create a supporting library 152 | cat > weather.swiftbar/lib/weather_api.sh << 'EOF' 153 | #!/bin/bash 154 | 155 | get_weather_data() { 156 | # This would normally call an API, but we'll use static data for the example 157 | echo "Weather data retrieved" 158 | } 159 | EOF 160 | 161 | # Make scripts executable 162 | chmod +x weather.swiftbar/plugin.sh 163 | chmod +x weather.swiftbar/lib/weather_api.sh 164 | 165 | # Copy to SwiftBar plugins directory 166 | cp -r weather.swiftbar ~/Documents/SwiftBar/ 167 | ``` -------------------------------------------------------------------------------- /SwiftBar/UI/Plugin Repository/PluginRepository.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import os 4 | 5 | class PluginRepository: ObservableObject { 6 | static let shared = PluginRepository() 7 | let prefs = PreferencesStore.shared 8 | 9 | @Published var searchString: String = "" 10 | var cancellables: Set = [] 11 | 12 | @Published var categories: [String] = [] 13 | @Published var plugins: [String: [RepositoryPlugin.Plugin]] = [:] 14 | 15 | init() { 16 | refreshRepositoryData() 17 | 18 | if #available(OSX 11.0, *) { 19 | NotificationCenter.default.publisher(for: .repositoirySearchUpdate) 20 | .compactMap { $0.userInfo?["query"] as? String } 21 | .map { $0 } 22 | .debounce(for: 0.2, scheduler: RunLoop.main) 23 | .assign(to: &$searchString) 24 | } 25 | } 26 | 27 | func getPlugins(for category: String) -> [RepositoryPlugin.Plugin] { 28 | plugins[category]?.sorted(by: { $0.title > $1.title }) ?? [] 29 | } 30 | 31 | func searchPlugins(with searchString: String) -> [RepositoryPlugin.Plugin] { 32 | plugins.flatMap(\.value) 33 | .filter { $0.bagOfWords.contains(searchString.lowercased()) } 34 | .sorted(by: { $0.title > $1.title }) 35 | } 36 | 37 | func refreshRepositoryData(ignoreCache: Bool = false) { 38 | PluginRepositoryAPI.categories(ignoreCache: ignoreCache) 39 | .map(\.categories) 40 | .sink(receiveCompletion: { _ in }, 41 | receiveValue: { [weak self] in 42 | let cats = $0.map(\.text) 43 | self?.categories = cats 44 | cats.forEach { self?.getPlugins(category: $0, ignoreCache: ignoreCache) } 45 | }) 46 | .store(in: &cancellables) 47 | } 48 | 49 | func getPlugins(category: String, ignoreCache: Bool = false) { 50 | PluginRepositoryAPI.plugins(category: category, ignoreCache: ignoreCache) 51 | .map(\.plugins) 52 | .sink(receiveCompletion: { _ in }, 53 | receiveValue: { [weak self] in 54 | self?.plugins[category] = $0 55 | }) 56 | .store(in: &cancellables) 57 | } 58 | 59 | static func categorySFImage(_ category: String) -> String { 60 | switch category.lowercased() { 61 | case "aws": 62 | "bolt" 63 | case "cryptocurrency": 64 | "bitcoinsign.circle" 65 | case "dev": 66 | "hammer" 67 | case "e-commerce": 68 | "bag.circle" 69 | case "email": 70 | "envelope" 71 | case "environment": 72 | "leaf" 73 | case "finance": 74 | "dollarsign.circle" 75 | case "games": 76 | "gamecontroller" 77 | case "lifestyle": 78 | "face.smiling" 79 | case "messenger": 80 | "message" 81 | case "music": 82 | "music.note" 83 | case "network": 84 | "network" 85 | case "politics": 86 | "person.2" 87 | case "science": 88 | "graduationcap" 89 | case "sports": 90 | "sportscourt" 91 | case "system": 92 | "gear" 93 | case "time": 94 | "clock" 95 | case "tools": 96 | "paintbrush" 97 | case "travel": 98 | "briefcase" 99 | case "tutorial": 100 | "bubble.left.and.bubble.right" 101 | case "weather": 102 | "cloud.sun" 103 | case "web": 104 | "globe" 105 | default: 106 | "questionmark.circle" 107 | } 108 | } 109 | } 110 | 111 | struct RepositoryCategory: Codable { 112 | struct Category: Codable { 113 | let path: String 114 | let text: String 115 | let lastUpdated: String 116 | } 117 | 118 | let version: String 119 | let lastUpdated: String 120 | let categories: [Category] 121 | } 122 | 123 | struct RepositoryPlugin: Codable { 124 | struct Plugin: Codable, Hashable { 125 | struct Author: Codable, Hashable { 126 | let name: String 127 | let githubUsername: String? 128 | let imageURL: String? 129 | let bio: String? 130 | let primary: Bool 131 | } 132 | 133 | let path: String 134 | let filename: String 135 | let dir: String 136 | let docsPlugin: String 137 | let docsCategory: String 138 | let title: String 139 | let version: String 140 | let desc: String 141 | let imageURL: String 142 | let dependencies: [String]? 143 | var authors: [Author] 144 | let aboutURL: String 145 | 146 | var image: URL? { 147 | URL(string: imageURL) 148 | } 149 | 150 | var gitHubURL: URL? { 151 | let url = PreferencesStore.shared.pluginSourceCodeURL 152 | return url.appendingPathComponent(path) 153 | } 154 | 155 | var sourceFileURL: URL? { 156 | let url = PreferencesStore.shared.pluginRepositoryURL 157 | if url.absoluteString.hasPrefix("https://xbarapp.com/") { 158 | return URL(string: "https://raw.githubusercontent.com/matryer/xbar-plugins/master/\(path)") 159 | } 160 | 161 | return url.appendingPathComponent(path) 162 | } 163 | 164 | var mainAuthor: Author? { 165 | authors.first { $0.primary } 166 | } 167 | 168 | var author: String { 169 | mainAuthor?.name ?? "" 170 | } 171 | 172 | var github: String? { 173 | mainAuthor?.githubUsername 174 | } 175 | 176 | var bagOfWords: [String] { 177 | var out: [String] = [] 178 | [title, author, desc].compactMap { $0 }.forEach { str in 179 | out.append(contentsOf: str.lowercased().components(separatedBy: .whitespaces)) 180 | } 181 | return out 182 | } 183 | } 184 | 185 | let version: String 186 | let lastUpdated: String 187 | let plugins: [Plugin] 188 | } 189 | -------------------------------------------------------------------------------- /SwiftBar/Utility/NSColor.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | extension NSColor { 4 | private static let cssColors: [String: String] = [ 5 | "lightseagreen": "20b2aa", "floralwhite": "fffaf0", "lightgray": "d3d3d3", "darkgoldenrod": "b8860b", "paleturquoise": "afeeee", "goldenrod": "daa520", "skyblue": "87ceeb", "indianred": "cd5c5c", "darkgray": "a9a9a9", "khaki": "f0e68c", "blue": "0000ff", "darkred": "8b0000", "lightyellow": "ffffe0", "midnightblue": "191970", "chartreuse": "7fff00", "lightsteelblue": "b0c4de", "slateblue": "6a5acd", "firebrick": "b22222", "moccasin": "ffe4b5", "salmon": "fa8072", "sienna": "a0522d", "slategray": "708090", "teal": "008080", "lightsalmon": "ffa07a", "pink": "ffc0cb", "burlywood": "deb887", "gold": "ffd700", "springgreen": "00ff7f", "lightcoral": "f08080", "black": "000000", "blueviolet": "8a2be2", "chocolate": "d2691e", "aqua": "00ffff", "darkviolet": "9400d3", "indigo": "4b0082", "darkcyan": "008b8b", "orange": "ffa500", "antiquewhite": "faebd7", "peru": "cd853f", "silver": "c0c0c0", "purple": "800080", "saddlebrown": "8b4513", "lawngreen": "7cfc00", "dodgerblue": "1e90ff", "lime": "00ff00", "linen": "faf0e6", "lightblue": "add8e6", "darkslategray": "2f4f4f", "lightskyblue": "87cefa", "mintcream": "f5fffa", "olive": "808000", "hotpink": "ff69b4", "papayawhip": "ffefd5", "mediumseagreen": "3cb371", "mediumspringgreen": "00fa9a", "cornflowerblue": "6495ed", "plum": "dda0dd", "seagreen": "2e8b57", "palevioletred": "db7093", "bisque": "ffe4c4", "beige": "f5f5dc", "darkorchid": "9932cc", "royalblue": "4169e1", "darkolivegreen": "556b2f", "darkmagenta": "8b008b", "orange red": "ff4500", "lavender": "e6e6fa", "fuchsia": "ff00ff", "darkseagreen": "8fbc8f", "lavenderblush": "fff0f5", "wheat": "f5deb3", "steelblue": "4682b4", "lightgoldenrodyellow": "fafad2", "lightcyan": "e0ffff", "mediumaquamarine": "66cdaa", "turquoise": "40e0d0", "dark blue": "00008b", "darkorange": "ff8c00", "brown": "a52a2a", "dimgray": "696969", "deeppink": "ff1493", "powderblue": "b0e0e6", "red": "ff0000", "darkgreen": "006400", "ghostwhite": "f8f8ff", "white": "ffffff", "navajowhite": "ffdead", "navy": "000080", "ivory": "fffff0", "palegreen": "98fb98", "whitesmoke": "f5f5f5", "gainsboro": "dcdcdc", "mediumslateblue": "7b68ee", "olivedrab": "6b8e23", "mediumpurple": "9370db", "darkslateblue": "483d8b", "blanchedalmond": "ffebcd", "darkkhaki": "bdb76b", "green": "008000", "limegreen": "32cd32", "snow": "fffafa", "tomato": "ff6347", "darkturquoise": "00ced1", "orchid": "da70d6", "yellow": "ffff00", "green yellow": "adff2f", "azure": "f0ffff", "mistyrose": "ffe4e1", "cadetblue": "5f9ea0", "oldlace": "fdf5e6", "gray": "808080", "honeydew": "f0fff0", "peachpuff": "ffdab9", "tan": "d2b48c", "thistle": "d8bfd8", "palegoldenrod": "eee8aa", "mediumorchid": "ba55d3", "rosybrown": "bc8f8f", "mediumturquoise": "48d1cc", "lemonchiffon": "fffacd", "maroon": "800000", "mediumvioletred": "c71585", "violet": "ee82ee", "yellow green": "9acd32", "coral": "ff7f50", "lightgreen": "90ee90", "cornsilk": "fff8dc", "mediumblue": "0000cd", "aliceblue": "f0f8ff", "forestgreen": "228b22", "aquamarine": "7fffd4", "deepskyblue": "00bfff", "lightslategray": "778899", "darksalmon": "e9967a", "crimson": "dc143c", "sandybrown": "f4a460", "lightpink": "ffb6c1", "seashell": "fff5ee", 6 | ] 7 | 8 | public static func webColor(from colorString: String?) -> NSColor? { 9 | guard let colorString else { return nil } 10 | 11 | if colorString.hasPrefix("#") { 12 | return fromHexString(hex: colorString) 13 | } 14 | 15 | if let color = NSColor.cssColors[colorString] { 16 | return fromHexString(hex: color) 17 | } 18 | 19 | return nil 20 | } 21 | 22 | class func fromHex(hex: Int, alpha _: Float) -> NSColor { 23 | let red = CGFloat((hex & 0xFF0000) >> 16) / 255.0 24 | let green = CGFloat((hex & 0xFF00) >> 8) / 255.0 25 | let blue = CGFloat(hex & 0xFF) / 255.0 26 | return NSColor(calibratedRed: red, green: green, blue: blue, alpha: 1.0) 27 | } 28 | 29 | class func fromHexString(hex: String, alpha _: Float = 1) -> NSColor? { 30 | var cleanedString = hex 31 | if hex.hasPrefix("0x") { 32 | cleanedString = String(hex.dropFirst(2)) 33 | } else if hex.hasPrefix("#") { 34 | cleanedString = String(hex.dropFirst()) 35 | } 36 | // Ensure it only contains valid hex characters 0 37 | let validHexPattern = "[a-fA-F0-9]+" 38 | if cleanedString.conformsTo(pattern: validHexPattern) { 39 | var theInt: UInt32 = 0 40 | let scanner = Scanner(string: cleanedString) 41 | scanner.scanHexInt32(&theInt) 42 | let red = CGFloat((theInt & 0xFF0000) >> 16) / 255.0 43 | let green = CGFloat((theInt & 0xFF00) >> 8) / 255.0 44 | let blue = CGFloat(theInt & 0xFF) / 255.0 45 | return NSColor(calibratedRed: red, green: green, blue: blue, alpha: 1.0) 46 | 47 | } else { 48 | return nil 49 | } 50 | } 51 | } 52 | 53 | extension String { 54 | func conformsTo(pattern: String) -> Bool { 55 | let pattern = NSPredicate(format: "SELF MATCHES %@", pattern) 56 | return pattern.evaluate(with: self) 57 | } 58 | } 59 | 60 | extension NSColor { 61 | static func colorForAnsi256ColorIndex(index: Int) -> NSColor? { 62 | var r: CGFloat 63 | var g: CGFloat 64 | var b: CGFloat 65 | 66 | if index >= 16, index < 232 { 67 | let i = CGFloat(index - 16) 68 | r = (i / 36.0) > 1.0 ? ((i / 36.0) * 40.0 + 55.0) / 255.0 : 0.0 69 | if i.truncatingRemainder(dividingBy: 36) / 6.0 > 1 { 70 | g = ((i.truncatingRemainder(dividingBy: 36) / 6.0) * 40.0 + 55.0) / 255.0 71 | } else { 72 | g = 0.0 73 | } 74 | if i.truncatingRemainder(dividingBy: 6) > 1 { 75 | b = (i.truncatingRemainder(dividingBy: 36) * 40.0 + 55.0) / 255.0 76 | } else { 77 | b = 0.0 78 | } 79 | } else if index >= 232, index < 256 { 80 | let i = CGFloat(index - 232) 81 | r = (i * 10 + 8) / 255.0 82 | g = (i * 10 + 8) / 255.0 83 | b = (i * 10 + 8) / 255.0 84 | } else { 85 | return nil 86 | } 87 | return NSColor(deviceRed: r, green: g, blue: b, alpha: 1.0).usingColorSpace(.sRGB) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /SwiftBar/Plugin/Plugin.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import Foundation 3 | import os 4 | 5 | enum PluginType: String { 6 | case Executable 7 | case Streamable 8 | case Shortcut 9 | case Ephemeral 10 | 11 | static var debugable: [Self] { 12 | [.Executable, .Streamable] 13 | } 14 | 15 | static var runnableInTerminal: [Self] { 16 | [.Executable, .Streamable] 17 | } 18 | 19 | static var disableable: [Self] { 20 | [.Executable, .Streamable, .Shortcut] 21 | } 22 | } 23 | 24 | enum PluginState { 25 | case Loading 26 | case Streaming 27 | case Success 28 | case Failed 29 | case Disabled 30 | } 31 | 32 | enum PluginRefreshReason: String { 33 | case FirstLaunch 34 | case Schedule 35 | case MenuAction 36 | case RefreshAllMenu 37 | case RefreshAllURLScheme 38 | case URLScheme 39 | case Shortcut 40 | case DebugView 41 | case NotificationAction 42 | case PluginSettings 43 | case MenuOpen 44 | case WakeFromSleep 45 | 46 | static func manualReasons() -> [Self] { 47 | [ 48 | .MenuAction, 49 | .RefreshAllMenu, 50 | .RefreshAllURLScheme, 51 | .URLScheme, 52 | .Shortcut, 53 | .NotificationAction, 54 | .PluginSettings, 55 | .DebugView, 56 | .MenuOpen, 57 | ] 58 | } 59 | } 60 | 61 | typealias PluginID = String 62 | 63 | protocol Plugin: AnyObject { 64 | var id: PluginID { get } 65 | var type: PluginType { get } 66 | var name: String { get } 67 | var file: String { get } 68 | var enabled: Bool { get } 69 | var metadata: PluginMetadata? { get set } 70 | var contentUpdatePublisher: PassthroughSubject { get set } 71 | var updateInterval: Double { get } 72 | var lastUpdated: Date? { get set } 73 | var lastState: PluginState { get set } 74 | var lastRefreshReason: PluginRefreshReason { get set } 75 | var content: String? { get set } 76 | var error: Error? { get set } 77 | var debugInfo: PluginDebugInfo { get set } 78 | var refreshEnv: [String: String] { get set } 79 | func refresh(reason: PluginRefreshReason) 80 | func enable() 81 | func disable() 82 | func start() 83 | func terminate() 84 | func invoke() -> String? 85 | func makeScriptExecutable(file: String) 86 | func refreshPluginMetadata() 87 | func writeStdin(_ input: String) throws 88 | } 89 | 90 | extension Plugin { 91 | var description: String { 92 | """ 93 | id: \(id) 94 | type: \(type) 95 | name: \(name) 96 | path: \(file) 97 | """ 98 | } 99 | 100 | var isStale: Bool { 101 | // Check if plugin has timed updates and hasn't updated within 2x the interval 102 | guard updateInterval > 0, 103 | updateInterval < 60 * 60 * 24 * 100, // Not a "never" update plugin 104 | let lastUpdated 105 | else { 106 | return false 107 | } 108 | 109 | let timeSinceLastUpdate = Date().timeIntervalSince(lastUpdated) 110 | return timeSinceLastUpdate > (updateInterval * 2) 111 | } 112 | 113 | var prefs: PreferencesStore { 114 | PreferencesStore.shared 115 | } 116 | 117 | var enabled: Bool { 118 | !prefs.disabledPlugins.contains(id) 119 | } 120 | 121 | func makeScriptExecutable(file: String) { 122 | guard prefs.makePluginExecutable else { return } 123 | _ = try? runScript(to: "chmod", args: ["+x", "\(file.escaped())"]) 124 | } 125 | 126 | func refreshPluginMetadata() { 127 | os_log("Refreshing plugin metadata \n%{public}@", log: Log.plugin, file) 128 | let url = URL(fileURLWithPath: file) 129 | 130 | // Parse metadata in a thread-safe way 131 | var newMetadata: PluginMetadata? 132 | if let script = try? String(contentsOf: url) { 133 | newMetadata = PluginMetadata.parser(script: script) 134 | } 135 | if let md = PluginMetadata.parser(fileURL: url) { 136 | newMetadata = md 137 | } 138 | 139 | // Only update if we got new metadata 140 | if let newMetadata = newMetadata { 141 | metadata = newMetadata 142 | 143 | // Update refresh environment if needed 144 | if !newMetadata.environment.isEmpty { 145 | refreshEnv = newMetadata.environment 146 | } 147 | } 148 | } 149 | 150 | var cacheDirectory: URL? { 151 | AppShared.cacheDirectory?.appendingPathComponent(id) 152 | } 153 | 154 | var cacheDirectoryPath: String { 155 | cacheDirectory?.path ?? "" 156 | } 157 | 158 | var dataDirectory: URL? { 159 | AppShared.dataDirectory?.appendingPathComponent(id) 160 | } 161 | 162 | var dataDirectoryPath: String { 163 | dataDirectory?.path ?? "" 164 | } 165 | 166 | func createSupportDirs() { 167 | if let cacheURL = cacheDirectory { 168 | try? FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true, attributes: nil) 169 | } 170 | if let dataURL = dataDirectory { 171 | try? FileManager.default.createDirectory(at: dataURL, withIntermediateDirectories: true, attributes: nil) 172 | } 173 | } 174 | 175 | var env: [String: String] { 176 | var pluginEnv = [ 177 | Environment.Variables.swiftBarPluginPath.rawValue: file, 178 | Environment.Variables.osAppearance.rawValue: AppShared.isDarkTheme ? "Dark" : "Light", 179 | Environment.Variables.swiftBarPluginCachePath.rawValue: cacheDirectoryPath, 180 | Environment.Variables.swiftBarPluginDataPath.rawValue: dataDirectoryPath, 181 | Environment.Variables.swiftBarPluginRefreshReason.rawValue: lastRefreshReason.rawValue, 182 | ] 183 | metadata?.environment.forEach { k, v in 184 | pluginEnv[k] = v 185 | } 186 | 187 | for (k, v) in refreshEnv { 188 | pluginEnv[k] = v 189 | } 190 | refreshEnv.removeAll() 191 | return pluginEnv 192 | } 193 | 194 | func writeStdin(_ input: String) throws { 195 | throw NSError(domain: "SwiftBar.Plugin", code: 1, userInfo: [NSLocalizedDescriptionKey: "Plugin type \(type.rawValue) does not support stdin input"]) 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /SwiftBar/UI/Preferences/PluginDetailsView.swift: -------------------------------------------------------------------------------- 1 | import Preferences 2 | import SwiftUI 3 | 4 | struct PluginDetailsView: View { 5 | @ObservedObject var md: PluginMetadata 6 | let plugin: Plugin 7 | @State var isEditing: Bool = false 8 | @State var dependencies: String = "" 9 | let screenProportion: CGFloat = 0.3 10 | let width: CGFloat = 400 11 | var body: some View { 12 | Preferences.Container(contentWidth: 500) { 13 | Preferences.Section(label: { 14 | HStack { 15 | Text("About Plugin") 16 | if #available(OSX 11.0, *) { 17 | Button(action: { 18 | AppShared.openPluginFolder(path: plugin.file) 19 | }) { 20 | Image(systemName: "folder") 21 | }.padding(.trailing) 22 | } 23 | Spacer() 24 | } 25 | }, content: {}) 26 | Preferences.Section(label: { 27 | PluginDetailsTextView(label: "Name", 28 | text: $md.name, 29 | width: width * screenProportion) 30 | }, content: {}) 31 | Preferences.Section(label: { 32 | PluginDetailsTextView(label: "Description", 33 | text: $md.desc, 34 | width: width * screenProportion) 35 | }, content: {}) 36 | Preferences.Section(label: { 37 | PluginDetailsTextView(label: "Dependencies", 38 | text: $dependencies, 39 | width: width * screenProportion) 40 | .onAppear(perform: { 41 | dependencies = md.dependencies.joined(separator: ",") 42 | }) 43 | }, content: {}) 44 | Preferences.Section(label: { 45 | HStack { 46 | PluginDetailsTextView(label: "GitHub", 47 | text: $md.github, 48 | width: width * screenProportion) 49 | PluginDetailsTextView(label: "Author", 50 | text: $md.author, 51 | width: width * 0.2) 52 | } 53 | }, content: {}) 54 | Preferences.Section(bottomDivider: true, label: { 55 | HStack { 56 | PluginDetailsTextView(label: "Version", 57 | text: $md.version, 58 | width: width * screenProportion) 59 | PluginDetailsTextView(label: "Schedule", 60 | text: $md.schedule, 61 | width: width * 0.2) 62 | } 63 | }, content: {}) 64 | Preferences.Section(label: { 65 | HStack { 66 | Text("Hide Menu Items:") 67 | Spacer() 68 | } 69 | }, content: {}) 70 | Preferences.Section(label: { 71 | HStack { 72 | PluginDetailsToggleView(label: "About", 73 | state: $md.hideAbout, 74 | width: width * screenProportion) 75 | PluginDetailsToggleView(label: "Run in Terminal", 76 | state: $md.hideRunInTerminal, 77 | width: width * screenProportion) 78 | PluginDetailsToggleView(label: "Last Updated", 79 | state: $md.hideLastUpdated, 80 | width: width * screenProportion) 81 | } 82 | }, content: {}) 83 | 84 | Preferences.Section(bottomDivider: true, label: { 85 | HStack { 86 | PluginDetailsToggleView(label: "SwiftBar", 87 | state: $md.hideSwiftBar, 88 | width: width * screenProportion) 89 | 90 | PluginDetailsToggleView(label: "Disable Plugin", 91 | state: $md.hideDisablePlugin, 92 | width: width * screenProportion) 93 | } 94 | }, content: {}) 95 | 96 | Preferences.Section(title: "", content: {}) 97 | Preferences.Section(label: { 98 | HStack { 99 | if #available(macOS 11.0, *) { 100 | Button(action: { 101 | NSWorkspace.shared.open(URL(string: "https://github.com/swiftbar/SwiftBar#metadata-for-binary-plugins")!) 102 | }, label: { 103 | Image(systemName: "questionmark.circle") 104 | }).buttonStyle(LinkButtonStyle()) 105 | } 106 | Spacer() 107 | Button("Reset", action: { 108 | PluginMetadata.cleanMetadata(fileURL: URL(fileURLWithPath: plugin.file)) 109 | plugin.refreshPluginMetadata() 110 | }) 111 | Button("Save in Plugin File", action: { 112 | PluginMetadata.writeMetadata(metadata: md, fileURL: URL(fileURLWithPath: plugin.file)) 113 | }) 114 | } 115 | 116 | }, content: {}) 117 | } 118 | } 119 | } 120 | 121 | struct PluginDetailsTextView: View { 122 | @EnvironmentObject var preferences: PreferencesStore 123 | let label: String 124 | @Binding var text: String 125 | let width: CGFloat 126 | var body: some View { 127 | HStack { 128 | HStack { 129 | Spacer() 130 | Text("\(label):") 131 | }.frame(width: width) 132 | TextField("", text: $text) 133 | .disabled(!PreferencesStore.shared.pluginDeveloperMode) 134 | Spacer() 135 | } 136 | } 137 | } 138 | 139 | struct PluginDetailsToggleView: View { 140 | let label: String 141 | @Binding var state: Bool 142 | let width: CGFloat 143 | var body: some View { 144 | HStack { 145 | HStack { 146 | Spacer() 147 | Text("\(label):") 148 | }.frame(width: width) 149 | Toggle("", isOn: $state) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /SWIFTBAR_CODE_REVIEW_REPORT.md: -------------------------------------------------------------------------------- 1 | # SwiftBar Comprehensive Code Review Report 2 | 3 | ## Executive Summary 4 | 5 | SwiftBar is a well-structured macOS menu bar application that allows users to run scripts and display their output in the menu bar. The codebase demonstrates good Swift practices and proper macOS integration, but has significant gaps in security, testing, and documentation that need to be addressed. 6 | 7 | ### Key Strengths 8 | - Clean architecture with clear separation of concerns 9 | - Good use of modern Swift patterns and SwiftUI 10 | - Excellent user-facing documentation 11 | - Proper memory management and error handling foundations 12 | - Strong localization support (7 languages) 13 | 14 | ### Critical Issues 15 | - **Security vulnerabilities** in script execution and URL handling 16 | - **Minimal test coverage** (<10% of codebase) 17 | - **Zero inline code documentation** 18 | - **No CI/CD pipeline** for automated testing 19 | - **Missing accessibility features** 20 | 21 | ## Detailed Findings 22 | 23 | ### 1. Architecture & Code Quality 24 | 25 | #### Strengths 26 | - Well-organized project structure with logical component separation 27 | - Proper use of Swift protocols and dependency injection 28 | - Good adoption of Combine framework for reactive programming 29 | - Appropriate mix of SwiftUI (new UI) and AppKit (system integration) 30 | 31 | #### Issues 32 | - **File naming typo**: `PluginManger.swift` should be `PluginManager.swift` 33 | - Some classes doing too much (e.g., `MenuBarItem` handles UI, business logic, drag & drop) 34 | - Code duplication in menu building logic 35 | - Complex methods that should be refactored into smaller functions 36 | 37 | ### 2. Security Assessment 38 | 39 | #### Critical Vulnerabilities 40 | 41 | 1. **Command Injection** (CRITICAL) 42 | ```swift 43 | // String+Escaped.swift - Vulnerable implementation 44 | func escaped() -> Self { 45 | guard contains(" ") else { return self } 46 | return "'\(self)'" // Doesn't escape single quotes within string! 47 | } 48 | ``` 49 | 50 | 2. **Arbitrary Code Execution** (CRITICAL) 51 | - URL scheme `addplugin` downloads and executes scripts from any URL without validation 52 | - No plugin signature verification or sandboxing 53 | 54 | 3. **Path Traversal Risk** (MEDIUM) 55 | - Plugin loading doesn't validate symlink destinations 56 | - No restrictions on file access from plugins 57 | 58 | #### Recommendations 59 | - Fix the `escaped()` function to properly escape shell arguments 60 | - Implement URL allowlisting for plugin sources 61 | - Add user confirmation dialogs for remote plugin installation 62 | - Consider plugin sandboxing or permission system 63 | 64 | ### 3. Testing Coverage 65 | 66 | #### Current State 67 | - Only 29 tests covering string utilities and parameter parsing 68 | - **No tests for**: 69 | - Core plugin system 70 | - Menu bar functionality 71 | - UI components 72 | - Script execution 73 | - Error handling paths 74 | - No CI/CD pipeline 75 | - No integration or UI tests 76 | 77 | #### Recommendations 78 | - Implement comprehensive unit tests for Plugin system 79 | - Add integration tests for plugin execution pipeline 80 | - Set up GitHub Actions for automated testing 81 | - Target minimum 70% code coverage 82 | 83 | ### 4. UI/UX Implementation 84 | 85 | #### Strengths 86 | - Native macOS look and feel 87 | - Good dark mode support 88 | - Comprehensive localization framework 89 | - Proper use of SF Symbols with fallbacks 90 | 91 | #### Issues 92 | - **Zero accessibility support** (no VoiceOver labels) 93 | - Some hardcoded strings not localized 94 | - Inconsistent button styles and shadow effects 95 | - Missing loading states for async operations 96 | 97 | ### 5. Documentation 98 | 99 | #### User Documentation: Excellent ✅ 100 | - Comprehensive README with clear instructions 101 | - Well-documented plugin API 102 | - Good examples and use cases 103 | 104 | #### Code Documentation: Critical Gap ❌ 105 | - **Zero Swift documentation comments** 106 | - No inline comments explaining complex logic 107 | - Missing API documentation 108 | - No architecture overview or developer guides 109 | 110 | ### 6. Build Configuration & Dependencies 111 | 112 | #### Strengths 113 | - Proper use of Swift Package Manager 114 | - Dependencies from reputable sources 115 | - Hardened runtime enabled 116 | - Appropriate code signing 117 | 118 | #### Issues 119 | - `NSAllowsArbitraryLoads = true` allows insecure HTTP 120 | - Entitlements file has a typo (duplicate key) 121 | - Over-broad permissions for non-MAS version 122 | - No dependency security scanning 123 | 124 | ## Priority Recommendations 125 | 126 | ### Immediate Actions (Critical) 127 | 128 | 1. **Fix Security Vulnerabilities** 129 | - Fix the `escaped()` function in String+Escaped.swift 130 | - Add URL validation for plugin downloads 131 | - Remove or restrict ephemeral plugin feature 132 | 133 | 2. **Add Basic Documentation** 134 | - Document the Plugin protocol and public APIs 135 | - Add inline comments for complex logic 136 | - Create a basic architecture overview 137 | 138 | ### Short-term Improvements (1-2 weeks) 139 | 140 | 3. **Implement Core Tests** 141 | - Add unit tests for Plugin and PluginManager 142 | - Test script execution safety 143 | - Set up GitHub Actions CI 144 | 145 | 4. **Fix Build Issues** 146 | - Correct the entitlements typo 147 | - Replace NSAllowsArbitraryLoads with domain exceptions 148 | - Rename PluginManger.swift to PluginManager.swift 149 | 150 | 5. **Improve Accessibility** 151 | - Add VoiceOver labels to all UI elements 152 | - Implement keyboard navigation hints 153 | - Test with accessibility tools 154 | 155 | ### Medium-term Goals (1-2 months) 156 | 157 | 6. **Enhance Security** 158 | - Implement plugin signing/verification 159 | - Add sandboxing for both MAS and non-MAS versions 160 | - Create plugin permission system 161 | 162 | 7. **Expand Testing** 163 | - Achieve 70% code coverage 164 | - Add integration tests 165 | - Implement UI testing 166 | 167 | 8. **Refactor Complex Components** 168 | - Break down MenuBarItem into smaller components 169 | - Extract view models from SwiftUI views 170 | - Consolidate duplicate code 171 | 172 | ### Long-term Vision (3-6 months) 173 | 174 | 9. **Developer Experience** 175 | - Create comprehensive developer documentation 176 | - Add contribution guidelines 177 | - Implement code quality tools (SwiftLint) 178 | 179 | 10. **Advanced Features** 180 | - Plugin marketplace with security scanning 181 | - Automated error recovery 182 | - Performance monitoring 183 | 184 | ## Conclusion 185 | 186 | SwiftBar is a well-designed application with a solid foundation. The main areas requiring attention are security hardening, test coverage, and code documentation. Addressing the critical security vulnerabilities should be the top priority, followed by establishing a testing framework and improving documentation. With these improvements, SwiftBar would be a more secure, maintainable, and contributor-friendly project. 187 | 188 | The codebase shows good understanding of Swift and macOS development practices, and with focused effort on the identified areas, it can become an exemplary open-source macOS application. -------------------------------------------------------------------------------- /SwiftBar/UI/Plugin Repository/PluginRepositoryView.swift: -------------------------------------------------------------------------------- 1 | import os 2 | import SwiftUI 3 | 4 | private let minWindowWidth: CGFloat = 1024 5 | private let minWindowHeight: CGFloat = 700 6 | private let minSidebarWidth: CGFloat = 180 7 | 8 | struct PluginRepositoryView: View { 9 | @ObservedObject var pluginRepository = PluginRepository.shared 10 | @State var pluginModalPresented = false 11 | 12 | var body: some View { 13 | if pluginRepository.categories.isEmpty { 14 | VStack { 15 | Text(Localizable.PluginRepository.RefreshingDataMessage.localized) 16 | .font(.largeTitle) 17 | .padding() 18 | 19 | Image(nsImage: NSImage(named: "AppIcon")!) 20 | .resizable() 21 | .aspectRatio(contentMode: .fit) 22 | .opacity(0.6) 23 | 24 | }.frame(width: 400, height: 200) 25 | 26 | } else { 27 | if pluginRepository.searchString.isEmpty { 28 | SplitView(categories: $pluginRepository.categories) 29 | .frame(minWidth: minWindowWidth, maxWidth: .infinity, minHeight: minWindowHeight, maxHeight: .infinity) 30 | } else { 31 | SearchScrollView(searchString: $pluginRepository.searchString) 32 | .frame(minWidth: minWindowWidth, maxWidth: .infinity, minHeight: minWindowHeight, maxHeight: .infinity) 33 | } 34 | } 35 | } 36 | } 37 | 38 | struct CategoryDetailScrollView: View { 39 | let category: String 40 | private let size: CGFloat = 150 41 | private let padding: CGFloat = 5 42 | @State var pluginModalPresented = false 43 | @State var index: Int = 0 44 | 45 | var body: some View { 46 | let plugins = PluginRepository.shared.getPlugins(for: category) 47 | ScrollView(showsIndicators: true) { 48 | if #available(OSX 11.0, *) { 49 | LazyVGrid( 50 | columns: [GridItem(.adaptive(minimum: 300, maximum: 300))], 51 | spacing: padding 52 | ) { 53 | ForEach(plugins, id: \.self) { plugin in 54 | PluginEntryView(pluginEntry: plugin) 55 | .padding() 56 | .shadow(radius: 5) 57 | .id(plugins.firstIndex(of: plugin)) 58 | .onTapGesture { 59 | pluginModalPresented = true 60 | index = plugins.firstIndex(of: plugin) ?? 0 61 | } 62 | .sheet(isPresented: $pluginModalPresented, content: { 63 | PluginEntryModalView(modalPresented: $pluginModalPresented, pluginEntry: plugins[index]) 64 | }) 65 | } 66 | }.padding(padding) 67 | } else { 68 | ForEach(plugins, id: \.self) { plugin in 69 | PluginEntryView(pluginEntry: plugin) 70 | .padding() 71 | .shadow(radius: 20) 72 | .id(plugins.firstIndex(of: plugin)) 73 | .onTapGesture { 74 | pluginModalPresented = true 75 | index = plugins.firstIndex(of: plugin) ?? 0 76 | } 77 | .sheet(isPresented: $pluginModalPresented, content: { 78 | PluginEntryModalView(modalPresented: $pluginModalPresented, pluginEntry: plugins[index]) 79 | }) 80 | } 81 | } 82 | }.frame(minWidth: 100, maxWidth: .infinity) 83 | } 84 | } 85 | 86 | struct SearchScrollView: View { 87 | @Binding var searchString: String 88 | private let size: CGFloat = 150 89 | private let padding: CGFloat = 5 90 | @State var pluginModalPresented = false 91 | @State var index: Int = 0 92 | 93 | var body: some View { 94 | let plugins = PluginRepository.shared.searchPlugins(with: searchString) 95 | if plugins.isEmpty { 96 | Text("No plugins found") 97 | .font(.title) 98 | } else { 99 | ScrollView(showsIndicators: true) { 100 | if #available(OSX 11.0, *) { 101 | LazyVGrid( 102 | columns: [GridItem(.adaptive(minimum: 300, maximum: 300))], 103 | spacing: padding 104 | ) { 105 | ForEach(plugins, id: \.self) { plugin in 106 | PluginEntryView(pluginEntry: plugin) 107 | .padding() 108 | .shadow(radius: 5) 109 | .id(plugins.firstIndex(of: plugin)) 110 | .onTapGesture { 111 | pluginModalPresented = true 112 | index = plugins.firstIndex(of: plugin) ?? 0 113 | } 114 | .sheet(isPresented: $pluginModalPresented, content: { 115 | PluginEntryModalView(modalPresented: $pluginModalPresented, pluginEntry: plugins[index]) 116 | }) 117 | } 118 | }.padding(padding) 119 | } 120 | } 121 | } 122 | } 123 | } 124 | 125 | struct CategoryDetailView: View { 126 | let category: String 127 | var body: some View { 128 | if #available(OSX 11.0, *) { 129 | ScrollViewReader { proxy in 130 | CategoryDetailScrollView(category: category) 131 | .onChange(of: category) { _ in 132 | proxy.scrollTo(0, anchor: .top) 133 | } 134 | } 135 | } else { 136 | CategoryDetailScrollView(category: category) 137 | } 138 | } 139 | } 140 | 141 | struct SplitView: View { 142 | @Binding var categories: [String] 143 | @State var selectedCategory: String? 144 | var body: some View { 145 | NavigationView { 146 | List { 147 | ForEach(categories, id: \.self) { category in 148 | NavigationLink( 149 | destination: CategoryDetailView(category: category).frame(minWidth: minWindowWidth - minSidebarWidth), 150 | tag: category, 151 | selection: $selectedCategory 152 | ) { 153 | HStack { 154 | if #available(OSX 11.0, *) { 155 | Image(systemName: PluginRepository.categorySFImage(category)) 156 | .frame(width: 20) 157 | } 158 | Text(Localizable.Categories(rawValue: "CAT_\(category.uppercased())")?.localized ?? category) 159 | .font(.headline) 160 | } 161 | } 162 | } 163 | }.listStyle(SidebarListStyle()) 164 | .frame(minWidth: minSidebarWidth) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /SwiftBar/Utility/RunScript.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | import Foundation 3 | import os 4 | 5 | let sharedEnv = Environment.shared 6 | 7 | func getEnvExportString(env: [String: String]) -> String { 8 | let dict = sharedEnv.systemEnvStr.merging(env) { current, _ in current } 9 | let shell = sharedEnv.userLoginShell.lowercased() 10 | 11 | // Check for tcsh/csh 12 | if shell.contains("tcsh") || shell.contains("csh") { 13 | // tcsh/csh uses: setenv VAR value 14 | return dict.map { "setenv \($0.key) \($0.value.quoteIfNeeded())" }.joined(separator: "; ") 15 | } 16 | 17 | // Check for fish 18 | if shell.contains("fish") { 19 | // fish uses: set -x VAR value 20 | return dict.map { "set -x \($0.key) \($0.value.quoteIfNeeded())" }.joined(separator: "; ") 21 | } 22 | 23 | // Default to bash/zsh/sh syntax: export VAR=value 24 | return "export \(dict.map { "\($0.key)=\($0.value.quoteIfNeeded())" }.joined(separator: " "))" 25 | } 26 | 27 | @discardableResult func runScript(to command: String, 28 | args: [String] = [], 29 | process: Process = Process(), 30 | env: [String: String] = [:], 31 | runInBash: Bool = true, 32 | streamOutput: Bool = false, 33 | stdinPipe: Pipe? = nil, 34 | onOutputUpdate: @escaping (String?) -> Void = { _ in }) throws -> (out: String, err: String?) 35 | { 36 | let swiftbarEnv = sharedEnv.systemEnvStr.merging(env) { current, _ in current } 37 | process.environment = swiftbarEnv.merging(ProcessInfo.processInfo.environment) { current, _ in current } 38 | return try process.launchScript(with: command, args: args, runInBash: runInBash, streamOutput: streamOutput, stdinPipe: stdinPipe, onOutputUpdate: onOutputUpdate) 39 | } 40 | 41 | // Code below is adopted from https://github.com/JohnSundell/ShellOut 42 | 43 | /// Error type thrown by the `shellOut()` function, in case the given command failed 44 | public struct ShellOutError: Swift.Error { 45 | /// The termination status of the command that was run 46 | public let terminationStatus: Int32 47 | /// The error message as a UTF8 string, as returned through `STDERR` 48 | public var message: String { errorData.shellOutput() } 49 | /// The raw error buffer data, as returned through `STDERR` 50 | public let errorData: Data 51 | /// The raw output buffer data, as retuned through `STDOUT` 52 | public let outputData: Data 53 | /// The output of the command as a UTF8 string, as returned through `STDOUT` 54 | public var output: String { outputData.shellOutput() } 55 | } 56 | 57 | // MARK: - Private 58 | 59 | private extension Process { 60 | @discardableResult func launchScript(with script: String, args: [String], runInBash: Bool = true, streamOutput: Bool, stdinPipe: Pipe? = nil, onOutputUpdate: @escaping (String?) -> Void) throws -> (out: String, err: String?) { 61 | if !runInBash { 62 | executableURL = URL(fileURLWithPath: script) 63 | arguments = args 64 | } else { 65 | let shell = delegate.prefs.shell 66 | executableURL = URL(fileURLWithPath: shell.path) 67 | // When executing in a shell, we need to properly escape arguments to handle special characters 68 | let escapedArgs = args.map { $0.quoteIfNeeded() } 69 | arguments = ["-c", "\(script.escaped()) \(escapedArgs.joined(separator: " "))"] 70 | if shell.path.hasSuffix("bash") || shell.path.hasSuffix("zsh") { 71 | arguments?.insert("-l", at: 1) 72 | } 73 | } 74 | 75 | guard let executableURL, FileManager.default.fileExists(atPath: executableURL.path) else { 76 | return (out: "", err: nil) 77 | } 78 | 79 | var outputData = Data() 80 | var errorData = Data() 81 | 82 | let outputPipe = Pipe() 83 | standardOutput = outputPipe 84 | 85 | let errorPipe = Pipe() 86 | standardError = errorPipe 87 | 88 | // Set up stdin pipe if provided 89 | if let stdinPipe = stdinPipe { 90 | standardInput = stdinPipe 91 | } 92 | 93 | guard streamOutput else { // horrible hack, code below this guard doesn't work reliably and I can't fugire out why. 94 | do { 95 | try run() 96 | } catch { 97 | os_log("Failed to launch plugin", log: Log.plugin, type: .error) 98 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile() 99 | let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 100 | throw ShellOutError(terminationStatus: terminationStatus, errorData: errorData, outputData: data) 101 | } 102 | 103 | outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() 104 | errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 105 | 106 | waitUntilExit() 107 | 108 | if terminationStatus != 0 { 109 | throw ShellOutError( 110 | terminationStatus: terminationStatus, 111 | errorData: errorData, 112 | outputData: outputData 113 | ) 114 | } 115 | let output = String(data: outputData, encoding: .utf8) ?? "" 116 | let err = String(data: errorData, encoding: .utf8) 117 | return (out: output, err: err) 118 | } 119 | 120 | let outputQueue = DispatchQueue(label: "bash-output-queue") 121 | 122 | outputPipe.fileHandleForReading.readabilityHandler = { handler in 123 | let data = handler.availableData 124 | outputQueue.async { 125 | outputData.append(data) 126 | onOutputUpdate(String(data: data, encoding: .utf8)) 127 | } 128 | } 129 | 130 | errorPipe.fileHandleForReading.readabilityHandler = { handler in 131 | let data = handler.availableData 132 | outputQueue.async { 133 | errorData.append(data) 134 | } 135 | } 136 | 137 | do { 138 | try run() 139 | } catch { 140 | os_log("Failed to launch plugin", log: Log.plugin, type: .error) 141 | throw ShellOutError(terminationStatus: terminationStatus, errorData: errorData, outputData: outputData) 142 | } 143 | 144 | waitUntilExit() 145 | 146 | outputPipe.fileHandleForReading.readabilityHandler = nil 147 | errorPipe.fileHandleForReading.readabilityHandler = nil 148 | 149 | return try outputQueue.sync { 150 | if terminationStatus != 0 { 151 | throw ShellOutError( 152 | terminationStatus: terminationStatus, 153 | errorData: errorData, 154 | outputData: outputData 155 | ) 156 | } 157 | let output = String(data: outputData, encoding: .utf8) ?? "" 158 | let err = String(data: errorData, encoding: .utf8) 159 | return (out: output, err: err) 160 | // return outputData.shellOutput() 161 | } 162 | } 163 | } 164 | 165 | private extension FileHandle { 166 | var isStandard: Bool { 167 | self === FileHandle.standardOutput || 168 | self === FileHandle.standardError || 169 | self === FileHandle.standardInput 170 | } 171 | } 172 | 173 | private extension Data { 174 | func shellOutput() -> String { 175 | guard let output = String(data: self, encoding: .utf8) else { 176 | return "" 177 | } 178 | 179 | guard !output.hasSuffix("\n") else { 180 | let endIndex = output.index(before: output.endIndex) 181 | return String(output[..() 31 | var operation: RunPluginOperation? 32 | 33 | var content: String? = "..." { 34 | didSet { 35 | // force update menu if refresh triggered manually, even if the output of the plugin didn't changed 36 | guard content != oldValue || PluginRefreshReason.manualReasons().contains(lastRefreshReason) else { return } 37 | contentUpdatePublisher.send(content) 38 | } 39 | } 40 | 41 | var error: Error? 42 | var debugInfo = PluginDebugInfo() 43 | 44 | lazy var invokeQueue: OperationQueue = delegate.pluginManager.pluginInvokeQueue 45 | 46 | var updateTimerPublisher: Timer.TimerPublisher { 47 | Timer.TimerPublisher(interval: updateInterval, runLoop: .main, mode: .default) 48 | } 49 | 50 | var cronTimer: Timer? 51 | 52 | var cancellable: Set = [] 53 | 54 | let prefs = PreferencesStore.shared 55 | 56 | init(fileURL: URL) { 57 | let nameComponents = fileURL.lastPathComponent.components(separatedBy: ".") 58 | // Use resolved path as ID to ensure uniqueness even with symlinks 59 | id = fileURL.resolvingSymlinksInPath().path 60 | name = nameComponents.first ?? "" 61 | file = fileURL.path 62 | 63 | lastState = .Loading 64 | makeScriptExecutable(file: file) 65 | refreshPluginMetadata() 66 | 67 | if metadata?.nextDate == nil, nameComponents.count > 2 { 68 | updateInterval = nameComponents.dropFirst().compactMap { parseRefreshInterval(intervalStr: $0, baseUpdateinterval: updateInterval) }.reduce(updateInterval, min) 69 | } 70 | createSupportDirs() 71 | os_log("Initialized executable plugin\n%{public}@", log: Log.plugin, description) 72 | refresh(reason: .FirstLaunch) 73 | } 74 | 75 | // this function called each time plugin updated(manual or scheduled) 76 | func enableTimer() { 77 | // handle cron scheduled plugins 78 | if let nextDate = metadata?.nextDate { 79 | cronTimer?.invalidate() 80 | cronTimer = Timer(fireAt: nextDate, interval: 0, target: self, selector: #selector(scheduledContentUpdate), userInfo: nil, repeats: false) 81 | if let cronTimer { 82 | RunLoop.main.add(cronTimer, forMode: .common) 83 | } 84 | return 85 | } 86 | guard cancellable.isEmpty else { return } 87 | updateTimerPublisher 88 | .autoconnect() 89 | .receive(on: invokeQueue) 90 | .sink(receiveValue: { [weak self] _ in 91 | self?.lastRefreshReason = .Schedule 92 | self?.invokeQueue.addOperation(RunPluginOperation(plugin: self!)) 93 | }).store(in: &cancellable) 94 | } 95 | 96 | func disableTimer() { 97 | cancellable.forEach { $0.cancel() } 98 | cancellable.removeAll() 99 | } 100 | 101 | func disable() { 102 | lastState = .Disabled 103 | disableTimer() 104 | prefs.disabledPlugins.append(id) 105 | } 106 | 107 | func terminate() { 108 | disableTimer() 109 | } 110 | 111 | func enable() { 112 | prefs.disabledPlugins.removeAll(where: { $0 == id }) 113 | refresh(reason: .FirstLaunch) 114 | } 115 | 116 | func start() { 117 | // Check if this is a wake from sleep event by checking if lastUpdated exists 118 | if lastUpdated != nil { 119 | // Handle wake from sleep differently - check if it's time to update based on schedule 120 | if let metadata, metadata.nextDate != nil { 121 | // For cron-scheduled plugins, calculate next date and set timer 122 | refreshPluginMetadata() 123 | enableTimer() 124 | } else if updateInterval > 0, updateInterval < 60 * 60 * 24 * 100 { 125 | // For interval-based plugins (excluding "never" plugins), check if the scheduled time has passed 126 | if let lastUpdated { 127 | let nextUpdateTime = lastUpdated.addingTimeInterval(updateInterval) 128 | if Date() > nextUpdateTime { 129 | // It's time to update 130 | refresh(reason: .WakeFromSleep) 131 | } else { 132 | // Not yet time to update, just re-enable the timer 133 | enableTimer() 134 | } 135 | } 136 | } else { 137 | // For plugins without a specific interval ("never" plugins), always refresh on wake 138 | refresh(reason: .WakeFromSleep) 139 | } 140 | } else { 141 | // First start of the plugin 142 | refresh(reason: .FirstLaunch) 143 | } 144 | } 145 | 146 | func refresh(reason: PluginRefreshReason) { 147 | guard enabled else { 148 | os_log("Skipping refresh for disabled plugin\n%{public}@", log: Log.plugin, description) 149 | return 150 | } 151 | os_log("Requesting manual refresh for plugin\n%{public}@", log: Log.plugin, description) 152 | debugInfo.addEvent(type: .PluginRefresh, value: "Requesting manual refresh") 153 | disableTimer() 154 | operation?.cancel() 155 | 156 | refreshPluginMetadata() 157 | lastRefreshReason = reason 158 | operation = RunPluginOperation(plugin: self) 159 | invokeQueue.addOperation(operation!) 160 | } 161 | 162 | func invoke() -> String? { 163 | lastUpdated = Date() 164 | do { 165 | let out = try runScript(to: file, env: env, 166 | runInBash: metadata?.shouldRunInBash ?? true) 167 | error = nil 168 | lastState = .Success 169 | os_log("Successfully executed script \n%{public}@", log: Log.plugin, file) 170 | debugInfo.addEvent(type: .ContentUpdate, value: out.out) 171 | if let err = out.err, err != "" { 172 | debugInfo.addEvent(type: .ContentUpdateError, value: err) 173 | os_log("Error output from the script: \n%{public}@:", log: Log.plugin, err) 174 | } 175 | return out.out 176 | } catch { 177 | guard let error = error as? ShellOutError else { return nil } 178 | os_log("Failed to execute script\n%{public}@\n%{public}@", log: Log.plugin, type: .error, file, error.message) 179 | os_log("Error output from the script: \n%{public}@", log: Log.plugin, error.message) 180 | self.error = error 181 | debugInfo.addEvent(type: .ContentUpdateError, value: error.message) 182 | lastState = .Failed 183 | } 184 | return nil 185 | } 186 | 187 | @objc func scheduledContentUpdate() { 188 | refresh(reason: .Schedule) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /SwiftBar/PreferencesStore.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Combine 3 | 4 | enum TerminalOptions: String, CaseIterable { 5 | case Terminal 6 | case iTerm 7 | case Ghostty 8 | } 9 | 10 | enum ShellOptions: String, CaseIterable { 11 | case Bash = "bash" 12 | case Zsh = "zsh" 13 | case Default = "default" 14 | 15 | var path: String { 16 | switch self { 17 | case .Bash: 18 | "/bin/bash" 19 | case .Zsh: 20 | "/bin/zsh" 21 | case .Default: 22 | "/bin/zsh" 23 | } 24 | } 25 | } 26 | 27 | class PreferencesStore: ObservableObject { 28 | static let shared = PreferencesStore() 29 | enum PreferencesKeys: String { 30 | case PluginDirectory 31 | case ShortcutsFolder 32 | case DisabledPlugins 33 | case Terminal 34 | case Shell 35 | case HideSwiftBarIcon 36 | case MakePluginExecutable 37 | case PluginDeveloperMode 38 | case DisableBashWrapper 39 | case StreamablePluginDebugOutput 40 | case PluginDebugMode 41 | case StealthMode 42 | case IncludeBetaUpdates 43 | case DimOnManualRefresh 44 | case CollectCrashReports 45 | case DebugLoggingEnabled 46 | case ShortcutPlugins 47 | case PluginRepositoryURL 48 | case PluginSourceCodeURL 49 | } 50 | 51 | let disabledPluginsPublisher = PassthroughSubject() 52 | 53 | @Published var pluginDirectoryPath: String? { 54 | didSet { 55 | PreferencesStore.setValue(value: pluginDirectoryPath, key: .PluginDirectory) 56 | } 57 | } 58 | 59 | @Published var shortcutsFolder: String { 60 | didSet { 61 | PreferencesStore.setValue(value: shortcutsFolder, key: .ShortcutsFolder) 62 | } 63 | } 64 | 65 | var pluginDirectoryResolvedURL: URL? { 66 | guard let path = pluginDirectoryPath as NSString? else { return nil } 67 | return URL(fileURLWithPath: path.expandingTildeInPath).resolvingSymlinksInPath() 68 | } 69 | 70 | var pluginDirectoryResolvedPath: String? { 71 | pluginDirectoryResolvedURL?.path 72 | } 73 | 74 | @Published var disabledPlugins: [PluginID] { 75 | didSet { 76 | let unique = Array(Set(disabledPlugins)) 77 | PreferencesStore.setValue(value: unique, key: .DisabledPlugins) 78 | disabledPluginsPublisher.send("") 79 | } 80 | } 81 | 82 | @Published var terminal: TerminalOptions { 83 | didSet { 84 | PreferencesStore.setValue(value: terminal.rawValue, key: .Terminal) 85 | } 86 | } 87 | 88 | @Published var shell: ShellOptions { 89 | didSet { 90 | PreferencesStore.setValue(value: shell.rawValue, key: .Shell) 91 | } 92 | } 93 | 94 | @Published var swiftBarIconIsHidden: Bool { 95 | didSet { 96 | PreferencesStore.setValue(value: swiftBarIconIsHidden, key: .HideSwiftBarIcon) 97 | delegate.pluginManager.rebuildAllMenus() 98 | } 99 | } 100 | 101 | @Published var includeBetaUpdates: Bool { 102 | didSet { 103 | PreferencesStore.setValue(value: includeBetaUpdates, key: .IncludeBetaUpdates) 104 | } 105 | } 106 | 107 | @Published var collectCrashReports: Bool { 108 | didSet { 109 | PreferencesStore.setValue(value: collectCrashReports, key: .CollectCrashReports) 110 | } 111 | } 112 | 113 | @Published var dimOnManualRefresh: Bool { 114 | didSet { 115 | PreferencesStore.setValue(value: dimOnManualRefresh, key: .DimOnManualRefresh) 116 | } 117 | } 118 | 119 | @Published var shortcutsPlugins: [PersistentShortcutPlugin] { 120 | didSet { 121 | PreferencesStore.setValue(value: try? PropertyListEncoder().encode(shortcutsPlugins), key: .ShortcutPlugins) 122 | } 123 | } 124 | 125 | var makePluginExecutable: Bool { 126 | guard let out = PreferencesStore.getValue(key: .MakePluginExecutable) as? Bool else { 127 | PreferencesStore.setValue(value: true, key: .MakePluginExecutable) 128 | return true 129 | } 130 | return out 131 | } 132 | 133 | var pluginDeveloperMode: Bool { 134 | PreferencesStore.getValue(key: .PluginDeveloperMode) as? Bool ?? false 135 | } 136 | 137 | var pluginDebugMode: Bool { 138 | PreferencesStore.getValue(key: .PluginDebugMode) as? Bool ?? false 139 | } 140 | 141 | var disableBashWrapper: Bool { 142 | PreferencesStore.getValue(key: .DisableBashWrapper) as? Bool ?? false 143 | } 144 | 145 | var streamablePluginDebugOutput: Bool { 146 | PreferencesStore.getValue(key: .StreamablePluginDebugOutput) as? Bool ?? false 147 | } 148 | 149 | @Published var stealthMode: Bool { 150 | didSet { 151 | PreferencesStore.setValue(value: stealthMode, key: .StealthMode) 152 | } 153 | } 154 | 155 | var debugLoggingEnabled: Bool { 156 | PreferencesStore.getValue(key: .DebugLoggingEnabled) as? Bool ?? false 157 | } 158 | 159 | var pluginRepositoryURL: URL { 160 | guard let str = PreferencesStore.getValue(key: .PluginRepositoryURL) as? String, 161 | let url = URL(string: str) 162 | else { 163 | return URL(string: "https://xbarapp.com/docs/plugins/")! 164 | } 165 | return url 166 | } 167 | 168 | var pluginSourceCodeURL: URL { 169 | guard let str = PreferencesStore.getValue(key: .PluginSourceCodeURL) as? String, 170 | let url = URL(string: str) 171 | else { 172 | return URL(string: "https://github.com/matryer/xbar-plugins/blob/master/")! 173 | } 174 | return url 175 | } 176 | 177 | init() { 178 | pluginDirectoryPath = PreferencesStore.getValue(key: .PluginDirectory) as? String 179 | shortcutsFolder = PreferencesStore.getValue(key: .ShortcutsFolder) as? String ?? "" 180 | disabledPlugins = PreferencesStore.getValue(key: .DisabledPlugins) as? [PluginID] ?? [] 181 | terminal = .Terminal 182 | shell = .Bash 183 | swiftBarIconIsHidden = PreferencesStore.getValue(key: .HideSwiftBarIcon) as? Bool ?? false 184 | includeBetaUpdates = PreferencesStore.getValue(key: .IncludeBetaUpdates) as? Bool ?? false 185 | collectCrashReports = PreferencesStore.getValue(key: .CollectCrashReports) as? Bool ?? true 186 | dimOnManualRefresh = PreferencesStore.getValue(key: .DimOnManualRefresh) as? Bool ?? true 187 | stealthMode = PreferencesStore.getValue(key: .StealthMode) as? Bool ?? false 188 | shortcutsPlugins = { 189 | guard let data = PreferencesStore.getValue(key: .ShortcutPlugins) as? Data, 190 | let plugins = try? PropertyListDecoder().decode([PersistentShortcutPlugin].self, from: data) else { return [] } 191 | return plugins 192 | }() 193 | if let savedTerminal = PreferencesStore.getValue(key: .Terminal) as? String, 194 | let value = TerminalOptions(rawValue: savedTerminal) 195 | { 196 | terminal = value 197 | } 198 | if let savedShell = PreferencesStore.getValue(key: .Shell) as? String, 199 | let value = ShellOptions(rawValue: savedShell) 200 | { 201 | shell = value 202 | } 203 | } 204 | 205 | static func removeAll() { 206 | let domain = Bundle.main.bundleIdentifier! 207 | UserDefaults.standard.removePersistentDomain(forName: domain) 208 | UserDefaults.standard.synchronize() 209 | } 210 | 211 | private static func setValue(value: Any?, key: PreferencesKeys) { 212 | UserDefaults.standard.setValue(value, forKey: key.rawValue) 213 | UserDefaults.standard.synchronize() 214 | } 215 | 216 | private static func getValue(key: PreferencesKeys) -> Any? { 217 | UserDefaults.standard.value(forKey: key.rawValue) 218 | } 219 | } 220 | --------------------------------------------------------------------------------