From 935c662e1d6df1fb3e5007e3f368f5b722b6851d Mon Sep 17 00:00:00 2001 From: BusyBee3333 Date: Wed, 31 Dec 2025 02:07:37 -0500 Subject: [PATCH] feat(signals): add ruleAdd action and UI wiring for no-code rule creation --- .../YabaiPro/Sources/ActionBuilderView.swift | 3 ++ .../YabaiPro/Sources/SignalActionModels.swift | 47 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/Desktop/YabaiPro/Sources/ActionBuilderView.swift b/Desktop/YabaiPro/Sources/ActionBuilderView.swift index 11c69c9..9642907 100644 --- a/Desktop/YabaiPro/Sources/ActionBuilderView.swift +++ b/Desktop/YabaiPro/Sources/ActionBuilderView.swift @@ -361,6 +361,7 @@ struct AddActionSheet: View { case .windowMoveToSpace: return "square.grid.2x2" case .windowMoveToDisplay: return "display" case .configWindowOpacity, .configWindowOpacityActive, .configWindowOpacityNormal: return "circle.opacity" + case .ruleAdd: return "list.bullet.rectangle" case .shellCommand: return "terminal" default: return "gear" } @@ -374,6 +375,7 @@ struct AddActionSheet: View { case .windowToggleFullscreen: return .green case .windowMoveAbsolute, .windowMoveRelative, .windowResizeAbsolute, .windowResizeRelative: return .orange case .configWindowOpacity, .configWindowOpacityActive, .configWindowOpacityNormal: return .pink + case .ruleAdd: return .teal case .shellCommand: return .gray default: return .secondary } @@ -434,6 +436,7 @@ extension SignalAction.YabaiCommand { case .configWindowShadow: return "Set Window Shadow" case .configMouseFollowsFocus: return "Set Mouse Follows Focus" case .configFocusFollowsMouse: return "Set Focus Follows Mouse" + case .ruleAdd: return "Add Yabai Rule" case .shellCommand: return "Run Shell Command" } } diff --git a/Desktop/YabaiPro/Sources/SignalActionModels.swift b/Desktop/YabaiPro/Sources/SignalActionModels.swift index 4439a10..34c8d40 100644 --- a/Desktop/YabaiPro/Sources/SignalActionModels.swift +++ b/Desktop/YabaiPro/Sources/SignalActionModels.swift @@ -75,6 +75,8 @@ struct SignalAction: Codable, Identifiable, Equatable { // Custom shell command case shellCommand = "shell_command" + // Rule creation (add a yabai rule) + case ruleAdd = "rule_add" } var displayName: String { @@ -130,6 +132,7 @@ struct SignalAction: Codable, Identifiable, Equatable { case .configMouseFollowsFocus: return "Set Mouse Follows Focus" case .configFocusFollowsMouse: return "Set Focus Follows Mouse" case .shellCommand: return "Run Shell Command" + case .ruleAdd: return "Add Yabai Rule" } } @@ -310,6 +313,36 @@ struct SignalAction: Codable, Identifiable, Equatable { case .shellCommand: return parameters["command"] ?? "" + case .ruleAdd: + // Build a yabai rule add command from provided parameters + var parts: [String] = ["yabai", "-m", "rule", "--add"] + + func addArg(_ key: String, _ fmt: ((String) -> String)? = nil) { + if let val = parameters[key], !val.isEmpty { + let v = fmt?(val) ?? val + parts.append("\(key)=\(v)") + } + } + + // app/title/role/subrole - wrap in quotes if they contain spaces + addArg("app", { v in "\"\(v.replacingOccurrences(of: "\"", with: "\\\""))\"" }) + addArg("title", { v in "\"\(v.replacingOccurrences(of: "\"", with: "\\\""))\"" }) + addArg("role", nil) + addArg("subrole", nil) + + // Boolean flags: manage, sticky, mouse_follows_focus -> on/off + addArg("manage", nil) + addArg("sticky", nil) + addArg("mouse_follows_focus", nil) + + // Opacity / sub-layer / label + if let opacity = parameters["opacity"], !opacity.isEmpty { + parts.append("opacity=\(opacity)") + } + addArg("sub-layer", nil) + addArg("label", { v in "\"\(v.replacingOccurrences(of: "\"", with: "\\\""))\"" }) + + return parts.joined(separator: " ") } return "# Invalid command configuration" @@ -443,6 +476,20 @@ struct CommandSchema { ParameterSchema(key: "opacity", label: "Opacity", type: .slider(min: 0.0, max: 1.0, step: 0.1), required: true, defaultValue: "1.0") ]) + case .ruleAdd: + return CommandSchema(command: command, parameters: [ + ParameterSchema(key: "app", label: "Application (bundle/display name)", type: .text, required: false, defaultValue: ""), + ParameterSchema(key: "title", label: "Window Title (optional)", type: .text, required: false, defaultValue: ""), + ParameterSchema(key: "role", label: "Role", type: .picker, required: false, defaultValue: "", options: ["AXWindow","AXDialog","AXSheet"]), + ParameterSchema(key: "subrole", label: "Subrole", type: .picker, required: false, defaultValue: "", options: ["AXStandardWindow","AXDialog","AXSystemDialog"]), + ParameterSchema(key: "manage", label: "Manage Window", type: .picker, required: false, defaultValue: "on", options: ["on","off"]), + ParameterSchema(key: "sticky", label: "Sticky", type: .picker, required: false, defaultValue: "off", options: ["on","off"]), + ParameterSchema(key: "mouse_follows_focus", label: "Mouse Follows Focus", type: .picker, required: false, defaultValue: "off", options: ["on","off"]), + ParameterSchema(key: "opacity", label: "Opacity (0.0-1.0)", type: .slider(min: 0.0, max: 1.0, step: 0.1), required: false, defaultValue: "1.0"), + ParameterSchema(key: "sub-layer", label: "Sub Layer", type: .picker, required: false, defaultValue: "normal", options: ["below","normal","above"]), + ParameterSchema(key: "label", label: "Label (optional)", type: .text, required: false, defaultValue: "") + ]) + case .shellCommand: return CommandSchema(command: command, parameters: [ ParameterSchema(key: "command", label: "Shell Command", type: .text, required: true, defaultValue: "")