Skip to content

Commit

Permalink
feat: 🎸 Add option to show internal and unmounted volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
yk4to committed Aug 19, 2023
1 parent 4da8e4d commit 55e3da1
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 108 deletions.
6 changes: 4 additions & 2 deletions EjectKey/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import AudioToolbox
extension AppModel {
func eject(_ volume: Volume) {
DispatchQueue.global().async {
guard let device = self.devices.filter({ $0.path == volume.path }).first,
guard let path = volume.url?.path(),
let device = self.devices.filter({ $0.path == path }).first,
let unit = device.units.filter({ $0.number == volume.unitNumber }).first else {
return
}
Expand Down Expand Up @@ -129,7 +130,8 @@ extension AppModel {
let mountedVolumes = new.filter({ !oldIds.contains($0.id) })

for volume in mountedVolumes {
guard let device = self.devices.filter({ $0.path == volume.path }).first else {
guard let path = volume.url?.path(),
let device = self.devices.filter({ $0.path == path }).first else {
return
}
if Defaults[.doNotSendNotificationsAboutVirtualVolumes] && device.isVirtual {
Expand Down
57 changes: 30 additions & 27 deletions EjectKey/MenuBar/MenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct MenuView: View {

@Default(.showEjectAllVolumesButton) var showEjectAllVolumesButton
@Default(.showEjectAllVolumesInDiskButtons) var showEjectAllVolumesInDiskButtons
@Default(.showInternalVolumes) var showInternalVolumes
@Default(.showActionMenu) var showActionMenu
@Default(.showDetailedInformation) var showDetailedInformation

Expand All @@ -31,36 +32,38 @@ struct MenuView: View {
.hidden(!showEjectAllVolumesButton || model.devices.count <= 1)

ForEach(model.devices.sorted(by: { $0.minUnitNumber < $1.minUnitNumber }), id: \.path) { device in
Menu {
ForEach(device.units.sorted(by: { $0.number < $1.number }), id: \.number) { unit in
if !unit.isApfs {
Text("\(unit.name ?? "Unknown") (\(unit.bsdName))")
Button(L10n.ejectNumVolumes(unit.volumes.count)) {
model.ejectAllVolumeInDisk(unit)
if !(!showInternalVolumes && device.isInternal) {
Menu {
ForEach(device.units.sorted(by: { $0.number < $1.number }), id: \.number) { unit in
if !unit.isApfs {
Text("\(unit.name ?? "Unknown") (\(unit.bsdName))")
Button(L10n.ejectNumVolumes(unit.volumes.count)) {
model.ejectAllVolumeInDisk(unit)
}
.hidden(!showEjectAllVolumesInDiskButtons || unit.volumes.count <= 1)

VolumeList(model: model, device: device, unit: unit)
}
.hidden(!showEjectAllVolumesInDiskButtons || unit.volumes.count <= 1)

VolumeList(model: model, device: device, unit: unit)
}
}
if showDetailedInformation {
Divider()
Text("Protocol: \(device.deviceProtocol ?? "Unknown")")
}
} label: {
if showDetailedInformation {
if device.isDiskImage {
Text(L10n.diskImage)
} else {
Text("\(device.vendor ?? "Unknown") \(device.model ?? "Unknown")")
}
} else {
let numbersStr = device.units.map({ String($0.number) }).joined(separator: ", ")
if device.isDiskImage {
Text(L10n.diskImageNum(numbersStr))
} else {
Text(L10n.diskNum(numbersStr))
if showDetailedInformation {
Divider()
Text("Protocol: \(device.deviceProtocol ?? "Unknown")")
}
} label: {
// if showDetailedInformation {
if device.isDiskImage {
Text(L10n.diskImage)
} else {
Text("\(device.vendor ?? "Unknown") \(device.model ?? "Unknown")")
}
/*} else {
let numbersStr = device.units.map({ String($0.number) }).joined(separator: ", ")
if device.isDiskImage {
Text(L10n.diskImageNum(numbersStr))
} else {
Text(L10n.diskNum(numbersStr))
}
}*/
}
}
}
Expand Down
84 changes: 48 additions & 36 deletions EjectKey/MenuBar/VolumeList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ struct VolumeList: View {
@State var unit: Unit

@Default(.showEjectAllVolumesInDiskButtons) private var showEjectAllVolumesInDiskButtons
@Default(.showUnmountedVolumes) private var showUnmountedVolumes
@Default(.showActionMenu) private var showActionMenu
@Default(.showDetailedInformation) private var showDetailedInformation

Expand All @@ -22,52 +23,63 @@ struct VolumeList: View {
// if showActionMenu {
if volume.type == .apfsContainer {
if let contentUnit = device?.units.filter({ $0.physicalStoreBsdName == volume.bsdName }).first {
if !(!showUnmountedVolumes && !contentUnit.existsMountedVolume) {
Menu {
// Text("APFS Container (\(unit.bsdName))")
Button(L10n.ejectNumVolumes(contentUnit.volumes.count)) {
model.ejectAllVolumeInDisk(unit)
}
.hidden(!showEjectAllVolumesInDiskButtons || contentUnit.volumes.count <= 1)

VolumeList(model: model, device: nil, unit: contentUnit)
if showDetailedInformation {
Divider()
Text(volume.type.displayName())
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown")")
Text("Physical Store: \(volume.bsdName)")
}
} label: {
Image(systemSymbol: .shippingbox)
Text("APFS Container (\(contentUnit.bsdName))")
}
}
}
} else {
if !(!showUnmountedVolumes && !volume.isMounted) {
Menu {
// Text("APFS Container (\(unit.bsdName))")
Button(L10n.ejectNumVolumes(contentUnit.volumes.count)) {
model.ejectAllVolumeInDisk(unit)
if volume.isMounted {
Button(L10n.eject) {
model.eject(volume)
}
} else {
Button("Mount") {
// model.eject(volume)
}
}
if let url = volume.url {
Button(L10n.showInFinder) {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
.hidden(!showEjectAllVolumesInDiskButtons || contentUnit.volumes.count <= 1)

VolumeList(model: model, device: nil, unit: contentUnit)
if showDetailedInformation {
Divider()
Text(volume.type.displayName())
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown")")
Text("Physical Store: \(volume.bsdName)")
if unit.isApfs {
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown") (Shared)")
} else {
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown")")
}
Text("ID: \(volume.bsdName)")
Text(volume.isMounted ? "Mounted" : "Not Mounted")
}
} label: {
Image(systemSymbol: .shippingbox)
Text("APFS Container (\(contentUnit.bsdName))")
}
}
} else {
Menu {
Button(L10n.eject) {
model.eject(volume)
}
if let url = volume.url {
Button(L10n.showInFinder) {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
if showDetailedInformation {
Divider()
Text(volume.type.displayName())
if unit.isApfs {
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown") (Shared)")
if let icon = volume.icon {
Image(nsImage: icon)
} else {
Text("\(L10n.size): \(volume.size?.formatted(.byteCount(style: .file)) ?? "Unknown")")
Image(systemSymbol: .externaldrive)
}
Text("ID: \(volume.bsdName)")
Text(volume.name ?? "Unknown")
}
} label: {
if let icon = volume.icon {
Image(nsImage: icon)
} else {
Image(systemSymbol: .externaldrive)
}
Text(volume.name ?? "Unknown")
}
}
/*} else {
Expand Down
2 changes: 2 additions & 0 deletions EjectKey/Objects/Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ extension Defaults.Keys {
static let doNotDisplayNumbersWhenNothingIsConnected = Key<Bool>("doNotDisplayNumbersWhenNothingIsConnected", default: false)
static let showEjectAllVolumesButton = Key<Bool>("showEjectAllButton", default: true)
static let showEjectAllVolumesInDiskButtons = Key<Bool>("showEjectAllVolumesInDiskButtons", default: true)
static let showInternalVolumes = Key<Bool>("showInternalVolumes", default: false)
static let showUnmountedVolumes = Key<Bool>("showUnmountedVolumes", default: false)
static let showActionMenu = Key<Bool>("showActionMenu", default: false)
static let showDetailedInformation = Key<Bool>("showDetailedInformation", default: false)
// Touch Bar
Expand Down
3 changes: 3 additions & 0 deletions EjectKey/Objects/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct Device {
let model: String?
let vendor: String?
let deviceProtocol: String?
let isInternal: Bool
let isVirtual: Bool
let isDiskImage: Bool
let units: [Unit]
Expand All @@ -32,10 +33,12 @@ struct Device {
let model = firstVolume.diskInfo[kDADiskDescriptionDeviceModelKey] as? String
let vendor = firstVolume.diskInfo[kDADiskDescriptionDeviceVendorKey] as? String
let deviceProtocol = firstVolume.diskInfo[kDADiskDescriptionDeviceProtocolKey] as? String
let isInternal = firstVolume.diskInfo[kDADiskDescriptionDeviceInternalKey] as? Bool ?? false

self.model = model
self.vendor = vendor
self.deviceProtocol = deviceProtocol
self.isInternal = isInternal

self.isVirtual = deviceProtocol == "Virtual Interface"
self.isDiskImage = isVirtual && vendor == "Apple" && model == "Disk Image"
Expand Down
2 changes: 2 additions & 0 deletions EjectKey/Objects/Unit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ struct Unit {
let bsdName: String
let name: String?
let volumes: [Volume]
let existsMountedVolume: Bool
let isApfs: Bool
var physicalStoreBsdName: String?

Expand All @@ -33,6 +34,7 @@ struct Unit {
self.bsdName = bsdName
self.name = name
self.volumes = volumes
self.existsMountedVolume = volumes.contains(where: \.isMounted)
self.isApfs = name == "AppleAPFSMedia"

self.physicalStoreBsdName = nil
Expand Down
67 changes: 24 additions & 43 deletions EjectKey/Objects/Volume.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,13 @@ class Volume {
let type: VolumeType

let name: String?
let path: String?
let url: URL?
let size: Int?
let id: String?

let isMounted: Bool

init?(bsdName: String) {
/* guard let resourceValues = try? url.resourceValues(forKeys: [.volumeIsInternalKey, .volumeLocalizedFormatDescriptionKey]) else {
return nil
}
let isInternalVolume = resourceValues.volumeIsInternal ?? false
if isInternalVolume {
return nil
} */

guard let session = DASessionCreate(kCFAllocatorDefault),
let disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, bsdName),
let diskInfo = DADiskCopyDescription(disk) as? [NSString: Any],
Expand All @@ -58,12 +49,10 @@ class Volume {

// Optional Properties
let name = (diskInfo[kDADiskDescriptionVolumeNameKey] as? String) ?? (diskInfo[kDADiskDescriptionMediaNameKey] as? String)
let path = diskInfo[kDADiskDescriptionVolumePathKey] as? String
let url = (path != nil) ? URL(fileURLWithPath: path!) : nil
let url = diskInfo[kDADiskDescriptionVolumePathKey] as? URL
let size = diskInfo[kDADiskDescriptionMediaSizeKey] as? Int

self.name = name
self.path = path
self.url = url
self.size = size

Expand All @@ -80,35 +69,13 @@ class Volume {
} else {
self.id = nil
}

self.isMounted = url != nil
}

var icon: NSImage? {
if let iconPath = getIconPath() {
return NSImage(byReferencingFile: iconPath)
} else {
return nil
}
}

func unmount(unmountAndEject: Bool, withoutUI: Bool, completionHandler: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let fileManager = FileManager.default
let options: FileManager.UnmountOptions = [
unmountAndEject ? .allPartitionsAndEjectDisk : [],
withoutUI ? .withoutUI : []
]

if self.url != nil {
fileManager.unmountVolume(at: self.url!, options: options, completionHandler: completionHandler)
}
}
}

private func getIconPath() -> String? {
if let iconPath = url?.appending(path: "/.VolumeIcon.icns").path() {
if FileManager.default.fileExists(atPath: iconPath) {
return iconPath
}
if url != nil {
return NSWorkspace.shared.icon(forFile: url!.path())
}

if let iconDict = diskInfo[kDADiskDescriptionMediaIconKey] as? NSDictionary,
Expand All @@ -120,19 +87,33 @@ class Volume {
let bundleUrl = Unmanaged.takeRetainedValue(KextManagerCreateURLForBundleIdentifier(kCFAllocatorDefault, identifier))() as URL
if let bundle = Bundle(url: bundleUrl),
let iconPath = bundle.path(forResource: iconName.deletingPathExtension, ofType: iconName.pathExtension) {
return iconPath
return NSImage(byReferencingFile: iconPath)
}
}

return nil
}

func unmount(unmountAndEject: Bool, withoutUI: Bool, completionHandler: @escaping (Error?) -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
let fileManager = FileManager.default
let options: FileManager.UnmountOptions = [
unmountAndEject ? .allPartitionsAndEjectDisk : [],
withoutUI ? .withoutUI : []
]

if self.url != nil {
fileManager.unmountVolume(at: self.url!, options: options, completionHandler: completionHandler)
}
}
}

func getCulpritApps() -> [NSRunningApplication] {
guard path != nil else {
guard let path = url?.path() else {
return []
}

let command = Command("/usr/sbin/lsof", ["-Fn", "+D", path!])
let command = Command("/usr/sbin/lsof", ["-Fn", "+D", path])

guard let result = command.run() else {
return []
Expand Down
2 changes: 2 additions & 0 deletions EjectKey/Settings/GeneralView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ struct GeneralView: View {
Section {
Defaults.Toggle(L10n.showEjectAllVolumesButton, key: .showEjectAllVolumesButton)
Defaults.Toggle(L10n.showEjectAllVolumesInDiskButtons, key: .showEjectAllVolumesInDiskButtons)
Defaults.Toggle("Show Internal Volumes", key: .showInternalVolumes)
Defaults.Toggle("Show Unmounted Volumes", key: .showUnmountedVolumes)
Defaults.Toggle(L10n.showActionMenu, key: .showActionMenu)
Defaults.Toggle(L10n.showDetailedInformation, key: .showDetailedInformation)
}
Expand Down

0 comments on commit 55e3da1

Please sign in to comment.