├── 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 |
--------------------------------------------------------------------------------