aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj4
-rw-r--r--Hotline/Models/Hotline.swift101
-rw-r--r--Hotline/macOS/FilesView.swift7
3 files changed, 98 insertions, 14 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index 54adc37..d35440f 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -656,7 +656,7 @@
CODE_SIGN_ENTITLEMENTS = Hotline/Hotline.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 22;
+ CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_ASSET_PATHS = "\"Hotline/Preview Content\"";
DEVELOPMENT_TEAM = 5AEEV7QB2U;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
@@ -696,7 +696,7 @@
CODE_SIGN_ENTITLEMENTS = Hotline/Hotline.entitlements;
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 22;
+ CURRENT_PROJECT_VERSION = 23;
DEVELOPMENT_ASSET_PATHS = "\"Hotline/Preview Content\"";
DEVELOPMENT_TEAM = 5AEEV7QB2U;
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index 786fb0c..1e2c6c5 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -1,12 +1,20 @@
import SwiftUI
struct FileSearchConfig: Equatable {
+ /// Number of folders we process before we start applying delay backoff.
var initialBurstCount: Int = 15
+ /// Base delay applied between folder requests during the backoff phase.
var initialDelay: TimeInterval = 0.02
+ /// Multiplier used to increase the delay after each processed folder in backoff.
var backoffMultiplier: Double = 1.1
- var maxDelay: TimeInterval = 1.3
+ /// Maximum delay cap so searches don't stall out during long walks.
+ var maxDelay: TimeInterval = 1.0
+ /// Maximum recursion depth allowed during file search.
var maxDepth: Int = 40
+ /// Limit for repeated folder loops (guards against circular server listings).
var loopRepetitionLimit: Int = 4
+ /// Number of child folders that get prioritized after a matching parent is found.
+ var hotBurstLimit: Int = 2
}
enum FileSearchStatus: Equatable {
@@ -1300,6 +1308,7 @@ final class FileSearchSession {
private struct FolderTask {
let path: [String]
let depth: Int
+ let isHot: Bool
}
private weak var hotline: Hotline?
@@ -1332,18 +1341,20 @@ final class FileSearchSession {
hotline.searchSession(self, didFocusOn: [])
let rootFiles = await hotline.getFileList(path: [], suppressErrors: true)
processedCount = max(processedCount, 1)
- processListing(rootFiles, depth: 0)
+ processListing(rootFiles, depth: 0, parentPath: [], parentIsHot: false)
}
else {
hotline.searchSession(self, didFocusOn: [])
processedCount = max(processedCount, 1)
- processListing(hotline.files, depth: 0)
+ processListing(hotline.files, depth: 0, parentPath: [], parentIsHot: false)
}
while !queue.isEmpty && !isCancelled {
await Task.yield()
- let task = queue.removeFirst()
+ guard let task = dequeueNextTask() else {
+ continue
+ }
if shouldSkip(path: task.path, depth: task.depth) {
hotline.searchSession(self, didEmit: [], processed: processedCount, pending: queue.count)
continue
@@ -1359,7 +1370,7 @@ final class FileSearchSession {
break
}
- processListing(children, depth: task.depth)
+ processListing(children, depth: task.depth, parentPath: task.path, parentIsHot: task.isHot)
await applyBackoff()
}
@@ -1371,27 +1382,61 @@ final class FileSearchSession {
isCancelled = true
}
- private func processListing(_ items: [FileInfo], depth: Int) {
+ private func processListing(_ items: [FileInfo], depth: Int, parentPath: [String], parentIsHot: Bool) {
guard let hotline else {
return
}
var matches: [FileInfo] = []
+ var folderEntries: [(file: FileInfo, isHot: Bool)] = []
+ var hasFileMatch = false
for file in items {
- if nameMatchesQuery(file.name) {
+ let matchesName = nameMatchesQuery(file.name)
+
+ if matchesName {
matches.append(file)
+ if !file.isFolder {
+ hasFileMatch = true
+ }
}
if file.isFolder {
- enqueueFolder(file, depth: depth + 1)
+ folderEntries.append((file, matchesName))
+ }
+ }
+
+ var remainingBurst = 0
+ if config.hotBurstLimit > 0 && (parentIsHot || hasFileMatch) {
+ remainingBurst = config.hotBurstLimit
+ }
+
+ if remainingBurst > 0 {
+ var candidateIndices: [Int] = []
+ for index in folderEntries.indices where !folderEntries[index].isHot {
+ candidateIndices.append(index)
+ }
+
+ if !candidateIndices.isEmpty {
+ candidateIndices.shuffle()
+ for index in candidateIndices {
+ folderEntries[index].isHot = true
+ remainingBurst -= 1
+ if remainingBurst == 0 {
+ break
+ }
+ }
}
}
+ for entry in folderEntries {
+ enqueueFolder(entry.file, depth: depth + 1, markHot: entry.isHot)
+ }
+
hotline.searchSession(self, didEmit: matches, processed: processedCount, pending: queue.count)
}
- private func enqueueFolder(_ folder: FileInfo, depth: Int) {
+ private func enqueueFolder(_ folder: FileInfo, depth: Int, markHot: Bool) {
guard !isCancelled else { return }
guard depth <= config.maxDepth else { return }
@@ -1403,7 +1448,43 @@ final class FileSearchSession {
return
}
- queue.append(FolderTask(path: path, depth: depth))
+ queue.append(FolderTask(path: path, depth: depth, isHot: markHot))
+ }
+
+ private func dequeueNextTask() -> FolderTask? {
+ guard !queue.isEmpty else {
+ return nil
+ }
+
+ if queue.count == 1 {
+ return queue.removeFirst()
+ }
+
+ let currentDepth = queue[0].depth
+ var lastSameDepthIndex = 0
+ var hotIndices: [Int] = []
+
+ for index in 0..<queue.count {
+ let candidate = queue[index]
+ if candidate.depth == currentDepth {
+ lastSameDepthIndex = index
+ if candidate.isHot {
+ hotIndices.append(index)
+ }
+ } else {
+ break
+ }
+ }
+
+ let selectionPool: [Int]
+ if !hotIndices.isEmpty {
+ selectionPool = hotIndices
+ } else {
+ selectionPool = Array(0...lastSameDepthIndex)
+ }
+
+ let randomIndex = selectionPool.randomElement() ?? 0
+ return queue.remove(at: randomIndex)
}
private func shouldSkip(path: [String], depth: Int) -> Bool {
diff --git a/Hotline/macOS/FilesView.swift b/Hotline/macOS/FilesView.swift
index 4498dc9..5d06b02 100644
--- a/Hotline/macOS/FilesView.swift
+++ b/Hotline/macOS/FilesView.swift
@@ -263,7 +263,7 @@ struct FilesView: View {
if path.isEmpty {
return "/"
}
- return "/" + path.joined(separator: "/") + "/"
+ return path.joined(separator: "/")
}
private func openPreviewWindow(_ previewInfo: PreviewFileInfo) {
@@ -464,7 +464,10 @@ struct FilesView: View {
Label("Upload", systemImage: "arrow.up")
}
.help("Upload")
- .disabled(model.access?.contains(.canUploadFiles) != true)
+ .disabled(
+ model.access?.contains(.canUploadFiles) != true ||
+ (model.fileSearchStatus.isActive && !(selection?.isFolder ?? false))
+ )
Button {
if let selectedFile = selection {