mirror of https://github.com/cirruslabs/tart.git
Add optional @PPI hint to --display for HiDPI pixel displays
`tart set --display WIDTHxHEIGHTpx` always configured the guest's pixel framebuffer at a hardcoded 72 PPI, so a macOS guest never exposed a HiDPI (2x/Retina) mode when the host display was non-Retina — or absent, which is common on headless/CI hosts. There was no way to ask for a Retina guest in that case. Allow an optional "@PPI" pixels-per-inch suffix on --display, e.g. `--display 3200x1800px@220`. At a Retina-class density the pixel display then offers a scaling:on (HiDPI) mode. The pixel path never consults NSScreen, so this works regardless of the host display or whether a user is logged in on the host. The hint lives in VMConfig (VMDisplayConfig.ppi) and defaults to the existing 72 when omitted, so behavior is unchanged for current VMs and configs. Covered by VMConfigTests (parsing, description round-trip, effective-PPI default, backward compatibility, malformed input). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cbc160a592
commit
521cc16e44
|
|
@ -14,7 +14,7 @@ struct Set: AsyncParsableCommand {
|
|||
@Option(help: "VM memory size in megabytes")
|
||||
var memory: UInt64?
|
||||
|
||||
@Option(help: "VM display resolution in a format of WIDTHxHEIGHT[pt|px]. For example, 1200x800, 1200x800pt or 1920x1080px. Units are treated as hints and default to \"pt\" (points) for macOS VMs and \"px\" (pixels) for Linux VMs when not specified.")
|
||||
@Option(help: "VM display resolution in a format of WIDTHxHEIGHT[pt|px][@PPI]. For example, 1200x800, 1200x800pt, 1920x1080px or 3200x1800px@220. Units are treated as hints and default to \"pt\" (points) for macOS VMs and \"px\" (pixels) for Linux VMs when not specified. The optional @PPI (pixels-per-inch) hint applies to pixel-unit displays and, at a Retina-class value such as 220, makes a macOS guest expose a HiDPI (2x) mode regardless of the host display; it defaults to 72 (non-Retina) when omitted.")
|
||||
var display: VMDisplayConfig?
|
||||
|
||||
@Flag(inversion: .prefixedNo, help: ArgumentHelp("Whether to automatically reconfigure the VM's display to fit the window"))
|
||||
|
|
@ -57,6 +57,7 @@ struct Set: AsyncParsableCommand {
|
|||
vmConfig.display.height = display.height
|
||||
}
|
||||
vmConfig.display.unit = display.unit
|
||||
vmConfig.display.ppi = display.ppi
|
||||
}
|
||||
|
||||
vmConfig.displayRefit = displayRefit
|
||||
|
|
@ -91,6 +92,14 @@ extension VMDisplayConfig: ExpressibleByArgument {
|
|||
public init(argument: String) {
|
||||
var argument = argument
|
||||
var unit: Unit? = nil
|
||||
var ppi: Int? = nil
|
||||
|
||||
// Optional "@PPI" pixels-per-inch hint, e.g. "3200x1800px@220". Parsed
|
||||
// before the unit suffix since it always trails the whole spec.
|
||||
if let atIndex = argument.lastIndex(of: "@") {
|
||||
ppi = Int(argument[argument.index(after: atIndex)...])
|
||||
argument = String(argument[..<atIndex])
|
||||
}
|
||||
|
||||
if argument.hasSuffix(Unit.pixel.rawValue) {
|
||||
argument = String(argument.dropLast(Unit.pixel.rawValue.count))
|
||||
|
|
@ -107,6 +116,7 @@ extension VMDisplayConfig: ExpressibleByArgument {
|
|||
width: parts[safe: 0] ?? 0,
|
||||
height: parts[safe: 1] ?? 0,
|
||||
unit: unit,
|
||||
ppi: ppi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,9 +95,11 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
|
|||
VZMacGraphicsDisplayConfiguration(
|
||||
widthInPixels: vmConfig.display.width,
|
||||
heightInPixels: vmConfig.display.height,
|
||||
// A reasonable guess according to Apple's documentation[1]
|
||||
// Defaults to 72 — a reasonable guess according to Apple's
|
||||
// documentation[1] — unless a --display "@PPI" hint asks for a
|
||||
// Retina-class density (which yields a HiDPI guest mode).
|
||||
// [1]: https://developer.apple.com/documentation/coregraphics/1456599-cgdisplayscreensize
|
||||
pixelsPerInch: 72
|
||||
pixelsPerInch: vmConfig.display.effectivePixelsPerInch
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -41,15 +41,27 @@ struct VMDisplayConfig: Codable, Equatable {
|
|||
var width: Int = 1024
|
||||
var height: Int = 768
|
||||
var unit: Unit?
|
||||
var ppi: Int?
|
||||
|
||||
// Pixels-per-inch handed to the guest's pixel-unit framebuffer. Defaults to
|
||||
// 72 (non-Retina) when no hint is given, matching Virtualization.framework's
|
||||
// historical assumption; a Retina-class value (e.g. 220) makes the guest
|
||||
// expose a HiDPI (scaling:on) mode regardless of the host display.
|
||||
var effectivePixelsPerInch: Int {
|
||||
ppi ?? 72
|
||||
}
|
||||
}
|
||||
|
||||
extension VMDisplayConfig: CustomStringConvertible {
|
||||
var description: String {
|
||||
var result = "\(width)x\(height)"
|
||||
if let unit {
|
||||
"\(width)x\(height)\(unit.rawValue)"
|
||||
} else {
|
||||
"\(width)x\(height)"
|
||||
result += unit.rawValue
|
||||
}
|
||||
if let ppi {
|
||||
result += "@\(ppi)"
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,4 +15,46 @@ final class VMConfigTests: XCTestCase {
|
|||
vmDisplayConfig = VMDisplayConfig.init(argument: "1234x5678px")
|
||||
XCTAssertEqual(VMDisplayConfig(width: 1234, height: 5678, unit: .pixel), vmDisplayConfig)
|
||||
}
|
||||
|
||||
func testEffectivePixelsPerInchUsesConfiguredValue() throws {
|
||||
let config = VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220)
|
||||
XCTAssertEqual(220, config.effectivePixelsPerInch)
|
||||
}
|
||||
|
||||
func testEffectivePixelsPerInchDefaultsTo72WhenUnset() throws {
|
||||
// Upstream behavior: a pixel display with no PPI hint stays non-Retina (72 PPI).
|
||||
let config = VMDisplayConfig(width: 1234, height: 5678, unit: .pixel, ppi: nil)
|
||||
XCTAssertEqual(72, config.effectivePixelsPerInch)
|
||||
}
|
||||
|
||||
func testParsesPixelsPerInchSuffix() throws {
|
||||
let config = VMDisplayConfig(argument: "3200x1800px@220")
|
||||
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220), config)
|
||||
}
|
||||
|
||||
func testWithoutSuffixHasNilPixelsPerInch() throws {
|
||||
// Backward compatibility: existing "WIDTHxHEIGHT[pt|px]" strings carry no PPI.
|
||||
XCTAssertEqual(
|
||||
VMDisplayConfig(width: 1234, height: 5678, unit: .pixel, ppi: nil),
|
||||
VMDisplayConfig(argument: "1234x5678px"))
|
||||
}
|
||||
|
||||
func testDescriptionRoundTripsPixelsPerInch() throws {
|
||||
let config = VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: 220)
|
||||
XCTAssertEqual("3200x1800px@220", config.description)
|
||||
XCTAssertEqual(config, VMDisplayConfig(argument: config.description))
|
||||
}
|
||||
|
||||
func testParsesPixelsPerInchWithoutExplicitUnit() throws {
|
||||
let config = VMDisplayConfig(argument: "3200x1800@220")
|
||||
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: nil, ppi: 220), config)
|
||||
}
|
||||
|
||||
func testMalformedPixelsPerInchDegradesToNil() throws {
|
||||
// A non-numeric PPI is ignored (falls back to the 72 default) rather than
|
||||
// failing the parse — consistent with the parser's lenient dimensions.
|
||||
let config = VMDisplayConfig(argument: "3200x1800px@notanumber")
|
||||
XCTAssertEqual(VMDisplayConfig(width: 3200, height: 1800, unit: .pixel, ppi: nil), config)
|
||||
XCTAssertEqual(72, config.effectivePixelsPerInch)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue