Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ private struct ExtensionSettingsSheet: View {
case "machines": 420
case "lidAwake": 400
case "music": 460
case "focusDim", "colorPicker", "keystrokeHighlight": 430
case "focusDim", "colorPicker", "keystrokeHighlight", "finderTools": 430
case "system": 500
case "notchShelf", "presenter": 580
default: 620
Expand Down Expand Up @@ -1063,6 +1063,7 @@ private struct ExtensionDetailRows: View {
case .notchShelf: NotchShelfRows()
case .audioMixer: AudioMixerRows()
case .clipboard: ClipboardRows()
case .finderTools: FinderToolsRows()
case .keystrokeHighlight: KeystrokeHighlightRows()
case .focusDim: FocusDimRows()
case .presenter: PresenterRows()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import EdithKit
import SwiftUI

struct FinderToolsRows: View {
@AppStorage(AppStorageKeys.FinderTools.enabled, store: SharedDefaults.store) private
var enabled =
false
@AppStorage(AppStorageKeys.FinderTools.cutPaste, store: SharedDefaults.store) private
var cutPaste = true
@AppStorage(AppStorageKeys.FinderTools.rename, store: SharedDefaults.store) private
var rename = true
@AppStorage(AppStorageKeys.FinderTools.pasteImages, store: SharedDefaults.store) private
var pasteImages = true
@AppStorage(AppStorageKeys.FinderTools.diskImageInstaller, store: SharedDefaults.store) private
var diskImageInstaller = true

var body: some View {
Group {
Section("Finder shortcuts") {
Toggle(
"Cut and paste files with ⌘X and ⌘V",
isOn: $cutPaste.configured(AppStorageKeys.FinderTools.cutPaste))
Text(
"Edith moves the selected files into the folder open in Finder. Existing files are never replaced, and a failed batch is rolled back when possible."
)
.settingsCaption()
Toggle(
"Rename the selection with F2",
isOn: $rename.configured(AppStorageKeys.FinderTools.rename))
Toggle(
"Paste copied images as PNG files",
isOn: $pasteImages.configured(AppStorageKeys.FinderTools.pasteImages))
Text(
"Press ⌘V in Finder to save a copied image into the open folder as a PNG with a timestamped name. Large or invalid images are rejected before decoding."
)
.settingsCaption()
}

Section("Disk images") {
Toggle(
"Offer one-click app installation",
isOn: $diskImageInstaller.configured(
AppStorageKeys.FinderTools.diskImageInstaller))
Text(
"When a mounted DMG contains exactly one verified app, Edith can install it in Applications, eject the image, and move the unchanged download to Trash. Edith rechecks the app before copying and never replaces an existing app."
)
.settingsCaption()
}

Section("Access") {
LabeledContent("Accessibility", value: "Finder keyboard shortcuts")
LabeledContent("Automation", value: "Finder selection and destination")
Text(
"Accessibility is only needed for keyboard features. Finder Automation is requested on first use of selection or destination access. Disk image installation needs neither permission nor Full Disk Access."
)
.settingsCaption()
}
}
.disabled(!enabled)
.opacity(enabled ? 1 : 0.5)
}
}
37 changes: 37 additions & 0 deletions Packages/Edith/Sources/EdithCore/ExtensionLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,43 @@ public enum ExtensionLifecycleCatalog {
"list", "List recent copies", "Confirm clipboard history can be read.",
"ed clipboard ls --json")
]),
descriptor(
"finderTools", "Make Finder file operations faster without replacing Finder.",
workflows: [
instruction(
"shortcuts", "Use Finder shortcuts",
"Move files with Command-X and Command-V, rename with F2, and paste images as PNG files."
),
instruction(
"install", "Install from disk images",
"Install the single verified app on a mounted DMG, then eject and trash the download."
),
],
prerequisites: [
instruction(
"access", "Grant Accessibility",
"Accessibility lets Edith handle Finder-only keyboard shortcuts.",
"ed permissions request accessibility")
],
examples: [
"ed extensions enable finderTools",
"ed config ls --group findertools --json",
],
docs: [
documentation("guide", "Finder Tools guide", "docs/cli/finder-tools/README.md")
],
recovery: [
instruction(
"doctor", "Check Finder Tools readiness",
"Inspect the helper, permissions, and enabled Finder features.",
"ed extensions doctor finderTools --json")
],
verification: [
instruction(
"status", "Inspect Finder Tools",
"Confirm the helper and selected features are ready.",
"ed extensions status finderTools --json")
]),
descriptor(
"keystrokeHighlight",
"Show keyboard input as clear keycaps that disappear automatically.",
Expand Down
6 changes: 6 additions & 0 deletions Packages/Edith/Sources/EdithCore/ExtensionRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ public enum ExtensionRegistry {
symbolName: "doc.on.clipboard", suite: .desk, host: .bar, featured: true,
defaultsKey: "clipboardEnabled", requiredCapabilities: [.clipboardHistory],
optionalCapabilities: [.globalPaste, .globalShortcuts]),
ExtensionRegistryEntry(
id: "finderTools", title: "Finder Tools",
subtitle: "Cut and paste, F2 rename, image paste, and safe DMG installs.",
symbolName: "folder.badge.gearshape", suite: .desk, host: .bar, featured: false,
defaultsKey: "finderToolsEnabled",
requiredCapabilities: [.globalShortcuts, .runningApplications]),
ExtensionRegistryEntry(
id: "emoji", title: "Emoji Picker",
subtitle: "Every macOS emoji on a hotkey, straight into the app you are typing in.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class AppServices {
private(set) var notchShelf: NotchShelfController?
private(set) var colorPicker: ColorPickerStore?
private(set) var clipboard: ClipboardStore?
private(set) var finderTools: FinderToolsService?
private(set) var emoji: EmojiStore?
private(set) var keystrokeHighlight: KeystrokeHighlightRuntime?
private(set) var focusDim: FocusDimEngine?
Expand Down Expand Up @@ -111,6 +112,7 @@ final class AppServices {
shutDownEmojiRuntime()
keystrokeHighlight?.shutdown()
if #available(macOS 14.4, *) { MixerEngine.shared.shutdown() }
finderTools?.shutdown()
await lidAwake?.shutdownForTermination()
await lidAwakeRestorationGate.wait()
}
Expand Down Expand Up @@ -315,6 +317,15 @@ final class AppServices {
notchShelf?.attachUsage(usage)
notchShelf?.attachCalendar(calendar)
notchShelf?.attachColorPicker(colorPicker)

let finderToolsOn =
ExtensionRegistry.entry("finderTools")?.isEnabled(in: SharedDefaults.store) == true
if finderToolsOn, finderTools == nil { finderTools = FinderToolsService() }
if !finderToolsOn {
finderTools?.shutdown()
finderTools = nil
}
finderTools?.syncSettings()
}

private func shutDownEmojiRuntime() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ struct EdithApp {
SharedDefaults.store.string(forKey: AppStorageKeys.General.appearance) ?? "system")
services.sync()
}
_ = IPC.observe(IPC.Name.permissionsRefreshed) {
services.finderTools?.syncSettings()
}
_ = IPC.observe(IPC.Name.requestEmojiPanel) {
MainActor.assumeIsolated { EmojiPanel.shared.toggle() }
}
Expand Down
Loading
Loading