From 4e35b4975e091ab8a773aa15c115d6c012e77f42 Mon Sep 17 00:00:00 2001 From: Nikolay Edigaryev Date: Tue, 4 Aug 2026 17:52:21 +0100 Subject: [PATCH] tart {list,get}: display humanized byte units --- Sources/tart/Commands/Get.swift | 19 +++++++++++--- Sources/tart/Commands/List.swift | 12 ++++----- .../Formatter/HumanReadableByteCount.swift | 26 +++++++++++++++++++ .../HumanReadableByteCountTests.swift | 15 +++++++++++ 4 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 Sources/tart/Formatter/HumanReadableByteCount.swift create mode 100644 Tests/TartTests/HumanReadableByteCountTests.swift diff --git a/Sources/tart/Commands/Get.swift b/Sources/tart/Commands/Get.swift index 7ffada69..c006a6d2 100644 --- a/Sources/tart/Commands/Get.swift +++ b/Sources/tart/Commands/Get.swift @@ -5,9 +5,9 @@ fileprivate struct VMInfo: Encodable { let OS: OS let CPU: Int let Memory: UInt64 - let Disk: Int + let Disk: HumanReadableByteCount let DiskFormat: String - let Size: String + let Size: HumanReadableByteCount let Display: String let Running: Bool let State: String @@ -27,7 +27,20 @@ struct Get: AsyncParsableCommand { let vmConfig = try VMConfig(fromURL: vmDir.configURL) let memorySizeInMb = vmConfig.memorySize / 1024 / 1024 - let info = VMInfo(OS: vmConfig.os, CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), DiskFormat: vmConfig.diskFormat.rawValue, Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000), Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state().rawValue) + let info = VMInfo( + OS: vmConfig.os, + CPU: vmConfig.cpuCount, + Memory: memorySizeInMb, + Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 }, + DiskFormat: vmConfig.diskFormat.rawValue, + Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) { + String(format: "%.3f", Float($0) / 1000 / 1000 / 1000) + }, + Display: vmConfig.display.description, + Running: try vmDir.running(), + State: try vmDir.state().rawValue + ) + print(format.renderSingle(info)) } } diff --git a/Sources/tart/Commands/List.swift b/Sources/tart/Commands/List.swift index 6261157f..86311e50 100644 --- a/Sources/tart/Commands/List.swift +++ b/Sources/tart/Commands/List.swift @@ -5,8 +5,8 @@ import SwiftUI fileprivate struct VMInfo: Encodable { let Source: String let Name: String - let Disk: Int - let Size: Int + let Disk: HumanReadableByteCount + let Size: HumanReadableByteCount let Accessed: String let Running: Bool let State: String @@ -42,8 +42,8 @@ struct List: AsyncParsableCommand { try VMInfo( Source: "local", Name: name, - Disk: vmDir.sizeGB(), - Size: vmDir.allocatedSizeGB(), + Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 }, + Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) { $0 / 1000 / 1000 / 1000 }, Accessed: formatAccessDate(try vmDir.accessDate()), Running: vmDir.running(), State: vmDir.state().rawValue @@ -56,8 +56,8 @@ struct List: AsyncParsableCommand { try VMInfo( Source: "OCI", Name: name, - Disk: vmDir.sizeGB(), - Size: vmDir.allocatedSizeGB(), + Disk: HumanReadableByteCount(try vmDir.sizeBytes()) { $0 / 1000 / 1000 / 1000 }, + Size: HumanReadableByteCount(try vmDir.allocatedSizeBytes()) { $0 / 1000 / 1000 / 1000 }, Accessed: formatAccessDate(try vmDir.accessDate()), Running: vmDir.running(), State: vmDir.state().rawValue diff --git a/Sources/tart/Formatter/HumanReadableByteCount.swift b/Sources/tart/Formatter/HumanReadableByteCount.swift new file mode 100644 index 00000000..d287b765 --- /dev/null +++ b/Sources/tart/Formatter/HumanReadableByteCount.swift @@ -0,0 +1,26 @@ +import Foundation + +struct HumanReadableByteCount: Encodable, CustomStringConvertible { + private let byteCount: Int + private let jsonValue: any Encodable + + init(_ byteCount: Int, encodedAs: (Int) -> JSONValue) { + self.byteCount = byteCount + self.jsonValue = encodedAs(byteCount) + } + + var description: String { + let formatter = MeasurementFormatter() + formatter.unitOptions = .naturalScale + formatter.unitStyle = .medium + formatter.numberFormatter.maximumFractionDigits = 0 + + return formatter.string( + from: Measurement(value: Double(byteCount), unit: UnitInformationStorage.bytes) + ) + } + + func encode(to encoder: Encoder) throws { + try jsonValue.encode(to: encoder) + } +} diff --git a/Tests/TartTests/HumanReadableByteCountTests.swift b/Tests/TartTests/HumanReadableByteCountTests.swift new file mode 100644 index 00000000..398158ea --- /dev/null +++ b/Tests/TartTests/HumanReadableByteCountTests.swift @@ -0,0 +1,15 @@ +import Foundation +import XCTest +@testable import tart + +final class HumanReadableByteCountTests: XCTestCase { + func testTextAndJSONRepresentations() throws { + let integer = HumanReadableByteCount(51_400_000_000) { _ in 51 } + let string = HumanReadableByteCount(17_234_000_000) { _ in "17.234" } + let encoder = JSONEncoder() + + XCTAssertEqual(string.description.compactMap(\.wholeNumberValue), [1, 7]) + XCTAssertEqual(try JSONDecoder().decode(Int.self, from: encoder.encode(integer)), 51) + XCTAssertEqual(try JSONDecoder().decode(String.self, from: encoder.encode(string)), "17.234") + } +}