101 lines
4.7 KiB
Swift
101 lines
4.7 KiB
Swift
import SwiftUI
|
|
import NucleicCore
|
|
|
|
/// Renders a git command pipeline (a `cd … && git add … && git commit -F - <<'EOF' … EOF`
|
|
/// block) as a structured card instead of a raw monospaced blob: the commit subject reads as
|
|
/// a headline, the message body renders as Markdown, and the surrounding ops show as a small
|
|
/// step list so the whole pipeline is legible at a glance.
|
|
///
|
|
/// The raw command is never hidden — it sits one tap away behind "Show command" — so an
|
|
/// approval still lets the user verify exactly what they are granting (a stray `rm` in the
|
|
/// pipeline can't be smuggled past the card). An optional `output` shows the result text
|
|
/// (e.g. the hash a trailing `git log --oneline -1` prints) once the command has run.
|
|
struct GitBlockCard: View {
|
|
@Environment(\.appPalette) private var palette
|
|
let block: GitCommandSummary.Block
|
|
/// The literal command, revealed under "Show command". Omit to hide the disclosure.
|
|
var rawCommand: String? = nil
|
|
/// The tool result text, shown beneath the card once the command has executed.
|
|
var output: String? = nil
|
|
/// The card's accent — `attention` in a pending approval, `accent` in the transcript.
|
|
var tint: Color? = nil
|
|
|
|
private var accent: Color { tint ?? palette.accent }
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
if let commit = block.commit { commitSection(commit) }
|
|
if block.steps.count > 1 { stepList }
|
|
if let output, !output.isEmpty { outputRow(output) }
|
|
if let rawCommand, !rawCommand.isEmpty {
|
|
CommandDisclosure(command: rawCommand, accent: accent)
|
|
}
|
|
}
|
|
.padding(.horizontal, 12).padding(.vertical, 10)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(accent.opacity(0.06))
|
|
.overlay(RoundedRectangle(cornerRadius: 8).strokeBorder(accent.opacity(0.16), lineWidth: 1))
|
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
|
}
|
|
|
|
/// The commit headline (glyph + subject) and its Markdown-rendered body.
|
|
private func commitSection(_ commit: GitCommandSummary.Block.Commit) -> some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(alignment: .firstTextBaseline, spacing: 9) {
|
|
Image(systemName: commit.amend ? "pencil.circle" : "checkmark.seal")
|
|
.font(.callout).foregroundStyle(accent).frame(width: 16)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(commit.amend ? "Amend commit" : "Commit")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
if !commit.subject.isEmpty {
|
|
Text(commit.subject)
|
|
.font(.callout.weight(.semibold))
|
|
.foregroundStyle(AppTheme.primaryText)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.textSelection(.enabled)
|
|
}
|
|
}
|
|
}
|
|
if !commit.body.isEmpty {
|
|
// The body is real commit-message Markdown (paragraphs, bullet lists) — render
|
|
// it formatted, indented under the subject, a touch smaller than the headline.
|
|
MarkdownText(markdown: commit.body, bodySize: 12)
|
|
.padding(.leading, 25)
|
|
.textSelection(.enabled)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The pipeline as a compact step list — every recognized git op in run order, each a
|
|
/// glyph + concise label, so the surrounding `add`/`log` around a commit stay visible.
|
|
private var stepList: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Divider().overlay(accent.opacity(0.14))
|
|
ForEach(block.steps) { step in
|
|
HStack(alignment: .firstTextBaseline, spacing: 9) {
|
|
Image(systemName: step.symbol)
|
|
.font(.caption2).foregroundStyle(accent).frame(width: 16)
|
|
Text(step.label)
|
|
.font(.caption).foregroundStyle(.secondary)
|
|
.lineLimit(1).truncationMode(.middle)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func outputRow(_ output: String) -> some View {
|
|
HStack(alignment: .top, spacing: 9) {
|
|
Image(systemName: "arrow.turn.down.right")
|
|
.font(.caption2).foregroundStyle(.tertiary).frame(width: 16)
|
|
Text(output)
|
|
.font(.system(.caption, design: .monospaced))
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(3).truncationMode(.tail)
|
|
.textSelection(.enabled)
|
|
Spacer(minLength: 0)
|
|
}
|
|
}
|
|
|
|
}
|