- 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
166 lines
8.0 KiB
Swift
166 lines
8.0 KiB
Swift
//
|
|
// CommandPaletteView.swift
|
|
// YabaiPro
|
|
//
|
|
// Created by Jake Shore
|
|
// Copyright © 2024 Jake Shore. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct CommandPaletteView: View {
|
|
@State private var searchText = ""
|
|
@State private var selectedCommand: Command?
|
|
|
|
private let commandRunner = YabaiCommandRunner()
|
|
|
|
var commands: [Command] {
|
|
[
|
|
// Window commands
|
|
Command(name: "Focus North", action: { try await commandRunner.focusWindow(direction: .north) }),
|
|
Command(name: "Focus East", action: { try await commandRunner.focusWindow(direction: .east) }),
|
|
Command(name: "Focus South", action: { try await commandRunner.focusWindow(direction: .south) }),
|
|
Command(name: "Focus West", action: { try await commandRunner.focusWindow(direction: .west) }),
|
|
Command(name: "Float Window", action: { try await commandRunner.toggleWindowFloat() }),
|
|
Command(name: "Fullscreen Window", action: { try await commandRunner.toggleWindowFullscreen() }),
|
|
Command(name: "Stack Window", action: { try await commandRunner.stackWindow(direction: .north) }),
|
|
Command(name: "Toggle Window Split", action: { try await commandRunner.toggleWindowSplit() }),
|
|
Command(name: "Zoom Parent", action: { try await commandRunner.toggleWindowZoomParent() }),
|
|
|
|
// Advanced Positioning Commands
|
|
Command(name: "Move to Top-Left", action: { try await commandRunner.moveWindowAbsolute(x: 100, y: 100) }),
|
|
Command(name: "Move to Top-Right", action: { try await commandRunner.moveWindowAbsolute(x: 1200, y: 100) }),
|
|
Command(name: "Move to Bottom-Left", action: { try await commandRunner.moveWindowAbsolute(x: 100, y: 700) }),
|
|
Command(name: "Move to Bottom-Right", action: { try await commandRunner.moveWindowAbsolute(x: 1200, y: 700) }),
|
|
Command(name: "Move Center", action: { try await commandRunner.moveWindowAbsolute(x: 600, y: 400) }),
|
|
Command(name: "Resize Editor Size", action: { try await commandRunner.resizeWindowAbsolute(width: 1400, height: 900) }),
|
|
Command(name: "Resize Sidebar", action: { try await commandRunner.resizeWindowAbsolute(width: 600, height: 900) }),
|
|
Command(name: "Resize Terminal", action: { try await commandRunner.resizeWindowAbsolute(width: 1400, height: 250) }),
|
|
Command(name: "Move Left 50px", action: { try await commandRunner.moveWindowRelative(deltaX: -50, deltaY: 0) }),
|
|
Command(name: "Move Right 50px", action: { try await commandRunner.moveWindowRelative(deltaX: 50, deltaY: 0) }),
|
|
Command(name: "Move Up 50px", action: { try await commandRunner.moveWindowRelative(deltaX: 0, deltaY: -50) }),
|
|
Command(name: "Move Down 50px", action: { try await commandRunner.moveWindowRelative(deltaX: 0, deltaY: 50) }),
|
|
|
|
// Cursor + Brave Layout Presets
|
|
Command(name: "Cursor + Brave: Main Layout", action: { try await commandRunner.applyCursorBraveMainLayout() }),
|
|
Command(name: "Cursor + Brave: Research Layout", action: { try await commandRunner.applyCursorBraveResearchLayout() }),
|
|
Command(name: "Cursor + Brave: API Layout", action: { try await commandRunner.applyCursorBraveAPILayout() }),
|
|
|
|
// Space commands
|
|
Command(name: "Create Space", action: { _ = try await commandRunner.createSpace() }),
|
|
Command(name: "Balance Space", action: { try await commandRunner.balanceSpace() }),
|
|
Command(name: "Mirror X", action: { try await commandRunner.mirrorSpace(axis: .x) }),
|
|
Command(name: "Mirror Y", action: { try await commandRunner.mirrorSpace(axis: .y) }),
|
|
Command(name: "Rotate 90°", action: { try await commandRunner.rotateSpace(degrees: 90) }),
|
|
Command(name: "Rotate 180°", action: { try await commandRunner.rotateSpace(degrees: 180) }),
|
|
Command(name: "Rotate 270°", action: { try await commandRunner.rotateSpace(degrees: 270) }),
|
|
|
|
// Display commands
|
|
Command(name: "Focus Next Display", action: { try await commandRunner.focusDisplayNext() }),
|
|
Command(name: "Focus Previous Display", action: { try await commandRunner.focusDisplayPrev() }),
|
|
Command(name: "Focus Recent Display", action: { try await commandRunner.focusDisplayRecent() }),
|
|
Command(name: "Focus Display with Mouse", action: { try await commandRunner.focusDisplayMouse() }),
|
|
Command(name: "Balance Display", action: { try await commandRunner.balanceDisplay() }),
|
|
|
|
// Layout commands
|
|
Command(name: "Set BSP Layout", action: { try await commandRunner.setConfig("layout", value: "bsp") }),
|
|
Command(name: "Set Stack Layout", action: { try await commandRunner.setConfig("layout", value: "stack") }),
|
|
Command(name: "Set Float Layout", action: { try await commandRunner.setConfig("layout", value: "float") }),
|
|
]
|
|
}
|
|
|
|
var filteredCommands: [Command] {
|
|
if searchText.isEmpty {
|
|
return commands
|
|
}
|
|
return commands.filter { $0.name.lowercased().contains(searchText.lowercased()) }
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Search field
|
|
HStack {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundColor(.secondary)
|
|
TextField("Search commands...", text: $searchText)
|
|
.textFieldStyle(.plain)
|
|
.font(.system(size: 14))
|
|
if !searchText.isEmpty {
|
|
Button(action: { searchText = "" }) {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.background(Color(.windowBackgroundColor))
|
|
|
|
Divider()
|
|
|
|
// Command list
|
|
ScrollView {
|
|
LazyVStack(spacing: 0) {
|
|
ForEach(filteredCommands, id: \.name) { command in
|
|
Button(action: {
|
|
Task {
|
|
do {
|
|
try await command.action()
|
|
} catch {
|
|
print("Command failed: \(error)")
|
|
}
|
|
}
|
|
}) {
|
|
HStack {
|
|
Text(command.name)
|
|
.font(.system(size: 13))
|
|
.foregroundColor(.primary)
|
|
Spacer()
|
|
Image(systemName: "arrow.right")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.background(
|
|
selectedCommand?.name == command.name ?
|
|
Color(.selectedControlColor) : Color.clear
|
|
)
|
|
|
|
if command.name != filteredCommands.last?.name {
|
|
Divider()
|
|
.padding(.horizontal, 12)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxHeight: 300)
|
|
}
|
|
.frame(width: 350)
|
|
.background(Color(.windowBackgroundColor))
|
|
.cornerRadius(8)
|
|
.shadow(radius: 10)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.stroke(Color(.separatorColor), lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct Command: Identifiable {
|
|
let id = UUID()
|
|
let name: String
|
|
let action: () async throws -> Void
|
|
}
|
|
|
|
// Preview
|
|
struct CommandPaletteView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
CommandPaletteView()
|
|
}
|
|
}
|