一个用于在 Swift 中创建 Stream Deck 插件的库。
你的插件应该遵循 Plugin
协议,它负责将事件路由到你的动作,并允许你与 Stream Deck 应用程序交互。
@main
class CounterPlugin: Plugin {
static var name: String = "Counter"
static var description: String = "Count things. On your Stream Deck!"
static var author: String = "Emory Dunn"
static var icon: String = "Icons/pluginIcon"
static var version: String = "0.4"
@ActionBuilder
static var actions: [any Action.Type] {
IncrementAction.self
DecrementAction.self
RotaryAction.self
}
static var layouts: [Layout] {
Layout(id: "counter") {
// The title of the layout
Text(title: "Current Count")
.textAlignment(.center)
.frame(width: 180, height: 24)
.position(x: (200 - 180) / 2, y: 10)
// A large counter label
Text(key: "count-text", value: "0")
.textAlignment(.center)
.font(size: 16, weight: 600)
.frame(width: 180, height: 24)
.position(x: (200 - 180) / 2, y: 30)
// A bar that shows the current count
Bar(key: "count-bar", value: 0, range: -50..<50)
.frame(width: 180, height: 20)
.position(x: (200 - 180) / 2, y: 60)
.barBackground(.black)
.barStyle(.doubleTrapezoid)
.barBorder("#943E93")
}
}
required init() { }
}
通过使用 @main
属性,你的插件将被自动初始化。
一个插件既定义了与 Stream Deck 交互的代码,也定义了插件的 manifest 文件。在声明你的插件时,需要定义一些静态属性来告诉 Stream Deck 应用程序关于你的插件是什么以及它可以做什么。并非所有属性都是必需的,例如,你的插件不需要添加自定义类别。可选属性具有默认重载,以帮助减少样板代码。
许多属性会显示给用户,例如名称和描述。其他属性由 Stream Deck 应用程序在内部使用。最重要的属性是 actions
,你可以在这里定义插件提供的操作。
// Define actions with a builder
@ActionBuilder
static var actions: [any Action.Type] {
IncrementAction.self
DecrementAction.self
RotaryAction.self
}
// Or define actions in an array
static var actions: [any Action.Type] = [
IncrementAction.self,
DecrementAction.self
]
动作被作为类型提供,因为插件将为每个可见的按键初始化一个新的实例。
有两种方式可以在动作之间共享全局状态
在使用中,它们非常相似,仅在几个协议上有所不同。重要的区别是环境变量不会被持久化,而全局设置会被存储,并且在 Stream Deck 应用程序的多次启动中保持一致。
有两种方法可以声明环境变量和全局设置。
首先创建一个遵循 EnvironmentKey
或 GlobalSettingKey
协议的结构体。这定义了默认值以及如何访问该值。
struct Count: EnvironmentKey {
static let defaultValue: Int = 0
}
接下来,向 EnvironmentValues
或 GlobalSettings
添加扩展。
extension EnvironmentValues {
var count: Int {
get { self[Count.self] }
set { self[Count.self] = newValue }
}
}
要在你的操作中使用该值,请使用相应的属性包装器。
@Environment(\.count) var count // For an environment key
@GlobalSetting(\.count) var count // For a global settings key
该值可以在动作回调中读取和更新。
此外,除了手动声明键之外,你还可以使用 @Entry
宏。该宏处理为键路径生成结构体和变量。默认情况下,名称是基于属性名称自动生成的,但可以提供自定义键。
extension EnvironmentValues {
@Entry var count = 42
}
extension GlobalSettings {
@Entry("CountDracula") var theCount = 42
}
你插件中的每个动作都被定义为一个单独的、遵循 Action
协议的结构体。对于特定类型的动作,有几个辅助协议可用。
协议 | 描述 |
---|---|
KeyAction |
一个具有多个状态的按键动作 |
StatelessKeyAction |
一个具有单个状态的按键动作 |
EncoderAction |
Stream Deck + 上的旋转编码器动作 |
使用上述协议之一只是在 Action
之上提供默认值,你可以根据需要提供自己的值。例如,KeyAction
默认将 controllers
属性设置为 [.keypad]
,而 EncoderAction
将其设置为 [.encoder]
。要创建一个同时提供按键和编码器动作的动作,无论你使用哪个便利协议,都将 controllers
设置为 [.keypad, .encoder]
。
对于所有动作,都需要定义几个常见的静态属性。
struct IncrementAction: KeyAction {
typealias Settings = NoSettings
static var name: String = "Increment"
static var uuid: String = "counter.increment"
static var icon: String = "Icons/actionIcon"
static var states: [PluginActionState]? = [
PluginActionState(image: "Icons/actionDefaultImage", titleAlignment: .middle)
]
var context: String
var coordinates: StreamDeck.Coordinates?
@GlobalSetting(\.count) var count
required init(context: String, coordinates: StreamDeck.Coordinates?) {
self.context = context
self.coordinates = coordinates
}
}
如果你的动作使用 属性检查器 进行配置,你可以使用一个 Codable
结构体作为 Settings
。当前的设置将在事件的 payload 中发送。
struct ChooseAction: KeyAction {
enum Settings: String, Codable {
case optionOne
case optionTwo
case optionThree
}
}
你的动作可以 接收 来自应用程序的事件,也可以 发送 事件到应用程序。大多数事件将来自用户交互、按键和拨盘旋转,以及来自系统事件,例如动作出现在 Stream Deck 上或属性检查器出现。
要接收事件,只需在你的动作中实现相应的方法,例如,要接收按键释放的通知,请使用 keyUp
。如果你的动作向用户显示设置,请使用 willAppear
来更新标题以反映当前值。
struct IncrementAction: KeyAction {
func willAppear(device: String, payload: AppearEvent<NoSettings>) {
setTitle(to: "\(count)", target: nil, state: nil)
}
func didReceiveGlobalSettings() {
log.log("Global settings changed, updating title with \(self.count)")
setTitle(to: "\(count)", target: nil, state: nil)
}
func keyUp(device: String, payload: KeyEvent<Settings>) {
count += 1
}
func longKeyPress(device: String, payload: KeyEvent<NoSettings>) {
count = 0
showOk()
log.log("Resetting count to \(self.count)")
}
}
在上面的示例中,setTitle
是动作可以发送的事件。在这种情况下,它设置动作的标题。它在两个地方被调用:当动作出现时设置初始标题,以及当全局设置更改时,以便它可以保持可见计数器同步。
为 Stream Deck Plus 设计 自定义布局 是使用 result builder 完成的。每个 Layout
都是由组件构建的,例如 Text
、Image
等。布局在插件 manifest 文件中定义。例如,要从示例 counter
插件构建自定义栏布局
extension LayoutName {
static let counter: LayoutName = "counter"
}
Layout(id: .counter) {
// The title of the layout
Text(title: "Current Count")
.textAlignment(.center)
.frame(width: 180, height: 24)
.position(x: (200 - 180) / 2, y: 10)
// A large counter label
Text(key: "countText", value: "0")
.textAlignment(.center)
.font(size: 16, weight: 600)
.frame(width: 180, height: 24)
.position(x: (200 - 180) / 2, y: 30)
// A bar that shows the current count
Bar(key: "countBar", value: 0, range: -50...50)
.frame(width: 180, height: 20)
.position(x: (200 - 180) / 2, y: 60)
.barBackground(.black)
.barStyle(.doubleTrapezoid)
.barBorder("#943E93")
}
struct CounterSettings: LayoutSettings {
var countText: TextLayoutSettings
var countBar: BarLayoutSettings
init(count: Int, bgColor: Color) {
self.countText = TextLayoutSettings(value: count.formatted())
self.countBar = BarLayoutSettings(value: Double(count), bar_fill_c: bgColor)
}
}
布局被保存到与 manifest 文件位于同一目录的 Layouts 文件夹中。为了在旋转动作上使用该布局,将编码器的 layout 属性设置为布局的文件夹和 ID,例如 Layouts/counter.json
。
可以使用符合 LayoutSettings
协议的结构体更新布局,如上面的 CounterSettings
。
let feedback = CounterSettings(count: count, bgColor: .red)
setFeedback(feedback)
任何可编辑的属性都可以通过这种方式更新。请参阅 文档 了解更多详情。
你的插件可执行文件提供了一种自动的方式来以类型安全的方式生成插件的 manifest.json
文件。在你的插件二进制文件上使用提供的 export
命令来导出 manifest 文件并复制二进制文件本身。你仍然需要使用 Elgato 的 DistributionTool
进行最终打包。
OVERVIEW: Conveniently export the plugin.
Automatically generate the manifest and copy the executable to the Plugins folder.
USAGE: plugin-command export <uri> [--output <output>] [--generate-manifest] [--preview-manifest] [--manifest-name <manifest-name>] [--copy-executable] [--executable-name <executable-name>]
ARGUMENTS:
<uri> The URI for your plugin
OPTIONS:
-o, --output <output> The folder in which to create the plugin's directory. (default: ~/Library/Application Support/com.elgato.StreamDeck/Plugins)
--generate-manifest/--preview-manifest
Encode the manifest for the plugin and either save or preview it.
-m, --manifest-name <manifest-name>
The name of the manifest file. (default: manifest.json)
-c, --copy-executable Copy the executable file.
-e, --executable-name <executable-name>
The name of the executable file.
--version Show the version.
-h, --help Show help information.
如果你正在构建一个通用二进制文件,宏似乎存在一个问题,阻止一次编译多个架构。一种解决方法是分别编译每个架构,然后使用 lipo
将它们组合起来。
# Build each architecture separately and then combine
echo "Building ARM binary"
swift build -c release --arch arm64
echo "Building Intel binary"
swift build -c release --arch x86_64
echo "Creating universal binary"
lipo -create \
.build/arm64-apple-macosx/counter-plugin \
.build/x86_64-apple-macosx/counter-plugin \
-output counter-plugin
要在 SwiftPM 项目中使用 StreamDeck
库,请将以下行添加到你的 Package.swift
文件中的依赖项中
.package(url: "https://github.com/emorydunn/StreamDeckPlugin.git", from: "0.5.0"),
最后,包含 "StreamDeck"
作为你的可执行目标的依赖项
let package = Package(
// name, products, etc.
platforms: [.macOS(.v11)],
dependencies: [
.package(url: "https://github.com/emorydunn/StreamDeckPlugin.git", from: "0.5.0"),
// other dependencies
],
targets: [
.executableTarget(name: "<command-line-tool>", dependencies: [
.product(name: "StreamDeck", package: "StreamDeckPlugin")
]),
// other targets
]
)