Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ adheres to [Semantic Versioning](https://semver.org) and the
offers one, and says so plainly when a device exposes no mute control instead
of offering a button that does nothing. Setting a device's mute switch is not
audio capture, so it needs no microphone permission.
- **Snippets**, a new menu bar tool: a searchable library of reusable text.
Click a snippet (or press ⏎ on the top search hit) to copy it and paste it
straight into the app you were working in, the way Clipboard History does.
Snippets are created, edited, reordered and deleted in the popover and stored
locally with owner-only permissions. Each one can opt into date/time
placeholders — `{date}`, `{time}` and `{datetime}` are replaced as it pastes;
anything else in braces is left alone. Pasting needs Accessibility access;
without it the snippet is still copied and the popover says so.
- Continuous integration: every push and pull request now builds the package
and runs the full test suite on a pinned macOS runner, using the same
toolchain the release workflow ships with. Previously only version tags ran
Expand Down
11 changes: 11 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ let package = Package(
.executable(
name: "DMonteMicControl",
targets: ["DMonteMicControl"]
),
.executable(
name: "DMonteSnippets",
targets: ["DMonteSnippets"]
)
],
dependencies: [
Expand Down Expand Up @@ -264,6 +268,13 @@ let package = Package(
],
path: "Sources/DMonteMicControlApp"
),
.executableTarget(
name: "DMonteSnippets",
dependencies: [
"DMonteCore"
],
path: "Sources/DMonteSnippetsApp"
),
.testTarget(
name: "DMonteCoreTests",
dependencies: ["DMonteCore"],
Expand Down
32 changes: 32 additions & 0 deletions Packaging/SnippetsInfo.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>DMonteSnippets</string>
<key>CFBundleIdentifier</key>
<string>com.havokentity.mactools.snippets</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDisplayName</key>
<string>DMonte Snippets</string>
<key>CFBundleName</key>
<string>DMonte Snippets</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.13.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSMultipleInstancesProhibited</key>
<true/>
<key>LSUIElement</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Yahushad Monte</string>
</dict>
</plist>
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ The app updates itself automatically via [Sparkle](https://sparkle-project.org);
| **Keep Awake** | Prevent sleep, optionally for a set duration |
| **Maintenance** | Handy Finder/system toggles and cache/index refreshes |
| **Dev Tools** | JSON, Base64, JWT, URL, hashing, UUID, timestamp, and case utilities |
| **Dev Tools** | JSON, Base64, URL, hashing, UUID, timestamp, and case utilities |
| **Snippets** | A searchable library of reusable text — click one to paste it into the app you came from |

### Permissions

A few tools ask macOS for access the first time you use them, and degrade gracefully if you decline:

- **Window Manager** — Accessibility (to move other apps' windows)
- **Clipboard History** — Accessibility (to paste into the active app)
- **Clipboard History** / **Snippets** — Accessibility (to paste into the active app)
- **Grab Text** / **QR Studio** (screen scan) — Screen Recording
- **Volume Mixer** — System Audio Recording (`NSAudioCaptureUsageDescription`, to tap each app's audio for per‑app volume and routing)
- **Calendar** — Calendar access
Expand Down
1 change: 1 addition & 0 deletions Scripts/package_app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ HELPERS=(
"DMonteFocusTimer|DMonte Focus Timer.app|FocusTimerInfo.plist"
"DMonteWindowManager|DMonte Window Manager.app|WindowManagerInfo.plist"
"DMonteMicControl|DMonte Mic Control.app|MicControlInfo.plist"
"DMonteSnippets|DMonte Snippets.app|SnippetsInfo.plist"
)

stamp_version() {
Expand Down
30 changes: 30 additions & 0 deletions Sources/DMonteCore/ClipboardPaste.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ public enum ClipboardPaste {
// it is — the caller would show the permission banner.
guard copyToPasteboard(entry, store: store, asPlainText: asPlainText) else { return true }

return pasteCopiedContent(into: target)
}

/// Puts plain `text` on the pasteboard and pastes it into `target`, stamped as coming from
/// `sourceBundleID` (the nspasteboard.org convention every clipboard manager reads). Shared
/// by the tools that paste text they generated rather than text they captured — Snippets —
/// so the permission handling and the ⌘V synthesis have exactly one implementation.
///
/// Same contract as `paste(_:store:asPlainText:into:)`: false means only that Accessibility
/// is missing; the text is on the clipboard either way, ready for a manual ⌘V.
@discardableResult
public static func pasteText(
_ text: String,
sourceBundleID: String,
into target: NSRunningApplication?
) -> Bool {
let item = NSPasteboardItem()
item.setString(text, forType: .string)
item.setString(sourceBundleID, forType: sourceType)

let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.writeObjects([item])

return pasteCopiedContent(into: target)
}

/// Activates `target` and synthesizes ⌘V into it. Assumes the pasteboard already holds what
/// should land there.
private static func pasteCopiedContent(into target: NSRunningApplication?) -> Bool {
guard hasAccessibilityPermission else {
promptForAccessibilityPermission()
return false
Expand Down
55 changes: 55 additions & 0 deletions Sources/DMonteCore/HelperMainMenu.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import AppKit

/// Installs the minimal main menu an agent app needs for text editing to work.
///
/// `LSUIElement` apps have no visible menu bar, which makes it easy to conclude they do not need
/// a main menu at all. They do: AppKit dispatches ⌘C / ⌘V / ⌘X / ⌘A / ⌘Z by matching the event
/// against the **main menu's key equivalents** before anything else sees it. With no main menu
/// there is nothing to match, so those shortcuts are silently dead in every text field the tool
/// presents — the field accepts typing, and copy and paste do nothing at all.
///
/// The menu is never drawn (there is no menu bar to draw it in); it exists purely so the key
/// equivalents resolve. The items use `nil` targets so each one travels the responder chain and
/// lands on whichever text view is focused.
@MainActor
public enum HelperMainMenu {

/// Idempotent: a helper that calls this twice, or one that already built its own menu, keeps
/// the menu it has.
public static func installEditMenuIfNeeded() {
guard NSApp.mainMenu == nil else { return }

let mainMenu = NSMenu()

// An application menu has to be first — AppKit treats item 0 as the app menu and will not
// look for key equivalents in it. Without this placeholder the Edit menu would occupy that
// slot and its shortcuts would go unmatched, which is the bug this type exists to fix.
let appItem = NSMenuItem()
appItem.submenu = NSMenu()
mainMenu.addItem(appItem)

let editItem = NSMenuItem()
let editMenu = NSMenu(title: "Edit")
editItem.submenu = editMenu
mainMenu.addItem(editItem)

// Selectors are built from strings rather than `#selector`: `copy(_:)` and friends are
// declared on several AppKit classes plus `NSObject.copy()`, so the literal is both
// unambiguous and exactly what the responder chain matches on.
editMenu.addItem(withTitle: "Undo", action: Selector(("undo:")), keyEquivalent: "z")
editMenu.addItem(withTitle: "Redo", action: Selector(("redo:")), keyEquivalent: "Z")
editMenu.addItem(.separator())
editMenu.addItem(withTitle: "Cut", action: Selector(("cut:")), keyEquivalent: "x")
editMenu.addItem(withTitle: "Copy", action: Selector(("copy:")), keyEquivalent: "c")
editMenu.addItem(withTitle: "Paste", action: Selector(("paste:")), keyEquivalent: "v")
editMenu.addItem(
withTitle: "Paste and Match Style",
action: Selector(("pasteAsPlainText:")),
keyEquivalent: "V"
)
editMenu.addItem(.separator())
editMenu.addItem(withTitle: "Select All", action: Selector(("selectAll:")), keyEquivalent: "a")

NSApp.mainMenu = mainMenu
}
}
Loading
Loading