feat(xbar/brew-services): add support for SwiftBar in addition to Xbar

This commit is contained in:
2025-08-24 17:01:39 +01:00
parent eb7f92740c
commit e63ea227f9

View File

@@ -2,7 +2,7 @@
# frozen_string_literal: true # frozen_string_literal: true
# <xbar.title>Brew Services</xbar.title> # <xbar.title>Brew Services</xbar.title>
# <xbar.version>v3.1.2</xbar.version> # <xbar.version>v3.2.0</xbar.version>
# <xbar.author>Jim Myhrberg</xbar.author> # <xbar.author>Jim Myhrberg</xbar.author>
# <xbar.author.github>jimeh</xbar.author.github> # <xbar.author.github>jimeh</xbar.author.github>
# <xbar.desc>List and manage Homebrew Services</xbar.desc> # <xbar.desc>List and manage Homebrew Services</xbar.desc>
@@ -23,15 +23,60 @@
# rubocop:disable Metrics/PerceivedComplexity # rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Style/IfUnlessModifier # rubocop:disable Style/IfUnlessModifier
require 'fileutils'
require 'open3' require 'open3'
require 'json' require 'json'
require 'set' require 'set'
# Xbar is a tiny helper library for creating Xbar and SwiftBar plugins in Ruby.
module Xbar module Xbar
class CommandError < StandardError; end class CommandError < StandardError; end
class RPCError < StandardError; end class RPCError < StandardError; end
module Helpers
def plugin_data_path
@plugin_data_path ||= (swiftbar_data_path || File.dirname(__FILE__))
end
def plugin_file_path
@plugin_file_path ||= (swiftbar_plugin_path || __FILE__)
end
def plugin_filename
@plugin_filename ||= File.basename(plugin_file_path)
end
def plugin_name
@plugin_name ||= begin
parts = plugin_filename.split('.')
if parts.size < 3
raise "Invalid plugin name: #{plugin_filename}"
end
parts[0..-3].join('.')
end
end
def swiftbar?
ENV['SWIFTBAR'] == '1'
end
def swiftbar_cache_path
@swiftbar_cache_path ||= ENV['SWIFTBAR_PLUGIN_CACHE_PATH'] if swiftbar?
end
def swiftbar_data_path
@swiftbar_data_path ||= ENV['SWIFTBAR_PLUGIN_DATA_PATH'] if swiftbar?
end
def swiftbar_plugin_path
@swiftbar_plugin_path ||= ENV['SWIFTBAR_PLUGIN_PATH'] if swiftbar?
end
end
module Service module Service
include Helpers
private private
def config def config
@@ -42,8 +87,11 @@ module Xbar
@printer ||= ::Xbar::Printer.new @printer ||= ::Xbar::Printer.new
end end
def cmd(*args) def cmd(*args, dir: nil)
out, err, s = Open3.capture3(*args) opts = {}
opts[:chdir] = File.expand_path(dir) if dir
out, err, s = Open3.capture3(*args, opts)
if s.exitstatus != 0 if s.exitstatus != 0
msg = "Command failed: #{args.join(' ')}" msg = "Command failed: #{args.join(' ')}"
msg += ": #{err}" unless err.empty? msg += ": #{err}" unless err.empty?
@@ -72,7 +120,16 @@ module Xbar
end end
end end
# Config is a simple wrapper around a JSON file that contains the plugin's
# configuration. It is compatible with Xbar's behavior of loading environment
# variables from it. We don't rely on that however and directly read the file
# ourselves.
#
# In SwiftBar, we use `SWIFTBAR_PLUGIN_DATA_PATH` to determine where to store
# the configuration in a `config.json` file.
class Config < Hash class Config < Hash
include Helpers
def initialize def initialize
super super
@@ -88,15 +145,25 @@ module Xbar
end end
def filename def filename
@filename ||= "#{__FILE__}.vars.json" @filename ||= File.join(
plugin_data_path,
swiftbar? ? 'config.json' : "#{plugin_filename}.vars.json"
)
end
def dirname
@dirname ||= File.dirname(filename)
end end
def save def save
FileUtils.mkdir_p(dirname)
File.write(filename, JSON.pretty_generate(self)) File.write(filename, JSON.pretty_generate(self))
end end
end end
class Printer class Printer
include Helpers
attr_reader :nested_level attr_reader :nested_level
SUB_STR = '--' SUB_STR = '--'
@@ -140,13 +207,25 @@ module Xbar
end end
def plugin_refresh_uri def plugin_refresh_uri
@plugin_refresh_uri ||= 'xbar://app.xbarapp.com/refreshPlugin' \ return @plugin_refresh_uri if @plugin_refresh_uri
"?path=#{File.basename(__FILE__)}"
if swiftbar?
@plugin_refresh_uri = "swiftbar://refreshplugin?name=#{plugin_name}"
else
@plugin_refresh_uri = 'xbar://app.xbarapp.com/refreshPlugin' \
"?path=#{plugin_filename}"
end
end end
def normalize_props(props = {}) def normalize_props(props = {})
props = props.dup props = props.dup
# Explicitly set terminal to false for SwiftBar, as it seems to default
# to true when not specified. At least with SwiftBar 2.0.1.
if swiftbar? && !props.key?(:terminal)
props[:terminal] = false
end
if props[:rpc] && props[:shell].nil? if props[:rpc] && props[:shell].nil?
props[:shell] = [__FILE__] + props[:rpc] props[:shell] = [__FILE__] + props[:rpc]
props.delete(:rpc) props.delete(:rpc)
@@ -160,8 +239,12 @@ module Xbar
end end
end end
# Refresh Xbar after shell command has run in terminal # Always refresh SwiftBar via refresh plugin URI so as to temporarily
if props[:terminal] && props[:refresh] && props[:shell] # disable the menu bar icon while refresh is running.
#
# For Xbar this does not work when terminal is false, so we only trigger
# refresh via the plugin refresh URI when terminal is true.
if props[:refresh] && props[:shell] && (swiftbar? || props[:terminal])
props[:refresh] = false props[:refresh] = false
i = 1 i = 1
i += 1 while props.key?("param#{i}".to_sym) i += 1 while props.key?("param#{i}".to_sym)