Internal tooling for Mac utility for storage management.
Swift
98.7%
JSON
1.1%
Markdown
0.2%
//
// FileItem.swift
// MUA
//
// Created by Mitchel Volkering on 21/12/2025.
//
import Foundation
import SwiftUI
/// Represents a file or folder in the disk analysis
/// Using nonisolated(unsafe) to allow creation and mutation from any thread
/// This is safe because we only mutate during scanning (single-threaded per item)
/// and then only read from the main thread after scanning completes
final class FileItem: Identifiable, Hashable, @unchecked Sendable {
let id = UUID()
let url: URL
let name: String
let isDirectory: Bool
nonisolated(unsafe) var size: Int64
nonisolated(unsafe) var children: [FileItem]
nonisolated(unsafe) weak var parent: FileItem?
/// Color for visualization - assigned based on file type or folder depth
var color: Color {
if isDirectory {
return folderColor
}
return fileTypeColor
}
private var folderColor: Color {
let colors: [Color] = [
.blue, .purple, .pink, .orange, .yellow, .green, .teal, .cyan, .indigo, .mint
]
let hash = abs(name.hashValue)
return colors[hash % colors.count]
}
private var fileTypeColor: Color {
let ext = url.pathExtension.lowercased()
switch ext {
case "app", "dmg", "pkg":
return .purple
case "jpg", "jpeg", "png", "gif", "heic", "raw", "svg":
return .pink
case "mp4", "mov", "avi", "mkv", "m4v":
return .red
case "mp3", "wav", "aac", "flac", "m4a":
return .orange
case "zip", "tar", "gz", "rar", "7z":
return .yellow
case "pdf", "doc", "docx", "txt", "rtf":
return .blue
case "swift", "js", "py", "html", "css", "json":
return .green
case "xls", "xlsx", "csv", "numbers":
return .teal
default:
return .gray
}
}
/// Formatted size string
var formattedSize: String {
ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
}
/// Percentage of parent's size
func percentageOfParent() -> Double {
guard let parent = parent, parent.size > 0 else { return 100 }
return (Double(size) / Double(parent.size)) * 100
}
/// System icon for file type
var icon: String {
if isDirectory {
return "folder.fill"
}
let ext = url.pathExtension.lowercased()
switch ext {
case "app":
return "app.fill"
case "dmg", "pkg":
return "shippingbox.fill"
case "jpg", "jpeg", "png", "gif", "heic", "raw", "svg":
return "photo.fill"
case "mp4", "mov", "avi", "mkv", "m4v":
return "film.fill"
case "mp3", "wav", "aac", "flac", "m4a":
return "music.note"
case "zip", "tar", "gz", "rar", "7z":
return "doc.zipper"
case "pdf":
return "doc.richtext.fill"
case "doc", "docx", "txt", "rtf":
return "doc.text.fill"
case "swift", "js", "py", "html", "css", "json":
return "chevron.left.forwardslash.chevron.right"
case "xls", "xlsx", "csv", "numbers":
return "tablecells.fill"
default:
return "doc.fill"
}
}
nonisolated init(url: URL, isDirectory: Bool, size: Int64 = 0, children: [FileItem] = [], parent: FileItem? = nil) {
self.url = url
self.name = url.lastPathComponent
self.isDirectory = isDirectory
self.size = size
self.children = children
self.parent = parent
}
static func == (lhs: FileItem, rhs: FileItem) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
/// Get top N children by size
func topChildren(count: Int = 10) -> [FileItem] {
children.sorted { $0.size > $1.size }.prefix(count).map { $0 }
}
/// Calculate total count of all descendants
var descendantCount: Int {
if !isDirectory { return 0 }
return children.reduce(0) { $0 + 1 + $1.descendantCount }
}
}
About
Internal tooling for Mac utility for storage management.
0 stars
0 forks