krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/Orgstar/Keys.swift

85c633a83a4744bef9dde8b52462f9c2a1c037e4
orgstar/Sources/Orgstar/Keys.swift history · blame · raw

113 lines · 4464 bytes

  1import OrgApp
  2import OrgCore
  3import OrgEditorAppKit
  4import SwiftUI
  5
  6struct KeyHint: Hashable {
  7    let key: String
  8    let title: String
  9}
 10
 11/// The minibuffer line: a pending prefix, or the last message for a few seconds. After a
 12/// short pause on a prefix, the keys that can follow it.
 13struct EchoArea: View {
 14    let commands: AppCommands
 15    @State private var showingHints = false
 16    @State private var answer = ""
 17    @FocusState private var answerFocused: Bool
 18
 19    var body: some View {
 20        VStack(alignment: .leading, spacing: 0) {
 21            if showingHints, commands.pendingKeys != nil, !commands.keyHints.isEmpty {
 22                Divider()
 23                LazyVGrid(columns: [GridItem(.adaptive(minimum: 220), alignment: .leading)], alignment: .leading, spacing: 4) {
 24                    ForEach(commands.keyHints, id: \.self) { hint in
 25                        HStack(spacing: 8) {
 26                            Text(hint.key).font(.system(.body, design: .monospaced)).foregroundStyle(.tint)
 27                            Text(hint.title).foregroundStyle(hint.title == "+prefix" ? .secondary : .primary).lineLimit(1)
 28                        }
 29                    }
 30                }
 31                .padding(10)
 32            }
 33            if let request = commands.prompt {
 34                Divider()
 35                HStack(spacing: 6) {
 36                    Text(request.prompt.message).font(.system(.callout, design: .monospaced)).foregroundStyle(.secondary)
 37                    TextField("", text: $answer)
 38                        .textFieldStyle(.plain)
 39                        .font(.system(.callout, design: .monospaced))
 40                        .focused($answerFocused)
 41                        .onSubmit { commands.answer(answer) }
 42                        .onExitCommand { commands.answer(nil) }
 43                }
 44                .padding(.horizontal, 10)
 45                .padding(.vertical, 4)
 46                .onAppear {
 47                    answer = request.prompt.initial
 48                    answerFocused = true
 49                }
 50                .id(request.id)
 51            } else if let text = commands.pendingKeys.map({ $0 + "-" }) ?? commands.message {
 52                Divider()
 53                Text(text)
 54                    .font(.system(.callout, design: .monospaced))
 55                    .lineLimit(1)
 56                    .padding(.horizontal, 10)
 57                    .padding(.vertical, 4)
 58                    .frame(maxWidth: .infinity, alignment: .leading)
 59            }
 60        }
 61        .background(.bar)
 62        .task(id: commands.pendingKeys) {
 63            showingHints = false
 64            guard commands.pendingKeys != nil else { return }
 65            try? await Task.sleep(for: .milliseconds(600))
 66            if !Task.isCancelled { showingHints = true }
 67        }
 68    }
 69}
 70
 71/// M-x and ⇧⌘P: every command by title, with its keys.
 72struct PaletteView: View {
 73    let keymap: Keymap
 74    let run: (String) -> Void
 75    @State private var query = ""
 76    @Environment(\.dismiss) private var dismiss
 77
 78    private var matches: [EditorCommand] {
 79        let scored: [(command: EditorCommand, score: Int)] = EditorCommands.all.filter(\.inPalette).compactMap { command in
 80            guard let score = FuzzyMatch.score(query, command.title) else { return nil }
 81            return (command, score)
 82        }
 83        return scored.sorted { $0.score != $1.score ? $0.score > $1.score : $0.command.title < $1.command.title }.map(\.command)
 84    }
 85
 86    var body: some View {
 87        let matches = matches
 88        VStack(spacing: 0) {
 89            TextField("Run command", text: $query)
 90                .textFieldStyle(.plain)
 91                .font(.title3)
 92                .padding(12)
 93                .onSubmit { if let first = matches.first { run(first.id) } }
 94            Divider()
 95            List(matches, id: \.id) { command in
 96                Button {
 97                    run(command.id)
 98                } label: {
 99                    HStack {
100                        Text(command.title)
101                        Spacer()
102                        if let keys = keymap.keys(for: command.id) {
103                            Text(KeySequence.format(keys)).font(.system(.caption, design: .monospaced)).foregroundStyle(.secondary)
104                        }
105                    }
106                }
107                .buttonStyle(.plain)
108            }
109        }
110        .frame(width: 520, height: 360)
111        .onExitCommand { dismiss() }
112    }
113}