Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ struct CaptureToolsRows: View {
var historySize = 10
@AppStorage(AppStorageKeys.Capture.saveScreenshots, store: SharedDefaults.store) private
var saveScreenshots = false
@AppStorage(AppStorageKeys.Capture.copyAfterCapture, store: SharedDefaults.store) private
var copyAfterCapture = false
@AppStorage(AppStorageKeys.Capture.saveFolder, store: SharedDefaults.store) private
var saveFolder = ""
@AppStorage(AppStorageKeys.Capture.filenameTemplate, store: SharedDefaults.store) private
var filenameTemplate = CaptureFilenameTemplate.fallback
@State private var history: [CaptureRecognition] = []

var body: some View {
Expand All @@ -23,21 +29,62 @@ struct CaptureToolsRows: View {
_ = CaptureToolOperationExecution.request(.read)
}
.buttonStyle(.edith(.primary))
Button("Screenshot") {
_ = CaptureToolOperationExecution.request(.screenshot)
Button("Area") {
_ = CaptureToolOperationExecution.request(.area)
}
.buttonStyle(.edith(.secondary))
Button("Window") {
_ = CaptureToolOperationExecution.request(.window)
}
.buttonStyle(.edith(.secondary))
Button("Screen") {
_ = CaptureToolOperationExecution.request(.screen)
}
.buttonStyle(.edith(.secondary))
Button("Library") {
_ = CaptureToolOperationExecution.request(.library)
}
.buttonStyle(.edith(.secondary))
}
LabeledContent("Read shortcut") {
HotKeyRecorderControl(
keyPrefix: "captureReadHotKey", defaultLabel: "⌃⌥⌘R")
}
LabeledContent("Screenshot shortcut") {
LabeledContent("Area shortcut") {
HotKeyRecorderControl(
keyPrefix: "captureScreenshotHotKey", defaultLabel: "⌃⌥⌘S")
}
}

Section("Saving") {
Toggle(
"Copy screenshots automatically",
isOn: $copyAfterCapture.configured(AppStorageKeys.Capture.copyAfterCapture))
LabeledContent("Save folder") {
HStack {
Text(saveFolder.isEmpty ? "Pictures/Edith Captures" : saveFolder)
.lineLimit(1)
.truncationMode(.middle)
Button("Choose") { chooseSaveFolder() }
.buttonStyle(.edith(.secondary))
if !saveFolder.isEmpty {
Button("Reset") {
saveFolder = ""
IPC.post(IPC.Name.settingsChanged)
}
.buttonStyle(.edith(.borderless))
}
}
}
TextField(
"Filename template", text: $filenameTemplate,
prompt: Text(CaptureFilenameTemplate.fallback)
)
.onSubmit { IPC.post(IPC.Name.settingsChanged) }
Text("Available tokens: {date}, {time}, {timestamp}, and {mode}.")
.settingsCaption()
}

Section("Recognition") {
Picker(
"Copy after reading",
Expand Down Expand Up @@ -118,4 +165,16 @@ struct CaptureToolsRows: View {
NSPasteboard.general.setString(value, forType: .string)
IPC.post(IPC.Name.clipboardChanged)
}

private func chooseSaveFolder() {
let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = false
panel.prompt = "Choose"
if panel.runModal() == .OK, let url = panel.url {
saveFolder = url.path
IPC.post(IPC.Name.settingsChanged)
}
}
}
5 changes: 4 additions & 1 deletion Packages/Edith/Sources/EdithCLI/CommandTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,10 @@ public enum CommandTree {

static let specs: [String: Spec] = [
"ed capture read": Spec(options: ["--json", "--help"]),
"ed capture screenshot": Spec(options: ["--json", "--help"]),
"ed capture area": Spec(options: ["--json", "--help"]),
"ed capture window": Spec(options: ["--json", "--help"]),
"ed capture screen": Spec(options: ["--json", "--help"]),
"ed capture library": Spec(options: ["--json", "--help"]),
"ed capture": Spec(options: ["--json", "--help"]),
"ed": Spec(options: ["--help", "--version"]),
"ed guide": Spec(options: ["--json"], arguments: [.guideTopic]),
Expand Down
54 changes: 47 additions & 7 deletions Packages/Edith/Sources/EdithCLI/Commands/CaptureCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import EdithKit
struct CaptureCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "capture",
abstract: "Offline screen recognition and quick screenshots.",
subcommands: [CaptureReadCommand.self, CaptureScreenshotCommand.self],
abstract: "Screen recognition, screenshots, and recent captures.",
subcommands: [
CaptureReadCommand.self, CaptureAreaCommand.self, CaptureWindowCommand.self,
CaptureScreenCommand.self, CaptureLibraryCommand.self,
],
defaultSubcommand: CaptureReadCommand.self)
}

Expand All @@ -20,7 +23,8 @@ enum CaptureCommandBridge {
"the Capture Tools extension is off",
hint: "run `ed extensions enable captureTools`, then retry")
}
try AppBridge.requireHelper("capturing the screen")
try AppBridge.requireHelper(
operation == .library ? "opening recent captures" : "capturing the screen")
let descriptor = CaptureToolOperationExecution.request(operation) {
AppBridge.post($0)
}
Expand All @@ -32,7 +36,7 @@ enum CaptureCommandBridge {
]))
return
}
CLIOut.out(operation == .read ? "screen read requested" : "screenshot requested")
CLIOut.out("\(operation.rawValue) requested")
}
}
}
Expand All @@ -49,14 +53,50 @@ struct CaptureReadCommand: AsyncParsableCommand {
}
}

struct CaptureScreenshotCommand: AsyncParsableCommand {
struct CaptureAreaCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "screenshot", abstract: "Select screen content for a quick preview.")
commandName: "area", abstract: "Capture a selected area.")

@Flag(name: .long, help: "Emit JSON on stdout.")
var json = false

func run() async throws {
try await CaptureCommandBridge.request(.screenshot, json: json)
try await CaptureCommandBridge.request(.area, json: json)
}
}

struct CaptureWindowCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "window", abstract: "Capture a selected window.")

@Flag(name: .long, help: "Emit JSON on stdout.")
var json = false

func run() async throws {
try await CaptureCommandBridge.request(.window, json: json)
}
}

struct CaptureScreenCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "screen", abstract: "Capture the full main display.")

@Flag(name: .long, help: "Emit JSON on stdout.")
var json = false

func run() async throws {
try await CaptureCommandBridge.request(.screen, json: json)
}
}

struct CaptureLibraryCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "library", abstract: "Open recent captures.")

@Flag(name: .long, help: "Emit JSON on stdout.")
var json = false

func run() async throws {
try await CaptureCommandBridge.request(.library, json: json)
}
}
4 changes: 4 additions & 0 deletions Packages/Edith/Sources/EdithCLI/Guide.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public enum Guide {
ed color pick open Edith's system colour sampler
ed color copy 1 --format hex
ed capture read copy text or codes from a selected screen region
ed capture area capture a selected screen region
ed capture window capture a selected window
ed capture screen capture the full main display
ed capture library open recent captures
ed capture screenshot open a quick preview of a selected screen region
ed emoji pick open Edith's emoji picker
ed usage sources the agents that produced your usage history
Expand Down
22 changes: 17 additions & 5 deletions Packages/Edith/Sources/EdithCore/ExtensionLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -966,16 +966,28 @@ public enum ExtensionLifecycleCatalog {
"Confirm the color history repository responds.", "ed color ls --json")
]),
descriptor(
"captureTools", "Read text and codes from the screen without sending pixels anywhere.",
"captureTools", "Capture, recognize, edit, and keep screenshots locally.",
workflows: [
instruction(
"read", "Read the screen",
"Select a region, recognize its text and codes, and copy the result.",
"ed capture read"),
instruction(
"screenshot", "Take a quick screenshot",
"Select a region and open a lightweight copy and save preview.",
"ed capture screenshot"),
"area", "Capture an area",
"Select a region and open the Capture Studio quick preview.",
"ed capture area"),
instruction(
"window", "Capture a window",
"Select a window and keep it in the recent captures library.",
"ed capture window"),
instruction(
"screen", "Capture the full screen",
"Capture the main display and open the quick preview.",
"ed capture screen"),
instruction(
"library", "Open recent captures",
"Copy, save, drag, edit, pin, or delete recent PNGs.",
"ed capture library"),
],
prerequisites: [
instruction(
Expand All @@ -985,7 +997,7 @@ public enum ExtensionLifecycleCatalog {
],
examples: [
"ed extensions enable captureTools", "ed capture read --json",
"ed capture screenshot --json",
"ed capture area --json", "ed capture library --json",
],
docs: [documentation("guide", "Capture Tools guide", "docs/cli/capture/README.md")],
recovery: [
Expand Down
2 changes: 1 addition & 1 deletion Packages/Edith/Sources/EdithCore/ExtensionRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ public enum ExtensionRegistry {
defaultsKey: "tabSEOAuditEnabled", requiredCapabilities: [.siteAuditing]),
ExtensionRegistryEntry(
id: "captureTools", title: "Capture Tools",
subtitle: "Offline screen OCR, QR recognition, and quick screenshots.",
subtitle: "Local screenshots, recognition, editing, pinning, and recent captures.",
symbolName: "viewfinder", suite: .media, host: .bar, featured: false,
defaultsKey: "captureToolsEnabled",
requiredCapabilities: [.screenContentRecognition],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -725,9 +725,28 @@ struct RootView: View {
}
Button {
dismissPanel()
captureTools.start(.screenshot)
captureTools.start(.area)
} label: {
Label("Screenshot", systemImage: "camera.viewfinder")
Label("Capture area", systemImage: "crop")
}
Button {
dismissPanel()
captureTools.start(.window)
} label: {
Label("Capture window", systemImage: "macwindow")
}
Button {
dismissPanel()
captureTools.start(.screen)
} label: {
Label("Capture screen", systemImage: "display")
}
Divider()
Button {
dismissPanel()
captureTools.start(.library)
} label: {
Label("Recent captures", systemImage: "photo.stack")
}
} label: {
Image(systemName: "viewfinder")
Expand Down
Loading
Loading