StreamDeck

Swift Documentation badge

一个用于在 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
]

动作被作为类型提供,因为插件将为每个可见的按键初始化一个新的实例。

环境和全局设置

有两种方式可以在动作之间共享全局状态

  1. 环境
  2. 全局设置

在使用中,它们非常相似,仅在几个协议上有所不同。重要的区别是环境变量不会被持久化,而全局设置会被存储,并且在 Stream Deck 应用程序的多次启动中保持一致。

有两种方法可以声明环境变量和全局设置。

首先创建一个遵循 EnvironmentKeyGlobalSettingKey 协议的结构体。这定义了默认值以及如何访问该值。

struct Count: EnvironmentKey {
    static let defaultValue: Int = 0
}

接下来,向 EnvironmentValuesGlobalSettings 添加扩展。

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 布局

为 Stream Deck Plus 设计 自定义布局 是使用 result builder 完成的。每个 Layout 都是由组件构建的,例如 TextImage 等。布局在插件 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

添加 StreamDeck 作为依赖

要在 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
    ]
)