Skip to content

Commit d422657

Browse files
committed
F2 inline rename — RenameKit pkg, char filter auf beiden dialogs, SwiftUI TextField 120% breite, Esc/click-outside cancel, nav keys cancel too
1 parent 3f2a111 commit d422657

11 files changed

Lines changed: 99 additions & 24 deletions

File tree

GUI/Resources/curr_version.asc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2026.04.22 03:48:05 at Host: NEVA
1+
2026.04.22 04:37:22 at Host: NEVA

GUI/Sources/ContextMenu/Dialogs/RenameDialog.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Copyright © 2026 Senatov. All rights reserved.
66

77
import FileModelKit
8+
import RenameKit
89
import SwiftUI
910

1011
// MARK: - Rename Dialog
@@ -27,7 +28,7 @@ struct RenameDialog: View {
2728
}
2829

2930
private var isValidName: Bool {
30-
!newName.isEmpty && !newName.contains("/") && !newName.contains(":") && newName != "." && newName != ".."
31+
FilenameCharacterFilter.validate(newName) == nil
3132
}
3233

3334
private var hasChanges: Bool {
@@ -72,7 +73,13 @@ struct RenameDialog: View {
7273
.higDialogStyle()
7374
.higAutoFocusTextField()
7475
.onAppear { isTextFieldFocused = true }
75-
.onChange(of: newName) { _, newValue in validateName(newValue) }
76+
.onChange(of: newName) { _, newValue in
77+
let filtered = FilenameCharacterFilter.sanitize(newValue)
78+
if filtered != newValue {
79+
newName = filtered
80+
}
81+
validateName(newValue)
82+
}
7683
.alert("File already exists", isPresented: $showOverwriteAlert) {
7784
Button("Cancel", role: .cancel) {
7885
isTextFieldFocused = true
@@ -103,14 +110,6 @@ struct RenameDialog: View {
103110

104111
private func validateName(_ name: String) {
105112
log.debug(#function)
106-
if name.isEmpty {
107-
errorMessage = L10n.Error.nameEmpty
108-
} else if name.contains("/") || name.contains(":") {
109-
errorMessage = L10n.Error.nameInvalidChars
110-
} else if name == "." || name == ".." {
111-
errorMessage = L10n.Error.invalidNameGeneric
112-
} else {
113-
errorMessage = nil
114-
}
113+
errorMessage = FilenameCharacterFilter.validate(name)
115114
}
116115
}

GUI/Sources/Features/Panels/FileTable/Duo/DuoFilePanelView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,16 @@ extension DuoFilePanelView {
342342
handler.onOpenSettings = {
343343
HotKeySettingsCoordinator.shared.showSettings()
344344
}
345+
handler.onRenameFile = { [appState] in
346+
let panel = appState.focusedPanel
347+
let file = panel == .left ? appState.selectedLeftFile : appState.selectedRightFile
348+
guard let file, !file.isParentEntry, file.nameStr != ".." else { return }
349+
appState.inlineRename.begin(
350+
fileID: AnyHashable(file.id),
351+
fileName: file.nameStr,
352+
panelTag: panel == .left ? 0 : 1
353+
)
354+
}
345355
handler.register()
346356
keyboardHandler = handler
347357
log.debug("\(#function) keyboard handler registered")

GUI/Sources/Features/Panels/FileTable/Duo/DuoPanelKbdHandler.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ final class DuoFilePanelKeyboardHandler {
2929
var onRefreshPanels: (() -> Void)?
3030
var onToggleHiddenFiles: (() -> Void)?
3131
var onOpenSettings: (() -> Void)?
32+
var onRenameFile: (() -> Void)?
3233

3334
init(appState: AppState) {
3435
self.appState = appState
@@ -146,6 +147,11 @@ final class DuoFilePanelKeyboardHandler {
146147
onDelete?()
147148
return nil
148149

150+
case .renameFile:
151+
log.info("[KEY] → Rename (F2)")
152+
onRenameFile?()
153+
return nil
154+
149155
// ── Clipboard ──
150156
case .clipboardCopy:
151157
log.info("[KEY] → Clipboard Copy")

GUI/Sources/Features/Panels/FileTable/FileRows/FileRow.swift

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import AppKit
88
import FileModelKit
9+
import RenameKit
910
import SwiftUI
1011
import UniformTypeIdentifiers
1112

@@ -318,18 +319,50 @@ struct FileRow: View, Equatable {
318319
// MARK: - Name column
319320
// Uses layout.nameWidth — same value as TableHeaderView nameColumnHeader.
320321
// This guarantees pixel-perfect alignment between header and rows.
322+
private var isInlineRenaming: Bool {
323+
appState.inlineRename.activeFileID == AnyHashable(file.id)
324+
}
325+
321326
@ViewBuilder
322327
private func nameColumnView() -> some View {
323-
FileRowView(
324-
file: file,
325-
isSelected: isSelected,
326-
isActivePanel: isActivePanel,
327-
isMarked: isMarked
328-
)
329-
.frame(width: layout.nameWidth, alignment: .leading)
330-
.clipped()
331-
.padding(.vertical, 2)
332-
.padding(.horizontal, 4)
328+
if isInlineRenaming {
329+
InlineRenameField(
330+
text: Bindable(appState.inlineRename).editedName,
331+
originalName: appState.inlineRename.originalName,
332+
nameWidth: layout.nameWidth,
333+
onCommit: { commitInlineRename() },
334+
onCancel: { appState.inlineRename.cancel() }
335+
)
336+
.frame(width: layout.nameWidth, alignment: .leading)
337+
.padding(.vertical, 2)
338+
.padding(.horizontal, 4)
339+
} else {
340+
FileRowView(
341+
file: file,
342+
isSelected: isSelected,
343+
isActivePanel: isActivePanel,
344+
isMarked: isMarked
345+
)
346+
.frame(width: layout.nameWidth, alignment: .leading)
347+
.clipped()
348+
.padding(.vertical, 2)
349+
.padding(.horizontal, 4)
350+
}
351+
}
352+
353+
private func commitInlineRename() {
354+
guard let result = appState.inlineRename.commit() else { return }
355+
let panel: FavPanelSide = result.panelTag == 0 ? .left : .right
356+
// Find the original file by matching the name that was active
357+
guard let selectedFile = panel == .left ? appState.selectedLeftFile : appState.selectedRightFile else { return }
358+
Task {
359+
await CntMenuCoord.shared.performRename(
360+
file: selectedFile,
361+
newName: result.newName,
362+
panel: panel,
363+
appState: appState
364+
)
365+
}
333366
}
334367

335368
// MARK: - Parent Row View

GUI/Sources/Features/Panels/FileTable/FileTableViews/FileTableView+Keyboard.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@ extension FileTableView {
1414
// MARK: - Keyboard Handling
1515
func handleUpArrow() -> KeyPress.Result {
1616
guard isFocused else { return .ignored }
17+
appState.inlineRename.cancel()
1718
keyboardNav.moveUp()
1819
return .handled
1920
}
2021

2122
func handleDownArrow() -> KeyPress.Result {
2223
guard isFocused else { return .ignored }
24+
appState.inlineRename.cancel()
2325
keyboardNav.moveDown()
2426
return .handled
2527
}
2628

2729
func handlePageUp() -> KeyPress.Result {
2830
guard isFocused else { return .ignored }
31+
appState.inlineRename.cancel()
2932
if pageNavThrottle.allow() {
3033
keyboardNav.pageUp()
3134
}
@@ -34,6 +37,7 @@ extension FileTableView {
3437

3538
func handlePageDown() -> KeyPress.Result {
3639
guard isFocused else { return .ignored }
40+
appState.inlineRename.cancel()
3741
if pageNavThrottle.allow() {
3842
keyboardNav.pageDown()
3943
}
@@ -42,12 +46,14 @@ extension FileTableView {
4246

4347
func handleHome() -> KeyPress.Result {
4448
guard isFocused else { return .ignored }
49+
appState.inlineRename.cancel()
4550
keyboardNav.jumpToFirst()
4651
return .handled
4752
}
4853

4954
func handleEnd() -> KeyPress.Result {
5055
guard isFocused else { return .ignored }
56+
appState.inlineRename.cancel()
5157
keyboardNav.jumpToLast()
5258
return .handled
5359
}

GUI/Sources/HotKeys/HotKeyAction.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ enum HotKeyAction: String, CaseIterable, Identifiable, Codable, Sendable {
2323
case unpackFiles = "unpackFiles"
2424
case compareContent = "compareContent"
2525
case syncDirectories = "syncDirectories"
26+
case renameFile = "renameFile"
2627

2728
// MARK: - Clipboard
2829
case clipboardCopy = "clipboardCopy"
@@ -84,6 +85,7 @@ enum HotKeyAction: String, CaseIterable, Identifiable, Codable, Sendable {
8485
case .unpackFiles: return "Unpack Files"
8586
case .compareContent: return "Compare by Content"
8687
case .syncDirectories: return "Synchronize Directories"
88+
case .renameFile: return "Rename File"
8789
case .clipboardCopy: return "Copy to Clipboard"
8890
case .clipboardCut: return "Cut to Clipboard"
8991
case .clipboardPaste: return "Paste from Clipboard"
@@ -123,7 +125,7 @@ enum HotKeyAction: String, CaseIterable, Identifiable, Codable, Sendable {
123125
switch self {
124126
case .viewFile, .editFile, .copyFile, .moveFile, .newFolder, .deleteFile,
125127
.packFiles, .unpackFiles, .compareContent, .syncDirectories,
126-
.clipboardCopy, .clipboardCut, .clipboardPaste:
128+
.clipboardCopy, .clipboardCut, .clipboardPaste, .renameFile:
127129
return .fileOperations
128130
case .togglePanelFocus, .moveUp, .moveDown, .pageUp, .pageDown, .moveToTop, .moveToBottom, .openSelected, .parentDirectory, .refreshPanels,
129131
.newTab, .closeTab, .nextTab, .prevTab:

GUI/Sources/HotKeys/HotKeyPresets.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ enum HotKeyPresets {
3838
HotKeyBinding(action: .moveFile, keyCode: 0x61, modifiers: .none), // F6
3939
HotKeyBinding(action: .newFolder, keyCode: 0x62, modifiers: .none), // F7
4040
HotKeyBinding(action: .deleteFile, keyCode: 0x64, modifiers: .none), // F8
41+
HotKeyBinding(action: .renameFile, keyCode: 0x78, modifiers: .none), // F2
4142
HotKeyBinding(action: .packFiles, keyCode: 0x60, modifiers: .option), // ⌥F5
4243
HotKeyBinding(action: .unpackFiles, keyCode: 0x65, modifiers: .option), // ⌥F9
4344
HotKeyBinding(action: .compareContent, keyCode: 0x08, modifiers: .control), // ⌃C
@@ -100,6 +101,7 @@ enum HotKeyPresets {
100101
HotKeyBinding(action: .moveFile, keyCode: 0x07, modifiers: .command), // ⌘X (then paste = move)
101102
HotKeyBinding(action: .newFolder, keyCode: 0x2D, modifiers: [.command, .shift]), // ⌘⇧N
102103
HotKeyBinding(action: .deleteFile, keyCode: 0x33, modifiers: .command), // ⌘Backspace
104+
HotKeyBinding(action: .renameFile, keyCode: 0x78, modifiers: .none), // F2
103105
HotKeyBinding(action: .packFiles, keyCode: 0x00, modifiers: .none), // Not standard
104106
HotKeyBinding(action: .unpackFiles, keyCode: 0x00, modifiers: .none), // Not standard
105107
HotKeyBinding(action: .compareContent, keyCode: 0x00, modifiers: .none), // Not standard

GUI/Sources/States/AppState/AppState.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import AppKit
1313
import FileModelKit
1414
import Foundation
15+
import RenameKit
1516

1617
// MARK: - AppState
1718
@MainActor
@@ -201,6 +202,9 @@ final class AppState {
201202
}
202203
var navigationCallbacks: [FavPanelSide: PanelNavigationCallbacks] = [:]
203204

205+
/// TC-style inline rename state (F2)
206+
var inlineRename = InlineRenameState()
207+
204208
// MARK: - Search Results (bridge)
205209
var leftSearchResultsPath: String? {
206210
get { leftPanel.searchResultsPath }

MiMiNavigator.xcodeproj/project.pbxproj

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
DD000003DD000003DD000003 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
2323
EE000003EE000003EE000003 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
2424
FF82030CE1584192AE6E4DEA /* NetworkKit in Frameworks */ = {isa = PBXBuildFile; productRef = 52D4533F441E4D78BD910304 /* NetworkKit */; };
25+
DD000014DD000014DD000014 /* RenameKit in Frameworks */ = {isa = PBXBuildFile; productRef = DD000015DD000015DD000015 /* RenameKit */; };
2526
/* End PBXBuildFile section */
2627

2728
/* Begin PBXContainerItemProxy section */
@@ -108,6 +109,7 @@
108109
97B3D6382DCA54B9002F1DB7 /* SystemPackage in Frameworks */,
109110
97D128762DB62DD400274684 /* FilesProvider in Frameworks */,
110111
CC000003CC000003CC000003 /* Citadel in Frameworks */,
112+
DD000014DD000014DD000014 /* RenameKit in Frameworks */,
111113
);
112114
};
113115
97AD1A3E2C624E040045C38D /* Frameworks */ = {
@@ -184,6 +186,7 @@
184186
BB000012BB000012BB000012 /* ScannerKit */,
185187
BB000013BB000013BB000013 /* ArchiveKit */,
186188
970978BD2F8C3BBB00C0BE9D /* VLC */,
189+
DD000015DD000015DD000015 /* RenameKit */,
187190
);
188191
productName = MiMiNavigator;
189192
productReference = 97AD1A2E2C624E020045C38D /* MiMiNavigator.app */;
@@ -268,6 +271,7 @@
268271
BB000002BB000002BB000002 /* XCLocalSwiftPackageReference "Packages/ScannerKit" */,
269272
BB000004BB000004BB000004 /* XCLocalSwiftPackageReference "Packages/ArchiveKit" */,
270273
CC000001CC000001CC000001 /* XCRemoteSwiftPackageReference "Citadel" */,
274+
DD000016DD000016DD000016 /* XCLocalSwiftPackageReference "Packages/RenameKit" */,
271275
970978BB2F8C341800C0BE9D /* XCRemoteSwiftPackageReference "VLC" */,
272276
);
273277
preferredProjectObjectVersion = 90;
@@ -708,6 +712,10 @@
708712
isa = XCLocalSwiftPackageReference;
709713
relativePath = Packages/ArchiveKit;
710714
};
715+
DD000016DD000016DD000016 /* XCLocalSwiftPackageReference "Packages/RenameKit" */ = {
716+
isa = XCLocalSwiftPackageReference;
717+
relativePath = Packages/RenameKit;
718+
};
711719
/* End XCLocalSwiftPackageReference section */
712720

713721
/* Begin XCRemoteSwiftPackageReference section */
@@ -824,6 +832,11 @@
824832
package = CC000001CC000001CC000001 /* XCRemoteSwiftPackageReference "Citadel" */;
825833
productName = Citadel;
826834
};
835+
DD000015DD000015DD000015 /* RenameKit */ = {
836+
isa = XCSwiftPackageProductDependency;
837+
package = DD000016DD000016DD000016 /* XCLocalSwiftPackageReference "Packages/RenameKit" */;
838+
productName = RenameKit;
839+
};
827840
/* End XCSwiftPackageProductDependency section */
828841
};
829842
rootObject = 97AD1A262C624E020045C38D /* Project object */;

0 commit comments

Comments
 (0)