Skip to content

Commit c523a07

Browse files
committed
feat: use real homebrew untracked diff in filesystem view (#403)
## Summary Start swapping out the fake data on the "Untracked" tab of file system view by integrating the same Homebrew diff that the pre-existing chip uses. This involves several changes and refactorings: - Add separate sections for brews/casks/taps simiar to how the chip popup menu works. - Add expanders since in practice some of these sections can get really long (see screenshot). - Fix pre-existing unrelated bug with the brew list command and add logging so it's not totally silent in the future. - Make the "untracked" banner on the begin step delay-loaded and dynamic with the correct count. - Remove the "Track all" button since it never worked right and would work even less right with potentially hundreds of items. We can look into bringing it back once the untracked support is "done". Also see screenshot. The data for "defaults" and "startup items" is still the fake data. I will work on defaults next. Note that there is a lot of demo/AI cruft remaining particularly in data.ts that will continue to disappear the closer we get to the goal. Another potential follow-up item is some caching or limitations on how often we automatically scan, although empirically right now it's not unreasonably slow imo. ![Screenshot 2026-06-12 at 11.25.48 AM.png](https://app.graphite.com/user-attachments/assets/635f022e-e918-4f81-9177-c300c7e34489.png) ![Screenshot 2026-06-12 at 10.45.38 AM.png](https://app.graphite.com/user-attachments/assets/dc8b351c-6bd1-4833-a402-3a3b442eb84f.png) <!-- What does this PR do? Why? --> ## Test Plan Some minor updates to unit tests, plus lots of manual testing. Also needed to update a bunch of snaps, hopefully this doesn't run into merge issues etc. - [ ] No test plan needed ## Docs - [ ] Docs updated (companion PR in darkmatter/nixmac-web: #\___) - [x] No docs update needed
1 parent 013ba51 commit c523a07

27 files changed

Lines changed: 709 additions & 269 deletions

apps/native/src-tauri/examples/specta_gen_ts.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ fn main() {
5959
.register::<shared_types::ThinkingEntry>()
6060
.register::<shared_types::ToolCallRecord>()
6161
.register::<shared_types::Evolution>()
62-
.register::<shared_types::HomebrewCaskItem>()
62+
.register::<shared_types::HomebrewItemType>()
63+
.register::<shared_types::HomebrewItem>()
6364
.register::<shared_types::HomebrewState>()
6465
.register::<shared_types::SummarizedChange>()
6566
.register::<shared_types::SummarizedChangeSet>()

apps/native/src-tauri/src/commands/homebrew.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ pub async fn homebrew_get_state_diff(
2626
}
2727

2828
#[tauri::command]
29-
pub async fn homebrew_add_casks(
29+
pub async fn homebrew_add_items(
3030
app: AppHandle,
31-
casks: Vec<shared_types::HomebrewCaskItem>,
31+
items: Vec<shared_types::HomebrewItem>,
3232
) -> Result<shared_types::ConfigEditApplyResult, String> {
33-
crate::managed_edits::homebrew_adopt::add_homebrew_casks(&app, casks)
33+
crate::managed_edits::homebrew_adopt::add_homebrew_items(&app, items)
3434
.await
35-
.map_err(|e| capture_err("homebrew_add_casks", e))
35+
.map_err(|e| capture_err("homebrew_add_items", e))
3636
}

apps/native/src-tauri/src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ fn run_gui_mode(
488488
#[cfg(debug_assertions)]
489489
commands::debug::e2e_mark_boot_stage,
490490
// Homebrew
491-
commands::homebrew::homebrew_add_casks,
491+
commands::homebrew::homebrew_add_items,
492492
commands::homebrew::homebrew_apply_diff,
493493
commands::homebrew::homebrew_get_state_diff,
494494
// Git

apps/native/src-tauri/src/managed_edits/homebrew_adopt.rs

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use crate::evolve::file_ops::resolve_path_in_dir_allow_create;
22
use crate::evolve::nix_file_editor::{apply_semantic_edit, nix_quote_values};
33
use crate::evolve::types::{FileEditAction, SemanticFileEdit};
4-
use crate::shared_types::HomebrewState;
4+
use crate::shared_types::{HomebrewItemType, HomebrewState};
55
use crate::system::nix_ast_lists::parse_string_lists_by_attrpath;
66
use crate::system::scanner::inject_module_import;
77
use crate::{managed_edits::managed_edit, shared_types};
88
use anyhow::{Context, Result};
99
use serde_json::{Map, Value};
10-
use shared_types::HomebrewCaskItem;
10+
use shared_types::HomebrewItem;
1111
use tauri::AppHandle;
1212

1313
const NIXMAC_HOMEBREW_DATA_PATH: &str = ".nixmac/homebrew/data.json";
@@ -202,7 +202,9 @@ pub async fn apply_homebrew_diff(
202202
}
203203

204204
/// Scan the system for Homebrew packages, casks, and taps.
205-
/// Only includes explicitly installed brews and casks (via `--installed-on-request`), not their dependencies.
205+
/// Only includes explicitly installed brews and casks not their dependencies.
206+
/// Note that generally speaking, `brew list --casks` does *not* count dependences, and
207+
/// the `--installed-on-request` flag is not valid for casks.
206208
/// Excludes default taps (homebrew/core, homebrew/cask).
207209
/// If homebrew is not installed, returns an empty state with is_installed set to false.
208210
pub fn scan_homebrew() -> HomebrewState {
@@ -221,10 +223,12 @@ pub fn scan_homebrew() -> HomebrewState {
221223
.lines()
222224
.map(str::to_owned)
223225
.collect();
226+
} else {
227+
log::warn!("failed to scan Homebrew brews: brew command returned non-zero exit code");
224228
}
225229
}
226230
if let Ok(output) = std::process::Command::new("brew")
227-
.args(["list", "--installed-on-request", "--cask"])
231+
.args(["list", "--cask"])
228232
.env("PATH", crate::system::nix::get_nix_path())
229233
.output()
230234
{
@@ -233,6 +237,8 @@ pub fn scan_homebrew() -> HomebrewState {
233237
.lines()
234238
.map(str::to_owned)
235239
.collect();
240+
} else {
241+
log::warn!("failed to scan Homebrew casks: brew command returned non-zero exit code");
236242
}
237243
}
238244
if let Ok(output) = std::process::Command::new("brew")
@@ -249,6 +255,8 @@ pub fn scan_homebrew() -> HomebrewState {
249255
})
250256
.map(str::to_owned)
251257
.collect();
258+
} else {
259+
log::warn!("failed to scan Homebrew taps: brew command returned non-zero exit code");
252260
}
253261
}
254262

@@ -451,30 +459,63 @@ fn ensure_nixmac_module_import(config_dir: &std::path::Path) -> Result<()> {
451459
Ok(())
452460
}
453461

454-
fn add_homebrew_casks_to_config(
462+
fn add_homebrew_items_to_config(
455463
config_dir: &std::path::Path,
456-
casks: &[HomebrewCaskItem],
464+
items: &[HomebrewItem],
457465
) -> Result<std::path::PathBuf> {
458-
if casks.is_empty() {
459-
return Err(anyhow::anyhow!("at least one Homebrew cask is required"));
466+
if items.is_empty() {
467+
return Err(anyhow::anyhow!("at least one Homebrew item is required"));
460468
}
461469

462-
let cask_names = casks
470+
let item_names = items
463471
.iter()
464-
.map(|cask| cask.name.trim())
472+
.map(|item| item.name.trim())
465473
.filter(|name| !name.is_empty())
466474
.map(str::to_string)
467475
.collect::<Vec<_>>();
468-
if cask_names.is_empty() {
476+
if item_names.is_empty() {
469477
return Err(anyhow::anyhow!(
470-
"at least one named Homebrew cask is required"
478+
"at least one named Homebrew item is required"
471479
));
472480
}
473481

474482
ensure_nixmac_module_import(config_dir)?;
475483
let data_path = ensure_nixmac_homebrew_module(config_dir)?;
476484
let mut data = read_homebrew_data(&data_path)?;
477-
merge_json_array(&mut data, "casks", &cask_names)?;
485+
486+
// Separate out the casks/brews/formulae so we can do 0..3 merge_json_array calls for each.
487+
let casks = items
488+
.iter()
489+
.filter(|item| item.item_type == HomebrewItemType::Cask)
490+
.map(|item| item.name.trim())
491+
.filter(|name| !name.is_empty())
492+
.map(str::to_string)
493+
.collect::<Vec<_>>();
494+
if !casks.is_empty() {
495+
merge_json_array(&mut data, "casks", &casks)?;
496+
}
497+
498+
let brews = items
499+
.iter()
500+
.filter(|item| item.item_type == HomebrewItemType::Brew)
501+
.map(|item| item.name.trim())
502+
.filter(|name| !name.is_empty())
503+
.map(str::to_string)
504+
.collect::<Vec<_>>();
505+
if !brews.is_empty() {
506+
merge_json_array(&mut data, "brews", &brews)?;
507+
}
508+
509+
let taps = items
510+
.iter()
511+
.filter(|item| item.item_type == HomebrewItemType::Tap)
512+
.map(|item| item.name.trim())
513+
.filter(|name| !name.is_empty())
514+
.map(str::to_string)
515+
.collect::<Vec<_>>();
516+
if !taps.is_empty() {
517+
merge_json_array(&mut data, "taps", &taps)?;
518+
}
478519

479520
let rendered = serde_json::to_string_pretty(&data)?;
480521
std::fs::write(&data_path, format!("{}\n", rendered))
@@ -483,31 +524,31 @@ fn add_homebrew_casks_to_config(
483524
Ok(data_path)
484525
}
485526

486-
/// Adds one or more Homebrew casks to the managed Nixmac Homebrew data file,
527+
/// Adds one or more Homebrew items to the managed Nixmac Homebrew data file,
487528
/// snapshots the pre-edit tree onto a rollback branch, and enters the managed
488529
/// review flow.
489-
pub async fn add_homebrew_casks(
530+
pub async fn add_homebrew_items(
490531
app: &AppHandle,
491-
casks: Vec<HomebrewCaskItem>,
532+
items: Vec<HomebrewItem>,
492533
) -> Result<shared_types::ConfigEditApplyResult> {
493-
let item_count = casks
534+
let item_count = items
494535
.iter()
495-
.filter(|cask| !cask.name.trim().is_empty())
536+
.filter(|item| !item.name.trim().is_empty())
496537
.count();
497-
if casks.is_empty() {
498-
return Err(anyhow::anyhow!("at least one Homebrew cask is required"));
538+
if items.is_empty() {
539+
return Err(anyhow::anyhow!("at least one Homebrew item is required"));
499540
}
500541
if item_count == 0 {
501542
return Err(anyhow::anyhow!(
502-
"at least one named Homebrew cask is required"
543+
"at least one named Homebrew item is required"
503544
));
504545
}
505546

506547
let context = managed_edit::prepare_managed_edit(app)?;
507548
let dir = context.dir.clone();
508549

509-
add_homebrew_casks_to_config(std::path::Path::new(&dir), &casks)
510-
.context("Failed to add Homebrew casks")?;
550+
add_homebrew_items_to_config(std::path::Path::new(&dir), &items)
551+
.context("Failed to add Homebrew items")?;
511552

512553
let working_tree_status =
513554
crate::git::status(&dir).context("Failed to get working tree status for evolve state")?;
@@ -516,7 +557,7 @@ pub async fn add_homebrew_casks(
516557
context,
517558
working_tree_status,
518559
item_count,
519-
"add_homebrew_casks",
560+
"add_homebrew_items",
520561
)
521562
.await
522563
}
@@ -665,6 +706,8 @@ pub fn apply_homebrew_import(diff: HomebrewState, config_dir: &std::path::Path)
665706

666707
#[cfg(test)]
667708
mod tests {
709+
use crate::shared_types::HomebrewItemType;
710+
668711
use super::*;
669712

670713
fn homebrew_state(
@@ -981,7 +1024,7 @@ mod tests {
9811024
}
9821025

9831026
#[test]
984-
fn add_homebrew_casks_writes_managed_data_and_injects_flake_import() {
1027+
fn add_homebrew_items_writes_managed_data_and_injects_flake_import() {
9851028
let temp = tempfile::tempdir().expect("tempdir should be created");
9861029
let flake = temp.path().join("flake.nix");
9871030
write_file(
@@ -1007,16 +1050,29 @@ mod tests {
10071050
"#,
10081051
);
10091052

1010-
let data_file = add_homebrew_casks_to_config(
1053+
// Make sure to test all three item types.
1054+
let data_file = add_homebrew_items_to_config(
10111055
temp.path(),
10121056
&[
1013-
HomebrewCaskItem {
1057+
HomebrewItem {
10141058
name: "docker".to_string(),
10151059
version: Some("4.32.0".to_string()),
1060+
item_type: HomebrewItemType::Cask,
10161061
},
1017-
HomebrewCaskItem {
1062+
HomebrewItem {
10181063
name: " obs ".to_string(),
10191064
version: Some("30.2.3".to_string()),
1065+
item_type: HomebrewItemType::Cask,
1066+
},
1067+
HomebrewItem {
1068+
name: "git".to_string(),
1069+
version: Some("2.42.0".to_string()),
1070+
item_type: HomebrewItemType::Brew,
1071+
},
1072+
HomebrewItem {
1073+
name: "homebrew/cask-fonts".to_string(),
1074+
version: None,
1075+
item_type: HomebrewItemType::Tap,
10201076
},
10211077
],
10221078
)
@@ -1029,22 +1085,26 @@ mod tests {
10291085
)
10301086
.expect("homebrew data should parse");
10311087
assert_eq!(json_string_array(&data, "casks"), vec!["docker", "obs"]);
1032-
assert_eq!(json_string_array(&data, "brews"), Vec::<String>::new());
1033-
assert_eq!(json_string_array(&data, "taps"), Vec::<String>::new());
1088+
assert_eq!(json_string_array(&data, "brews"), vec!["git"]);
1089+
assert_eq!(
1090+
json_string_array(&data, "taps"),
1091+
vec!["homebrew/cask-fonts"]
1092+
);
10341093

10351094
let flake_content = std::fs::read_to_string(flake).expect("flake should remain readable");
10361095
assert!(flake_content.contains("./.nixmac"));
10371096
}
10381097

10391098
#[test]
1040-
fn add_homebrew_casks_requires_flake_before_writing_module() {
1099+
fn add_homebrew_items_requires_flake_before_writing_module() {
10411100
let temp = tempfile::tempdir().expect("tempdir should be created");
10421101

1043-
let err = add_homebrew_casks_to_config(
1102+
let err = add_homebrew_items_to_config(
10441103
temp.path(),
1045-
&[HomebrewCaskItem {
1104+
&[HomebrewItem {
10461105
name: "iterm2".to_string(),
10471106
version: None,
1107+
item_type: HomebrewItemType::Cask,
10481108
}],
10491109
)
10501110
.expect_err("managed Homebrew casks should require flake.nix");

apps/native/src-tauri/src/shared_types/managed_edits.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@ use specta::Type;
33

44
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Type)]
55
#[serde(rename_all = "camelCase")]
6-
pub struct HomebrewCaskItem {
6+
pub enum HomebrewItemType {
7+
Tap,
8+
Cask,
9+
Brew,
10+
}
11+
12+
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Type)]
13+
#[serde(rename_all = "camelCase")]
14+
pub struct HomebrewItem {
715
pub name: String,
816
pub version: Option<String>,
17+
pub item_type: HomebrewItemType,
918
}

apps/native/src/components/widget/__snapshots__/evolve-flow.stories.tsx.snap

Lines changed: 5 additions & 5 deletions
Large diffs are not rendered by default.

apps/native/src/components/widget/__snapshots__/widget.stories.tsx.snap

Lines changed: 19 additions & 19 deletions
Large diffs are not rendered by default.

apps/native/src/components/widget/filesystem/__snapshots__/file-list.stories.tsx.snap

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/native/src/components/widget/filesystem/__snapshots__/filesystem-step.stories.tsx.snap

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)