From 6b70ebdff3048604a45a2c8a5b15d85576ce69fb Mon Sep 17 00:00:00 2001 From: fus1ondev Date: Sat, 18 Feb 2023 20:04:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Improve=20display=20of?= =?UTF-8?q?=20time=20machine=20disks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- EjectKey/AppModel.swift | 3 ++ EjectKey/Commands.swift | 40 +++++++++++++++++++++++++++ EjectKey/MenuBar/MenuView.swift | 11 +++++++- EjectKey/Objects/Unit.swift | 2 ++ EjectKey/Objects/Volume.swift | 15 +++++++++- EjectKey/en.lproj/Localizable.strings | 16 +++++++---- EjectKey/ja.lproj/Localizable.strings | 6 ++++ 7 files changed, 86 insertions(+), 7 deletions(-) diff --git a/EjectKey/AppModel.swift b/EjectKey/AppModel.swift index 492be72..2cde400 100644 --- a/EjectKey/AppModel.swift +++ b/EjectKey/AppModel.swift @@ -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" diff --git a/EjectKey/Commands.swift b/EjectKey/Commands.swift index 015ecc6..5d34bab 100644 --- a/EjectKey/Commands.swift +++ b/EjectKey/Commands.swift @@ -8,6 +8,7 @@ import AppKit import Defaults import AudioToolbox +import SwiftShell extension AppModel { func eject(_ volume: Volume) { @@ -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()) + } } diff --git a/EjectKey/MenuBar/MenuView.swift b/EjectKey/MenuBar/MenuView.swift index ee18d6d..a8356c0 100644 --- a/EjectKey/MenuBar/MenuView.swift +++ b/EjectKey/MenuBar/MenuView.swift @@ -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))") @@ -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) @@ -93,6 +99,9 @@ struct MenuView: View { quitApp() } .keyboardShortcut("Q") + .onAppear { + model.setTimeMachines() + } } private func showSettingsWindow() { diff --git a/EjectKey/Objects/Unit.swift b/EjectKey/Objects/Unit.swift index d20a84e..4fe601e 100644 --- a/EjectKey/Objects/Unit.swift +++ b/EjectKey/Objects/Unit.swift @@ -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 @@ -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 diff --git a/EjectKey/Objects/Volume.swift b/EjectKey/Objects/Volume.swift index 574013b..b00ddb6 100644 --- a/EjectKey/Objects/Volume.swift +++ b/EjectKey/Objects/Volume.swift @@ -18,6 +18,11 @@ struct Culprit: Equatable { let application: NSRunningApplication } +struct TimeMachine { + let id: String + let mountPoint: String? +} + class Volume { let disk: DADisk @@ -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 @@ -94,6 +104,8 @@ class Volume { let type = resourceValues?.volumeLocalizedFormatDescription ?? "" + let isLocal = resourceValues?.volumeIsLocal ?? true + self.disk = disk self.bsdName = bsdName self.name = name @@ -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) { diff --git a/EjectKey/en.lproj/Localizable.strings b/EjectKey/en.lproj/Localizable.strings index 2960166..50a4873 100644 --- a/EjectKey/en.lproj/Localizable.strings +++ b/EjectKey/en.lproj/Localizable.strings @@ -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"; @@ -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"; @@ -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"; @@ -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"; diff --git a/EjectKey/ja.lproj/Localizable.strings b/EjectKey/ja.lproj/Localizable.strings index 1f11a8d..19b0e59 100644 --- a/EjectKey/ja.lproj/Localizable.strings +++ b/EjectKey/ja.lproj/Localizable.strings @@ -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";