Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve display of Time Machine disks #7

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
3 changes: 3 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 units: [Unit] = []

var timeMachineMountPoints: [String] = []
var remoteTimeMachineMountPoints: [String] = []

// Workaround for switching tabs of Settings View programmatically
@Published var settingsTabSelection = "general"

Expand Down
40 changes: 40 additions & 0 deletions EjectKey/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import AppKit
import Defaults
import AudioToolbox
import SwiftShell

extension AppModel {
func eject(_ volume: Volume) {
Expand Down Expand Up @@ -157,4 +158,43 @@ extension AppModel {
}
}
}

func setTimeMachines() {
let result = run("/usr/bin/tmutil", "destinationinfo", "-X")
if !result.succeeded {
return
}
guard let data = result.stdout.data(using: .utf8) else {
return
}
guard let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? NSMutableDictionary else {
return
}
guard let destinations = plist["Destinations"] as? [NSMutableDictionary] else {
return
}

var _timeMachineMountPoints: [String] = []

for destination in destinations {
guard let mountPoint = destination["MountPoint"] as? String else {
return
}
guard let encoded = mountPoint.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
return
}
_timeMachineMountPoints.append(encoded + "/")
}

timeMachineMountPoints = _timeMachineMountPoints
}

func isTimeMachine(_ unit: Unit) -> Bool {
let firstVolume = unit.volumes.first!
return isTimeMachine(firstVolume)
}

func isTimeMachine(_ volume: Volume) -> Bool {
return timeMachineMountPoints.contains(volume.url.path())
}
}
11 changes: 10 additions & 1 deletion EjectKey/MenuBar/MenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ struct MenuView: View {

ForEach(model.units.sorted(by: { $0.minNumber < $1.minNumber }), id: \.devicePath) { unit in
if showDetailedInformation {
if unit.isDiskImage {
if model.isTimeMachine(unit) {
Text(unit.isLocal ? "Time Machine" : L10n.timeMachineOnYourNetwork)
} else if unit.isDiskImage {
Text(L10n.diskImage)
} else {
Text("\(unit.deviceVendor) \(unit.deviceModel) (\(unit.deviceProtocol))")
Expand Down Expand Up @@ -64,6 +66,10 @@ struct MenuView: View {
Text(volume.type)
Text("\(L10n.size): \(volume.size.formatted(.byteCount(style: .file)))")
Text("ID: \(volume.bsdName)")
if model.isTimeMachine(volume) {
Divider()
Text(L10n.thisVolumeIsUsedAsTimeMachine)
}
}
} label: {
Image(nsImage: volume.icon)
Expand Down Expand Up @@ -93,6 +99,9 @@ struct MenuView: View {
quitApp()
}
.keyboardShortcut("Q")
.onAppear {
model.setTimeMachines()
}
}

private func showSettingsWindow() {
Expand Down
2 changes: 2 additions & 0 deletions EjectKey/Objects/Unit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct Unit {
let deviceProtocol: String
let devicePath: String
let isDiskImage: Bool
let isLocal: Bool
let volumes: [Volume]
let numbers: [Int]
let minNumber: Int
Expand All @@ -27,6 +28,7 @@ struct Unit {
self.deviceVendor = firstVolume.deviceVendor
self.deviceProtocol = firstVolume.deviceProtocol
self.isDiskImage = firstVolume.isDiskImage
self.isLocal = firstVolume.isLocal

self.numbers = volumes.map(\.unitNumber).unique.sorted()
self.minNumber = numbers.min() ?? 0
Expand Down
15 changes: 14 additions & 1 deletion EjectKey/Objects/Volume.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ struct Culprit: Equatable {
let application: NSRunningApplication
}

struct TimeMachine {
let id: String
let mountPoint: String?
}

class Volume {

let disk: DADisk
Expand All @@ -35,9 +40,14 @@ class Volume {
let icon: NSImage
let isVirtual: Bool
let isDiskImage: Bool
let isLocal: Bool

init?(url: URL) {
let resourceValues = try? url.resourceValues(forKeys: [.volumeIsInternalKey, .volumeLocalizedFormatDescriptionKey])
let resourceValues = try? url.resourceValues(forKeys: [
.volumeIsInternalKey,
.volumeIsLocalKey,
.volumeLocalizedFormatDescriptionKey
])

// let isExternalVolume = url.pathComponents.count > 1 && url.pathComponents[1] == "Volumes"
let isInternalVolume = resourceValues?.volumeIsInternal ?? false
Expand Down Expand Up @@ -94,6 +104,8 @@ class Volume {

let type = resourceValues?.volumeLocalizedFormatDescription ?? ""

let isLocal = resourceValues?.volumeIsLocal ?? true

self.disk = disk
self.bsdName = bsdName
self.name = name
Expand All @@ -109,6 +121,7 @@ class Volume {
self.icon = icon
self.isVirtual = deviceProtocol == "Virtual Interface"
self.isDiskImage = self.isVirtual && deviceVendor == "Apple" && deviceModel == "Disk Image"
self.isLocal = isLocal
}

func unmount(unmountAndEject: Bool, withoutUI: Bool, completionHandler: @escaping (Error?) -> Void) {
Expand Down
16 changes: 11 additions & 5 deletions EjectKey/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@
"disk_num" = "Disk %@";


"do_not_display_numbers_when_nothing_is_connected" = "Do not display the number when nothing is connected";
"display_only_when_external_volume_is_connected" = "Display only when external volume is connected";


"display_only_when_external_volume_is_connected" = "Display only when external volume is connected";
"do_not_display_numbers_when_nothing_is_connected" = "Do not display the number when nothing is connected";


"eject" = "Eject";
Expand Down Expand Up @@ -114,9 +114,6 @@
"show_control_strip_button" = "Show eject button on Control Strip";


"show_quit_dialog_when_ejection_fails" = "Show a dialog to quit applications using the volume when ejection fails";


"show_detailed_information" = "Show detailed information";


Expand All @@ -135,6 +132,9 @@
"show_number_of_connected_volumes" = "Show number of connected volumes";


"show_quit_dialog_when_ejection_fails" = "Show a dialog to quit applications using the volume when ejection fails";


"size" = "Size";


Expand All @@ -147,6 +147,12 @@
"this_volume_is_a_virtual_interface" = "This volume is a virtual interface.";


"this_volume_is_used_as_time_machine" = "This volume is used as Time Machine.";


"time_machine_on_your_network" = "Time Machine on your network";


"touch_bar" = "Touch Bar";


Expand Down
6 changes: 6 additions & 0 deletions EjectKey/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@
"this_volume_is_a_virtual_interface" = "このボリュームは仮想インターフェースです。";


"this_volume_is_used_as_time_machine" = "このボリュームはTime Machineとして使用されています。";


"time_machine_on_your_network" = "ネットワーク上のTime Machine";


"touch_bar" = "Touch Bar";


Expand Down