Skip to content

Commit

Permalink
feat: 🎸 Add an observer to detect when a device is connected
Browse files Browse the repository at this point in the history
  • Loading branch information
yk4to committed Aug 24, 2023
1 parent 2f1b5d8 commit 6fe2152
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 62 deletions.
8 changes: 8 additions & 0 deletions EjectKey.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
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 @@ -104,6 +106,8 @@
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 @@ -228,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 @@ -344,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 @@ -363,6 +370,7 @@
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 Down
5 changes: 5 additions & 0 deletions EjectKey/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ final class AppModel: ObservableObject {
@Published var allVolumes: [Volume] = []
@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 @@ -22,6 +25,8 @@ final class AppModel: ObservableObject {

let ioDetector = IOUSBDetector()

let debouncer = Debouncer(interval: 0.5)

init() {
// For debug
// Defaults[.isFirstLaunch] = true
Expand Down
63 changes: 49 additions & 14 deletions EjectKey/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,30 @@ import AudioToolbox
extension AppModel {
func eject(_ volume: Volume) {
DispatchQueue.global().async {
guard let path = volume.url?.path(),
let device = self.devices.filter({ $0.path == path }).first,
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] {
Expand Down Expand Up @@ -74,7 +91,7 @@ extension AppModel {
}
}
}
}
}*/
}
}

Expand All @@ -90,10 +107,10 @@ extension AppModel {
}
}

func setUnitsAndVolumes() {
private func getConnectedVolumeBsdNames() -> [String] {
let matchingDict: CFMutableDictionary = IOServiceMatching("IOMedia")
var entryIterator: io_iterator_t = 0
var volumeBsdNames: [String] = []
var bsdNames: [String] = []
if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &entryIterator) == kIOReturnSuccess {
var serviceObject: io_registry_entry_t = 0
repeat {
Expand All @@ -108,16 +125,34 @@ extension AppModel {
if let dict = serviceDictionary?.takeRetainedValue() as? [String: Any],
let bsdName = dict[kIOBSDNameKey] as? String,
bsdName.dropFirst(4).contains("s") {
volumeBsdNames.append(bsdName)
bsdNames.append(bsdName)
}
}
} while serviceObject != 0
IOObjectRelease(entryIterator)
}
allVolumes = volumeBsdNames.compactMap(Volume.init)
return bsdNames
}

func setUnitsAndVolumes() {
let _connectedVolumeBsdNames = getConnectedVolumeBsdNames()
let _mountedVolumeUrls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: nil)

let devicePaths = allVolumes.map(\.devicePath).unique
devices = devicePaths.compactMap({ Device(path: $0, allVolumes: allVolumes) })
guard (connectedVolumeBsdNames != _connectedVolumeBsdNames) || (mountedVolumeUrls != _mountedVolumeUrls) else {
return
}

connectedVolumeBsdNames = _connectedVolumeBsdNames
mountedVolumeUrls = _mountedVolumeUrls ?? []

allVolumes = connectedVolumeBsdNames.compactMap(Volume.init)

// 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 Down Expand Up @@ -150,16 +185,16 @@ 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
}

Expand All @@ -170,7 +205,7 @@ extension AppModel {
return
}

for volume in ejectedVolumes {
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: "[ -_]*"),
Expand Down
12 changes: 6 additions & 6 deletions EjectKey/MenuBar/MenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ struct MenuView: View {
Divider()
.hidden(!showEjectAllVolumesButton || model.devices.count <= 1)

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

VolumeList(model: model, device: device, unit: unit)
}
}
Expand Down
24 changes: 13 additions & 11 deletions EjectKey/MenuBar/VolumeList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Defaults

struct VolumeList: View {
@ObservedObject var model: AppModel
@State var device: Device?
@State var device: Device
@State var unit: Unit

@Default(.showEjectAllVolumesInDiskButtons) private var showEjectAllVolumesInDiskButtons
Expand All @@ -19,10 +19,10 @@ struct VolumeList: View {
@Default(.showDetailedInformation) private var showDetailedInformation

var body: some View {
ForEach(unit.volumes.sorted(by: {$0.bsdName < $1.bsdName}), id: \.bsdName) { volume in
ForEach(unit.volumes.sorted(by: {$0.bsdName < $1.bsdName})) { volume in
// if showActionMenu {
if volume.type == .apfsContainer {
if let contentUnit = device?.units.filter({ $0.physicalStoreBsdName == volume.bsdName }).first {
if let contentUnit = device.units.filter({ $0.physicalStoreBsdName == volume.bsdName }).first {
if !(!showUnmountedVolumes && !contentUnit.existsMountedVolume) {
Menu {
// Text("APFS Container (\(unit.bsdName))")
Expand All @@ -31,7 +31,7 @@ struct VolumeList: View {
}
.hidden(!showEjectAllVolumesInDiskButtons || contentUnit.volumes.count <= 1)

VolumeList(model: model, device: nil, unit: contentUnit)
VolumeList(model: model, device: device, unit: contentUnit)
if showDetailedInformation {
Divider()
Text(volume.type.displayName())
Expand All @@ -47,13 +47,15 @@ struct VolumeList: View {
} else {
if !(!showUnmountedVolumes && !volume.isMounted) {
Menu {
if volume.isMounted {
Button(L10n.eject) {
model.eject(volume)
}
} else {
Button(L10n.mount) {
// model.eject(volume)
if volume.isMountable {
if volume.isMounted {
Button(L10n.unmount) {
// volume.unmount()
}
} else {
Button(L10n.mount) {
// model.eject(volume)
}
}
}
if let url = volume.url {
Expand Down
4 changes: 3 additions & 1 deletion EjectKey/Objects/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import Foundation

struct Device {
struct Device: Identifiable {
let id = UUID()

let path: String
let model: String?
let vendor: String?
Expand Down
11 changes: 10 additions & 1 deletion EjectKey/Objects/Unit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import Foundation

struct Unit {
struct Unit: Identifiable {
let id = UUID()

let number: Int
let bsdName: String
let name: String?
Expand Down Expand Up @@ -49,4 +51,11 @@ struct Unit {
self.physicalStoreBsdName = physicalStore.bsdName
}
}

func eject() {
guard !existsMountedVolume else {
return
}

}
}
Loading

0 comments on commit 6fe2152

Please sign in to comment.