695 lines
28 KiB
Swift
695 lines
28 KiB
Swift
import SwiftUI
|
||
import MacToolsPluginKit
|
||
|
||
// MARK: - ZshConfigEditorView
|
||
|
||
struct ZshConfigEditorView: View {
|
||
@ObservedObject var store: ZshConfigStore
|
||
private let localization: PluginLocalization
|
||
@State private var contentTab: ContentTab = .editor
|
||
@State private var activeSnippet: ZshSnippet? = nil
|
||
@State private var snippetInput: String = ""
|
||
@State private var showFileInfo: Bool = false
|
||
@State private var scrollToBottomID: Int = 0
|
||
@State private var isRunningSource = false
|
||
@State private var sourceResult: Bool? = nil
|
||
@State private var sourceResultToken = 0
|
||
|
||
init(
|
||
store: ZshConfigStore,
|
||
localization: PluginLocalization = PluginLocalization(bundle: .main)
|
||
) {
|
||
self.store = store
|
||
self.localization = localization
|
||
}
|
||
|
||
private enum ContentTab: CaseIterable {
|
||
case editor
|
||
case quickInsert
|
||
|
||
var icon: String {
|
||
switch self {
|
||
case .editor: return "square.and.pencil"
|
||
case .quickInsert: return "bolt.fill"
|
||
}
|
||
}
|
||
|
||
func title(localization: PluginLocalization) -> String {
|
||
switch self {
|
||
case .editor:
|
||
return localization.string("editor.tab.editor", defaultValue: "编辑")
|
||
case .quickInsert:
|
||
return localization.string("editor.tab.quickInsert", defaultValue: "快速插入")
|
||
}
|
||
}
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.section) {
|
||
fileSelectorSection
|
||
contentTabBar
|
||
contentBody
|
||
}
|
||
}
|
||
|
||
// MARK: - File Selector
|
||
|
||
private var fileSelectorSection: some View {
|
||
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
||
sectionHeader(
|
||
title: localization.string("editor.fileSelector.title", defaultValue: "配置文件"),
|
||
icon: "doc.text"
|
||
)
|
||
ScrollView(.horizontal, showsIndicators: false) {
|
||
HStack(spacing: 8) {
|
||
ForEach(ZshConfigFileType.allCases) { type in
|
||
fileTabButton(type: type)
|
||
}
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, 2)
|
||
}
|
||
.padding(.horizontal, -PluginSettingsTheme.Spacing.rowHorizontal)
|
||
}
|
||
}
|
||
|
||
private func fileTabButton(type: ZshConfigFileType) -> some View {
|
||
let isSelected = store.selectedType == type
|
||
let status = store.statusMap[type]
|
||
return Button {
|
||
store.select(type)
|
||
} label: {
|
||
HStack(spacing: 5) {
|
||
Circle()
|
||
.fill(fileStatusColor(status))
|
||
.frame(width: 7, height: 7)
|
||
Text(type.filename)
|
||
.font(.system(size: 12, weight: isSelected ? .semibold : .regular, design: .monospaced))
|
||
}
|
||
.padding(.horizontal, 10)
|
||
.padding(.vertical, 5)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 7, style: .continuous)
|
||
.fill(isSelected
|
||
? Color.accentColor.opacity(0.15)
|
||
: PluginSettingsTheme.Palette.recessedControlBackground)
|
||
)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 7, style: .continuous)
|
||
.stroke(isSelected ? Color.accentColor.opacity(0.4) : Color.clear, lineWidth: 1)
|
||
)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
// MARK: - Content Tab Bar
|
||
|
||
private var contentTabBar: some View {
|
||
HStack(spacing: 0) {
|
||
ForEach(ContentTab.allCases, id: \.self) { tab in
|
||
contentTabButton(tab)
|
||
}
|
||
}
|
||
.padding(3)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.card, style: .continuous)
|
||
.fill(PluginSettingsTheme.Palette.recessedControlBackground)
|
||
)
|
||
}
|
||
|
||
private func contentTabButton(_ tab: ContentTab) -> some View {
|
||
let isSelected = contentTab == tab
|
||
return Button {
|
||
withAnimation(.easeInOut(duration: 0.15)) {
|
||
contentTab = tab
|
||
}
|
||
} label: {
|
||
Label(tab.title(localization: localization), systemImage: tab.icon)
|
||
.font(.system(size: 12, weight: isSelected ? .semibold : .regular))
|
||
.foregroundStyle(isSelected ? .primary : .secondary)
|
||
.padding(.horizontal, 14)
|
||
.padding(.vertical, 6)
|
||
.frame(maxWidth: .infinity)
|
||
.background(
|
||
Group {
|
||
if isSelected {
|
||
RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.card - 2, style: .continuous)
|
||
.fill(PluginSettingsTheme.Palette.cardBackground)
|
||
.shadow(color: .black.opacity(0.06), radius: 2, y: 1)
|
||
}
|
||
}
|
||
)
|
||
.contentShape(Rectangle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
// MARK: - Content Body
|
||
|
||
@ViewBuilder
|
||
private var contentBody: some View {
|
||
switch contentTab {
|
||
case .editor:
|
||
editorBodySection
|
||
case .quickInsert:
|
||
snippetSection
|
||
}
|
||
}
|
||
|
||
// MARK: - Editor Tab
|
||
|
||
@ViewBuilder
|
||
private var editorBodySection: some View {
|
||
if let status = store.statusMap[store.selectedType] {
|
||
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
||
fileInfoRow(status: status)
|
||
if status.exists {
|
||
editorField(isWritable: status.isWritable)
|
||
actionRow(status: status)
|
||
} else {
|
||
notExistBanner(status: status)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func fileInfoRow(status: ZshFileStatus) -> some View {
|
||
HStack(alignment: .top, spacing: PluginSettingsTheme.Spacing.rowContentControl) {
|
||
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.rowTitleDescription) {
|
||
Text(store.selectedType.role(localization: localization))
|
||
.font(PluginSettingsTheme.Typography.rowTitle)
|
||
HStack(spacing: 4) {
|
||
if status.exists {
|
||
Text(status.formattedSize)
|
||
.font(PluginSettingsTheme.Typography.monospacedValue)
|
||
.foregroundStyle(.secondary)
|
||
if let date = status.modifiedDate {
|
||
Text("·").foregroundStyle(.secondary)
|
||
Text(date, style: .relative)
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
Text(localization.string("editor.fileInfo.modifiedAgo", defaultValue: "前修改"))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
} else {
|
||
Text(localization.string("editor.fileInfo.missing", defaultValue: "文件不存在"))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
Spacer()
|
||
Button {
|
||
withAnimation(.easeInOut(duration: 0.15)) { showFileInfo.toggle() }
|
||
} label: {
|
||
Image(systemName: showFileInfo ? "info.circle.fill" : "info.circle")
|
||
.pluginSettingsRowIconStyle(showFileInfo ? Color.accentColor : .secondary)
|
||
}
|
||
.buttonStyle(.plain)
|
||
.help(localization.string("editor.fileInfo.help", defaultValue: "查看加载时机与推荐用途"))
|
||
}
|
||
.pluginSettingsListRowPadding()
|
||
.pluginSettingsCardBackground(.host)
|
||
|
||
if showFileInfo {
|
||
fileInfoPanel
|
||
}
|
||
}
|
||
|
||
private var fileInfoPanel: some View {
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
infoRow(
|
||
label: localization.string("editor.fileInfo.whenLoaded", defaultValue: "加载时机"),
|
||
value: store.selectedType.whenLoaded(localization: localization)
|
||
)
|
||
PluginSettingsListDivider(leadingInset: 0, trailingInset: 0)
|
||
infoRow(
|
||
label: localization.string("editor.fileInfo.recommendedUse", defaultValue: "推荐用途"),
|
||
value: store.selectedType.recommendedUse(localization: localization)
|
||
)
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, PluginSettingsTheme.Spacing.rowVertical)
|
||
.pluginSettingsCardBackground(.host)
|
||
.transition(.opacity.combined(with: .offset(y: -4)))
|
||
}
|
||
|
||
private func infoRow(label: String, value: String) -> some View {
|
||
HStack(alignment: .top, spacing: 8) {
|
||
Text(label)
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
.frame(width: 60, alignment: .leading)
|
||
Text(value)
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.primary)
|
||
}
|
||
}
|
||
|
||
private func editorField(isWritable: Bool) -> some View {
|
||
ZStack(alignment: .topLeading) {
|
||
if store.editingContent.isEmpty {
|
||
Text(localization.string(
|
||
"editor.emptyFile.placeholder",
|
||
defaultValue: "文件为空。可直接在此输入,或切换到「快速插入」标签添加常用配置。"
|
||
))
|
||
.font(.system(size: 12, design: .monospaced))
|
||
.foregroundStyle(.tertiary)
|
||
.padding(.top, 8)
|
||
.padding(.leading, 6)
|
||
.allowsHitTesting(false)
|
||
}
|
||
ZshSyntaxHighlightingEditor(
|
||
text: isWritable ? $store.editingContent : .constant(store.editingContent),
|
||
isEditable: isWritable,
|
||
onChange: { store.markEdited() },
|
||
scrollToBottomID: scrollToBottomID
|
||
)
|
||
.frame(minHeight: 200, maxHeight: .infinity)
|
||
}
|
||
.padding(8)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.field, style: .continuous)
|
||
.fill(PluginSettingsTheme.Palette.fieldBackground)
|
||
)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.field, style: .continuous)
|
||
.stroke(PluginSettingsTheme.Palette.separator, lineWidth: PluginSettingsTheme.Stroke.hairline)
|
||
)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func actionRow(status: ZshFileStatus) -> some View {
|
||
HStack(spacing: PluginSettingsTheme.Spacing.controlCluster) {
|
||
if status.isWritable {
|
||
Button {
|
||
store.saveCurrentFile()
|
||
} label: {
|
||
HStack(spacing: 4) {
|
||
if store.isBusy {
|
||
ProgressView().controlSize(.mini)
|
||
} else if store.lastSaveSucceeded {
|
||
Image(systemName: "checkmark")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
}
|
||
Text(saveButtonTitle)
|
||
}
|
||
.font(PluginSettingsTheme.Typography.controlLabel)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.small)
|
||
.disabled(!store.hasUnsavedChanges || store.isBusy)
|
||
.help(localization.format(
|
||
"editor.action.save.help",
|
||
defaultValue: "保存前会自动备份为 %@.bak",
|
||
store.selectedType.filename
|
||
))
|
||
} else {
|
||
readOnlyBadge
|
||
}
|
||
Spacer()
|
||
Button {
|
||
store.openInExternalEditor(store.selectedType)
|
||
} label: {
|
||
Label(
|
||
localization.string("editor.action.openExternal", defaultValue: "在系统编辑器中打开"),
|
||
systemImage: "square.and.pencil"
|
||
)
|
||
.font(PluginSettingsTheme.Typography.controlLabel)
|
||
}
|
||
.buttonStyle(.bordered)
|
||
.controlSize(.small)
|
||
.help(localization.string("editor.action.openExternal.help", defaultValue: "在系统默认文本编辑器中打开此文件"))
|
||
}
|
||
|
||
if let error = store.saveError {
|
||
errorBanner(message: error)
|
||
}
|
||
if store.lastSaveSucceeded {
|
||
savedHintBanner
|
||
}
|
||
}
|
||
|
||
private func notExistBanner(status: ZshFileStatus) -> some View {
|
||
VStack(spacing: 14) {
|
||
Image(systemName: "doc.badge.plus")
|
||
.font(.system(size: PluginSettingsTheme.Size.emptyStateIcon + 4))
|
||
.foregroundStyle(.tertiary)
|
||
VStack(spacing: 5) {
|
||
Text(localization.string("editor.missingFile.title", defaultValue: "文件尚不存在"))
|
||
.font(PluginSettingsTheme.Typography.rowTitle)
|
||
Text(status.isWritable
|
||
? localization.format(
|
||
"editor.missingFile.createDescription",
|
||
defaultValue: "点击「创建文件」将在家目录生成 %@ 并填入说明注释。",
|
||
store.selectedType.filename
|
||
)
|
||
: localization.string(
|
||
"editor.missingFile.readOnlyDescription",
|
||
defaultValue: "家目录不可写,无法创建此文件。请检查磁盘权限。"
|
||
))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
if status.isWritable {
|
||
Button(localization.string("editor.action.createFile", defaultValue: "创建文件")) {
|
||
store.createCurrentFile()
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.small)
|
||
.disabled(store.isBusy)
|
||
.padding(.top, 2)
|
||
}
|
||
if let error = store.saveError {
|
||
errorBanner(message: error)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, minHeight: 200)
|
||
.padding(PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.pluginSettingsCardBackground(.host)
|
||
}
|
||
|
||
// MARK: - Quick Insert Tab
|
||
|
||
private var snippetSection: some View {
|
||
VStack(alignment: .leading, spacing: PluginSettingsTheme.Spacing.sectionHeaderContent) {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "arrow.right.circle")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
.foregroundStyle(.secondary)
|
||
Text(localization.format(
|
||
"editor.quickInsert.target",
|
||
defaultValue: "插入到 %@",
|
||
store.selectedType.filename
|
||
))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
Spacer()
|
||
Button {
|
||
withAnimation(.easeInOut(duration: 0.15)) { contentTab = .editor }
|
||
} label: {
|
||
Text(localization.string("editor.quickInsert.switchToEditor", defaultValue: "切换到编辑器"))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(Color.accentColor)
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, PluginSettingsTheme.Spacing.rowVertical)
|
||
.pluginSettingsCardBackground(.recessed)
|
||
|
||
VStack(spacing: 0) {
|
||
ForEach(Array(snippets.enumerated()), id: \.element.id) { index, snippet in
|
||
VStack(spacing: 0) {
|
||
snippetRow(snippet: snippet)
|
||
if activeSnippet?.id == snippet.id {
|
||
snippetExpandedContent(snippet: snippet)
|
||
.transition(.opacity.combined(with: .move(edge: .top)))
|
||
}
|
||
}
|
||
if index < snippets.count - 1 {
|
||
PluginSettingsListDivider()
|
||
}
|
||
}
|
||
}
|
||
.pluginSettingsCardBackground(.host)
|
||
}
|
||
}
|
||
|
||
private func snippetRow(snippet: ZshSnippet) -> some View {
|
||
let isActive = activeSnippet?.id == snippet.id
|
||
return Button {
|
||
withAnimation(.easeInOut(duration: 0.15)) {
|
||
if isActive {
|
||
activeSnippet = nil
|
||
snippetInput = ""
|
||
} else {
|
||
activeSnippet = snippet
|
||
snippetInput = ""
|
||
}
|
||
}
|
||
} label: {
|
||
HStack(spacing: PluginSettingsTheme.Spacing.rowContentControl) {
|
||
Image(systemName: snippet.icon)
|
||
.pluginSettingsRowIconStyle(isActive ? Color.accentColor : .secondary)
|
||
VStack(alignment: .leading, spacing: 2) {
|
||
Text(snippet.title)
|
||
.font(PluginSettingsTheme.Typography.rowTitle)
|
||
.foregroundStyle(.primary)
|
||
Text(snippet.description)
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
Spacer()
|
||
Image(systemName: "chevron.right")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
.foregroundStyle(.tertiary)
|
||
.rotationEffect(.degrees(isActive ? 90 : 0))
|
||
.animation(.easeInOut(duration: 0.2), value: isActive)
|
||
}
|
||
.pluginSettingsListRowPadding()
|
||
.contentShape(Rectangle())
|
||
}
|
||
.buttonStyle(.plain)
|
||
}
|
||
|
||
private func snippetExpandedContent(snippet: ZshSnippet) -> some View {
|
||
VStack(alignment: .leading, spacing: 10) {
|
||
TextField(snippet.placeholder, text: $snippetInput)
|
||
.textFieldStyle(.roundedBorder)
|
||
.font(.system(size: 12, design: .monospaced))
|
||
.onSubmit { insertCurrentSnippet(snippet) }
|
||
|
||
if !snippetInput.isEmpty {
|
||
Text(snippet.buildContent(snippetInput))
|
||
.font(.system(size: 11, design: .monospaced))
|
||
.foregroundStyle(.secondary)
|
||
.padding(8)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: PluginSettingsTheme.Radius.field, style: .continuous)
|
||
.fill(PluginSettingsTheme.Palette.recessedControlBackground)
|
||
)
|
||
}
|
||
|
||
HStack {
|
||
Spacer()
|
||
Button(localization.string("editor.quickInsert.insertAtEnd", defaultValue: "插入到末尾")) {
|
||
insertCurrentSnippet(snippet)
|
||
}
|
||
.buttonStyle(.borderedProminent)
|
||
.controlSize(.small)
|
||
.disabled(snippetInput.isEmpty)
|
||
}
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, PluginSettingsTheme.Spacing.rowVertical)
|
||
.background(PluginSettingsTheme.Palette.cardBackground)
|
||
.overlay(alignment: .top) {
|
||
PluginSettingsListDivider(leadingInset: 0, trailingInset: 0)
|
||
}
|
||
}
|
||
|
||
// MARK: - Helper Views
|
||
|
||
private var readOnlyBadge: some View {
|
||
HStack(spacing: 4) {
|
||
Image(systemName: "lock")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
Text(localization.string("editor.readOnly", defaultValue: "只读"))
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
}
|
||
.foregroundStyle(.secondary)
|
||
.padding(.horizontal, 8)
|
||
.padding(.vertical, 4)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||
.fill(PluginSettingsTheme.Palette.recessedControlBackground)
|
||
)
|
||
}
|
||
|
||
private func errorBanner(message: String) -> some View {
|
||
HStack(spacing: 6) {
|
||
Image(systemName: "exclamationmark.triangle.fill")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
.foregroundStyle(.red)
|
||
Text(message)
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.red)
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, 8)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.pluginSettingsCardBackground(.recessed)
|
||
}
|
||
|
||
private var savedHintBanner: some View {
|
||
HStack(alignment: .top, spacing: 6) {
|
||
Image(systemName: "checkmark.circle.fill")
|
||
.font(PluginSettingsTheme.Typography.statusBadge)
|
||
.foregroundStyle(.green)
|
||
.padding(.top, 1)
|
||
VStack(alignment: .leading, spacing: 6) {
|
||
Text(localization.format(
|
||
"editor.savedHint.title",
|
||
defaultValue: "已保存(已备份为 %@.bak)",
|
||
store.selectedType.filename
|
||
))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
if store.selectedType == .zshrc || store.selectedType == .zshenv {
|
||
HStack(spacing: 8) {
|
||
Image(systemName: "terminal")
|
||
.font(.system(size: 10))
|
||
.foregroundStyle(.tertiary)
|
||
.allowsHitTesting(false)
|
||
Text("source ~/\(store.selectedType.filename)")
|
||
.font(.system(size: 11, design: .monospaced))
|
||
.foregroundStyle(.primary)
|
||
.textSelection(.enabled)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.allowsHitTesting(false)
|
||
Group {
|
||
if isRunningSource {
|
||
ProgressView()
|
||
.controlSize(.mini)
|
||
.frame(width: 16, height: 16)
|
||
} else if let result = sourceResult {
|
||
Image(systemName: result ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||
.font(.system(size: 14))
|
||
.foregroundStyle(result ? Color.green : Color.red)
|
||
.frame(width: 16, height: 16)
|
||
} else {
|
||
Button {
|
||
runSourceDirectly(for: store.selectedType)
|
||
} label: {
|
||
Image(systemName: "play.circle.fill")
|
||
.font(.system(size: 14))
|
||
.foregroundStyle(Color.accentColor)
|
||
.frame(width: 16, height: 16)
|
||
}
|
||
.buttonStyle(.plain)
|
||
.fixedSize()
|
||
.contentShape(Circle())
|
||
}
|
||
}
|
||
.fixedSize()
|
||
}
|
||
.padding(.horizontal, 10)
|
||
.padding(.vertical, 7)
|
||
.background(
|
||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||
.fill(Color(.controlBackgroundColor))
|
||
)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||
.stroke(PluginSettingsTheme.Palette.separator, lineWidth: PluginSettingsTheme.Stroke.hairline)
|
||
)
|
||
|
||
Text(localization.string("editor.savedHint.reloadDescription", defaultValue: "执行后无需重启终端即可生效。"))
|
||
.font(PluginSettingsTheme.Typography.rowDescription)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, PluginSettingsTheme.Spacing.rowHorizontal)
|
||
.padding(.vertical, 8)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.pluginSettingsCardBackground(.recessed)
|
||
.onAppear {
|
||
sourceResult = nil
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private func sectionHeader(title: String, icon: String) -> some View {
|
||
Label(title, systemImage: icon)
|
||
.font(PluginSettingsTheme.Typography.sectionTitle)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
// MARK: - Helper Functions
|
||
|
||
private var snippets: [ZshSnippet] {
|
||
ZshSnippet.localizedSnippets(localization: localization)
|
||
}
|
||
|
||
private var saveButtonTitle: String {
|
||
if store.hasUnsavedChanges {
|
||
return localization.string("editor.action.saveUnsaved", defaultValue: "保存*")
|
||
}
|
||
if store.lastSaveSucceeded {
|
||
return localization.string("editor.action.saved", defaultValue: "已保存")
|
||
}
|
||
return localization.string("editor.action.save", defaultValue: "保存")
|
||
}
|
||
|
||
private func runSourceDirectly(for type: ZshConfigFileType) {
|
||
guard !isRunningSource else { return }
|
||
isRunningSource = true
|
||
sourceResult = nil
|
||
sourceResultToken += 1
|
||
let token = sourceResultToken
|
||
let filename = type.filename
|
||
Task {
|
||
let succeeded: Bool = await withCheckedContinuation { continuation in
|
||
DispatchQueue.global(qos: .userInitiated).async {
|
||
let process = Process()
|
||
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||
process.arguments = ["--login", "-c", "source ~/\(filename)"]
|
||
do {
|
||
try process.run()
|
||
process.waitUntilExit()
|
||
continuation.resume(returning: process.terminationStatus == 0)
|
||
} catch {
|
||
continuation.resume(returning: false)
|
||
}
|
||
}
|
||
}
|
||
withAnimation(.easeInOut(duration: 0.2)) {
|
||
isRunningSource = false
|
||
sourceResult = succeeded
|
||
}
|
||
try? await Task.sleep(for: .seconds(3))
|
||
guard sourceResultToken == token else { return }
|
||
withAnimation(.easeInOut(duration: 0.2)) {
|
||
sourceResult = nil
|
||
}
|
||
}
|
||
}
|
||
|
||
private func runSourceInTerminal(for type: ZshConfigFileType) {
|
||
let script = """
|
||
tell application "Terminal"
|
||
activate
|
||
do script "source ~/\(type.filename)"
|
||
end tell
|
||
"""
|
||
let process = Process()
|
||
process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||
process.arguments = ["-e", script]
|
||
try? process.run()
|
||
}
|
||
|
||
private func fileStatusColor(_ status: ZshFileStatus?) -> Color {
|
||
guard let status else { return .gray.opacity(0.4) }
|
||
if !status.exists { return .orange.opacity(0.6) }
|
||
if !status.isWritable { return .yellow }
|
||
return .green
|
||
}
|
||
|
||
private func insertCurrentSnippet(_ snippet: ZshSnippet) {
|
||
guard !snippetInput.isEmpty else { return }
|
||
store.appendSnippet(snippet.buildContent(snippetInput))
|
||
scrollToBottomID += 1
|
||
withAnimation(.easeInOut(duration: 0.15)) {
|
||
activeSnippet = nil
|
||
snippetInput = ""
|
||
contentTab = .editor
|
||
}
|
||
}
|
||
}
|