diff --git a/Sources/Trimmy/AccessibilityPermissionCallout.swift b/Sources/Trimmy/AccessibilityPermissionCallout.swift index d8fd738..0b2a3b4 100644 --- a/Sources/Trimmy/AccessibilityPermissionCallout.swift +++ b/Sources/Trimmy/AccessibilityPermissionCallout.swift @@ -10,11 +10,11 @@ struct AccessibilityPermissionCallout: View { VStack(alignment: .leading, spacing: 8) { Label { VStack(alignment: .leading, spacing: 2) { - Text("Accessibility needed to paste") + Text("Bedienungshilfen für Einfügen benötigt") .font(.callout.weight(.semibold)) Text( - "Enable Trimmy in System Settings → Privacy & Security → Accessibility " - + "so ⌘V can be sent to the front app.") + "Trimmy in Systemeinstellungen → Datenschutz & Sicherheit → Bedienungshilfen " + + "aktivieren, damit ⌘V an die aktive App gesendet werden kann.") .font(.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -25,13 +25,13 @@ struct AccessibilityPermissionCallout: View { } HStack(spacing: 10) { - Button("Grant Accessibility") { + Button("Bedienungshilfen erlauben") { self.permissions.requestPermissionPrompt() } .buttonStyle(.borderedProminent) .controlSize(self.compactButtons ? .small : .regular) - Button("Open Settings") { + Button("Einstellungen öffnen") { self.permissions.openSystemSettings() } .buttonStyle(.bordered) diff --git a/Sources/Trimmy/GeneralAggressiveness.swift b/Sources/Trimmy/GeneralAggressiveness.swift index e4ec3c8..31dae94 100644 --- a/Sources/Trimmy/GeneralAggressiveness.swift +++ b/Sources/Trimmy/GeneralAggressiveness.swift @@ -12,7 +12,7 @@ public enum GeneralAggressiveness: String, CaseIterable, Identifiable, Codable, public var title: String { switch self { case .none: - "None (no auto-trim)" + "Keine (kein Auto-Trim)" case .low: Aggressiveness.low.title case .normal: @@ -24,7 +24,7 @@ public enum GeneralAggressiveness: String, CaseIterable, Identifiable, Codable, public var titleShort: String { switch self { - case .none: "None" + case .none: "Keine" case .low: Aggressiveness.low.titleShort case .normal: Aggressiveness.normal.titleShort case .high: Aggressiveness.high.titleShort @@ -34,7 +34,7 @@ public enum GeneralAggressiveness: String, CaseIterable, Identifiable, Codable, public var blurb: String { switch self { case .none: - "Skip auto-flattening for non-terminal apps. Manual “Paste Trimmed” still uses High." + "Auto-Flattening für Nicht-Terminal-Apps überspringen. Manuelles “Getrimmt einfügen” nutzt weiterhin Hoch.” case .low: Aggressiveness.low.blurb case .normal: diff --git a/Sources/Trimmy/MenuContentView.swift b/Sources/Trimmy/MenuContentView.swift index f43c1c8..e1fae29 100644 --- a/Sources/Trimmy/MenuContentView.swift +++ b/Sources/Trimmy/MenuContentView.swift @@ -40,15 +40,15 @@ struct MenuContentView: View { Text("Auto-Trim") } .toggleStyle(.checkbox) - Button("Settings…") { + Button("Einstellungen …") { self.open(tab: .general) } .keyboardShortcut(",", modifiers: [.command]) - Button("About Trimmy") { + Button("Über Trimmy") { self.open(tab: .about) } if self.updater.isAvailable, self.updateStatus.isUpdateReady { - Button("Update ready, restart now?") { self.updater.checkForUpdates(nil) } + Button("Update bereit, jetzt neu starten?") { self.updater.checkForUpdates(nil) } } } .padding(.vertical, 6) @@ -139,7 +139,7 @@ struct MenuContentView: View { extension MenuContentView { private var pasteButtons: some View { VStack(alignment: .leading, spacing: 4) { - Button("Paste Trimmed to \(self.targetAppLabel)\(self.trimmedStatsSuffix)") { + Button("Getrimmt einfügen in \(self.targetAppLabel)\(self.trimmedStatsSuffix)") { self.handlePasteTrimmed() } .applyKeyboardShortcut(self.pasteTrimmedKeyboardShortcut) @@ -155,7 +155,7 @@ extension MenuContentView { let markdownPreviewSource = self.markdownPreviewSource { let markdownStatsSuffix = self.statsSuffix(for: markdownPreviewSource, showTruncations: true) - Button("Paste Reformatted Markdown to \(self.targetAppLabel)\(markdownStatsSuffix)") { + Button("Markdown neu formatiert einfügen in \(self.targetAppLabel)\(markdownStatsSuffix)") { self.handlePasteReformattedMarkdown() } Text(self.markdownPreviewLine(for: markdownPreviewSource)) @@ -167,7 +167,7 @@ extension MenuContentView { .frame(maxWidth: 260, alignment: .leading) } - Button("Paste Original to \(self.targetAppLabel)\(self.originalStatsSuffix)") { + Button("Original einfügen in \(self.targetAppLabel)\(self.originalStatsSuffix)") { self.handlePasteOriginal() } .applyKeyboardShortcut(self.pasteOriginalKeyboardShortcut) @@ -207,7 +207,7 @@ extension MenuContentView { original.count > trimmed.count { let removed = original.count - trimmed.count - return "\(base) · \(removed) trimmed" + return "\(base) · \(removed) getrimmt" } return base } diff --git a/Sources/Trimmy/SettingsAboutPane.swift b/Sources/Trimmy/SettingsAboutPane.swift index 1e4ecf0..b2b5a0d 100644 --- a/Sources/Trimmy/SettingsAboutPane.swift +++ b/Sources/Trimmy/SettingsAboutPane.swift @@ -59,11 +59,11 @@ struct AboutPane: View { if let buildTimestamp { let git = Bundle.main.object(forInfoDictionaryKey: "TrimmyGitCommit") as? String let suffix = Self.buildSuffix(for: git) - Text("Built \(buildTimestamp)\(suffix)") + Text("Erstellt \(buildTimestamp)\(suffix)") .font(.footnote) .foregroundStyle(.secondary) } - Text("Paste-once, run-once clipboard cleaner for terminal snippets.") + Text("Einmal einfügen, einmal ausführen — Zwischenablage-Bereiniger für Terminal-Snippets.") .font(.footnote) .foregroundStyle(.secondary) } @@ -86,14 +86,14 @@ struct AboutPane: View { .padding(.vertical, 8) if updater.isAvailable { VStack(spacing: 10) { - Toggle("Check for updates automatically", isOn: self.$autoCheckEnabled) + Toggle("Automatisch nach Updates suchen", isOn: self.$autoCheckEnabled) .toggleStyle(.checkbox) .frame(maxWidth: .infinity, alignment: .center) - Button("Check for Updates…") { updater.checkForUpdates(nil) } + Button("Nach Updates suchen …") { updater.checkForUpdates(nil) } } } else { - Text(updater.unavailableReason ?? "Updates unavailable in this build.") + Text(updater.unavailableReason ?? "Updates in diesem Build nicht verfügbar.") .foregroundStyle(.secondary) .padding(.top, 4) } diff --git a/Sources/Trimmy/SettingsAdvancedPane.swift b/Sources/Trimmy/SettingsAdvancedPane.swift index af729b6..d5f5ba7 100644 --- a/Sources/Trimmy/SettingsAdvancedPane.swift +++ b/Sources/Trimmy/SettingsAdvancedPane.swift @@ -9,16 +9,16 @@ struct AdvancedSettingsPane: View { var body: some View { VStack(alignment: .leading, spacing: 18) { PreferenceToggleRow( - title: "Use extra clipboard fallbacks", - subtitle: "Try RTF and public text types when plain text is missing.", + title: "Zusätzliche Zwischenablage-Fallbacks", + subtitle: "RTF und öffentliche Texttypen versuchen, wenn Klartext fehlt.", binding: self.$settings.usePasteboardFallbacks) self.cliInstallerSection #if DEBUG PreferenceToggleRow( - title: "Enable debug tools", - subtitle: "Show the Debug tab for sample previews and dev-only controls.", + title: "Debug-Werkzeuge aktivieren", + subtitle: "Debug-Tab für Vorschau und Entwickler-Optionen anzeigen.", binding: self.$settings.debugPaneEnabled) #endif } @@ -35,7 +35,7 @@ struct AdvancedSettingsPane: View { if self.isInstallingCLI { ProgressView().controlSize(.small) } else { - Text("Install CLI") + Text("CLI installieren") } } .disabled(self.isInstallingCLI) @@ -48,7 +48,7 @@ struct AdvancedSettingsPane: View { } } - Text("Install `trimmy` into /usr/local/bin and /opt/homebrew/bin.") + Text("`trimmy` nach /usr/local/bin und /opt/homebrew/bin installieren.") .font(.callout) .foregroundStyle(.secondary) .lineLimit(2) @@ -69,7 +69,7 @@ struct AdvancedSettingsPane: View { .appendingPathComponent("TrimmyCLI") guard FileManager.default.isExecutableFile(atPath: helperURL.path) else { - await MainActor.run { self.cliStatus = "Helper missing; reinstall Trimmy." } + await MainActor.run { self.cliStatus = "Helper fehlt; Trimmy neu installieren." } return } @@ -106,15 +106,15 @@ struct AdvancedSettingsPane: View { process.waitUntilExit() let status: String if process.terminationStatus == 0 { - status = "Installed. Try: trimmy --help" + status = "Installiert. Teste: trimmy --help" } else { let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() let msg = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) - status = "Failed: \(msg ?? "error")" + status = "Fehlgeschlagen: \(msg ?? "Fehler")" } await MainActor.run { self.cliStatus = status } } catch { - await MainActor.run { self.cliStatus = "Failed: \(error.localizedDescription)" } + await MainActor.run { self.cliStatus = "Fehlgeschlagen: \(error.localizedDescription)" } } } } diff --git a/Sources/Trimmy/SettingsAggressivenessPane.swift b/Sources/Trimmy/SettingsAggressivenessPane.swift index 5d44b2b..4466b59 100644 --- a/Sources/Trimmy/SettingsAggressivenessPane.swift +++ b/Sources/Trimmy/SettingsAggressivenessPane.swift @@ -9,7 +9,7 @@ struct AggressivenessSettingsPane: View { VStack(alignment: .leading, spacing: 14) { Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 10) { GridRow { - Text("General apps") + Text("Allgemeine Apps") .frame(minWidth: 110, alignment: .leading) Picker("", selection: self.$settings.generalAggressiveness) { ForEach(GeneralAggressiveness.allCases) { level in @@ -36,14 +36,14 @@ struct AggressivenessSettingsPane: View { } Text( - """ - Automatic trimming uses separate aggressiveness levels for regular apps and terminals. \ - The terminal setting only applies when Context-aware trimming is enabled. “None” disables \ - command flattening for regular apps, but manual “Paste Trimmed” always runs at High. \ - Low/Normal skip code-like snippets (braces + language keywords) unless there are strong \ - command cues. Leading shell prompts (#/$) are stripped when they look like commands, but \ - Markdown-style headings stay. - """) + “”” + Automatisches Trimmen verwendet separate Aggressivitätsstufen für normale Apps und Terminals. \ + Die Terminal-Einstellung gilt nur bei aktiviertem kontextabhängigem Trimmen. „Keine” deaktiviert \ + das Befehlsflattening für normale Apps, aber manuelles „Getrimmt einfügen” nutzt immer Hoch. \ + Niedrig/Normal überspringen code-ähnliche Snippets (Klammern + Schlüsselwörter), außer bei \ + starken Befehlshinweisen. Führende Shell-Prompts (#/$) werden entfernt, wenn sie wie Befehle \ + aussehen, aber Markdown-Überschriften bleiben erhalten. + “””) .font(.footnote) .foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) @@ -80,9 +80,9 @@ struct AggressivenessPreview: View { .fixedSize(horizontal: false, vertical: true) VStack(alignment: .leading, spacing: 8) { - PreviewCard(title: "Before", text: self.example.sample) + PreviewCard(title: "Vorher", text: self.example.sample) PreviewCard( - title: "After", + title: "Nachher", text: AggressivenessPreviewEngine.previewAfter( for: self.example.sample, level: self.level.coreAggressiveness, @@ -165,13 +165,13 @@ struct AggressivenessExample { switch level { case .none: AggressivenessExample( - title: "None keeps regular app copies intact", - caption: "Auto-trim stays off for non-terminal apps.", - sample: """ + title: “Keine: Kopien aus normalen Apps bleiben unverändert”, + caption: “Auto-Trim bleibt für Nicht-Terminal-Apps deaktiviert.”, + sample: “”” brew update \\ && brew upgrade - """, - note: "Manual “Paste Trimmed” still uses High, and terminals use their own level.") + “””, + note: “Manuelles „Getrimmt einfügen” nutzt weiterhin Hoch, und Terminals verwenden ihre eigene Stufe.”) case .low: self.example(for: Aggressiveness.low) case .normal: @@ -185,33 +185,33 @@ struct AggressivenessExample { switch level { case .low: AggressivenessExample( - title: "Low only flattens obvious shell commands", - caption: "Continuations plus pipes are obvious enough to collapse.", + title: "Niedrig: Nur offensichtliche Shell-Befehle zusammenfalten", + caption: "Fortsetzungen und Pipes sind offensichtlich genug zum Zusammenfalten.", sample: """ ls -la \\ | grep '^d' \\ > dirs.txt """, - note: "Because of the continuation, pipe, and redirect, even Low collapses this into one line.") + note: "Wegen Fortsetzung, Pipe und Redirect faltet selbst Niedrig dies in eine Zeile zusammen.") case .normal: AggressivenessExample( - title: "Normal flattens typical blog commands", - caption: "Perfect for README snippets with pipes or continuations.", + title: "Normal: Typische Blog-Befehle zusammenfalten", + caption: "Perfekt für README-Snippets mit Pipes oder Fortsetzungen.", sample: """ kubectl get pods \\ -n kube-system \\ | jq '.items[].metadata.name' """, - note: "Normal trims this to a single runnable line.") + note: "Normal trimmt dies zu einer einzigen ausführbaren Zeile.") case .high: AggressivenessExample( - title: "High collapses almost anything command-shaped", - caption: "Use when you want Trimmy to be bold. Even short two-liners get flattened.", + title: "Hoch: Fast alles Befehlsähnliche zusammenfalten", + caption: "Wenn Trimmy mutig sein soll. Selbst kurze Zweizeiler werden zusammengefaltet.", sample: """ echo "hello" print status """, - note: "High trims this even though it barely looks like a command.") + note: "Hoch trimmt dies, obwohl es kaum wie ein Befehl aussieht.") } } } diff --git a/Sources/Trimmy/SettingsGeneralPane.swift b/Sources/Trimmy/SettingsGeneralPane.swift index b12dcca..5a586c3 100644 --- a/Sources/Trimmy/SettingsGeneralPane.swift +++ b/Sources/Trimmy/SettingsGeneralPane.swift @@ -12,42 +12,42 @@ struct GeneralSettingsPane: View { AccessibilityPermissionCallout(permissions: self.permissions) } PreferenceToggleRow( - title: "Auto-trim enabled", - subtitle: "Automatically trim clipboard content when it looks like a command.", + title: "Auto-Trim aktiviert", + subtitle: "Zwischenablage automatisch kürzen, wenn der Inhalt wie ein Befehl aussieht.", binding: self.$settings.autoTrimEnabled) PreferenceToggleRow( - title: "Context-aware trimming", - subtitle: "Use the terminal-specific aggressiveness when a terminal is detected " - + "(Cmd-C + app snapshot).", + title: "Kontextabhängiges Trimmen", + subtitle: "Terminal-spezifische Aggressivität verwenden, wenn ein Terminal erkannt wird " + + "(Cmd-C + App-Snapshot).", binding: self.$settings.contextAwareTrimmingEnabled) PreferenceToggleRow( - title: "Keep blank lines", - subtitle: "Preserve intentional blank lines instead of collapsing them.", + title: "Leerzeilen beibehalten", + subtitle: "Beabsichtigte Leerzeilen beibehalten statt sie zusammenzufassen.", binding: self.$settings.preserveBlankLines) PreferenceToggleRow( - title: "Remove box drawing chars (│┃)", - subtitle: "Strip prompt-style box gutters (any count, leading/trailing) before trimming.", + title: "Box-Drawing-Zeichen entfernen (│┃)", + subtitle: "Prompt-Box-Rahmen (beliebige Anzahl, führend/nachfolgend) vor dem Trimmen entfernen.", binding: self.$settings.removeBoxDrawing) PreferenceToggleRow( - title: "Show Markdown reformat option", - subtitle: "Expose a menu-only paste action that reflows markdown bullets and headings.", + title: "Markdown-Neuformatierung anzeigen", + subtitle: "Einfüge-Aktion im Menü, die Markdown-Aufzählungen und Überschriften neu formatiert.", binding: self.$settings.showMarkdownReformatOption) Divider() .padding(.vertical, 4) PreferenceToggleRow( - title: "Start at Login", - subtitle: "Automatically opens the app when you start your Mac.", + title: "Bei Anmeldung starten", + subtitle: "Öffnet die App automatisch beim Mac-Start.", binding: self.$settings.launchAtLogin) HStack { Spacer() - Button("Quit Trimmy") { + Button("Trimmy beenden") { NSApp.terminate(nil) } .buttonStyle(.borderedProminent) diff --git a/Sources/Trimmy/SettingsShortcutsPane.swift b/Sources/Trimmy/SettingsShortcutsPane.swift index fec4f90..9ca48f5 100644 --- a/Sources/Trimmy/SettingsShortcutsPane.swift +++ b/Sources/Trimmy/SettingsShortcutsPane.swift @@ -9,33 +9,33 @@ struct HotkeySettingsPane: View { var body: some View { VStack(alignment: .leading, spacing: 18) { PreferenceToggleRow( - title: "Enable global “Paste Trimmed” hotkey", - subtitle: "Trim on-the-fly and paste without permanently changing the clipboard.", + title: “Globalen „Getrimmt einfügen”-Hotkey aktivieren”, + subtitle: “Spontan trimmen und einfügen, ohne die Zwischenablage dauerhaft zu ändern.”, binding: self.$settings.pasteTrimmedHotkeyEnabled) VStack(alignment: .leading, spacing: 6) { - KeyboardShortcuts.Recorder("", name: .pasteTrimmed) + KeyboardShortcuts.Recorder(“”, name: .pasteTrimmed) .labelsHidden() .opacity(self.settings.pasteTrimmedHotkeyEnabled ? 1.0 : 0.4) .disabled(!self.settings.pasteTrimmedHotkeyEnabled) - Text("Paste Trimmed always uses High aggressiveness and then restores your clipboard.") + Text(“Getrimmt einfügen nutzt immer Aggressivität Hoch und stellt danach die Zwischenablage wieder her.”) .font(.footnote) .foregroundStyle(.tertiary) } PreferenceToggleRow( - title: "Enable global “Paste Original” hotkey", - subtitle: "Paste the unedited copy even if Trimmy already auto-trimmed it.", + title: “Globalen „Original einfügen”-Hotkey aktivieren”, + subtitle: “Die unveränderte Kopie einfügen, auch wenn Trimmy bereits getrimmt hat.”, binding: self.$settings.pasteOriginalHotkeyEnabled) - KeyboardShortcuts.Recorder("", name: .pasteOriginal) + KeyboardShortcuts.Recorder(“”, name: .pasteOriginal) .labelsHidden() .opacity(self.settings.pasteOriginalHotkeyEnabled ? 1.0 : 0.4) .disabled(!self.settings.pasteOriginalHotkeyEnabled) PreferenceToggleRow( - title: "Enable global Auto-Trim toggle hotkey", - subtitle: "Quickly turn Auto-Trim on or off without opening the menu.", + title: “Globalen Auto-Trim-Umschalt-Hotkey aktivieren”, + subtitle: “Auto-Trim schnell ein- oder ausschalten, ohne das Menü zu öffnen.”, binding: self.$settings.autoTrimHotkeyEnabled) KeyboardShortcuts.Recorder("", name: .toggleAutoTrim) diff --git a/Sources/Trimmy/SettingsView.swift b/Sources/Trimmy/SettingsView.swift index ad3d0dd..85f91ef 100644 --- a/Sources/Trimmy/SettingsView.swift +++ b/Sources/Trimmy/SettingsView.swift @@ -12,19 +12,19 @@ struct SettingsView: View { var body: some View { TabView(selection: self.$selectedTab) { GeneralSettingsPane(settings: self.settings, permissions: self.permissions) - .tabItem { Label("General", systemImage: "gearshape") } + .tabItem { Label("Allgemein", systemImage: "gearshape") } .tag(SettingsTab.general) AdvancedSettingsPane(settings: self.settings) - .tabItem { Label("Advanced", systemImage: "gearshape.2") } + .tabItem { Label("Erweitert", systemImage: "gearshape.2") } .tag(SettingsTab.advanced) AggressivenessSettingsPane(settings: self.settings) - .tabItem { Label("Aggressiveness", systemImage: "speedometer") } + .tabItem { Label("Aggressivität", systemImage: "speedometer") } .tag(SettingsTab.aggressiveness) HotkeySettingsPane(settings: self.settings, hotkeyManager: self.hotkeyManager) - .tabItem { Label("Shortcuts", systemImage: "command") } + .tabItem { Label("Kurzbefehle", systemImage: "command") } .tag(SettingsTab.shortcuts) #if DEBUG @@ -36,7 +36,7 @@ struct SettingsView: View { #endif AboutPane(updater: self.updater) - .tabItem { Label("About", systemImage: "info.circle") } + .tabItem { Label("Über", systemImage: "info.circle") } .tag(SettingsTab.about) } .padding(12) diff --git a/Sources/TrimmyCore/Aggressiveness.swift b/Sources/TrimmyCore/Aggressiveness.swift index 5584b4d..41e68e0 100644 --- a/Sources/TrimmyCore/Aggressiveness.swift +++ b/Sources/TrimmyCore/Aggressiveness.swift @@ -14,17 +14,17 @@ public enum Aggressiveness: String, CaseIterable, Identifiable, Codable, Sendabl public var title: String { switch self { - case .low: "Low (safer)" + case .low: "Niedrig (sicherer)" case .normal: "Normal" - case .high: "High (more eager)" + case .high: "Hoch (aggressiver)" } } public var titleShort: String { switch self { - case .low: "Low" + case .low: "Niedrig" case .normal: "Normal" - case .high: "High" + case .high: "Hoch" } } @@ -32,11 +32,11 @@ public enum Aggressiveness: String, CaseIterable, Identifiable, Codable, Sendabl public var blurb: String { switch self { case .low: - "Keeps light multi-line snippets intact unless they clearly look like shell commands." + "Lässt mehrzeilige Snippets intakt, außer sie sehen eindeutig wie Shell-Befehle aus." case .normal: - "Good default: flattens typical blog/README commands with pipes or continuations." + "Guter Standard: Faltet typische Blog-/README-Befehle mit Pipes oder Fortsetzungen zusammen." case .high: - "Most eager: will flatten almost any short multi-line text that resembles a command." + "Am aggressivsten: Faltet fast jeden kurzen mehrzeiligen Text zusammen, der wie ein Befehl aussieht." } } } diff --git a/Sources/TrimmyCore/TextCleaner.swift b/Sources/TrimmyCore/TextCleaner.swift index 1e15d52..ac032a6 100644 --- a/Sources/TrimmyCore/TextCleaner.swift +++ b/Sources/TrimmyCore/TextCleaner.swift @@ -434,10 +434,15 @@ public struct TextCleaner: Sendable { if stripLeading || stripTrailing { var rebuilt: [String] = [] rebuilt.reserveCapacity(lines.count) + var hadLeadingBox: [Bool] = [] + hadLeadingBox.reserveCapacity(lines.count) for line in lines { var lineStr = String(line) - if stripLeading { + let matchedLeading = stripLeading + && lineStr.range(of: leadingPattern, options: .regularExpression) != nil + hadLeadingBox.append(matchedLeading) + if matchedLeading { lineStr = lineStr.replacingOccurrences( of: leadingPattern, with: "", @@ -453,6 +458,32 @@ public struct TextCleaner: Sendable { } result = rebuilt.joined(separator: "\n") + + // Dedent common leading whitespace left by box-char removal, + // only considering lines that actually had box chars stripped. + if stripLeading { + let strippedNonEmpty = rebuilt.enumerated().compactMap { + idx, line -> String? in + guard hadLeadingBox[idx], + !line.trimmingCharacters(in: .whitespaces).isEmpty + else { return nil } + return line + } + if !strippedNonEmpty.isEmpty { + let minIndent = strippedNonEmpty.reduce(Int.max) { current, line in + min(current, line.prefix(while: { $0 == " " }).count) + } + if minIndent > 0 { + result = rebuilt.enumerated().map { idx, line in + guard hadLeadingBox[idx] else { return line } + guard !line.trimmingCharacters(in: .whitespaces).isEmpty else { + return "" + } + return String(line.dropFirst(minIndent)) + }.joined(separator: "\n") + } + } + } } } diff --git a/Tests/TrimmyTests/BoxDrawingCleanupTests.swift b/Tests/TrimmyTests/BoxDrawingCleanupTests.swift index 8d04b6c..fffc6ea 100644 --- a/Tests/TrimmyTests/BoxDrawingCleanupTests.swift +++ b/Tests/TrimmyTests/BoxDrawingCleanupTests.swift @@ -41,6 +41,32 @@ struct BoxDrawingCleanupTests { #expect(cleaned == nil, "No box glyphs present → no change") } + @Test + func dedentsResidualWhitespaceAfterBoxCharRemoval() { + let input = "│ Sehr geehrtes Netcup-Team,\n│ \n│ ich möchte für meinen Managed Server" + let cleaned = CommandDetector.stripBoxDrawingCharacters(in: input) + #expect(cleaned == "Sehr geehrtes Netcup-Team,\n\nich möchte für meinen Managed Server") + } + + @Test + func dedentsPreservesRelativeIndentation() { + let input = "│ line one\n│ indented\n│ line three" + let cleaned = CommandDetector.stripBoxDrawingCharacters(in: input) + // After box-char removal: " line one", " indented", " line three" + // Dedent by 1 (min common indent): "line one", " indented", "line three" + // Stage 5 collapses 4 spaces to 1, so final: "line one", " indented", "line three" + #expect(cleaned == "line one\n indented\nline three") + } + + @Test + func dedentOnlyAffectsLinesWithBoxChars() { + // Mixed content: majority has box chars, one plain line with intentional indent + let input = "│ line one\n│ line two\n│ line three\n plain line" + let cleaned = CommandDetector.stripBoxDrawingCharacters(in: input) + // Box-char lines get dedented; the plain line keeps its original indent + #expect(cleaned?.contains(" plain line") == true, "Plain line indent must be preserved") + } + @Test func preservesIndentationWhenNoBoxDrawing() { let input = """