From 104f2e63f4dd8b21c28248276ef708a2f6846d65 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 8 Jun 2026 22:22:08 +1000 Subject: [PATCH 01/18] Fix sidebar layout --- .../MainWindow/Views/MainWindowView.swift | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Brew/Features/MainWindow/Views/MainWindowView.swift b/Brew/Features/MainWindow/Views/MainWindowView.swift index 8c87aca..2c3e87d 100644 --- a/Brew/Features/MainWindow/Views/MainWindowView.swift +++ b/Brew/Features/MainWindow/Views/MainWindowView.swift @@ -16,24 +16,25 @@ struct MainWindowView: View { @SceneStorage("consoleHeight") private var consoleHeight: Double = BrewLayout.consoleDefaultExpandedHeight var body: some View { - AnimatedSplit( - collapsed: !consoleExpanded, - collapsedHeight: BrewLayout.consoleCollapsedHeight, - expandedHeight: consoleHeight, - minExpandedHeight: BrewLayout.consoleMinExpandedHeight, - maxExpandedHeight: BrewLayout.consoleMaxExpandedHeight, - animation: .brewFast, - ) { - NavigationSplitView { - sidebarColumn - } detail: { + NavigationSplitView { + sidebarColumn + } detail: { + AnimatedSplit( + collapsed: !consoleExpanded, + collapsedHeight: BrewLayout.consoleCollapsedHeight, + expandedHeight: consoleHeight, + minExpandedHeight: BrewLayout.consoleMinExpandedHeight, + maxExpandedHeight: BrewLayout.consoleMaxExpandedHeight, + animation: .brewFast, + ) { featureColumn + } bottom: { + ConsolePanelRoot(expanded: $consoleExpanded) } - .background(.bar) - .navigationSplitViewStyle(.prominentDetail) - } bottom: { - ConsolePanelRoot(expanded: $consoleExpanded) + .focusedSceneValue(\.consoleExpanded, $consoleExpanded) } + .background(.bar) + .navigationSplitViewStyle(.prominentDetail) .focusedSceneValue(\.consoleExpanded, $consoleExpanded) .environment(\.navigateToInstalledPackage) { id in pendingInstalledSelection = id From d9af625581f90cb2c4bd01059e45e0103fddb2a6 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 8 Jun 2026 22:25:58 +1000 Subject: [PATCH 02/18] Update app icon --- .../AppIcon.appiconset/Contents.json | 80 ++++++++++-------- .../AppIcon.appiconset/icon_1024.png | Bin 0 -> 51218 bytes .../AppIcon.appiconset/icon_128.png | Bin 0 -> 5064 bytes .../AppIcon.appiconset/icon_16.png | Bin 0 -> 473 bytes .../AppIcon.appiconset/icon_256.png | Bin 0 -> 11086 bytes .../AppIcon.appiconset/icon_32.png | Bin 0 -> 964 bytes .../AppIcon.appiconset/icon_512.png | Bin 0 -> 23584 bytes .../AppIcon.appiconset/icon_64.png | Bin 0 -> 2281 bytes 8 files changed, 45 insertions(+), 35 deletions(-) create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_1024.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_128.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_16.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_256.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_32.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_512.png create mode 100644 Brew/Assets.xcassets/AppIcon.appiconset/icon_64.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/Contents.json b/Brew/Assets.xcassets/AppIcon.appiconset/Contents.json index 3f00db4..3ba5775 100644 --- a/Brew/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Brew/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,58 +1,68 @@ { - "images" : [ + "images": [ { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" + "idiom": "mac", + "size": "16x16", + "scale": "1x", + "filename": "icon_16.png" }, { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" + "idiom": "mac", + "size": "16x16", + "scale": "2x", + "filename": "icon_32.png" }, { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" + "idiom": "mac", + "size": "32x32", + "scale": "1x", + "filename": "icon_32.png" }, { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" + "idiom": "mac", + "size": "32x32", + "scale": "2x", + "filename": "icon_64.png" }, { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" + "idiom": "mac", + "size": "128x128", + "scale": "1x", + "filename": "icon_128.png" }, { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" + "idiom": "mac", + "size": "128x128", + "scale": "2x", + "filename": "icon_256.png" }, { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" + "idiom": "mac", + "size": "256x256", + "scale": "1x", + "filename": "icon_256.png" }, { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" + "idiom": "mac", + "size": "256x256", + "scale": "2x", + "filename": "icon_512.png" }, { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" + "idiom": "mac", + "size": "512x512", + "scale": "1x", + "filename": "icon_512.png" }, { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "idiom": "mac", + "size": "512x512", + "scale": "2x", + "filename": "icon_1024.png" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "version": 1, + "author": "xcode" } -} +} \ No newline at end of file diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/Brew/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..dd68ed74b9cde4662f803d8a81436234f040c7b5 GIT binary patch literal 51218 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 F{s&+w=QjWV literal 0 HcmV?d00001 diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_32.png b/Brew/Assets.xcassets/AppIcon.appiconset/icon_32.png new file mode 100644 index 0000000000000000000000000000000000000000..44c34315ef8c12a474ff0cd1784bcc1fb8f4d0f0 GIT binary patch literal 964 zcmV;#13UbQP)%|1Yp}#bTw>vZc|7K=);3ocKh-MF^lmc`0Nfc!SDf@wrHuIN&X%z9U5b6)U z1V|~nFpW=v{cVyqgGBNc!SG8$2+LK#+RZCpuQPcAfp}f09WT2JsFe;O8U0tt*I*&# zZV~F7u0K%h3grk$yiC`1jvo7vTCM8o=VA&9&o@#A9Rm7WT_L2ZR84s2y`uzz3OnxI z#?YPz{MSHYbGBCiD+(4~mPl;CtX3#h3}$9dGq`;#rdjojaagR8(LMu4@f?tL6JIKo zdH;itxJ8SzaPblyp%9v;vASAe`@m-QKA81WgE
  • a8E_EkJrkj*flFrgVm6Dn&RP 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 AGXMYp literal 0 HcmV?d00001 diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_64.png b/Brew/Assets.xcassets/AppIcon.appiconset/icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f2859107794a613c8cd67df8f73be4c84233c98 GIT binary patch literal 2281 zcmV)_qgX?fI$WsWROArf1zL+F*7-xT|ZKhDi4Gd z0>=T+hFF^~91PrqgmQ<=c0{iqS`$ns0Xcw~)soY(j0DybQF1T_!-T}l$DM9haYTf% zERcqNJD|I}JzcAcI6_1Z2Ci?wa7u1zr_ky)5Rv3h1HT;L?6PL7)aqkI!mLc+z`!gW zB6VaT5jlL-_341#zr5x&5q&QPe-CCpMC!-_B4Tg#gpW?tT9x|9_igY1Kt%bH%eKa6 zJ#U5X?v7`KO7+{&$r^Ba9Cqh8oyk-oS3x!cn3;&gE8)>KU}!Zu)h2HhWF6q_vQ8p_ zWPqXcs;{rd=`&{(_Y=$|1kLT}sE-TS0V4oPi0;r zfFeUBz+lj0YIYWmAO8mu5)#nb){b^-C(18>jo<(7-B6kFhF-2fE#F3hk7GhWYz-y-b0&lSDL4sp%_M%klb_ z?Jydn5I6h*Ts(gPw`yyVIW`lG4Yy%3MI$3K6L(tfpryGH+qP^(>ZlZ@X8Xa+CxtG0 zx~DQP5ultaSIbfS+GZ55TMwhr2mqK@Fdt5*6E<5Xhy+X~Q(t|n)ryzbyo&eU+KI#w z;fjX-c!_{k#SvgBSd2Br>tQe${BG0K)QFM;`>^NTUn?=)kKPD~@fZQ2o8;f%^qI35 zoso&MvN9Yyb`;gs)qb^$jg3QRrww*{SGc70Lw*V{E9&^T!an271Qxj1m( zAd*u?B5y`szj~K1U&j9Z`(QK}V6)k-8_yuEu5 zez|@PhCdJ=l!kAQ#WNva1eqBto?C?_%a$W|+O&RUX0sU?8DsF~mN(#VIAF0@Fn8WO z=yW;&fKI2wj_upwa=D<+s?G@4#sEe+>x~9}Wn78eA>EhOy(u+l!(7!UH?J#45Mw`x2m zXzvaF9up=^=-VDWQF`tyQj(HFb59_Hmk0=M5gazujEN6rV%gFn{N#~rOwG2p~o0EmK)R94Jxld4wVDLs)6fJ<=Zima|!hv_zqO$5H zSUm(0hG-0ni-#dx61I~d z5n=MANqA^dJGfDRbS5}jZs1PG9oQXiwAoxJuXJMVOJ}ib_a8t+IQj8M*t^*R=ji3w z^!i3T`_e0Lxm@URIIv;e%ScZf6>^>O4v-{3CT#uUzxG}coeYW^0ZrRY#7EzMfHZLw z(DBbQ{Q1~Ncyjg}sH85agn&k`$MzjN`|4S(Ry@6Y8A|pBnVysea2vb_5rLU~Cf*lh znXqK{HvH?vU-58KZ{_CZX1w^q^DspXfz{f9oxl1GQbwhsp|J_^aj_xQ8S-uL9z^~< zbkawtuy=(K&F$q#j2!|1xNz|zX68-D!U7A9{^etQSz3xny&iVEU6CeFgk=#7w0)#j zATd@a(+7L`u3U@v2vFz)f2M#>`=J=W7s3_+{!Rh!_P?ni^bruY+hO2t1boK{TTW1M z000Qx90+C`px4W0 zwH4#-$&jK;$s#yq$`rUL0z2OR0Ja{1soB|RY^X_k0FtZz61JRRzX*`ry_cWV)YM@0suwZT90N&`@bH98?ArQT@R~TNdjX|S zhVF{hn&7_mDX6vDcM}poR4T4qy^1GiK8l6;vpn}1tQHS=+Xd`OPH}|ZMNo`;8Zlx+Rsq;KsBk`1 zYAupRjzm&YGNNN*5EXU*aadbxE9&d(P<6cmEiKK6jxwP5g(8ee9~HWDzl0ldMY2KO zQFkrYXwck72zCldl5q3ZZQQD@LuaLfMNg|MGC_RGne)_y7G1L6MPOH z2qI(WsK5N6MyuLJLd*c53Rq#7VCG6m;w>t3`iK7huK=JRWofUukPuLkD6{2I`00000NkvXXu0mjf D4|7f* literal 0 HcmV?d00001 From 5a1c8dac12784898bc45689db56b1e8ef9e2e503 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 8 Jun 2026 22:50:02 +1000 Subject: [PATCH 03/18] Fix min window size --- Brew/BrewApp.swift | 4 ++++ Sources/BrewUIComponents/Theme/BrewSpacing.swift | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Brew/BrewApp.swift b/Brew/BrewApp.swift index 084fd83..f03902b 100644 --- a/Brew/BrewApp.swift +++ b/Brew/BrewApp.swift @@ -84,6 +84,10 @@ struct BrewApp: App { configRepository.invalidate() envFileRepository.invalidate() } + .frame( + minWidth: BrewLayout.minWindowWidth, + minHeight: BrewLayout.minWindowHeight, + ) } .defaultSize( width: BrewLayout.minWindowWidth, diff --git a/Sources/BrewUIComponents/Theme/BrewSpacing.swift b/Sources/BrewUIComponents/Theme/BrewSpacing.swift index a624455..4b69bdc 100644 --- a/Sources/BrewUIComponents/Theme/BrewSpacing.swift +++ b/Sources/BrewUIComponents/Theme/BrewSpacing.swift @@ -48,10 +48,8 @@ public enum BrewLayout { public static let installedDetailColumnMaxWidth: CGFloat = 1200 public static let installedThreePaneMinWindowWidth: CGFloat = 960 - /// Minimum window width for the main window: sidebar + feature surface. - /// Installed detail is handled inside the feature view when selected. - public static let minWindowWidth: CGFloat = - Self.sidebarWidth + Self.installedListColumnMinWidth + /// Minimum supported window width. + public static let minWindowWidth: CGFloat = 820 /// Minimum supported window height. public static let minWindowHeight: CGFloat = 520 From 6d3d7d88c0069e90e2ede10204a27c7b806e144f Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 9 Jun 2026 19:13:41 +1000 Subject: [PATCH 04/18] Fix the divider line... sometimes --- Brew/Features/MainWindow/Views/MainWindowView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Brew/Features/MainWindow/Views/MainWindowView.swift b/Brew/Features/MainWindow/Views/MainWindowView.swift index 2c3e87d..ad6538d 100644 --- a/Brew/Features/MainWindow/Views/MainWindowView.swift +++ b/Brew/Features/MainWindow/Views/MainWindowView.swift @@ -33,8 +33,7 @@ struct MainWindowView: View { } .focusedSceneValue(\.consoleExpanded, $consoleExpanded) } - .background(.bar) - .navigationSplitViewStyle(.prominentDetail) + .navigationSplitViewStyle(.automatic) .focusedSceneValue(\.consoleExpanded, $consoleExpanded) .environment(\.navigateToInstalledPackage) { id in pendingInstalledSelection = id From 088faa5e72efe9a82327e1689f5e782d23ef8746 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Tue, 9 Jun 2026 19:19:43 +1000 Subject: [PATCH 05/18] Update background colour of sidebar --- Brew/Views/MainSidebarView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Brew/Views/MainSidebarView.swift b/Brew/Views/MainSidebarView.swift index eba100a..0a4cf18 100644 --- a/Brew/Views/MainSidebarView.swift +++ b/Brew/Views/MainSidebarView.swift @@ -85,7 +85,7 @@ struct MainSidebarView: View { Spacer(minLength: 0) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .background(Color.brewSurfaceRecessed) + .background(Color.brewSurface) } @ViewBuilder From c4febc27d4cae142a4ab789a35aad05b5a349b69 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 11 Jun 2026 21:38:28 +1000 Subject: [PATCH 06/18] Rename app to Homebrew Rename the Xcode app target, product, and project from Brew to Homebrew: - Brew.xcodeproj -> Homebrew.xcodeproj, and fix the file-system- synchronized group path (Brew -> Homebrew) so the app target picks up its sources again - Source folder Brew/ -> Homebrew/ - Update schemes, test plans, CI workflows, README, and bootstrap script - PRODUCT_BUNDLE_IDENTIFIER and pkgbuild identifier -> sh.brew.app The package and repository names are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/pr_build_test.yml | 8 ++-- .github/workflows/release.yml | 4 +- .github/workflows/ui_smoke.yml | 4 +- Brew-UI.xctestplan | 2 +- Brew-Unit.xctestplan | 2 +- .../project.pbxproj | 36 +++++++++--------- .../contents.xcworkspacedata | 0 .../xcshareddata/swiftpm/Package.resolved | 0 .../xcshareddata/xcschemes/Brew-UI.xcscheme | 18 ++++----- .../xcshareddata/xcschemes/Brew-Unit.xcscheme | 18 ++++----- .../xcshareddata/xcschemes/Brew.xcscheme | 22 +++++------ .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../AppIcon.appiconset/icon_1024.png | Bin .../AppIcon.appiconset/icon_128.png | Bin .../AppIcon.appiconset/icon_16.png | Bin .../AppIcon.appiconset/icon_256.png | Bin .../AppIcon.appiconset/icon_32.png | Bin .../AppIcon.appiconset/icon_512.png | Bin .../AppIcon.appiconset/icon_64.png | Bin .../Assets.xcassets/Contents.json | 0 .../MenuIcon.imageset/Contents.json | 20 ++++++++++ {Brew => Homebrew}/BrewApp.swift | 0 .../Debug/DebugMenuCommands.swift | 0 .../MainWindow/Views/MainWindowView.swift | 0 .../Utilities/UserDefaultsDebug.swift | 0 .../Views/MainSidebarView.swift | 2 - README.md | 2 +- scripts/bootstrap | 6 +-- 29 files changed, 81 insertions(+), 63 deletions(-) rename {Brew.xcodeproj => Homebrew.xcodeproj}/project.pbxproj (96%) rename {Brew.xcodeproj => Homebrew.xcodeproj}/project.xcworkspace/contents.xcworkspacedata (100%) rename {Brew.xcodeproj => Homebrew.xcodeproj}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved (100%) rename {Brew.xcodeproj => Homebrew.xcodeproj}/xcshareddata/xcschemes/Brew-UI.xcscheme (85%) rename {Brew.xcodeproj => Homebrew.xcodeproj}/xcshareddata/xcschemes/Brew-Unit.xcscheme (85%) rename {Brew.xcodeproj => Homebrew.xcodeproj}/xcshareddata/xcschemes/Brew.xcscheme (84%) rename {Brew => Homebrew}/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_1024.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_128.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_16.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_256.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_32.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_512.png (100%) rename {Brew => Homebrew}/Assets.xcassets/AppIcon.appiconset/icon_64.png (100%) rename {Brew => Homebrew}/Assets.xcassets/Contents.json (100%) create mode 100644 Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json rename {Brew => Homebrew}/BrewApp.swift (100%) rename {Brew => Homebrew}/Debug/DebugMenuCommands.swift (100%) rename {Brew => Homebrew}/Features/MainWindow/Views/MainWindowView.swift (100%) rename {Brew => Homebrew}/Utilities/UserDefaultsDebug.swift (100%) rename {Brew => Homebrew}/Views/MainSidebarView.swift (98%) diff --git a/.github/workflows/pr_build_test.yml b/.github/workflows/pr_build_test.yml index 24db00f..89496ac 100644 --- a/.github/workflows/pr_build_test.yml +++ b/.github/workflows/pr_build_test.yml @@ -8,7 +8,7 @@ on: - "Brew/**" - "BrewTests/**" - "BrewUITests/**" - - "Brew.xcodeproj/**" + - "Homebrew.xcodeproj/**" - "Brew.entitlements" - "Sources/**" - "Tests/**" @@ -22,7 +22,7 @@ on: - "Brew/**" - "BrewTests/**" - "BrewUITests/**" - - "Brew.xcodeproj/**" + - "Homebrew.xcodeproj/**" - "Brew.entitlements" - "Sources/**" - "Tests/**" @@ -48,13 +48,13 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Resolve package dependencies - run: xcodebuild -resolvePackageDependencies -project Brew.xcodeproj + run: xcodebuild -resolvePackageDependencies -project Homebrew.xcodeproj - name: Build and test (Xcode app target) run: | set -o pipefail xcodebuild test \ - -project Brew.xcodeproj \ + -project Homebrew.xcodeproj \ -scheme Brew-Unit \ -destination "platform=macOS" \ -skipPackagePluginValidation \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9377a37..198d590 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: - name: Build and archive run: | xcodebuild archive \ - -project Brew.xcodeproj \ + -project Homebrew.xcodeproj \ -scheme Brew \ -configuration Release \ -archivePath "$RUNNER_TEMP/Brew.xcarchive" \ @@ -140,7 +140,7 @@ jobs: echo "VERSION=$VERSION" >> "$GITHUB_ENV" pkgbuild \ --root "$RUNNER_TEMP/export" \ - --identifier "sh.brew.BrewUI" \ + --identifier "sh.brew.app" \ --version "$VERSION" \ --install-location "/Applications" \ --scripts scripts \ diff --git a/.github/workflows/ui_smoke.yml b/.github/workflows/ui_smoke.yml index 87cfeb0..fc2a1be 100644 --- a/.github/workflows/ui_smoke.yml +++ b/.github/workflows/ui_smoke.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Resolve package dependencies - run: xcodebuild -resolvePackageDependencies -project Brew.xcodeproj + run: xcodebuild -resolvePackageDependencies -project Homebrew.xcodeproj - name: Run UI smoke test run: | @@ -53,7 +53,7 @@ jobs: XCODEBUILD_ARGS=( test - -project Brew.xcodeproj + -project Homebrew.xcodeproj -scheme Brew-UI -destination "platform=macOS" -skipPackagePluginValidation diff --git a/Brew-UI.xctestplan b/Brew-UI.xctestplan index 0bc9b00..5147964 100644 --- a/Brew-UI.xctestplan +++ b/Brew-UI.xctestplan @@ -14,7 +14,7 @@ "testTargets" : [ { "target" : { - "containerPath" : "container:Brew.xcodeproj", + "containerPath" : "container:Homebrew.xcodeproj", "identifier" : "EEC39ED52F5AB4B900269514", "name" : "BrewUITests" } diff --git a/Brew-Unit.xctestplan b/Brew-Unit.xctestplan index 51d1816..48e0b8b 100644 --- a/Brew-Unit.xctestplan +++ b/Brew-Unit.xctestplan @@ -14,7 +14,7 @@ "testTargets" : [ { "target" : { - "containerPath" : "container:Brew.xcodeproj", + "containerPath" : "container:Homebrew.xcodeproj", "identifier" : "EEC39ECB2F5AB4B900269514", "name" : "BrewTests" } diff --git a/Brew.xcodeproj/project.pbxproj b/Homebrew.xcodeproj/project.pbxproj similarity index 96% rename from Brew.xcodeproj/project.pbxproj rename to Homebrew.xcodeproj/project.pbxproj index 5138215..8087824 100644 --- a/Brew.xcodeproj/project.pbxproj +++ b/Homebrew.xcodeproj/project.pbxproj @@ -45,15 +45,15 @@ EE00AA000000000000000002 /* Signing.local.xcconfig.example */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.local.xcconfig.example; sourceTree = ""; }; EE2FB0352F653F89004AE92A /* Brew-Unit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Brew-Unit.xctestplan"; sourceTree = ""; }; EE2FB0372F654024004AE92A /* Brew-UI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Brew-UI.xctestplan"; sourceTree = ""; }; - EEC39EBF2F5AB4B900269514 /* Brew.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Brew.app; sourceTree = BUILT_PRODUCTS_DIR; }; + EEC39EBF2F5AB4B900269514 /* Homebrew.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Homebrew.app; sourceTree = BUILT_PRODUCTS_DIR; }; EEC39ECC2F5AB4B900269514 /* BrewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BrewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; EEC39ED62F5AB4B900269514 /* BrewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BrewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - EEC39EC12F5AB4B900269514 /* Brew */ = { + EEC39EC12F5AB4B900269514 /* Homebrew */ = { isa = PBXFileSystemSynchronizedRootGroup; - path = Brew; + path = Homebrew; sourceTree = ""; }; EEC39ECF2F5AB4B900269514 /* BrewTests */ = { @@ -119,7 +119,7 @@ children = ( EE2FB0352F653F89004AE92A /* Brew-Unit.xctestplan */, EE2FB0372F654024004AE92A /* Brew-UI.xctestplan */, - EEC39EC12F5AB4B900269514 /* Brew */, + EEC39EC12F5AB4B900269514 /* Homebrew */, EEC39ECF2F5AB4B900269514 /* BrewTests */, EEC39ED92F5AB4B900269514 /* BrewUITests */, EEC39EC02F5AB4B900269514 /* Products */, @@ -130,7 +130,7 @@ EEC39EC02F5AB4B900269514 /* Products */ = { isa = PBXGroup; children = ( - EEC39EBF2F5AB4B900269514 /* Brew.app */, + EEC39EBF2F5AB4B900269514 /* Homebrew.app */, EEC39ECC2F5AB4B900269514 /* BrewTests.xctest */, EEC39ED62F5AB4B900269514 /* BrewUITests.xctest */, ); @@ -140,9 +140,9 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - EEC39EBE2F5AB4B900269514 /* Brew */ = { + EEC39EBE2F5AB4B900269514 /* Homebrew */ = { isa = PBXNativeTarget; - buildConfigurationList = EEC39EE02F5AB4B900269514 /* Build configuration list for PBXNativeTarget "Brew" */; + buildConfigurationList = EEC39EE02F5AB4B900269514 /* Build configuration list for PBXNativeTarget "Homebrew" */; buildPhases = ( EEC39EBB2F5AB4B900269514 /* Sources */, EEC39EBC2F5AB4B900269514 /* Frameworks */, @@ -154,9 +154,9 @@ EEAA00012F70000000000003 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( - EEC39EC12F5AB4B900269514 /* Brew */, + EEC39EC12F5AB4B900269514 /* Homebrew */, ); - name = Brew; + name = Homebrew; packageProductDependencies = ( EEAA100100000000000000C1 /* BrewCore */, EEAA100200000000000000C2 /* BrewUIComponents */, @@ -172,7 +172,7 @@ EEAA100C00000000000000CC /* BrewFeatureConfig */, ); productName = Brew; - productReference = EEC39EBF2F5AB4B900269514 /* Brew.app */; + productReference = EEC39EBF2F5AB4B900269514 /* Homebrew.app */; productType = "com.apple.product-type.application"; }; EEC39ECB2F5AB4B900269514 /* BrewTests */ = { @@ -244,7 +244,7 @@ }; }; }; - buildConfigurationList = EEC39EBA2F5AB4B900269514 /* Build configuration list for PBXProject "Brew" */; + buildConfigurationList = EEC39EBA2F5AB4B900269514 /* Build configuration list for PBXProject "Homebrew" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -262,7 +262,7 @@ projectDirPath = ""; projectRoot = ""; targets = ( - EEC39EBE2F5AB4B900269514 /* Brew */, + EEC39EBE2F5AB4B900269514 /* Homebrew */, EEC39ECB2F5AB4B900269514 /* BrewTests */, EEC39ED52F5AB4B900269514 /* BrewUITests */, ); @@ -326,12 +326,12 @@ }; EEC39ECE2F5AB4B900269514 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEC39EBE2F5AB4B900269514 /* Brew */; + target = EEC39EBE2F5AB4B900269514 /* Homebrew */; targetProxy = EEC39ECD2F5AB4B900269514 /* PBXContainerItemProxy */; }; EEC39ED82F5AB4B900269514 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = EEC39EBE2F5AB4B900269514 /* Brew */; + target = EEC39EBE2F5AB4B900269514 /* Homebrew */; targetProxy = EEC39ED72F5AB4B900269514 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -482,7 +482,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = sh.brew.BrewUI; + PRODUCT_BUNDLE_IDENTIFIER = sh.brew.app; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -513,7 +513,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = sh.brew.BrewUI; + PRODUCT_BUNDLE_IDENTIFIER = sh.brew.app; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -604,7 +604,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - EEC39EBA2F5AB4B900269514 /* Build configuration list for PBXProject "Brew" */ = { + EEC39EBA2F5AB4B900269514 /* Build configuration list for PBXProject "Homebrew" */ = { isa = XCConfigurationList; buildConfigurations = ( EEC39EDE2F5AB4B900269514 /* Debug */, @@ -613,7 +613,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - EEC39EE02F5AB4B900269514 /* Build configuration list for PBXNativeTarget "Brew" */ = { + EEC39EE02F5AB4B900269514 /* Build configuration list for PBXNativeTarget "Homebrew" */ = { isa = XCConfigurationList; buildConfigurations = ( EEC39EE12F5AB4B900269514 /* Debug */, diff --git a/Brew.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Homebrew.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from Brew.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to Homebrew.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Brew.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Homebrew.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved similarity index 100% rename from Brew.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename to Homebrew.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/Brew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme similarity index 85% rename from Brew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme rename to Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme index eaf3f04..464384d 100644 --- a/Brew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme +++ b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-UI.xcscheme @@ -16,9 +16,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -50,9 +50,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -67,9 +67,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> diff --git a/Brew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme similarity index 85% rename from Brew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme rename to Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme index f257935..1425766 100644 --- a/Brew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme +++ b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew-Unit.xcscheme @@ -16,9 +16,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -50,9 +50,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -67,9 +67,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> diff --git a/Brew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme similarity index 84% rename from Brew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme rename to Homebrew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme index f88e41c..fad3828 100644 --- a/Brew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme +++ b/Homebrew.xcodeproj/xcshareddata/xcschemes/Brew.xcscheme @@ -16,9 +16,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -38,7 +38,7 @@ BlueprintIdentifier = "EEC39ECB2F5AB4B900269514" BuildableName = "BrewTests.xctest" BlueprintName = "BrewTests" - ReferencedContainer = "container:Brew.xcodeproj"> + ReferencedContainer = "container:Homebrew.xcodeproj"> + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -69,9 +69,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> @@ -86,9 +86,9 @@ + BuildableName = "Homebrew.app" + BlueprintName = "Homebrew" + ReferencedContainer = "container:Homebrew.xcodeproj"> diff --git a/Brew/Assets.xcassets/AccentColor.colorset/Contents.json b/Homebrew/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from Brew/Assets.xcassets/AccentColor.colorset/Contents.json rename to Homebrew/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/Contents.json b/Homebrew/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/Contents.json rename to Homebrew/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_1024.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_1024.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_1024.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_128.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_128.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_128.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_128.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_16.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_16.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_16.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_16.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_256.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_256.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_256.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_256.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_32.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_32.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_32.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_32.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_512.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_512.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_512.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_512.png diff --git a/Brew/Assets.xcassets/AppIcon.appiconset/icon_64.png b/Homebrew/Assets.xcassets/AppIcon.appiconset/icon_64.png similarity index 100% rename from Brew/Assets.xcassets/AppIcon.appiconset/icon_64.png rename to Homebrew/Assets.xcassets/AppIcon.appiconset/icon_64.png diff --git a/Brew/Assets.xcassets/Contents.json b/Homebrew/Assets.xcassets/Contents.json similarity index 100% rename from Brew/Assets.xcassets/Contents.json rename to Homebrew/Assets.xcassets/Contents.json diff --git a/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json b/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json new file mode 100644 index 0000000..a19a549 --- /dev/null +++ b/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json @@ -0,0 +1,20 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Brew/BrewApp.swift b/Homebrew/BrewApp.swift similarity index 100% rename from Brew/BrewApp.swift rename to Homebrew/BrewApp.swift diff --git a/Brew/Debug/DebugMenuCommands.swift b/Homebrew/Debug/DebugMenuCommands.swift similarity index 100% rename from Brew/Debug/DebugMenuCommands.swift rename to Homebrew/Debug/DebugMenuCommands.swift diff --git a/Brew/Features/MainWindow/Views/MainWindowView.swift b/Homebrew/Features/MainWindow/Views/MainWindowView.swift similarity index 100% rename from Brew/Features/MainWindow/Views/MainWindowView.swift rename to Homebrew/Features/MainWindow/Views/MainWindowView.swift diff --git a/Brew/Utilities/UserDefaultsDebug.swift b/Homebrew/Utilities/UserDefaultsDebug.swift similarity index 100% rename from Brew/Utilities/UserDefaultsDebug.swift rename to Homebrew/Utilities/UserDefaultsDebug.swift diff --git a/Brew/Views/MainSidebarView.swift b/Homebrew/Views/MainSidebarView.swift similarity index 98% rename from Brew/Views/MainSidebarView.swift rename to Homebrew/Views/MainSidebarView.swift index 0a4cf18..dd37776 100644 --- a/Brew/Views/MainSidebarView.swift +++ b/Homebrew/Views/MainSidebarView.swift @@ -26,8 +26,6 @@ struct MainSidebarView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { HStack(spacing: BrewSpacing.sm) { - Text("🍺") - .font(.brewTitle2) Text("Homebrew") .font(.brewTitle2) .foregroundStyle(Color.brewTextPrimary) diff --git a/README.md b/README.md index 65c82d6..b96315d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ After cloning: ./scripts/bootstrap ``` -This installs Mint from `Brewfile`, runs `mint bootstrap` to build the SwiftFormat and SwiftLint versions pinned in `Mintfile`, enables repository git hooks, and resolves Swift package dependencies for `Brew.xcodeproj`. +This installs Mint from `Brewfile`, runs `mint bootstrap` to build the SwiftFormat and SwiftLint versions pinned in `Mintfile`, enables repository git hooks, and resolves Swift package dependencies for `Homebrew.xcodeproj`. ### Pre-commit formatting and linting diff --git a/scripts/bootstrap b/scripts/bootstrap index 7eb5016..29f284a 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -45,7 +45,7 @@ echo "==> Installing repository git hooks" "$ROOT_DIR/scripts/install-git-hooks" echo "==> Resolving Swift package dependencies" -xcodebuild -resolvePackageDependencies -project "$ROOT_DIR/Brew.xcodeproj" +xcodebuild -resolvePackageDependencies -project "$ROOT_DIR/Homebrew.xcodeproj" # --------------------------------------------------------------------------- # Per-developer signing config @@ -59,11 +59,11 @@ if [ ! -f "$SIGNING_LOCAL" ]; then echo "==> Created Configurations/Signing.local.xcconfig" echo " Open that file and replace YOUR_TEAM_ID_HERE with your" echo " 10-character Apple Team ID (Xcode → Settings → Accounts)." - echo " Then open Brew.xcodeproj — signing will resolve automatically." + echo " Then open Homebrew.xcodeproj — signing will resolve automatically." else echo "==> Signing config already exists (Configurations/Signing.local.xcconfig)" fi echo "" echo "==> Bootstrap complete" -echo "Next step: open Brew.xcodeproj" +echo "Next step: open Homebrew.xcodeproj" From 9a82ce4747c9e1d1acedc99692c3f04793e60d5c Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 11 Jun 2026 21:40:25 +1000 Subject: [PATCH 07/18] Fix pre-commit hook path after Brew -> Homebrew folder rename The BrewUILint step scanned 'Brew/', which no longer exists after the source folder was renamed to 'Homebrew/', so the hook errored on every commit. Point the whole-tree scan at 'Homebrew/'. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/pre-commit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pre-commit b/scripts/pre-commit index ba51c85..0fb1ce6 100755 --- a/scripts/pre-commit +++ b/scripts/pre-commit @@ -18,7 +18,7 @@ if ! command -v mint >/dev/null 2>&1; then exit 1 fi -# BrewUILint runs over the WHOLE tree (Brew + Sources), not just the staged files. Its +# BrewUILint runs over the WHOLE tree (Homebrew + Sources), not just the staged files. Its # nonisolated-extension rule needs to see every `nonisolated` type declaration to flag a bad # extension on it, and that declaration may live in an unstaged file in another package. Tests # (Tests/) are intentionally excluded. Builds the tool first (cached in Tools/BrewUILint/.build). @@ -29,7 +29,7 @@ if ! xcrun swift build --package-path Tools/BrewUILint -c release >/dev/null 2>& fi brewuilint_bin="$(xcrun swift build --package-path Tools/BrewUILint -c release --show-bin-path)/BrewUILint" set +e -brewuilint_output="$(find Brew Sources -name '*.swift' -print0 | xargs -0 "${brewuilint_bin}" 2>&1)" +brewuilint_output="$(find Homebrew Sources -name '*.swift' -print0 | xargs -0 "${brewuilint_bin}" 2>&1)" brewuilint_status=$? set -e if [[ ${brewuilint_status} -ne 0 ]]; then From 0188fb42691d4a303c4cd1f830f2f8f139676e44 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 11 Jun 2026 21:43:05 +1000 Subject: [PATCH 08/18] memory --- .ai/memory.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ai/memory.md b/.ai/memory.md index cc6f38d..2c721a5 100644 --- a/.ai/memory.md +++ b/.ai/memory.md @@ -66,7 +66,7 @@ - `BrewTests` = unit tests - `BrewUITests` = UI tests - Repository root folder remains `BrewUI` (part of a larger parent project layout). -- App bundle/package identifier remains unchanged for compatibility (`sh.brew.BrewUI`), while test bundle identifiers were updated to match renamed targets (`sh.brew.BrewTests` and `sh.brew.BrewUITests`). +- App bundle identifier and installer package identifier changed to `sh.brew.app` (was `sh.brew.BrewUI`). Test bundle identifiers track renamed targets (`sh.brew.BrewTests` and `sh.brew.BrewUITests`). ## 2026-03-15 — Actionlint Policy-Compliant Pattern From f85dff27dfa85667f5d6da5f4d483e5a62e21969 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Sat, 13 Jun 2026 20:59:42 +1000 Subject: [PATCH 09/18] Fix sidebar images --- .../Mark.imageset/Contents.json | 15 ++++++++++++ .../Assets.xcassets/Mark.imageset/Mark.svg | 7 ++++++ .../MenuIcon.imageset/Contents.json | 20 ---------------- Homebrew/Views/MainSidebarView.swift | 23 ++++++++++--------- 4 files changed, 34 insertions(+), 31 deletions(-) create mode 100644 Homebrew/Assets.xcassets/Mark.imageset/Contents.json create mode 100644 Homebrew/Assets.xcassets/Mark.imageset/Mark.svg delete mode 100644 Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json diff --git a/Homebrew/Assets.xcassets/Mark.imageset/Contents.json b/Homebrew/Assets.xcassets/Mark.imageset/Contents.json new file mode 100644 index 0000000..7e651ad --- /dev/null +++ b/Homebrew/Assets.xcassets/Mark.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Mark.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/Homebrew/Assets.xcassets/Mark.imageset/Mark.svg b/Homebrew/Assets.xcassets/Mark.imageset/Mark.svg new file mode 100644 index 0000000..90fc9d0 --- /dev/null +++ b/Homebrew/Assets.xcassets/Mark.imageset/Mark.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json b/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json deleted file mode 100644 index a19a549..0000000 --- a/Homebrew/Assets.xcassets/MenuIcon.imageset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Homebrew/Views/MainSidebarView.swift b/Homebrew/Views/MainSidebarView.swift index dd37776..e589f99 100644 --- a/Homebrew/Views/MainSidebarView.swift +++ b/Homebrew/Views/MainSidebarView.swift @@ -25,7 +25,11 @@ struct MainSidebarView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - HStack(spacing: BrewSpacing.sm) { + HStack(alignment: .bottom, spacing: BrewSpacing.sm) { + Image("Mark") + .resizable() + .scaledToFit() + .frame(height: 24) Text("Homebrew") .font(.brewTitle2) .foregroundStyle(Color.brewTextPrimary) @@ -41,7 +45,7 @@ struct MainSidebarView: View { sidebarRow( title: "Installed", - systemImage: "cube.box.fill", + emoji: "📦", item: .installed, ) .padding(.horizontal, BrewSpacing.sm) @@ -49,7 +53,7 @@ struct MainSidebarView: View { sidebarRow( title: "Upgrades", - systemImage: "arrow.triangle.2.circlepath", + emoji: "⬆️", item: .upgrades, trailingAccessory: { UpgradesSidebarBadge() }, ) @@ -58,7 +62,7 @@ struct MainSidebarView: View { sidebarRow( title: "Discover", - systemImage: "magnifyingglass", + emoji: "🔍", item: .discover, ) .padding(.horizontal, BrewSpacing.sm) @@ -66,7 +70,7 @@ struct MainSidebarView: View { sidebarRow( title: "Doctor", - systemImage: "stethoscope", + emoji: "🩺", item: .doctor, ) .padding(.horizontal, BrewSpacing.sm) @@ -74,7 +78,7 @@ struct MainSidebarView: View { sidebarRow( title: "Configuration", - systemImage: "gearshape", + emoji: "⚙️", item: .configuration, ) .padding(.horizontal, BrewSpacing.sm) @@ -89,7 +93,7 @@ struct MainSidebarView: View { @ViewBuilder private func sidebarRow( title: String, - systemImage: String, + emoji: String, item: SidebarItem, @ViewBuilder trailingAccessory: () -> some View = { EmptyView() }, ) -> some View { @@ -98,10 +102,7 @@ struct MainSidebarView: View { selection = item } label: { HStack(spacing: BrewSpacing.sm) { - Image(systemName: systemImage) - .foregroundStyle(isSelected ? Color.brewBrandPrimary : Color.brewTextSecondary) - .imageScale(.medium) - Text(title) + Text("\(emoji) \(title)") .font(.brewBody) .foregroundStyle(isSelected ? Color.brewBrandPrimary : Color.brewTextPrimary) Spacer(minLength: 0) From f62eba6ecbfdfc2d860445f03d7c8a766dab789a Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Sat, 13 Jun 2026 21:02:52 +1000 Subject: [PATCH 10/18] UPdate lists to inset style --- Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift | 2 +- Sources/BrewFeatureDoctor/Views/DoctorView.swift | 2 +- Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift | 2 +- Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index faa9769..3726a08 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -102,7 +102,7 @@ private struct DiscoverPackageSections: View { } } } - .listStyle(.plain) + .listStyle(.inset) .accessibilityLabel("Discover packages") .onAppear { scrollToSelection(viewModel.selectedPackageID, with: proxy) diff --git a/Sources/BrewFeatureDoctor/Views/DoctorView.swift b/Sources/BrewFeatureDoctor/Views/DoctorView.swift index 0c0dbde..625f8f7 100644 --- a/Sources/BrewFeatureDoctor/Views/DoctorView.swift +++ b/Sources/BrewFeatureDoctor/Views/DoctorView.swift @@ -101,7 +101,7 @@ struct DoctorView: View { } } } - .listStyle(.plain) + .listStyle(.inset) .accessibilityLabel("Doctor issues") } } diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index a34f6f9..c36f97c 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -78,7 +78,7 @@ struct InstalledPackagesView: View { } } } - .listStyle(.plain) + .listStyle(.inset) .accessibilityLabel("Installed packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index 8bda933..455d410 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -110,7 +110,7 @@ struct UpgradesPackagesView: View { } } } - .listStyle(.plain) + .listStyle(.inset) .accessibilityLabel("Outdated packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) From 2a9bbd0672d11537cd6cf91b1f534a06f5fef159 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 17 Jun 2026 19:51:15 +1000 Subject: [PATCH 11/18] Allow arrow navigation of lists --- .../Views/DiscoverPackagesView.swift | 5 +- .../BrewFeatureDoctor/Views/DoctorView.swift | 8 ++-- .../Views/InstalledPackagesView.swift | 46 +++++++++---------- .../Views/UpgradesPackagesView.swift | 46 +++++++++---------- 4 files changed, 54 insertions(+), 51 deletions(-) diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index 3726a08..dddd28c 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -90,7 +90,10 @@ private struct DiscoverPackageSections: View { var body: some View { ScrollViewReader { proxy in - List { + List(selection: Binding( + get: { viewModel.selectedPackageID }, + set: { viewModel.setSelection($0) }, + )) { if viewModel.showsFormulaeSection { Section(viewModel.formulaeSectionTitle) { sectionContent(formulae, kind: .formula) diff --git a/Sources/BrewFeatureDoctor/Views/DoctorView.swift b/Sources/BrewFeatureDoctor/Views/DoctorView.swift index 625f8f7..d5f10e1 100644 --- a/Sources/BrewFeatureDoctor/Views/DoctorView.swift +++ b/Sources/BrewFeatureDoctor/Views/DoctorView.swift @@ -82,16 +82,16 @@ struct DoctorView: View { } private func issuesList(groups: [DoctorIssueGroup]) -> some View { - List { + List(selection: Binding( + get: { viewModel.selectedIssueID }, + set: { viewModel.setSelection($0) }, + )) { ForEach(groups) { group in Section { ForEach(group.items) { item in DoctorIssueRowView(item: item) .id(item.id) .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(item.id) - } .listRowBackground( viewModel.selectedIssueID == item.id ? Color.brewBrandTint : Color.clear, ) diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index c36f97c..ae9be9e 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -45,36 +45,25 @@ struct InstalledPackagesView: View { private func installedList(_ content: InstalledPackagesContent) -> some View { ScrollViewReader { proxy in - List { + List(selection: Binding( + get: { viewModel.activeSelectedPackageID }, + set: { newValue in + if let newValue { + viewModel.setSelection(newValue) + } else { + viewModel.clearSelection() + } + }, + )) { if content.shouldShowFormulaeSection { Section("Formulae") { - ForEach(content.formulaPackages) { package in - listRow(for: package) - .id(package.id) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(package.id) - } - .listRowBackground( - viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, - ) - } + sectionContent(for: content.formulaPackages) } } if content.shouldShowCasksSection { Section("Casks") { - ForEach(content.caskPackages) { package in - listRow(for: package) - .id(package.id) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(package.id) - } - .listRowBackground( - viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, - ) - } + sectionContent(for: content.caskPackages) } } } @@ -95,6 +84,17 @@ struct InstalledPackagesView: View { } } + private func sectionContent(for packages: [InstalledBrewPackage]) -> some View { + ForEach(packages) { package in + listRow(for: package) + .id(package.id) + .contentShape(Rectangle()) + .listRowBackground( + viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, + ) + } + } + private func listRow(for package: InstalledBrewPackage) -> some View { InstalledListRowRoot(package: package) } diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index 455d410..f88db01 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -77,36 +77,25 @@ struct UpgradesPackagesView: View { private func upgradesList(_ content: InstalledPackagesContent) -> some View { ScrollViewReader { proxy in - List { + List(selection: Binding( + get: { viewModel.activeSelectedPackageID }, + set: { newValue in + if let newValue { + viewModel.setSelection(newValue) + } else { + viewModel.clearSelection() + } + }, + )) { if content.shouldShowFormulaeSection { Section("Formulae") { - ForEach(content.formulaPackages) { package in - listRow(for: package) - .id(package.id) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(package.id) - } - .listRowBackground( - viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, - ) - } + sectionContent(for: content.formulaPackages) } } if content.shouldShowCasksSection { Section("Casks") { - ForEach(content.caskPackages) { package in - listRow(for: package) - .id(package.id) - .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(package.id) - } - .listRowBackground( - viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, - ) - } + sectionContent(for: content.caskPackages) } } } @@ -127,6 +116,17 @@ struct UpgradesPackagesView: View { } } + private func sectionContent(for packages: [InstalledBrewPackage]) -> some View { + ForEach(packages) { package in + listRow(for: package) + .id(package.id) + .contentShape(Rectangle()) + .listRowBackground( + viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, + ) + } + } + private func listRow(for package: InstalledBrewPackage) -> some View { InstalledListRowRoot(package: package) } From fa43a228324238b59978f4df0c61e3326bbb9f03 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 17 Jun 2026 19:51:37 +1000 Subject: [PATCH 12/18] Allow keboard navigation of sidebar --- Homebrew/BrewApp.swift | 1 + .../MainWindow/Views/MainWindowView.swift | 1 + Homebrew/Views/MainSidebarView.swift | 13 ----- Homebrew/Views/SidebarItem.swift | 54 +++++++++++++++++++ 4 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 Homebrew/Views/SidebarItem.swift diff --git a/Homebrew/BrewApp.swift b/Homebrew/BrewApp.swift index f03902b..5a598fa 100644 --- a/Homebrew/BrewApp.swift +++ b/Homebrew/BrewApp.swift @@ -95,6 +95,7 @@ struct BrewApp: App { ) .commands { ConsoleCommands() + SidebarCommands() } #if DEBUG .commands { diff --git a/Homebrew/Features/MainWindow/Views/MainWindowView.swift b/Homebrew/Features/MainWindow/Views/MainWindowView.swift index ad6538d..18c37c7 100644 --- a/Homebrew/Features/MainWindow/Views/MainWindowView.swift +++ b/Homebrew/Features/MainWindow/Views/MainWindowView.swift @@ -35,6 +35,7 @@ struct MainWindowView: View { } .navigationSplitViewStyle(.automatic) .focusedSceneValue(\.consoleExpanded, $consoleExpanded) + .focusedSceneValue(\.sidebarSelection, $selectedSidebarItem) .environment(\.navigateToInstalledPackage) { id in pendingInstalledSelection = id selectedSidebarItem = .installed diff --git a/Homebrew/Views/MainSidebarView.swift b/Homebrew/Views/MainSidebarView.swift index e589f99..09098e5 100644 --- a/Homebrew/Views/MainSidebarView.swift +++ b/Homebrew/Views/MainSidebarView.swift @@ -7,19 +7,6 @@ import BrewFeatureInstalled import BrewUIComponents import SwiftUI -/// Primary navigation items for the main window sidebar. -enum SidebarItem: String, CaseIterable, Hashable, Identifiable { - case installed - case upgrades - case discover - case doctor - case configuration - - var id: String { - rawValue - } -} - struct MainSidebarView: View { @Binding var selection: SidebarItem diff --git a/Homebrew/Views/SidebarItem.swift b/Homebrew/Views/SidebarItem.swift new file mode 100644 index 0000000..57bebb4 --- /dev/null +++ b/Homebrew/Views/SidebarItem.swift @@ -0,0 +1,54 @@ +// +// SidebarItem.swift +// Homebrew +// +// Created by Graeme Arthur on 17/6/2026. +// + +import SwiftUI + +/// Primary navigation items for the main window sidebar. +enum SidebarItem: String, CaseIterable, Hashable, Identifiable { + case installed + case upgrades + case discover + case doctor + case configuration + + var id: String { + rawValue + } + + var title: LocalizedStringKey { + switch self { + case .installed: "Installed" + case .upgrades: "Upgrades" + case .discover: "Discover" + case .doctor: "Doctor" + case .configuration: "Configuration" + } + } +} + +extension FocusedValues { + @Entry var sidebarSelection: Binding? +} + +/// View menu commands for the command console. Currently a single `⌘\`` toggle matching Xcode / VS Code / Terminal. +public struct SidebarCommands: Commands { + @FocusedValue(\.sidebarSelection) private var selection + + public init() {} + + public var body: some Commands { + CommandGroup(after: .sidebar) { + ForEach(Array(SidebarItem.allCases.enumerated()), id: \.element) { index, item in + Button(item.title) { + selection?.wrappedValue = item + } + .keyboardShortcut(KeyEquivalent(Character("\(index + 1)"))) + } + .disabled(selection == nil) + } + } +} From a7b51258f08002e912b3f564f5d30d8f7d7f47a2 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 17 Jun 2026 22:47:45 +1000 Subject: [PATCH 13/18] Cmd+F for search focus --- Homebrew/BrewApp.swift | 1 + .../Views/DiscoverPackagesView.swift | 3 +++ .../Views/InstalledPackagesView.swift | 3 +++ .../Views/UpgradesPackagesView.swift | 3 +++ .../Commands/SearchCommands.swift | 25 +++++++++++++++++++ 5 files changed, 35 insertions(+) create mode 100644 Sources/BrewUIComponents/Commands/SearchCommands.swift diff --git a/Homebrew/BrewApp.swift b/Homebrew/BrewApp.swift index 5a598fa..dd0c611 100644 --- a/Homebrew/BrewApp.swift +++ b/Homebrew/BrewApp.swift @@ -96,6 +96,7 @@ struct BrewApp: App { .commands { ConsoleCommands() SidebarCommands() + SearchCommands() } #if DEBUG .commands { diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index dddd28c..76b3bcf 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -6,6 +6,7 @@ import SwiftUI /// Middle column of the main window: Discover package list. struct DiscoverPackagesView: View { @Bindable var viewModel: DiscoverViewModel + @State private var searchPresented = false var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -24,9 +25,11 @@ struct DiscoverPackagesView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .searchable( text: $viewModel.query, + isPresented: $searchPresented, placement: .toolbar, prompt: "Search Homebrew's Catalogue", ) + .focusedSceneValue(\.searchPresented, $searchPresented) .task(id: viewModel.query) { // Debounce so intermediate keystrokes don't each fire a search; cancellation handles the rest. try? await Task.sleep(for: .milliseconds(250)) diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index ae9be9e..8b2cf08 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -11,6 +11,7 @@ import SwiftUI /// Middle column of the main window: “Installed” chrome and the package list. struct InstalledPackagesView: View { @Bindable var viewModel: InstalledViewModel + @State private var searchPresented = false var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -38,9 +39,11 @@ struct InstalledPackagesView: View { } .searchable( text: $viewModel.searchQuery, + isPresented: $searchPresented, placement: .toolbar, prompt: "Search Installed Packages", ) + .focusedSceneValue(\.searchPresented, $searchPresented) } private func installedList(_ content: InstalledPackagesContent) -> some View { diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index f88db01..7edf68e 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -11,6 +11,7 @@ import SwiftUI /// and a friendly empty state when nothing is outdated. struct UpgradesPackagesView: View { @Bindable var viewModel: UpgradesViewModel + @State private var searchPresented = false var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -35,9 +36,11 @@ struct UpgradesPackagesView: View { } .searchable( text: $viewModel.searchQuery, + isPresented: $searchPresented, placement: .toolbar, prompt: "Search Upgrades", ) + .focusedSceneValue(\.searchPresented, $searchPresented) } private var header: some View { diff --git a/Sources/BrewUIComponents/Commands/SearchCommands.swift b/Sources/BrewUIComponents/Commands/SearchCommands.swift new file mode 100644 index 0000000..57cbc02 --- /dev/null +++ b/Sources/BrewUIComponents/Commands/SearchCommands.swift @@ -0,0 +1,25 @@ +// +// SearchCommands.swift +// BrewKit +// +// + +import SwiftUI + +public struct SearchCommands: Commands { + @FocusedValue(\.searchPresented) private var searchPresented + + public init() {} + + public var body: some Commands { + CommandGroup(after: .textEditing) { + Button("Find") { searchPresented?.wrappedValue = true } + .keyboardShortcut("f") // ⌘F + .disabled(searchPresented == nil) + } + } +} + +public extension FocusedValues { + @Entry var searchPresented: Binding? +} From ea0bf5b60a5ee0cd4f25118c4485df173ed02554 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 17 Jun 2026 23:04:41 +1000 Subject: [PATCH 14/18] Focus on list on loading --- Sources/BrewCore/Support/LoadState.swift | 7 +++ .../ViewModels/DiscoverViewModel.swift | 4 ++ .../Views/DiscoverPackagesView.swift | 6 +++ .../BrewFeatureDoctor/Views/DoctorView.swift | 5 ++ .../Views/InstalledPackagesView.swift | 3 ++ .../Views/UpgradesPackagesView.swift | 3 ++ .../DiscoverViewModelTests.swift | 51 +++++++++++++++++++ 7 files changed, 79 insertions(+) diff --git a/Sources/BrewCore/Support/LoadState.swift b/Sources/BrewCore/Support/LoadState.swift index e530539..1549ed3 100644 --- a/Sources/BrewCore/Support/LoadState.swift +++ b/Sources/BrewCore/Support/LoadState.swift @@ -21,6 +21,13 @@ public enum LoadState { } return value } + + public var isLoaded: Bool { + guard case .loaded = self else { + return false + } + return true + } } extension LoadState: Equatable where Value: Equatable, Failure: Equatable {} diff --git a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift index 85dc987..6df5d7e 100644 --- a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift +++ b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift @@ -77,6 +77,10 @@ final class DiscoverViewModel { isSearching ? results : trending } + var isListFocused: Bool { + trending.isLoaded && !isSearching + } + /// Search results have no analytics, so install-count metadata is suppressed in that mode. var showsInstallMetrics: Bool { !isSearching diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index 76b3bcf..6ee26b9 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -79,6 +79,8 @@ struct DiscoverPackagesView: View { /// Sectioned Discover list, split by package kind and filtered by the active scope. Renders an inline /// empty-state message when a visible section has no rows (e.g. a scope filter that excludes everything). private struct DiscoverPackageSections: View { + @FocusState private var isFocused: Bool + let viewModel: DiscoverViewModel /// The redacted-placeholder or loaded packages handed down by `AsyncContentView` for this render. let packages: [DiscoveryBrewPackage] @@ -112,7 +114,11 @@ private struct DiscoverPackageSections: View { .accessibilityLabel("Discover packages") .onAppear { scrollToSelection(viewModel.selectedPackageID, with: proxy) + Task { + isFocused = viewModel.trending.isLoaded && !viewModel.isSearching + } } + .focused($isFocused) .onChange(of: viewModel.selectedPackageID) { _, selectedID in scrollToSelection(selectedID, with: proxy) } diff --git a/Sources/BrewFeatureDoctor/Views/DoctorView.swift b/Sources/BrewFeatureDoctor/Views/DoctorView.swift index d5f10e1..32de77c 100644 --- a/Sources/BrewFeatureDoctor/Views/DoctorView.swift +++ b/Sources/BrewFeatureDoctor/Views/DoctorView.swift @@ -12,6 +12,7 @@ import SwiftUI /// a small "checking" spinner in the header. struct DoctorView: View { @Bindable var viewModel: DoctorViewModel + @FocusState private var isFocused: Bool var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -101,6 +102,10 @@ struct DoctorView: View { } } } + .onAppear { + Task { isFocused = viewModel.state.isLoaded } + } + .focused($isFocused) .listStyle(.inset) .accessibilityLabel("Doctor issues") } diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index 8b2cf08..5d4b304 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -12,6 +12,7 @@ import SwiftUI struct InstalledPackagesView: View { @Bindable var viewModel: InstalledViewModel @State private var searchPresented = false + @FocusState private var isFocused: Bool var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -74,7 +75,9 @@ struct InstalledPackagesView: View { .accessibilityLabel("Installed packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) + Task { isFocused = viewModel.state.isLoaded } } + .focused($isFocused) .onChange(of: viewModel.activeSelectedPackageID) { _, selectedID in scrollToSelection(selectedID, in: content, with: proxy) } diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index 7edf68e..67991cc 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -12,6 +12,7 @@ import SwiftUI struct UpgradesPackagesView: View { @Bindable var viewModel: UpgradesViewModel @State private var searchPresented = false + @FocusState private var isFocused: Bool var body: some View { VStack(alignment: .leading, spacing: 0) { @@ -106,7 +107,9 @@ struct UpgradesPackagesView: View { .accessibilityLabel("Outdated packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) + Task { isFocused = viewModel.state.isLoaded } } + .focused($isFocused) .onChange(of: viewModel.activeSelectedPackageID) { _, selectedID in scrollToSelection(selectedID, in: content, with: proxy) } diff --git a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift index 8275d94..2def3d7 100644 --- a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift +++ b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift @@ -21,6 +21,7 @@ struct DiscoverViewModelTests { thirtyDayInstallCount: 100, ), ], + topCasks: [ discoveryPackage( name: "iterm2", @@ -498,6 +499,52 @@ struct DiscoverViewModelTests { } #expect(message == "Something went wrong searching the catalogue.") } + + @Test @MainActor func `isListFocused is true when trending has loaded and not searching`() { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot( + topFormulae: [discoveryPackage(name: "git", thirtyDayInstallCount: 100)], + topCasks: [], + ), + ), + catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + installedRepository: installedRepo(), + ) + + viewModel.query = nil + + #expect(viewModel.isListFocused) + } + + @Test @MainActor func `isListFocused is false when trending has NOT loaded and not searching`() { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: ThrowingDiscoverPackagesRepository(error: DiscoverOddError()), + catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + installedRepository: installedRepo(), + ) + + viewModel.query = nil + + #expect(!viewModel.isListFocused) + } + + @Test @MainActor func `isListFocused is false when trending has loaded and searching`() { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot( + topFormulae: [discoveryPackage(name: "git", thirtyDayInstallCount: 100)], + topCasks: [], + ), + ), + catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + installedRepository: installedRepo(), + ) + + viewModel.query = "foo" + + #expect(!viewModel.isListFocused) + } } @MainActor @@ -532,6 +579,10 @@ private final class MutableDiscoverPackagesRepository: DiscoverPackagesRepositor private struct ThrowingDiscoverPackagesRepository: DiscoverPackagesRepository { let error: Error + init(error: Error = DiscoverOddError()) { + self.error = error + } + func loadTopPackages( limit _: Int, window _: BrewAnalyticsWindow, From 7dd96dad1ecdb50634378e50160ca6d72404ac35 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Thu, 18 Jun 2026 22:35:57 +1000 Subject: [PATCH 15/18] Disable type / file length for all tests --- BrewTests/.swiftlint.yml | 4 +++- BrewUITests/.swiftlint.yml | 1 + Tests/.swiftlint.yml | 4 +++- Tools/BrewUILint/Tests/BrewUILintTests/.swiftlint.yml | 6 ++++++ 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/BrewTests/.swiftlint.yml b/BrewTests/.swiftlint.yml index 66794ad..bb34732 100644 --- a/BrewTests/.swiftlint.yml +++ b/BrewTests/.swiftlint.yml @@ -1,6 +1,8 @@ # Merge with repo root `.swiftlint.yml`. Integration/UI test scaffolding often grows beyond -# the default struct type-body cap without adding real complexity. +# the default type-body and file caps without adding real complexity, so test files +# and test types are allowed to be any length. parent_config: ../.swiftlint.yml disabled_rules: - type_body_length + - file_length diff --git a/BrewUITests/.swiftlint.yml b/BrewUITests/.swiftlint.yml index 2c22be6..f8da0bd 100644 --- a/BrewUITests/.swiftlint.yml +++ b/BrewUITests/.swiftlint.yml @@ -3,3 +3,4 @@ parent_config: ../.swiftlint.yml disabled_rules: - type_body_length + - file_length diff --git a/Tests/.swiftlint.yml b/Tests/.swiftlint.yml index 66794ad..bb34732 100644 --- a/Tests/.swiftlint.yml +++ b/Tests/.swiftlint.yml @@ -1,6 +1,8 @@ # Merge with repo root `.swiftlint.yml`. Integration/UI test scaffolding often grows beyond -# the default struct type-body cap without adding real complexity. +# the default type-body and file caps without adding real complexity, so test files +# and test types are allowed to be any length. parent_config: ../.swiftlint.yml disabled_rules: - type_body_length + - file_length diff --git a/Tools/BrewUILint/Tests/BrewUILintTests/.swiftlint.yml b/Tools/BrewUILint/Tests/BrewUILintTests/.swiftlint.yml index ef7ac61..0a90c10 100644 --- a/Tools/BrewUILint/Tests/BrewUILintTests/.swiftlint.yml +++ b/Tools/BrewUILint/Tests/BrewUILintTests/.swiftlint.yml @@ -1,3 +1,9 @@ +# Test scaffolding is allowed to grow without tripping length caps. parent_config: ../../../.swiftlint.yml + +disabled_rules: + - type_body_length + - file_length + identifier_name: min_length: 2 From 80b0a5a329078160da90eb7a2da8881a5d6830a7 Mon Sep 17 00:00:00 2001 From: Graeme Arthur <2030310+graeme@users.noreply.github.com> Date: Fri, 19 Jun 2026 18:58:24 +1000 Subject: [PATCH 16/18] Fix copy/paste error in comment Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- Homebrew/Views/SidebarItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Homebrew/Views/SidebarItem.swift b/Homebrew/Views/SidebarItem.swift index 57bebb4..1f793e3 100644 --- a/Homebrew/Views/SidebarItem.swift +++ b/Homebrew/Views/SidebarItem.swift @@ -34,7 +34,7 @@ extension FocusedValues { @Entry var sidebarSelection: Binding? } -/// View menu commands for the command console. Currently a single `⌘\`` toggle matching Xcode / VS Code / Terminal. +/// View menu commands for navigating the main window sidebar (⌘1–⌘5). public struct SidebarCommands: Commands { @FocusedValue(\.sidebarSelection) private var selection From d536010b7077074210db90f0e48dad211183134d Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Mon, 22 Jun 2026 22:10:10 +1000 Subject: [PATCH 17/18] Update focus logic for when loaded --- .../ViewModels/DiscoverViewModel.swift | 4 +- .../Views/DiscoverPackagesView.swift | 6 +-- .../ViewModels/DoctorViewModel.swift | 6 +++ .../BrewFeatureDoctor/Views/DoctorView.swift | 4 +- .../ViewModels/InstalledViewModel.swift | 6 +++ .../ViewModels/UpgradesViewModel.swift | 6 +++ .../Views/InstalledPackagesView.swift | 4 +- .../Views/UpgradesPackagesView.swift | 4 +- .../DiscoverViewModelTests.swift | 38 +++++++++++++------ .../DoctorViewModelTests.swift | 31 +++++++++++++++ .../InstalledViewModelTests.swift | 30 +++++++++++++++ .../UpgradesViewModelTests.swift | 36 ++++++++++++++++++ 12 files changed, 156 insertions(+), 19 deletions(-) diff --git a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift index 6df5d7e..f78527b 100644 --- a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift +++ b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift @@ -77,7 +77,9 @@ final class DiscoverViewModel { isSearching ? results : trending } - var isListFocused: Bool { + /// Drives the list view's `@FocusState`. The list only owns keyboard focus on the trending landing + /// once it has loaded — while a search is active focus belongs to the catalogue search field. + var shouldFocusList: Bool { trending.isLoaded && !isSearching } diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index 6ee26b9..6224820 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -114,9 +114,9 @@ private struct DiscoverPackageSections: View { .accessibilityLabel("Discover packages") .onAppear { scrollToSelection(viewModel.selectedPackageID, with: proxy) - Task { - isFocused = viewModel.trending.isLoaded && !viewModel.isSearching - } + } + .task(id: viewModel.shouldFocusList) { + isFocused = viewModel.shouldFocusList } .focused($isFocused) .onChange(of: viewModel.selectedPackageID) { _, selectedID in diff --git a/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift b/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift index de78822..900cc20 100644 --- a/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift +++ b/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift @@ -74,6 +74,12 @@ final class DoctorViewModel { doctorRepository.isRefreshing } + /// Drives the issues list's `@FocusState`. The list only owns keyboard focus once a report has + /// loaded — loading and failure states have no rows to focus. + var shouldFocusList: Bool { + state.isLoaded + } + var issueItems: [DoctorIssueItem] { guard case let .loaded(report) = state else { return [] diff --git a/Sources/BrewFeatureDoctor/Views/DoctorView.swift b/Sources/BrewFeatureDoctor/Views/DoctorView.swift index 32de77c..f985785 100644 --- a/Sources/BrewFeatureDoctor/Views/DoctorView.swift +++ b/Sources/BrewFeatureDoctor/Views/DoctorView.swift @@ -102,8 +102,8 @@ struct DoctorView: View { } } } - .onAppear { - Task { isFocused = viewModel.state.isLoaded } + .task(id: viewModel.shouldFocusList) { + isFocused = viewModel.shouldFocusList } .focused($isFocused) .listStyle(.inset) diff --git a/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift b/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift index d62caf1..0beca08 100644 --- a/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift +++ b/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift @@ -78,6 +78,12 @@ final class InstalledViewModel { return false } + /// Drives the list view's `@FocusState`. The list only owns keyboard focus once the inventory has + /// loaded — while loading or in an error state focus belongs elsewhere (or to nothing). + var shouldFocusList: Bool { + state.isLoaded + } + var packageCountSubtitle: String { if shouldShowInitialLoadingIndicator { return String(localized: "Loading packages…", comment: "Installed tab subtitle while fetching") diff --git a/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift b/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift index 0a0326b..088c4ed 100644 --- a/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift +++ b/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift @@ -78,6 +78,12 @@ final class UpgradesViewModel { return false } + /// Drives the list view's `@FocusState`. The list only owns keyboard focus once the outdated + /// inventory has loaded — while loading or in an error state focus belongs elsewhere. + var shouldFocusList: Bool { + state.isLoaded + } + /// Subtitle for the in-page Upgrades header. Reflects the unfiltered /// inventory when no search is active, and "Showing N of M" / "No matches /// in M outdated packages" once a query narrows the list. The window-chrome diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index 5d4b304..e99c101 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -75,7 +75,9 @@ struct InstalledPackagesView: View { .accessibilityLabel("Installed packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) - Task { isFocused = viewModel.state.isLoaded } + } + .task(id: viewModel.shouldFocusList) { + isFocused = viewModel.shouldFocusList } .focused($isFocused) .onChange(of: viewModel.activeSelectedPackageID) { _, selectedID in diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index 67991cc..749fadb 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -107,7 +107,9 @@ struct UpgradesPackagesView: View { .accessibilityLabel("Outdated packages") .onAppear { scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) - Task { isFocused = viewModel.state.isLoaded } + } + .task(id: viewModel.shouldFocusList) { + isFocused = viewModel.shouldFocusList } .focused($isFocused) .onChange(of: viewModel.activeSelectedPackageID) { _, selectedID in diff --git a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift index 2def3d7..0d6692f 100644 --- a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift +++ b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift @@ -500,7 +500,9 @@ struct DiscoverViewModelTests { #expect(message == "Something went wrong searching the catalogue.") } - @Test @MainActor func `isListFocused is true when trending has loaded and not searching`() { + // MARK: - shouldFocusList + + @Test @MainActor func `shouldFocusList is true when trending has loaded and not searching`() async { let viewModel = DiscoverViewModel( discoverPackagesRepository: StubDiscoverPackagesRepository( snapshot: DiscoverTopPackagesSnapshot( @@ -508,28 +510,41 @@ struct DiscoverViewModelTests { topCasks: [], ), ), - catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + catalogueRepository: StubCatalogueRepository(), installedRepository: installedRepo(), ) - viewModel.query = nil + await viewModel.load() + + #expect(viewModel.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is false while trending is still loading`() { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot(topFormulae: [], topCasks: []), + ), + catalogueRepository: StubCatalogueRepository(), + installedRepository: installedRepo(), + ) - #expect(viewModel.isListFocused) + // trending starts as .loading and load() has not been awaited yet. + #expect(!viewModel.shouldFocusList) } - @Test @MainActor func `isListFocused is false when trending has NOT loaded and not searching`() { + @Test @MainActor func `shouldFocusList is false when trending failed to load`() async { let viewModel = DiscoverViewModel( discoverPackagesRepository: ThrowingDiscoverPackagesRepository(error: DiscoverOddError()), - catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + catalogueRepository: StubCatalogueRepository(), installedRepository: installedRepo(), ) - viewModel.query = nil + await viewModel.load() - #expect(!viewModel.isListFocused) + #expect(!viewModel.shouldFocusList) } - @Test @MainActor func `isListFocused is false when trending has loaded and searching`() { + @Test @MainActor func `shouldFocusList is false when trending has loaded but a search is active`() async { let viewModel = DiscoverViewModel( discoverPackagesRepository: StubDiscoverPackagesRepository( snapshot: DiscoverTopPackagesSnapshot( @@ -537,13 +552,14 @@ struct DiscoverViewModelTests { topCasks: [], ), ), - catalogueRepository: StubCatalogueRepository(searchError: DiscoverOddError()), + catalogueRepository: StubCatalogueRepository(), installedRepository: installedRepo(), ) + await viewModel.load() viewModel.query = "foo" - #expect(!viewModel.isListFocused) + #expect(!viewModel.shouldFocusList) } } diff --git a/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift b/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift index 11d14eb..72710d3 100644 --- a/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift +++ b/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift @@ -338,6 +338,37 @@ struct DoctorViewModelTests { ) #expect(viewModel.subtitle == "The check could not be completed") } + + // MARK: - shouldFocusList + + @Test func `shouldFocusList is false while the doctor check is running`() { + let viewModel = Self.viewModel(repository: LoadingDoctorRepository()) + #expect(!viewModel.shouldFocusList) + } + + @Test func `shouldFocusList is true once a report with issues has loaded`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: Self.issuesReport())) + + await viewModel.load() + + #expect(viewModel.shouldFocusList) + } + + @Test func `shouldFocusList is true on a healthy report`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: DoctorReport(issues: []))) + + await viewModel.load() + + #expect(viewModel.shouldFocusList) + } + + @Test func `shouldFocusList is false when the doctor check fails`() { + let viewModel = Self.viewModel( + repository: StubDoctorRepository(error: BrewLookupError.executableNotFound), + ) + + #expect(!viewModel.shouldFocusList) + } } /// Test-scoped doctor repository pinned in the `.loading` state. Used to exercise the loading branches of diff --git a/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift b/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift index df94e3c..83d3bc7 100644 --- a/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift +++ b/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift @@ -220,4 +220,34 @@ struct InstalledViewModelTests { } #expect(message == InstalledPackagesTestSupport.localizedGenericLoadFailureMessage()) } + + // MARK: - shouldFocusList + + @Test @MainActor func `shouldFocusList is false before the inventory has loaded`() { + let vm = makeInstalledViewModel(repository: unloadedInstalledRepository()) + + #expect(!vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is true once the inventory has loaded`() async { + let vm = await InstalledFeatureTestSupport.loadedViewModel( + formulae: [.fixture(name: "git", kind: .formula)], + ) + + #expect(vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is true when loaded with an empty inventory`() async { + let vm = await InstalledFeatureTestSupport.loadedViewModel() + + #expect(vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is false when the load fails`() async { + let vm = makeInstalledViewModel(repository: failingInstalledRepository(error: OddRepositoryError())) + + await vm.load() + + #expect(!vm.shouldFocusList) + } } diff --git a/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift b/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift index dde1a8d..977816e 100644 --- a/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift +++ b/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift @@ -266,6 +266,42 @@ struct UpgradesViewModelTests { await center.emit(id: unrelated, phase: .idle) } + // MARK: - shouldFocusList + + @Test @MainActor func `shouldFocusList is false while the outdated inventory is still loading`() { + let vm = UpgradesViewModel( + repository: StubInstalledPackagesRepository(state: .loading), + brewCommandCenter: StubBrewCommandCenter(), + commandFactory: StubMutatingCommandFactory(), + ) + + #expect(!vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is true once the outdated inventory has loaded`() { + let vm = Self.makeViewModel(packages: Self.mixedPackages) + + #expect(vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is true when loaded with no outdated rows`() { + let vm = Self.makeViewModel(packages: [ + .fixture(name: "wget", kind: .formula, outdated: false), + ]) + + #expect(vm.shouldFocusList) + } + + @Test @MainActor func `shouldFocusList is false when the inventory load failed`() { + let vm = UpgradesViewModel( + repository: StubInstalledPackagesRepository(state: .failed(OddRepositoryError())), + brewCommandCenter: StubBrewCommandCenter(), + commandFactory: StubMutatingCommandFactory(), + ) + + #expect(!vm.shouldFocusList) + } + // MARK: - Helpers private static var mixedPackages: [InstalledBrewPackage] { From 8485bdbd89f99dfc78decc136efa53bf09029004 Mon Sep 17 00:00:00 2001 From: Graeme Arthur Date: Wed, 24 Jun 2026 22:35:39 +1000 Subject: [PATCH 18/18] Fix arrow nav + selection state --- .../ViewModels/DiscoverViewModel.swift | 152 +++++++++++------- .../Views/DiscoverPackagesView.swift | 20 ++- .../ViewModels/DoctorViewModel.swift | 45 ++++++ .../BrewFeatureDoctor/Views/DoctorView.swift | 17 +- .../ViewModels/InstalledViewModel.swift | 40 +++++ .../ViewModels/UpgradesViewModel.swift | 72 ++++++--- .../Views/InstalledPackagesView.swift | 23 +-- .../Views/UpgradesPackagesView.swift | 23 +-- .../DiscoverViewModelTests.swift | 79 +++++++++ .../DoctorViewModelTests.swift | 88 ++++++++++ .../InstalledViewModelTests.swift | 47 ++++++ .../UpgradesViewModelTests.swift | 59 +++++++ 12 files changed, 555 insertions(+), 110 deletions(-) diff --git a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift index f78527b..7435894 100644 --- a/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift +++ b/Sources/BrewFeatureDiscover/ViewModels/DiscoverViewModel.swift @@ -202,8 +202,94 @@ final class DiscoverViewModel { return package } - // MARK: - Loading + // MARK: - Helpers + + static func sortedSection( + _ packages: [DiscoveryBrewPackage], + kind: HomebrewPackageKind, + ) -> [DiscoveryBrewPackage] { + packages + .filter { $0.kind == kind } + .sorted(by: sortByPopularityThenName) + } + + private static func sortByPopularityThenName( + _ lhs: DiscoveryBrewPackage, + _ rhs: DiscoveryBrewPackage, + ) -> Bool { + if lhs.thirtyDayInstallCount == rhs.thirtyDayInstallCount { + return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending + } + return lhs.thirtyDayInstallCount > rhs.thirtyDayInstallCount + } + + private static func userMessage(for error: Error, searching: Bool) -> String { + if case let BrewAPIClientError.transport(underlying) = error { + return underlying + } + if searching { + return String( + localized: "Something went wrong searching the catalogue.", + comment: "Discover tab generic search failure", + ) + } + return String( + localized: "Something went wrong loading Discover packages.", + comment: "Discover tab generic load failure", + ) + } +} + +// MARK: - Selection + +extension DiscoverViewModel { + func setSelection(_ packageID: BrewPackage.ID?) { + if let packageID { + guard visiblePackages.contains(where: { $0.id == packageID }) else { + return + } + selectedPackageID = packageID + } else { + selectedPackageID = visiblePackages.first?.id + } + } + + func selectNext() { + let orderedIDs = visiblePackages.map(\.id) + guard let currentID = selectedPackageID else { + if let first = orderedIDs.first { setSelection(first) } + return + } + if let nextID = orderedIDs.item(after: currentID) { + setSelection(nextID) + } + } + + func selectPrevious() { + let orderedIDs = visiblePackages.map(\.id) + guard let currentID = selectedPackageID else { + if let last = orderedIDs.last { setSelection(last) } + return + } + if let previousID = orderedIDs.item(before: currentID) { + setSelection(previousID) + } + } + private func synchronizeSelectionWithVisibleRows() { + let visibleIDs = Set(visiblePackages.map(\.id)) + if let selectedPackageID, !visibleIDs.contains(selectedPackageID) { + self.selectedPackageID = nil + } + if selectedPackageID == nil { + selectedPackageID = visiblePackages.first?.id + } + } +} + +// MARK: - Loading + +extension DiscoverViewModel { func load() async { trending = .loading do { @@ -248,64 +334,20 @@ final class DiscoverViewModel { await load() } } +} - // MARK: - Selection - - func setSelection(_ packageID: BrewPackage.ID?) { - if let packageID { - guard visiblePackages.contains(where: { $0.id == packageID }) else { - return - } - selectedPackageID = packageID - } else { - selectedPackageID = visiblePackages.first?.id - } - } - - private func synchronizeSelectionWithVisibleRows() { - let visibleIDs = Set(visiblePackages.map(\.id)) - if let selectedPackageID, !visibleIDs.contains(selectedPackageID) { - self.selectedPackageID = nil - } - if selectedPackageID == nil { - selectedPackageID = visiblePackages.first?.id - } - } - - // MARK: - Helpers - - static func sortedSection( - _ packages: [DiscoveryBrewPackage], - kind: HomebrewPackageKind, - ) -> [DiscoveryBrewPackage] { - packages - .filter { $0.kind == kind } - .sorted(by: sortByPopularityThenName) - } - - private static func sortByPopularityThenName( - _ lhs: DiscoveryBrewPackage, - _ rhs: DiscoveryBrewPackage, - ) -> Bool { - if lhs.thirtyDayInstallCount == rhs.thirtyDayInstallCount { - return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending +extension Array where Element: Equatable { + func item(after value: Element) -> Element? { + guard let index = firstIndex(of: value), index + 1 < count else { + return nil } - return lhs.thirtyDayInstallCount > rhs.thirtyDayInstallCount + return self[index + 1] } - private static func userMessage(for error: Error, searching: Bool) -> String { - if case let BrewAPIClientError.transport(underlying) = error { - return underlying - } - if searching { - return String( - localized: "Something went wrong searching the catalogue.", - comment: "Discover tab generic search failure", - ) + func item(before value: Element) -> Element? { + guard let index = firstIndex(of: value), index - 1 >= 0 else { + return nil } - return String( - localized: "Something went wrong loading Discover packages.", - comment: "Discover tab generic load failure", - ) + return self[index - 1] } } diff --git a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift index 6224820..b103f2a 100644 --- a/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift +++ b/Sources/BrewFeatureDiscover/Views/DiscoverPackagesView.swift @@ -95,10 +95,7 @@ private struct DiscoverPackageSections: View { var body: some View { ScrollViewReader { proxy in - List(selection: Binding( - get: { viewModel.selectedPackageID }, - set: { viewModel.setSelection($0) }, - )) { + List { if viewModel.showsFormulaeSection { Section(viewModel.formulaeSectionTitle) { sectionContent(formulae, kind: .formula) @@ -125,6 +122,14 @@ private struct DiscoverPackageSections: View { .onChange(of: packages.map(\.id)) { _, _ in scrollToSelection(viewModel.selectedPackageID, with: proxy) } + .onKeyPress(.upArrow) { + viewModel.selectPrevious() + return .handled + } + .onKeyPress(.downArrow) { + viewModel.selectNext() + return .handled + } .onExitCommand { viewModel.setSelection(nil) } @@ -140,12 +145,13 @@ private struct DiscoverPackageSections: View { listRow(package) .id(package.id) .contentShape(Rectangle()) - .onTapGesture { - viewModel.setSelection(package.id) - } .listRowBackground( viewModel.selectedPackageID == package.id ? Color.brewBrandTint : Color.clear, ) + .onTapGesture { + // Needed to suppress the default ugly blue macOS highlight state + viewModel.setSelection(package.id) + } } } } diff --git a/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift b/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift index 900cc20..dae41b9 100644 --- a/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift +++ b/Sources/BrewFeatureDoctor/ViewModels/DoctorViewModel.swift @@ -135,6 +135,35 @@ final class DoctorViewModel { selectedIssueID = id } + /// Issue ids in the order their rows render — grouped by descending severity, matching + /// `DoctorIssueGroup.grouped(from:)` — so keyboard navigation steps through the list as shown. + var orderedIssueIDs: [Int] { + guard case let .loaded(report) = state else { + return [] + } + return DoctorIssueGroup.grouped(from: report).flatMap { $0.items.map(\.id) } + } + + func selectNext() { + guard let currentID = selectedIssueID else { + if let first = orderedIssueIDs.first { setSelection(first) } + return + } + if let nextID = orderedIssueIDs.item(after: currentID) { + setSelection(nextID) + } + } + + func selectPrevious() { + guard let currentID = selectedIssueID else { + if let last = orderedIssueIDs.last { setSelection(last) } + return + } + if let previousID = orderedIssueIDs.item(before: currentID) { + setSelection(previousID) + } + } + func isFixRunning(_ item: DoctorIssueItem) -> Bool { guard let token = item.fixToken else { return false @@ -226,3 +255,19 @@ final class DoctorViewModel { } } } + +extension Array where Element: Equatable { + func item(after value: Element) -> Element? { + guard let index = firstIndex(of: value), index + 1 < count else { + return nil + } + return self[index + 1] + } + + func item(before value: Element) -> Element? { + guard let index = firstIndex(of: value), index - 1 >= 0 else { + return nil + } + return self[index - 1] + } +} diff --git a/Sources/BrewFeatureDoctor/Views/DoctorView.swift b/Sources/BrewFeatureDoctor/Views/DoctorView.swift index f985785..3707f51 100644 --- a/Sources/BrewFeatureDoctor/Views/DoctorView.swift +++ b/Sources/BrewFeatureDoctor/Views/DoctorView.swift @@ -83,10 +83,7 @@ struct DoctorView: View { } private func issuesList(groups: [DoctorIssueGroup]) -> some View { - List(selection: Binding( - get: { viewModel.selectedIssueID }, - set: { viewModel.setSelection($0) }, - )) { + List { ForEach(groups) { group in Section { ForEach(group.items) { item in @@ -96,6 +93,10 @@ struct DoctorView: View { .listRowBackground( viewModel.selectedIssueID == item.id ? Color.brewBrandTint : Color.clear, ) + .onTapGesture { + // Needed to suppress the default ugly blue macOS highlight state + viewModel.setSelection(item.id) + } } } header: { DoctorSeveritySectionHeader(severity: group.severity, issueCount: group.items.count) @@ -108,6 +109,14 @@ struct DoctorView: View { .focused($isFocused) .listStyle(.inset) .accessibilityLabel("Doctor issues") + .onKeyPress(.upArrow) { + viewModel.selectPrevious() + return .handled + } + .onKeyPress(.downArrow) { + viewModel.selectNext() + return .handled + } } } diff --git a/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift b/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift index 0beca08..9e8192b 100644 --- a/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift +++ b/Sources/BrewFeatureInstalled/ViewModels/InstalledViewModel.swift @@ -27,6 +27,10 @@ struct InstalledPackagesContent: Equatable { var caskPackages: [InstalledBrewPackage] { packages.filter { $0.kind == .cask } } + + var orderedPackageIDs: [InstalledBrewPackage.ID] { + formulaPackages.map(\.id) + caskPackages.map(\.id) + } } @Observable @@ -132,6 +136,26 @@ final class InstalledViewModel { } } + func selectNext() { + guard let currentID = activeSelectedPackageID else { + if let first = state.value?.orderedPackageIDs.first { setSelection(first) } + return + } + if let nextID = state.value?.orderedPackageIDs.item(after: currentID) { + setSelection(nextID) + } + } + + func selectPrevious() { + guard let currentID = activeSelectedPackageID else { + if let last = state.value?.orderedPackageIDs.last { setSelection(last) } + return + } + if let previousID = state.value?.orderedPackageIDs.item(before: currentID) { + setSelection(previousID) + } + } + func clearSelection() { selectedPackageID = firstVisibleRowID() searchPreviewSelectedPackageID = nil @@ -235,3 +259,19 @@ final class InstalledViewModel { } } } + +extension Array where Element: Equatable { + func item(after value: Element) -> Element? { + guard let index = firstIndex(of: value), index + 1 < count else { + return nil + } + return self[index + 1] + } + + func item(before value: Element) -> Element? { + guard let index = firstIndex(of: value), index - 1 >= 0 else { + return nil + } + return self[index - 1] + } +} diff --git a/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift b/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift index 088c4ed..6b831e4 100644 --- a/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift +++ b/Sources/BrewFeatureInstalled/ViewModels/UpgradesViewModel.swift @@ -175,30 +175,6 @@ final class UpgradesViewModel { await repository.load(forceRefresh: true) } - func setSelection(_ selection: InstalledBrewPackage.ID?) { - if isSearchActive { - didCommitSelectionDuringSearch = true - searchPreviewSelectedPackageID = nil - } - if let selection { - selectedPackageID = selection - } else { - selectedPackageID = firstVisibleRowID() - } - } - - func clearSelection() { - selectedPackageID = firstVisibleRowID() - searchPreviewSelectedPackageID = nil - } - - func selectInstalledPackage(id: InstalledBrewPackage.ID) { - guard allRows.contains(where: { $0.id == id }) else { - return - } - setSelection(id) - } - /// User-facing command rendered by the Updates header's `CommandBlockView`. Reads the canonical /// literal from ``BrewOperationID/bulkUpgradeDisplayCommand`` so the view, the console job, and /// the live `BulkUpgradeCommand` all share one source of truth. @@ -328,3 +304,51 @@ final class UpgradesViewModel { } } } + +// MARK: - Selection + +extension UpgradesViewModel { + func setSelection(_ selection: InstalledBrewPackage.ID?) { + if isSearchActive { + didCommitSelectionDuringSearch = true + searchPreviewSelectedPackageID = nil + } + if let selection { + selectedPackageID = selection + } else { + selectedPackageID = firstVisibleRowID() + } + } + + func selectNext() { + guard let currentID = activeSelectedPackageID else { + if let first = state.value?.orderedPackageIDs.first { setSelection(first) } + return + } + if let nextID = state.value?.orderedPackageIDs.item(after: currentID) { + setSelection(nextID) + } + } + + func selectPrevious() { + guard let currentID = activeSelectedPackageID else { + if let last = state.value?.orderedPackageIDs.last { setSelection(last) } + return + } + if let previousID = state.value?.orderedPackageIDs.item(before: currentID) { + setSelection(previousID) + } + } + + func clearSelection() { + selectedPackageID = firstVisibleRowID() + searchPreviewSelectedPackageID = nil + } + + func selectInstalledPackage(id: InstalledBrewPackage.ID) { + guard allRows.contains(where: { $0.id == id }) else { + return + } + setSelection(id) + } +} diff --git a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift index e99c101..ecc8d17 100644 --- a/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/InstalledPackagesView.swift @@ -49,16 +49,7 @@ struct InstalledPackagesView: View { private func installedList(_ content: InstalledPackagesContent) -> some View { ScrollViewReader { proxy in - List(selection: Binding( - get: { viewModel.activeSelectedPackageID }, - set: { newValue in - if let newValue { - viewModel.setSelection(newValue) - } else { - viewModel.clearSelection() - } - }, - )) { + List { if content.shouldShowFormulaeSection { Section("Formulae") { sectionContent(for: content.formulaPackages) @@ -86,6 +77,14 @@ struct InstalledPackagesView: View { .onChange(of: content.packages.map(\.id)) { _, _ in scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) } + .onKeyPress(.upArrow) { + viewModel.selectPrevious() + return .handled + } + .onKeyPress(.downArrow) { + viewModel.selectNext() + return .handled + } .onExitCommand { viewModel.clearSelection() } @@ -100,6 +99,10 @@ struct InstalledPackagesView: View { .listRowBackground( viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, ) + .onTapGesture { + // Needed to suppress the default ugly blue macOS highlight state + viewModel.setSelection(package.id) + } } } diff --git a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift index 749fadb..f80dbaf 100644 --- a/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift +++ b/Sources/BrewFeatureInstalled/Views/UpgradesPackagesView.swift @@ -81,16 +81,7 @@ struct UpgradesPackagesView: View { private func upgradesList(_ content: InstalledPackagesContent) -> some View { ScrollViewReader { proxy in - List(selection: Binding( - get: { viewModel.activeSelectedPackageID }, - set: { newValue in - if let newValue { - viewModel.setSelection(newValue) - } else { - viewModel.clearSelection() - } - }, - )) { + List { if content.shouldShowFormulaeSection { Section("Formulae") { sectionContent(for: content.formulaPackages) @@ -118,6 +109,14 @@ struct UpgradesPackagesView: View { .onChange(of: content.packages.map(\.id)) { _, _ in scrollToSelection(viewModel.activeSelectedPackageID, in: content, with: proxy) } + .onKeyPress(.upArrow) { + viewModel.selectPrevious() + return .handled + } + .onKeyPress(.downArrow) { + viewModel.selectNext() + return .handled + } .onExitCommand { viewModel.clearSelection() } @@ -132,6 +131,10 @@ struct UpgradesPackagesView: View { .listRowBackground( viewModel.activeSelectedPackageID == package.id ? Color.brewBrandTint : Color.clear, ) + .onTapGesture { + // Needed to suppress the default ugly blue macOS highlight state + viewModel.setSelection(package.id) + } } } diff --git a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift index 0d6692f..791ea03 100644 --- a/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift +++ b/Tests/BrewFeatureDiscoverTests/DiscoverViewModelTests.swift @@ -345,6 +345,85 @@ struct DiscoverViewModelTests { #expect(viewModel.selectedPackage?.id == .formula(name: "git")) } + // MARK: - Keyboard navigation + + @Test @MainActor func `selectNext steps through visible rows and stops at the last`() async { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot( + topFormulae: [ + discoveryPackage(name: "git", thirtyDayInstallCount: 100), + discoveryPackage(name: "node", thirtyDayInstallCount: 80), + ], + topCasks: [discoveryPackage(name: "docker", kind: .cask, thirtyDayInstallCount: 70)], + ), + ), + catalogueRepository: StubCatalogueRepository(), + installedRepository: installedRepo(), + ) + + await viewModel.load() + #expect(viewModel.selectedPackage?.id == .formula(name: "git")) + + viewModel.selectNext() + #expect(viewModel.selectedPackage?.id == .formula(name: "node")) + // Crosses the formulae → casks section boundary. + viewModel.selectNext() + #expect(viewModel.selectedPackage?.id == .cask(token: "docker")) + // Clamps at the last visible row. + viewModel.selectNext() + #expect(viewModel.selectedPackage?.id == .cask(token: "docker")) + } + + @Test @MainActor func `selectPrevious steps backward through visible rows and stops at the first`() async { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot( + topFormulae: [ + discoveryPackage(name: "git", thirtyDayInstallCount: 100), + discoveryPackage(name: "node", thirtyDayInstallCount: 80), + ], + topCasks: [discoveryPackage(name: "docker", kind: .cask, thirtyDayInstallCount: 70)], + ), + ), + catalogueRepository: StubCatalogueRepository(), + installedRepository: installedRepo(), + ) + + await viewModel.load() + viewModel.setSelection(.cask(token: "docker")) + + viewModel.selectPrevious() + #expect(viewModel.selectedPackage?.id == .formula(name: "node")) + viewModel.selectPrevious() + #expect(viewModel.selectedPackage?.id == .formula(name: "git")) + viewModel.selectPrevious() + #expect(viewModel.selectedPackage?.id == .formula(name: "git")) + } + + @Test @MainActor func `selectNext navigates only within the active scope`() async { + let viewModel = DiscoverViewModel( + discoverPackagesRepository: StubDiscoverPackagesRepository( + snapshot: DiscoverTopPackagesSnapshot( + topFormulae: [discoveryPackage(name: "git", thirtyDayInstallCount: 100)], + topCasks: [discoveryPackage(name: "docker", kind: .cask, thirtyDayInstallCount: 70)], + ), + ), + catalogueRepository: StubCatalogueRepository(), + installedRepository: installedRepo(), + ) + + await viewModel.load() + viewModel.scope = .casks + #expect(viewModel.selectedPackage?.id == .cask(token: "docker")) + + // Only the cask is visible, so there's nothing to advance to. + viewModel.selectNext() + #expect(viewModel.selectedPackage?.id == .cask(token: "docker")) + viewModel.selectPrevious() + #expect(viewModel.selectedPackage?.id == .cask(token: "docker")) + } + // MARK: - Search @Test @MainActor func `search populates results and switches into searching mode`() async { diff --git a/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift b/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift index 72710d3..f944a1b 100644 --- a/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift +++ b/Tests/BrewFeatureDoctorTests/DoctorViewModelTests.swift @@ -285,6 +285,94 @@ struct DoctorViewModelTests { DoctorIssue(title: title, severity: severity, blocks: [], rawBody: "") } + // MARK: - Keyboard navigation + + @Test func `orderedIssueIDs follows grouped descending-severity order`() async { + let caution = Self.minimal(.caution, "c1") + let danger = Self.minimal(.danger, "d1") + let unsupported = Self.minimal(.unsupported, "u1") + let viewModel = Self.viewModel( + repository: StubDoctorRepository(report: DoctorReport(issues: [caution, danger, unsupported])), + ) + await viewModel.load() + + #expect(viewModel.orderedIssueIDs == [ + DoctorIssueItem.contentID(for: unsupported), + DoctorIssueItem.contentID(for: danger), + DoctorIssueItem.contentID(for: caution), + ]) + } + + @Test func `selectNext steps through issues in grouped order and stops at the last`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: Self.threeSeverityReport())) + await viewModel.load() + let ordered = viewModel.orderedIssueIDs + #expect(ordered.count == 3) + + viewModel.setSelection(ordered[0]) + viewModel.selectNext() + #expect(viewModel.selectedIssueID == ordered[1]) + viewModel.selectNext() + #expect(viewModel.selectedIssueID == ordered[2]) + // Clamps at the final issue. + viewModel.selectNext() + #expect(viewModel.selectedIssueID == ordered[2]) + } + + @Test func `selectPrevious steps backward through issues and stops at the first`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: Self.threeSeverityReport())) + await viewModel.load() + let ordered = viewModel.orderedIssueIDs + + viewModel.setSelection(ordered[2]) + viewModel.selectPrevious() + #expect(viewModel.selectedIssueID == ordered[1]) + viewModel.selectPrevious() + #expect(viewModel.selectedIssueID == ordered[0]) + // Clamps at the first issue. + viewModel.selectPrevious() + #expect(viewModel.selectedIssueID == ordered[0]) + } + + @Test func `selectNext from no selection selects the first issue`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: Self.threeSeverityReport())) + await viewModel.load() + viewModel.setSelection(nil) + + viewModel.selectNext() + + #expect(viewModel.selectedIssueID == viewModel.orderedIssueIDs.first) + } + + @Test func `selectPrevious from no selection selects the last issue`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: Self.threeSeverityReport())) + await viewModel.load() + viewModel.setSelection(nil) + + viewModel.selectPrevious() + + #expect(viewModel.selectedIssueID == viewModel.orderedIssueIDs.last) + } + + @Test func `selectNext is a no-op on a healthy report`() async { + let viewModel = Self.viewModel(repository: StubDoctorRepository(report: DoctorReport(issues: []))) + await viewModel.load() + #expect(viewModel.selectedIssueID == nil) + + viewModel.selectNext() + #expect(viewModel.selectedIssueID == nil) + viewModel.selectPrevious() + #expect(viewModel.selectedIssueID == nil) + } + + private static func threeSeverityReport() -> DoctorReport { + DoctorReport(issues: [ + minimal(.caution, "c1"), + minimal(.danger, "d1"), + minimal(.unsupported, "u1"), + ]) + } + // MARK: - Header chrome @Test func `showsHeaderControls is hidden while loading and on failure`() { diff --git a/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift b/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift index 83d3bc7..75e3422 100644 --- a/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift +++ b/Tests/BrewFeatureInstalledTests/InstalledViewModelTests.swift @@ -60,6 +60,53 @@ struct InstalledViewModelTests { #expect(vm.selectedPackage?.id == selectedID) } + // MARK: - Keyboard navigation + + @Test @MainActor func `selectNext steps forward through the rows and stops at the last`() async { + let vm = await InstalledFeatureTestSupport.loadedViewModel( + formulae: [.fixture(name: "git", kind: .formula), .fixture(name: "wget", kind: .formula)], + casks: [.fixture(name: "slack", kind: .cask)], + ) + let ordered = vm.loadedFormulaPackages.map(\.id) + vm.loadedCaskPackages.map(\.id) + #expect(ordered.count == 3) + + vm.setSelection(ordered[0]) + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[1]) + // Crosses the formulae → casks section boundary. + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[2]) + // Clamps at the final row. + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[2]) + } + + @Test @MainActor func `selectPrevious steps backward through the rows and stops at the first`() async { + let vm = await InstalledFeatureTestSupport.loadedViewModel( + formulae: [.fixture(name: "git", kind: .formula), .fixture(name: "wget", kind: .formula)], + casks: [.fixture(name: "slack", kind: .cask)], + ) + let ordered = vm.loadedFormulaPackages.map(\.id) + vm.loadedCaskPackages.map(\.id) + + vm.setSelection(ordered[2]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[1]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[0]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[0]) + } + + @Test @MainActor func `selectNext and selectPrevious are no-ops with an empty inventory`() async { + let vm = await InstalledFeatureTestSupport.loadedViewModel() + #expect(vm.selectedPackage == nil) + + vm.selectNext() + #expect(vm.selectedPackage == nil) + vm.selectPrevious() + #expect(vm.selectedPackage == nil) + } + @Test @MainActor func `refresh preserves selection when package still exists`() async { let firstJSON = """ { diff --git a/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift b/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift index 977816e..3ae713d 100644 --- a/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift +++ b/Tests/BrewFeatureInstalledTests/UpgradesViewModelTests.swift @@ -266,6 +266,65 @@ struct UpgradesViewModelTests { await center.emit(id: unrelated, phase: .idle) } + // MARK: - Keyboard navigation + + @Test @MainActor func `selectNext steps forward through outdated rows and stops at the last`() { + let vm = Self.makeViewModel(packages: [ + .fixture(name: "git", kind: .formula, outdated: true), + .fixture(name: "wget", kind: .formula, outdated: true), + .fixture(name: "slack", kind: .cask, outdated: true), + ]) + guard case let .loaded(content) = vm.state else { + Issue.record("expected loaded state") + return + } + let ordered = content.formulaPackages.map(\.id) + content.caskPackages.map(\.id) + #expect(ordered.count == 3) + + vm.setSelection(ordered[0]) + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[1]) + // Crosses the formulae → casks section boundary. + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[2]) + // Clamps at the final row. + vm.selectNext() + #expect(vm.selectedPackage?.id == ordered[2]) + } + + @Test @MainActor func `selectPrevious steps backward through outdated rows and stops at the first`() { + let vm = Self.makeViewModel(packages: [ + .fixture(name: "git", kind: .formula, outdated: true), + .fixture(name: "wget", kind: .formula, outdated: true), + .fixture(name: "slack", kind: .cask, outdated: true), + ]) + guard case let .loaded(content) = vm.state else { + Issue.record("expected loaded state") + return + } + let ordered = content.formulaPackages.map(\.id) + content.caskPackages.map(\.id) + + vm.setSelection(ordered[2]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[1]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[0]) + vm.selectPrevious() + #expect(vm.selectedPackage?.id == ordered[0]) + } + + @Test @MainActor func `selectNext and selectPrevious are no-ops when nothing is outdated`() { + let vm = Self.makeViewModel(packages: [ + .fixture(name: "wget", kind: .formula, outdated: false), + ]) + #expect(vm.selectedPackage == nil) + + vm.selectNext() + #expect(vm.selectedPackage == nil) + vm.selectPrevious() + #expect(vm.selectedPackage == nil) + } + // MARK: - shouldFocusList @Test @MainActor func `shouldFocusList is false while the outdated inventory is still loading`() {