Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions EjectKey.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
D696B793291367FD001DD6FE /* Hidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = D696B792291367FD001DD6FE /* Hidden.swift */; };
D696B7962913CD57001DD6FE /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D696B7952913CD57001DD6FE /* AboutView.swift */; };
D6B238CE29601E690049CF72 /* DependencyList in Frameworks */ = {isa = PBXBuildFile; productRef = D6B238CD29601E690049CF72 /* DependencyList */; };
D6EB67862A8EF0ED00C479FF /* IOUSBDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB67852A8EF0ED00C479FF /* IOUSBDetector.swift */; };
D6EB67932A8F237A00C479FF /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB67922A8F237A00C479FF /* Device.swift */; };
D6EB67972A90706D00C479FF /* VolumeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB67962A90706D00C479FF /* VolumeType.swift */; };
D6EB679B2A909C4A00C479FF /* VolumeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB679A2A909C4A00C479FF /* VolumeList.swift */; };
D6EB679F2A94488D00C479FF /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB679E2A94488D00C479FF /* Debouncer.swift */; };
D6EB67A32A95A0E800C479FF /* printDAReturn.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EB67A22A95A0E800C479FF /* printDAReturn.swift */; };
D6EDD7F62A5FD752005FFF3F /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = D6EDD7F52A5FD752005FFF3F /* Introspect */; };
D6EEFE162962BBA2002B64AA /* ExperimentalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEFE152962BBA2002B64AA /* ExperimentalView.swift */; };
D6EEFE192962C335002B64AA /* Unit.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EEFE182962C335002B64AA /* Unit.swift */; };
Expand Down Expand Up @@ -96,6 +102,12 @@
D696B78E2912A19D001DD6FE /* AppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModel.swift; sourceTree = "<group>"; };
D696B792291367FD001DD6FE /* Hidden.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hidden.swift; sourceTree = "<group>"; };
D696B7952913CD57001DD6FE /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
D6EB67852A8EF0ED00C479FF /* IOUSBDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOUSBDetector.swift; sourceTree = "<group>"; };
D6EB67922A8F237A00C479FF /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = "<group>"; };
D6EB67962A90706D00C479FF /* VolumeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeType.swift; sourceTree = "<group>"; };
D6EB679A2A909C4A00C479FF /* VolumeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeList.swift; sourceTree = "<group>"; };
D6EB679E2A94488D00C479FF /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
D6EB67A22A95A0E800C479FF /* printDAReturn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = printDAReturn.swift; sourceTree = "<group>"; };
D6EEFE152962BBA2002B64AA /* ExperimentalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalView.swift; sourceTree = "<group>"; };
D6EEFE182962C335002B64AA /* Unit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Unit.swift; sourceTree = "<group>"; };
D6EF55AB2962F76C002E36EC /* UpdaterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdaterViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -172,8 +184,11 @@
D6288AFD289EC45000F80FF1 /* Objects */ = {
isa = PBXGroup;
children = (
D6EB67922A8F237A00C479FF /* Device.swift */,
D6EEFE182962C335002B64AA /* Unit.swift */,
D6288AFE289EC45A00F80FF1 /* Volume.swift */,
D6EB67962A90706D00C479FF /* VolumeType.swift */,
D6EB67852A8EF0ED00C479FF /* IOUSBDetector.swift */,
D6288B00289EC47D00F80FF1 /* Defaults.swift */,
D6288B4228A0CBB500F80FF1 /* Shortcuts.swift */,
);
Expand All @@ -184,6 +199,7 @@
isa = PBXGroup;
children = (
D696B78A29126D65001DD6FE /* MenuView.swift */,
D6EB679A2A909C4A00C479FF /* VolumeList.swift */,
);
path = MenuBar;
sourceTree = "<group>";
Expand Down Expand Up @@ -216,13 +232,15 @@
D6288B15289EC78900F80FF1 /* Utils */ = {
isa = PBXGroup;
children = (
D6EB679E2A94488D00C479FF /* Debouncer.swift */,
D696B792291367FD001DD6FE /* Hidden.swift */,
D6288B16289EC7A200F80FF1 /* Resize.swift */,
D6288B18289EC7B600F80FF1 /* Unique.swift */,
D65AB5602A6CAACB00311461 /* Command.swift */,
D6EF55AB2962F76C002E36EC /* UpdaterViewModel.swift */,
D65DE28D2A1A0A810051AAE3 /* HideSidebarToggle.swift */,
D65DE28B2A19F4F40051AAE3 /* EffectView.swift */,
D6EB67A22A95A0E800C479FF /* printDAReturn.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -332,6 +350,7 @@
buildActionMask = 2147483647;
files = (
D6288AFC289EC43400F80FF1 /* Notifications.swift in Sources */,
D6EB679F2A94488D00C479FF /* Debouncer.swift in Sources */,
D6288B01289EC47D00F80FF1 /* Defaults.swift in Sources */,
D65DE2852A19F40D0051AAE3 /* SettingsForm.swift in Sources */,
D6288B19289EC7B600F80FF1 /* Unique.swift in Sources */,
Expand All @@ -341,13 +360,17 @@
D6288AFA289EC41900F80FF1 /* Observers.swift in Sources */,
D65DE28C2A19F4F40051AAE3 /* EffectView.swift in Sources */,
D696B78B29126D65001DD6FE /* MenuView.swift in Sources */,
D6EB67932A8F237A00C479FF /* Device.swift in Sources */,
D6288B12289EC72A00F80FF1 /* TouchBarView.swift in Sources */,
D6288B4728A0CE2E00F80FF1 /* ShortcutsView.swift in Sources */,
D6EB67862A8EF0ED00C479FF /* IOUSBDetector.swift in Sources */,
D6288B07289EC54A00F80FF1 /* TouchBar.swift in Sources */,
D65DE28E2A1A0A810051AAE3 /* HideSidebarToggle.swift in Sources */,
D696B78F2912A19D001DD6FE /* AppModel.swift in Sources */,
D696B793291367FD001DD6FE /* Hidden.swift in Sources */,
D6EB679B2A909C4A00C479FF /* VolumeList.swift in Sources */,
D6EEFE162962BBA2002B64AA /* ExperimentalView.swift in Sources */,
D6EB67A32A95A0E800C479FF /* printDAReturn.swift in Sources */,
D6EF55AC2962F76C002E36EC /* UpdaterViewModel.swift in Sources */,
D6288B17289EC7A200F80FF1 /* Resize.swift in Sources */,
D6288AF8289EC3F200F80FF1 /* Commands.swift in Sources */,
Expand All @@ -358,6 +381,7 @@
D696B7962913CD57001DD6FE /* AboutView.swift in Sources */,
D6288B0C289EC69D00F80FF1 /* SettingsView.swift in Sources */,
D6288B14289EC73F00F80FF1 /* NotificationsView.swift in Sources */,
D6EB67972A90706D00C479FF /* VolumeType.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
9 changes: 8 additions & 1 deletion EjectKey/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ import UserNotifications
final class AppModel: ObservableObject {

@Published var allVolumes: [Volume] = []
@Published var units: [Unit] = []
@Published var devices: [Device] = []

@Published var mountedVolumeUrls: [URL] = []
@Published var connectedVolumeBsdNames: [String] = []

// Workaround for switching tabs of Settings View programmatically
@Published var settingsTabSelection: SettingsPage.Name = .general
Expand All @@ -20,6 +23,10 @@ final class AppModel: ObservableObject {

var touchBarItem: NSCustomTouchBarItem?

let ioDetector = IOUSBDetector()

let debouncer = Debouncer(interval: 0.5)

init() {
// For debug
// Defaults[.isFirstLaunch] = true
Expand Down
114 changes: 88 additions & 26 deletions EjectKey/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,36 @@ import AudioToolbox
extension AppModel {
func eject(_ volume: Volume) {
DispatchQueue.global().async {
guard let unit = self.units.filter({ $0.devicePath == volume.devicePath }).first else {
guard let device = self.devices.filter({ $0.path == volume.devicePath }).first,
let unit = device.units.filter({ $0.number == volume.unitNumber }).first else {
return
}
let isLastVolume = unit.volumes.count == 1
volume.unmount(unmountAndEject: isLastVolume, withoutUI: false) { error in
/*let callback: DADiskUnmountCallback = { _, dissenter, context in
if dissenter == nil || context == nil {
// Suceeded
if Defaults[.sendWhenVolumeIsEjected] {
self.sendNotification(
title: L10n.volWasSuccessfullyEjected(volume.name ?? L10n.unknown),
body: device.isVirtual ? L10n.thisVolumeIsAVirtualInterface : L10n.safelyRemoved,
sound: .default,
identifier: UUID().uuidString
)
}
} else {
// Failes
print("unmount failure: " + printDAReturn(r: DADissenterGetStatus(dissenter!)))
}
CFRunLoopStop(CFRunLoopGetCurrent())
}
volume.unmount(force: false, callback: callback)*/
/*volume.unmount(unmountAndEject: isLastVolume, withoutUI: false) { error in
if error.isNil {
// Succeeded
if Defaults[.sendWhenVolumeIsEjected] {
self.sendNotification(
title: L10n.volWasSuccessfullyEjected(volume.name),
body: volume.isVirtual ? L10n.thisVolumeIsAVirtualInterface : L10n.safelyRemoved,
title: L10n.volWasSuccessfullyEjected(volume.name ?? L10n.unknown),
body: device.isVirtual ? L10n.thisVolumeIsAVirtualInterface : L10n.safelyRemoved,
sound: .default,
identifier: UUID().uuidString
)
Expand All @@ -31,7 +50,7 @@ extension AppModel {
// Failed
if Defaults[.sendWhenVolumeIsEjected] {
self.sendNotification(
title: L10n.failedToEjectVol(volume.name),
title: L10n.failedToEjectVol(volume.name ?? L10n.unknown),
body: error!.localizedDescription,
sound: .defaultCritical,
identifier: UUID().uuidString
Expand Down Expand Up @@ -60,7 +79,7 @@ extension AppModel {
DispatchQueue.main.async {
self.alert(
alertStyle: .warning,
messageText: L10n.theFollowingApplicationsAreUsingVol(volume.name),
messageText: L10n.theFollowingApplicationsAreUsingVol(volume.name ?? L10n.unknown),
informativeText: infoText,
buttonTitle: L10n.quit,
showCancelButton: true,
Expand All @@ -72,7 +91,7 @@ extension AppModel {
}
}
}
}
}*/
}
}

Expand All @@ -88,12 +107,52 @@ extension AppModel {
}
}

private func getConnectedVolumeBsdNames() -> [String] {
let matchingDict: CFMutableDictionary = IOServiceMatching("IOMedia")
var entryIterator: io_iterator_t = 0
var bsdNames: [String] = []
if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &entryIterator) == kIOReturnSuccess {
var serviceObject: io_registry_entry_t = 0
repeat {
serviceObject = IOIteratorNext(entryIterator)
if serviceObject != 0 {
var serviceDictionary: Unmanaged<CFMutableDictionary>?
if IORegistryEntryCreateCFProperties(serviceObject, &serviceDictionary, kCFAllocatorDefault, 0) != kIOReturnSuccess {
IOObjectRelease(serviceObject)
continue
}

if let dict = serviceDictionary?.takeRetainedValue() as? [String: Any],
let bsdName = dict[kIOBSDNameKey] as? String,
bsdName.dropFirst(4).contains("s") {
bsdNames.append(bsdName)
}
}
} while serviceObject != 0
IOObjectRelease(entryIterator)
}
return bsdNames
}

func setUnitsAndVolumes() {
let mountedVolumeURLs = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil, options: [])
allVolumes = mountedVolumeURLs?.compactMap(Volume.init) ?? []
let _connectedVolumeBsdNames = getConnectedVolumeBsdNames()
let _mountedVolumeUrls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil)

guard (connectedVolumeBsdNames != _connectedVolumeBsdNames) || (mountedVolumeUrls != _mountedVolumeUrls) else {
return
}

connectedVolumeBsdNames = _connectedVolumeBsdNames
mountedVolumeUrls = _mountedVolumeUrls ?? []

allVolumes = connectedVolumeBsdNames.compactMap(Volume.init)

let devicePaths = allVolumes.map(\.devicePath).unique
units = devicePaths.map({ Unit(devicePath: $0, allVolumes: allVolumes) })
// Prevent multiple changes to `devices` in a short period of time (and thus updating the UI)
// by a device with multiple volumes.
debouncer.debounce {
let devicePaths = self.allVolumes.map(\.devicePath).unique
self.devices = devicePaths.compactMap({ Device(path: $0, allVolumes: self.allVolumes) })
}
}

func checkMountedVolumes(old: [Volume], new: [Volume]) {
Expand All @@ -106,14 +165,18 @@ extension AppModel {
let mountedVolumes = new.filter({ !oldIds.contains($0.id) })

for volume in mountedVolumes {
if Defaults[.doNotSendNotificationsAboutVirtualVolumes] && volume.isVirtual {
guard let path = volume.url?.path(),
let device = self.devices.filter({ $0.path == path }).first else {
return
}
if Defaults[.doNotSendNotificationsAboutVirtualVolumes] && device.isVirtual {
return
}

DispatchQueue.main.async {
self.sendNotification(
title: L10n.volumeConnected,
body: volume.isVirtual ? L10n.volIsAVirtualInterface(volume.name) : L10n.volIsAPhysicalDevice(volume.name),
body: device.isVirtual ? L10n.volIsAVirtualInterface(volume.name ?? L10n.unknown) : L10n.volIsAPhysicalDevice(volume.name ?? L10n.unknown),
sound: .default,
identifier: UUID().uuidString
)
Expand All @@ -122,36 +185,35 @@ extension AppModel {
}
}

func checkEjectedVolumes(old: [Volume], new: [Volume]) {
func checkUnmountedVolumes(old: [Volume], new: [Volume]) {
if !Defaults[.showMoveToTrashDialog] {
return
}

DispatchQueue.global().async {
let newIds = new.map(\.id)
let ejectedVolumes = old.filter({ !newIds.contains($0.id) })
let unmountedVolumes = old.filter({ !newIds.contains($0.id) })

if ejectedVolumes.isEmpty {
if unmountedVolumes.isEmpty {
return
}

let fileManager = FileManager.default
guard let downloadsDir = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first else {
return
}
guard let files = try? fileManager.contentsOfDirectory(atPath: downloadsDir.path()) else {
guard let downloadsDir = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask).first,
let files = try? fileManager.contentsOfDirectory(atPath: downloadsDir.path())
else {
return
}

for volume in ejectedVolumes {
if !volume.isDiskImage {
for volume in unmountedVolumes {
guard let device = self.devices.filter({ $0.path == volume.devicePath }).first,
!device.isDiskImage,
let fixedVolumeName = volume.name?.lowercased().replacingOccurrences(of: " ", with: "[ -_]*"),
let regex = try? Regex("\(fixedVolumeName).*\\.dmg$")
else {
return
}

let fixedVolumeName = volume.name.lowercased().replacingOccurrences(of: " ", with: "[ -_]*")
guard let regex = try? Regex("\(fixedVolumeName).*\\.dmg$") else {
return
}
let dmgFileNames = files.filter({$0.lowercased().firstMatch(of: regex)?.0 != nil})
if dmgFileNames.isEmpty {
return
Expand Down
3 changes: 2 additions & 1 deletion EjectKey/EjectKeyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ struct EjectKeyApp: App {
var body: some Scene {

MenuBarExtra {
MenuView(model: model)
MenuView()
.environmentObject(model)
.environmentObject(updaterViewModel)
} label: {
Image(systemSymbol: .ejectFill)
Expand Down
Loading