Skip to content
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to OpenWritr will be documented in this file.
Each release's notes on GitHub are generated from its section here, and the
broker publication handoff fails when the section for a tag is missing or empty.

## [1.7.3] — 2026-10-10
## [1.7.3] — 2026-10-13

### Fixed
- Switching between Dock Only and Dock and Menu Bar no longer fails. macOS refuses to set an activation policy that is already in effect, and both Dock modes use the same one, so the change was rejected
Expand All @@ -14,6 +14,9 @@ broker publication handoff fails when the section for a tag is missing or empty.
### Changed
- Microphone error logs now include a privacy-safe device summary (how the microphone was chosen, transport such as Bluetooth or USB, channels, sample rate, whether another app is using it, and microphone permission) so Core Audio failures can be diagnosed without asking for details. Device names and identifiers are never logged

### Added
- Experimental setting **Type Text in Remote Desktop Apps**. Windows App copies the Mac clipboard to the remote PC only after you leave its window, so an automatic paste could insert old text. With the setting on, and Windows App frontmost, OpenWritr types the text key by key using your current Mac keyboard layout instead and leaves the clipboard untouched. The remote PC needs the same layout; characters without a plain key (emoji, Option-layer symbols) are skipped. Off by default

## [1.7.2] — 2026-10-09

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<key>CFBundleIdentifier</key>
<string>com.openwritr.app</string>
<key>CFBundleVersion</key>
<string>1.7.2</string>
<string>1.7.3</string>
<key>CFBundleShortVersionString</key>
<string>1.7.2</string>
<string>1.7.3</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 Torsten Mahr. Released under the MIT License.</string>
<key>OpenWritrDescription</key>
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ Clipboard-only works even with Auto-Paste off. Successful copying intentionally

Normal Auto-Paste restores the original clipboard, including an empty clipboard, unless another application or the user has changed it. Output errors are shown explicitly; delayed restoration errors appear as a dismissible menu warning. With Auto-Paste off, standard recordings remain inside OpenWritr and are not copied automatically.

### Remote desktop apps (experimental)

Windows App copies the Mac clipboard to the remote PC only after you leave its window, even with clipboard redirection set to Bidirectional. A paste into the remote session can therefore insert old text. Turn on **Type Text in Remote Desktop Apps (Experimental)** in Settings: while Windows App is frontmost, Auto-Paste then types the text key by key instead of pasting, and your clipboard is not touched. It applies only to Windows App (`com.microsoft.rdc.macos`), only with Auto-Paste on, and not to clipboard-only recordings (Option), which still copy.

Windows App sends physical key codes and ignores the Unicode text of a key event, so OpenWritr presses the keys that produce each character on your **current Mac keyboard layout**. The remote PC must use the same keyboard layout, otherwise accents and symbols come out wrong. Only characters on the two base layers (no modifier, Shift) are typed: typographic quotes, dashes, and ellipses are replaced by their plain equivalents, and characters that need Option (such as `@`, `€`, or brackets on a German layout), dead keys, or emoji have no equivalent on the remote PC's layout and are skipped. The number of skipped characters is logged; the full text stays available in OpenWritr. Typing runs in the background and stops as soon as another app becomes frontmost, so the rest of the text is never typed into a different app.

The option is off by default. It was verified by the maintainer against a real Windows 365 Cloud PC session through Windows App 11.4.0 with matching German keyboard layouts; other remote layouts, clients, and character sets have not been tested. Test it in a harmless text field first. Typing needs the same Accessibility permission as pasting.

## Install

Download the latest signed app from [Releases](https://github.com/trsdn/OpenWritr/releases), unzip it, and move `OpenWritr.app` to `/Applications`.
Expand Down
21 changes: 20 additions & 1 deletion Sources/OpenWritr/OpenWritrApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ final class AppViewModel {
var debugModeEnabled: Bool = false
var soundEnabled: Bool = true
var autoPasteEnabled: Bool = true
/// Experimental: type instead of paste when a remote desktop app is frontmost.
var typeIntoRemoteDesktopsEnabled: Bool = false
var launchAtLogin: Bool = false
var appPresence: AppPresence
var hotkeyChoice: HotkeyChoice = .fn
Expand Down Expand Up @@ -484,6 +486,9 @@ final class AppViewModel {
if defaults.object(forKey: "autoPasteEnabled") != nil {
autoPasteEnabled = defaults.bool(forKey: "autoPasteEnabled")
}
if defaults.object(forKey: "typeIntoRemoteDesktopsEnabled") != nil {
typeIntoRemoteDesktopsEnabled = defaults.bool(forKey: "typeIntoRemoteDesktopsEnabled")
}
if let raw = defaults.string(forKey: "hotkeyChoice"),
let choice = HotkeyChoice(rawValue: raw) {
hotkeyChoice = choice
Expand Down Expand Up @@ -1671,7 +1676,8 @@ final class AppViewModel {
if pasteManager.outputText(
text,
destination: outputDestination,
autoPasteEnabled: autoPasteEnabled
autoPasteEnabled: autoPasteEnabled,
typeIntoRemoteDesktops: typeIntoRemoteDesktopsEnabled
) == .cancelled {
if let (operationID, _) = captureOperation {
clearCaptureOperation(ifCurrent: operationID)
Expand Down Expand Up @@ -1723,6 +1729,19 @@ final class AppViewModel {
)
return
}
if pasteManager.lastError == .typingFailed {
presentRuntimeError(
AppErrorPresentation(
kind: .paste,
title: "Typing Failed",
message: pasteManager.lastError?.localizedDescription
?? "OpenWritr could not type the transcript.",
recoverySuggestion: "The transcript is retained in OpenWritr. Check that your keyboard layout has keys for the text and that Accessibility access is granted, then try again."
),
overlayMessage: "Typing failed"
)
return
}
presentRuntimeError(
AppErrorPresentation(
kind: .paste,
Expand Down
40 changes: 37 additions & 3 deletions Sources/OpenWritr/PasteManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,16 @@ protocol TextPasting: AnyObject {
func pasteText(_ text: String) -> PasteOutcome
@discardableResult
func copyText(_ text: String) -> PasteOutcome
/// Types `text` instead of pasting when the frontmost app is a remote desktop client, or
/// returns nil so the caller falls back to pasting.
func typeTextIfRemoteDesktop(_ text: String) -> PasteOutcome?
func flushPendingRestore()
}

extension TextPasting {
func typeTextIfRemoteDesktop(_ text: String) -> PasteOutcome? { nil }
}

enum PasteOutcome: Sendable, Equatable {
case pasted
case copied
Expand All @@ -71,9 +78,12 @@ enum PasteManagerError: LocalizedError, Sendable, Equatable {
case writeFailed
case restoreFailed
case rollbackFailed
case typingFailed

var errorDescription: String? {
switch self {
case .typingFailed:
return "OpenWritr could not type the text into the remote desktop app."
case .unreadableRepresentation(let type):
return "OpenWritr could not preserve clipboard content of type \(type)."
case .preparationFailed:
Expand All @@ -94,13 +104,18 @@ extension TextPasting {
func outputText(
_ text: String,
destination: RecordingOutputDestination,
autoPasteEnabled: Bool
autoPasteEnabled: Bool,
typeIntoRemoteDesktops: Bool = false
) -> PasteOutcome {
switch destination {
case .clipboard:
return copyText(text)
case .standard:
return autoPasteEnabled ? pasteText(text) : .retained
guard autoPasteEnabled else { return .retained }
if typeIntoRemoteDesktops, let outcome = typeTextIfRemoteDesktop(text) {
return outcome
}
return pasteText(text)
}
}
}
Expand Down Expand Up @@ -211,17 +226,23 @@ final class PasteManager: TextPasting {
private let commandPoster: any PasteCommandPosting
private let restoreScheduler: any PasteRestoreScheduling
private let errorLogger: any ErrorLogging
private let textTyper: any TextTyping
private let remoteDesktopDetector: any RemoteDesktopDetecting

init(
pasteboard: any PasteboardManaging = SystemPasteboard(),
commandPoster: any PasteCommandPosting = SystemPasteCommandPoster(),
restoreScheduler: any PasteRestoreScheduling = SystemPasteRestoreScheduler(),
errorLogger: any ErrorLogging = UnifiedErrorLogger(category: "PasteManager")
errorLogger: any ErrorLogging = UnifiedErrorLogger(category: "PasteManager"),
textTyper: any TextTyping = SystemTextTyper(),
remoteDesktopDetector: any RemoteDesktopDetecting = SystemRemoteDesktopDetector()
) {
self.pasteboard = pasteboard
self.commandPoster = commandPoster
self.restoreScheduler = restoreScheduler
self.errorLogger = errorLogger
self.textTyper = textTyper
self.remoteDesktopDetector = remoteDesktopDetector
}

@discardableResult
Expand All @@ -234,6 +255,19 @@ final class PasteManager: TextPasting {
writeText(text, destination: .clipboard)
}

func typeTextIfRemoteDesktop(_ text: String) -> PasteOutcome? {
guard remoteDesktopDetector.isRemoteDesktopFrontmost() else { return nil }

lastError = nil
// The clipboard is not used, but a restore left over from an earlier paste is still due.
flushPendingRestore()
guard textTyper.typeText(text) else {
recordError(.typingFailed, message: "Failed to type text into the remote desktop app")
return .cancelled
}
return .pasted
}

private func writeText(_ text: String, destination: RecordingOutputDestination) -> PasteOutcome {
if hasUnreportedRestoreFailure {
hasUnreportedRestoreFailure = false
Expand Down
19 changes: 19 additions & 0 deletions Sources/OpenWritr/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ struct SettingsView: View {
}
))

Toggle("Type Text in Remote Desktop Apps (Experimental)", isOn: Binding(
get: { viewModel.typeIntoRemoteDesktopsEnabled },
set: {
viewModel.typeIntoRemoteDesktopsEnabled = $0
viewModel.savePreference("typeIntoRemoteDesktopsEnabled", value: $0)
}
))
.disabled(!viewModel.autoPasteEnabled)

Text(
"Windows App copies your clipboard to the remote PC only after you leave its window, "
+ "so a paste can insert old text. When Windows App is frontmost, OpenWritr types "
+ "the text key by key instead and leaves your clipboard untouched. The remote PC "
+ "needs the same keyboard layout as this Mac. Emoji and symbols without a key are skipped."
)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)

Toggle("Sound Effects", isOn: Binding(
get: { viewModel.soundEnabled },
set: {
Expand Down
Loading
Loading