Show a deprecated badge on installed formulae

brew info already returns deprecated. Without a badge, a
formulae-only filter hid those rows and there was no visual
cue on the list.

Fixes #180
This commit is contained in:
Gyanu Mayank
2026-09-16 08:42:53 +05:30
parent feb9a3a752
commit 1271f400ac
7 changed files with 59 additions and 0 deletions
@@ -46,6 +46,7 @@ private extension BrewInfoFormula {
linkedKeg: BrewInfoJSON.trimmedOrNil(linkedKeg),
pinned: pinned,
kegOnly: kegOnly,
deprecated: deprecated,
caveats: BrewInfoJSON.trimmedOrNil(caveats),
)
}
@@ -69,6 +70,7 @@ private extension BrewInfoCask {
outdated: outdated,
tap: BrewInfoJSON.trimmedOrNil(tap),
installedOnRequest: installedOnRequest,
deprecated: deprecated,
)
}
}
+6
View File
@@ -40,6 +40,7 @@ struct BrewInfoFormula: Decodable {
var kegOnly: Bool
var caveats: String?
var outdated: Bool
var deprecated: Bool
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -61,6 +62,7 @@ struct BrewInfoFormula: Decodable {
kegOnly = (try? container.decode(Bool.self, forKey: .kegOnly)) ?? false
caveats = try? container.decode(String.self, forKey: .caveats)
outdated = (try? container.decode(Bool.self, forKey: .outdated)) ?? false
deprecated = (try? container.decode(Bool.self, forKey: .deprecated)) ?? false
}
private enum CodingKeys: String, CodingKey {
@@ -80,6 +82,7 @@ struct BrewInfoFormula: Decodable {
case kegOnly = "keg_only"
case caveats
case outdated
case deprecated
}
}
@@ -123,6 +126,7 @@ struct BrewInfoCask: Decodable {
var installedOnRequest: Bool
var dependencies: [HomebrewPackageID]
var outdated: Bool
var deprecated: Bool
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@@ -140,6 +144,7 @@ struct BrewInfoCask: Decodable {
forKeys: [.dependencies, .dependsOn],
)
outdated = (try? container.decode(Bool.self, forKey: .outdated)) ?? false
deprecated = (try? container.decode(Bool.self, forKey: .deprecated)) ?? false
}
private enum CodingKeys: String, CodingKey {
@@ -155,6 +160,7 @@ struct BrewInfoCask: Decodable {
case dependencies
case dependsOn = "depends_on"
case outdated
case deprecated
}
var firstDisplayName: String? {
@@ -27,6 +27,8 @@ public struct InstalledBrewPackage: Identifiable, Hashable, Sendable {
public var pinned: Bool
/// True when the formula is keg-only (not linked into the prefix by default).
public var kegOnly: Bool
/// True when Homebrew marks the formula/cask deprecated.
public var deprecated: Bool
/// Post-install caveats text, if any.
public var caveats: String?
@@ -97,6 +99,7 @@ public struct InstalledBrewPackage: Identifiable, Hashable, Sendable {
linkedKeg: String? = nil,
pinned: Bool = false,
kegOnly: Bool = false,
deprecated: Bool = false,
caveats: String? = nil,
) {
self.package = package
@@ -111,6 +114,7 @@ public struct InstalledBrewPackage: Identifiable, Hashable, Sendable {
self.linkedKeg = linkedKeg
self.pinned = pinned
self.kegOnly = kegOnly
self.deprecated = deprecated
self.caveats = caveats
}
}
@@ -56,6 +56,10 @@ final class InstalledListRowViewModel {
package.outdated && availableVersionLabel != nil
}
var isDeprecated: Bool {
package.deprecated
}
var versionPresentation: InstalledListRowVersionPresentation {
if showsUpgradeAvailable, let latest = availableVersionLabel {
return .upgrade(current: installedVersionLabel, latest: latest)
@@ -71,6 +75,8 @@ final class InstalledListRowViewModel {
parts.append(installedVersionLabel)
if showsUpgradeAvailable, let latest = availableVersionLabel {
parts.append("Upgrade available to \(latest)")
} else if isDeprecated {
parts.append("Deprecated")
} else {
parts.append("Installed and up to date")
}
@@ -118,10 +118,16 @@ struct InstalledListRowView: View {
private func statusBadge(viewModel: InstalledListRowViewModel) -> some View {
if viewModel.showsUpgradeAvailable {
InstalledOutdatedBadge()
} else if viewModel.isDeprecated {
Image(systemName: "exclamationmark.triangle.fill")
.font(.body)
.foregroundStyle(Color.brewStatusWarning)
.help("Deprecated")
} else {
Image(systemName: "checkmark.circle.fill")
.font(.body)
.foregroundStyle(Color.brewStatusSuccess)
.help("Installed and up to date")
}
}
@@ -104,4 +104,27 @@ struct BrewInfoJSONMappingTests {
#expect(payload.installedPackages().map(\.name) == ["aria2", "Firefox", "Zsh"])
}
@Test func `deprecated formulae and casks keep the flag from brew info json`() throws {
let json = """
{
"formulae": [
{ "name": "youtube-dl", "deprecated": true },
{ "name": "git", "deprecated": false }
],
"casks": [
{ "token": "docker", "deprecated": true },
{ "token": "alfred" }
]
}
"""
let payload = try JSONDecoder().decode(BrewInfoJSON.self, from: Data(json.utf8))
let packages = payload.installedPackages()
#expect(packages.first { $0.name == "youtube-dl" }?.deprecated == true)
#expect(packages.first { $0.name == "git" }?.deprecated == false)
#expect(packages.first { $0.name == "docker" }?.deprecated == true)
#expect(packages.first { $0.name == "alfred" }?.deprecated == false)
}
}
@@ -52,6 +52,18 @@ struct InstalledListRowViewModelTests {
#expect(viewModel.name == "Visual Studio Code")
}
@Test func `deprecated package is not described as up to date`() {
var package = InstalledBrewPackage.fixture(name: "youtube-dl")
package.deprecated = true
let viewModel = InstalledListRowViewModel(
package: package,
brewCommandCenter: NoopBrewCommandCenter.forTesting(),
)
#expect(viewModel.isDeprecated)
#expect(viewModel.accessibilitySummary.contains("Deprecated"))
#expect(!viewModel.accessibilitySummary.contains("up to date"))
}
@Test func `observeRowUpdates applies first phase from noop center`() async {
let package = InstalledBrewPackage.fixture(name: "git", kind: .formula)
let center = NoopBrewCommandCenter.forTesting()