Navigate a directory full of directories, identifying repos and worktrees
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add beads-last-changed column and --sort flag

rektide dddae5a6 87f4857f

+183 -2
+29
src/detect/beads.rs
··· 29 29 30 30 Some(prefix.to_string()) 31 31 } 32 + 33 + pub fn get_beads_last_changed(path: &Path) -> Option<String> { 34 + let beads_dir = path.join(".beads"); 35 + if !beads_dir.exists() { 36 + return None; 37 + } 38 + 39 + let output = Command::new("bd") 40 + .current_dir(path) 41 + .args(["sql", "--json"]) 42 + .arg("SELECT MAX(updated_at) AS last_changed FROM issues") 43 + .output() 44 + .ok()?; 45 + 46 + if !output.status.success() { 47 + return None; 48 + } 49 + 50 + let stdout = String::from_utf8_lossy(&output.stdout); 51 + let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).ok()?; 52 + let first = parsed.first()?; 53 + let val = first.get("last_changed")?; 54 + 55 + match val { 56 + serde_json::Value::Null => None, 57 + serde_json::Value::String(s) => Some(s.clone()), 58 + other => Some(other.to_string()), 59 + } 60 + }
+1 -1
src/detect/mod.rs
··· 8 8 mod workparent; 9 9 10 10 pub use ahead::get_ahead; 11 - pub use beads::get_beads_prefix; 11 + pub use beads::{get_beads_last_changed, get_beads_prefix}; 12 12 pub use date::{get_change_date, get_commit_date}; 13 13 pub use repo::{detect_repo, RepoInfo, RepoType}; 14 14 pub use variant::get_variant;
+45
src/main.rs
··· 61 61 ) 62 62 .await?; 63 63 64 + if let Some(sort_key) = matches.get_one::<String>("sort").map(String::as_str) { 65 + sort_rows(&mut rows, sort_key, registry.columns())?; 66 + } 67 + 64 68 if matches.get_flag("json") { 65 69 render_json(&rows, &format_template, registry.columns())?; 66 70 } else { ··· 129 133 .value_name("STRING") 130 134 .help("Replace spaces in rendered text output") 131 135 .default_value(" "), 136 + ) 137 + .arg( 138 + Arg::new("sort") 139 + .long("sort") 140 + .value_name("COLUMN_KEY") 141 + .help("Sort output rows by the given column key (descending)"), 132 142 ) 133 143 .arg( 134 144 Arg::new("directories") ··· 340 350 CellValue::Text(value) => value.clone(), 341 351 CellValue::Number(value) => value.to_string(), 342 352 CellValue::Empty => String::new(), 353 + } 354 + } 355 + 356 + fn sort_rows( 357 + rows: &mut [OutputRow], 358 + sort_key: &str, 359 + columns: &ColumnCatalog, 360 + ) -> PluginResult<()> { 361 + let col_ix = columns 362 + .column_ix(sort_key) 363 + .ok_or_else(|| format!("unknown column key for --sort: {sort_key}"))?; 364 + 365 + let spec = &columns.columns[col_ix]; 366 + if !spec.sortable { 367 + return Err(format!("column '{}' is not sortable", sort_key)); 368 + } 369 + 370 + rows.sort_by(|a, b| { 371 + let a_val = a.cells.get(col_ix).unwrap_or(&CellValue::Empty); 372 + let b_val = b.cells.get(col_ix).unwrap_or(&CellValue::Empty); 373 + compare_cells(b_val, a_val) 374 + }); 375 + 376 + Ok(()) 377 + } 378 + 379 + fn compare_cells(a: &CellValue, b: &CellValue) -> std::cmp::Ordering { 380 + match (a, b) { 381 + (CellValue::Empty, CellValue::Empty) => std::cmp::Ordering::Equal, 382 + (CellValue::Empty, _) => std::cmp::Ordering::Less, 383 + (_, CellValue::Empty) => std::cmp::Ordering::Greater, 384 + (CellValue::Number(an), CellValue::Number(bn)) => an.cmp(bn), 385 + (CellValue::Text(at), CellValue::Text(bt)) => at.cmp(bt), 386 + (CellValue::Number(_), CellValue::Text(_)) => std::cmp::Ordering::Less, 387 + (CellValue::Text(_), CellValue::Number(_)) => std::cmp::Ordering::Greater, 343 388 } 344 389 } 345 390
+108 -1
src/plugin.rs
··· 7 7 use futures_util::stream; 8 8 use futures_util::StreamExt; 9 9 10 - use crate::detect::{detect_repo, get_ahead, RepoInfo, RepoType}; 10 + use crate::detect::{ 11 + detect_repo, get_ahead, get_beads_last_changed, RepoInfo, RepoType, 12 + }; 11 13 12 14 pub type RowId = usize; 13 15 pub type PluginIx = usize; ··· 785 787 pub fn default_registry() -> PluginRegistry { 786 788 let mut registry = PluginRegistry::new(Box::new(CoreRepoProbe)); 787 789 registry.register(Box::new(JjPlugin)); 790 + registry.register(Box::new(BeadsPlugin)); 788 791 registry 789 792 } 790 793 ··· 909 912 Box::pin(stream::iter(microbatches)) 910 913 } 911 914 } 915 + 916 + struct BeadsPlugin; 917 + 918 + const BEADS_COL_LAST_CHANGED: usize = 0; 919 + 920 + const BEADS_COLUMNS: &[ColumnDecl] = &[ColumnDecl { 921 + key: "beads-last-changed", 922 + title: "BEADS_LAST_CHANGED", 923 + description: "Timestamp of the most recently updated beads issue", 924 + sortable: true, 925 + default_in_base_format: false, 926 + }]; 927 + 928 + const BEADS_ARGS: &[ArgKind] = &[ 929 + ArgKind::PluginToggle { 930 + help: "Enable Beads plugin columns", 931 + }, 932 + ArgKind::ColumnToggle { 933 + local_col_ix: BEADS_COL_LAST_CHANGED, 934 + help: "Enable beads last-changed column", 935 + }, 936 + ]; 937 + 938 + #[derive(Clone, Copy)] 939 + struct BeadsSubProcessor { 940 + local_col_ix: usize, 941 + collect_cell: fn(&RepoWorkItem) -> CellValue, 942 + } 943 + 944 + const BEADS_SUBPROCESSORS: &[BeadsSubProcessor] = &[BeadsSubProcessor { 945 + local_col_ix: BEADS_COL_LAST_CHANGED, 946 + collect_cell: beads_collect_last_changed, 947 + }]; 948 + 949 + fn beads_collect_last_changed(item: &RepoWorkItem) -> CellValue { 950 + get_beads_last_changed(&item.path) 951 + .map(CellValue::Text) 952 + .unwrap_or(CellValue::Empty) 953 + } 954 + 955 + fn collect_beads_subprocessor_batches( 956 + req: &CollectRequest<'_>, 957 + global_col_ix: ColumnIx, 958 + processor: BeadsSubProcessor, 959 + ) -> MicroBatch { 960 + req.items 961 + .iter() 962 + .map(|item| RowPatch { 963 + row_id: item.row_id, 964 + updates: vec![(global_col_ix, (processor.collect_cell)(item))], 965 + }) 966 + .collect() 967 + } 968 + 969 + impl DetectorPlugin for BeadsPlugin { 970 + fn id(&self) -> &'static str { 971 + "beads" 972 + } 973 + 974 + fn description(&self) -> &'static str { 975 + "Beads issue tracker metrics" 976 + } 977 + 978 + fn column_decls(&self) -> &'static [ColumnDecl] { 979 + BEADS_COLUMNS 980 + } 981 + 982 + fn arg_kinds(&self) -> &'static [ArgKind] { 983 + BEADS_ARGS 984 + } 985 + 986 + fn configure(&self, _args: PluginArgs<'_>) -> PluginConfig { 987 + PluginConfig::enabled() 988 + } 989 + 990 + fn applies_to(&self, _repo: &RepoInfo) -> bool { 991 + true 992 + } 993 + 994 + fn collect_stream<'a>(&'a self, req: CollectRequest<'a>) -> BatchStream<'a> { 995 + let mut selected_global_by_local = vec![None; BEADS_COLUMNS.len()]; 996 + for (local_col_ix, global_col_ix) in req.requested_columns { 997 + if *local_col_ix < selected_global_by_local.len() { 998 + selected_global_by_local[*local_col_ix] = Some(*global_col_ix); 999 + } 1000 + } 1001 + 1002 + let mut microbatches = Vec::new(); 1003 + for processor in BEADS_SUBPROCESSORS { 1004 + let Some(global_col_ix) = selected_global_by_local[processor.local_col_ix] else { 1005 + continue; 1006 + }; 1007 + 1008 + let patches = 1009 + collect_beads_subprocessor_batches(&req, global_col_ix, *processor); 1010 + let batches = microbatch_rows(patches, req.microbatch_rows); 1011 + for batch in batches { 1012 + microbatches.push(Ok(batch)); 1013 + } 1014 + } 1015 + 1016 + Box::pin(stream::iter(microbatches)) 1017 + } 1018 + }