- Add AppDiscovery provider for running app enumeration - Implement AppDropdownView with auto-launch functionality - Create SignalAction models for 40+ yabai commands - Build ActionBuilderView with nested parameter controls - Add LiveShellPreview for real-time shell command generation - Implement ActionValidator for conflict detection - Add migration parser for existing raw action strings - Include feature flag for safe rollout - Maintain full backward compatibility
162 lines
5.6 KiB
Swift
162 lines
5.6 KiB
Swift
//
|
|
// LiveShellPreview.swift
|
|
// YabaiPro
|
|
//
|
|
// Created by Jake Shore
|
|
// Copyright © 2024 Jake Shore. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct LiveShellPreview: View {
|
|
@Binding var structuredActions: [SignalAction]
|
|
@Binding var rawAction: String
|
|
let title: String
|
|
|
|
@State private var manualOverride = false
|
|
@State private var editedCommand = ""
|
|
|
|
private var computedCommand: String {
|
|
if structuredActions.isEmpty {
|
|
return ""
|
|
}
|
|
return structuredActions.map { $0.toShellCommand() }.joined(separator: " && ")
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "eye")
|
|
.foregroundColor(.green)
|
|
Text(title)
|
|
.font(.headline)
|
|
.foregroundColor(.primary)
|
|
|
|
Spacer()
|
|
|
|
if manualOverride {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundColor(.orange)
|
|
.font(.caption)
|
|
|
|
Text("Manual Edit")
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
}
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Shell Command:")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
ZStack(alignment: .topLeading) {
|
|
if manualOverride {
|
|
TextEditor(text: $editedCommand)
|
|
.font(.system(.body, design: .monospaced))
|
|
.frame(minHeight: 80)
|
|
.padding(8)
|
|
.background(Color(.textBackgroundColor))
|
|
.cornerRadius(6)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color.orange.opacity(0.3), lineWidth: manualOverride ? 2 : 0)
|
|
)
|
|
.onChange(of: editedCommand) { newValue in
|
|
rawAction = newValue
|
|
}
|
|
} else {
|
|
ScrollView {
|
|
Text(computedCommand.isEmpty ? "No actions configured" : computedCommand)
|
|
.font(.system(.body, design: .monospaced))
|
|
.foregroundColor(computedCommand.isEmpty ? .secondary : .primary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(12)
|
|
.textSelection(.enabled)
|
|
}
|
|
.frame(minHeight: 60)
|
|
.background(Color(.textBackgroundColor).opacity(0.5))
|
|
.cornerRadius(6)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.stroke(Color.gray.opacity(0.2), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
if manualOverride {
|
|
Button(action: revertToStructured) {
|
|
Text("Revert to Structured")
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
|
|
Text("Manual edits won't round-trip to structured actions")
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
} else {
|
|
Button(action: enableManualEdit) {
|
|
Text("Edit Manually")
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.bordered)
|
|
|
|
Text("Click to edit the command directly")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
.onChange(of: structuredActions) { _ in
|
|
if !manualOverride {
|
|
rawAction = computedCommand
|
|
}
|
|
}
|
|
.onChange(of: rawAction) { newValue in
|
|
if !manualOverride && newValue != computedCommand {
|
|
// Raw action was modified externally, enable manual override
|
|
manualOverride = true
|
|
editedCommand = newValue
|
|
}
|
|
}
|
|
.onAppear {
|
|
// Initialize
|
|
if !manualOverride {
|
|
rawAction = computedCommand
|
|
}
|
|
}
|
|
}
|
|
|
|
private func enableManualEdit() {
|
|
manualOverride = true
|
|
editedCommand = computedCommand
|
|
}
|
|
|
|
private func revertToStructured() {
|
|
manualOverride = false
|
|
editedCommand = ""
|
|
rawAction = computedCommand
|
|
}
|
|
}
|
|
|
|
struct LiveShellPreview_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
LiveShellPreview(
|
|
structuredActions: .constant([
|
|
SignalAction(command: .windowFocus, parameters: ["direction": "next"]),
|
|
SignalAction(command: .configWindowOpacityActive, parameters: ["opacity": "0.8"])
|
|
]),
|
|
rawAction: .constant("yabai -m window --focus next && yabai -m config active_window_opacity 0.8"),
|
|
title: "Live Preview"
|
|
)
|
|
.padding()
|
|
.frame(width: 500, height: 200)
|
|
}
|
|
}
|