use std::path::Path;
use super::{App, InputMode};
use crate::compare::{BranchCompare, CompareTab};
use crate::config::FilterPreset;
use crate::export::{
export_heatmap_csv, export_heatmap_json, export_ownership_csv, export_ownership_json,
export_stats_csv, export_stats_json, export_timeline_csv, export_timeline_json,
};
use crate::git::{BlameLine, FileHistoryEntry, FilePatch, StashEntry};
use crate::insights::{ActionRecommendation, HandoffContext, ReviewPack};
use crate::related_files::RelatedFiles;
use crate::stats::{
ActivityTimeline, ChangeCouplingAnalysis, CodeOwnership, CommitImpactAnalysis,
CommitQualityAnalysis, FileHeatmap, RepoStats,
};
use crate::topology::{analyze_branch_recommendations, BranchTopology};
impl App {
pub fn start_topology_view(&mut self, topology: BranchTopology) {
let recommendations = analyze_branch_recommendations(&topology);
self.branch_recommendations_cache = Some(recommendations);
self.topology_cache = Some(topology);
self.topology_nav.reset();
self.input_mode = InputMode::TopologyView;
}
pub fn end_topology_view(&mut self) {
self.input_mode = InputMode::Normal;
self.topology_cache = None;
self.branch_recommendations_cache = None;
}
pub fn selected_branch_recommendation(&self) -> Option<&crate::topology::BranchRecommendation> {
let branch_name = self.selected_topology_branch()?;
self.branch_recommendations_cache.as_ref().and_then(|r| {
r.recommendations
.iter()
.find(|rec| rec.branch_name == branch_name)
})
}
pub fn topology_move_up(&mut self) {
self.topology_nav.move_up();
}
pub fn topology_move_down(&mut self) {
if let Some(ref topology) = self.topology_cache {
self.topology_nav.move_down(topology.branches.len());
}
}
pub fn topology_adjust_scroll(&mut self, visible_lines: usize) {
self.topology_nav.adjust_scroll(visible_lines);
}
pub fn selected_topology_branch(&self) -> Option<&str> {
self.topology_cache.as_ref().and_then(|t| {
t.branches
.get(self.topology_nav.selected_index)
.map(|b| b.name.as_str())
})
}
pub fn generate_commit_suggestions(&mut self) {
use crate::suggestion::generate_suggestions;
let staged: Vec<crate::git::FileStatus> = self
.file_statuses
.iter()
.filter(|s| s.kind.is_staged())
.cloned()
.collect();
self.commit_suggestions = generate_suggestions(&staged);
self.suggestion_selected_index = 0;
}
pub fn apply_suggestion(&mut self, index: usize) {
if let Some(suggestion) = self.commit_suggestions.get(index) {
self.commit_type = Some(suggestion.commit_type);
self.commit_message = suggestion.full_message();
}
}
pub fn suggestion_move_up(&mut self) {
if self.suggestion_selected_index > 0 {
self.suggestion_selected_index -= 1;
}
}
pub fn suggestion_move_down(&mut self) {
if !self.commit_suggestions.is_empty()
&& self.suggestion_selected_index < self.commit_suggestions.len() - 1
{
self.suggestion_selected_index += 1;
}
}
pub fn start_stats_view(&mut self, stats: RepoStats) {
self.stats_view.cache = Some(stats);
self.stats_view.nav.reset();
self.input_mode = InputMode::StatsView;
}
pub fn end_stats_view(&mut self) {
self.input_mode = InputMode::Normal;
self.stats_view.cache = None;
}
pub fn export_stats(&mut self) -> Option<String> {
if let Some(ref stats) = self.stats_view.cache {
let csv_path = Path::new("gitstack_stats.csv");
let json_path = Path::new("gitstack_stats.json");
let mut results = Vec::new();
if let Err(e) = export_stats_csv(stats, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_stats_json(stats, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn stats_move_up(&mut self) {
self.stats_view.nav.move_up();
}
pub fn stats_move_down(&mut self) {
if let Some(ref stats) = self.stats_view.cache {
self.stats_view.nav.move_down(stats.authors.len());
}
}
pub fn stats_adjust_scroll(&mut self, visible_lines: usize) {
self.stats_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_author(&self) -> Option<&str> {
self.stats_view.cache.as_ref().and_then(|s| {
s.authors
.get(self.stats_view.nav.selected_index)
.map(|a| a.name.as_str())
})
}
pub fn filter_by_author(&mut self, author: &str) {
self.filter_text = format!("/author:{}", author);
self.reapply_filter();
}
pub fn start_heatmap_view(&mut self, heatmap: FileHeatmap) {
self.heatmap_view.cache = Some(heatmap);
self.heatmap_view.nav.reset();
self.input_mode = InputMode::HeatmapView;
}
pub fn end_heatmap_view(&mut self) {
self.input_mode = InputMode::Normal;
self.heatmap_view.cache = None;
}
pub fn export_heatmap(&mut self) -> Option<String> {
if let Some(ref heatmap) = self.heatmap_view.cache {
let csv_path = Path::new("gitstack_heatmap.csv");
let json_path = Path::new("gitstack_heatmap.json");
let mut results = Vec::new();
if let Err(e) = export_heatmap_csv(heatmap, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_heatmap_json(heatmap, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn heatmap_increase_aggregation(&mut self) {
if let Some(heatmap) = self.heatmap_view.cache.take() {
let new_level = heatmap.aggregation_level.next();
self.heatmap_view.cache = Some(heatmap.with_aggregation(new_level));
self.heatmap_view.nav.selected_index = 0;
self.heatmap_view.nav.scroll_offset = 0;
self.status_message = Some(format!("Aggregation: {}", new_level.display_name()));
}
}
pub fn heatmap_decrease_aggregation(&mut self) {
if let Some(heatmap) = self.heatmap_view.cache.take() {
let new_level = heatmap.aggregation_level.prev();
self.heatmap_view.cache = Some(heatmap.with_aggregation(new_level));
self.heatmap_view.nav.selected_index = 0;
self.heatmap_view.nav.scroll_offset = 0;
self.status_message = Some(format!("Aggregation: {}", new_level.display_name()));
}
}
pub fn heatmap_move_up(&mut self) {
self.heatmap_view.nav.move_up();
}
pub fn heatmap_move_down(&mut self) {
if let Some(ref heatmap) = self.heatmap_view.cache {
self.heatmap_view.nav.move_down(heatmap.files.len());
}
}
pub fn heatmap_adjust_scroll(&mut self, visible_lines: usize) {
self.heatmap_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_heatmap_file(&self) -> Option<&str> {
self.heatmap_view.cache.as_ref().and_then(|h| {
h.files
.get(self.heatmap_view.nav.selected_index)
.map(|f| f.path.as_str())
})
}
pub fn filter_by_file(&mut self, file_path: &str) {
self.filter_text = format!("/file:{}", file_path);
self.reapply_filter();
}
pub fn start_file_history_view(&mut self, path: String, history: Vec<FileHistoryEntry>) {
self.file_history_view.path = Some(path);
self.file_history_view.cache = Some(history);
self.file_history_view.nav.reset();
self.input_mode = InputMode::FileHistoryView;
}
pub fn end_file_history_view(&mut self) {
self.input_mode = InputMode::Normal;
self.file_history_view.cache = None;
self.file_history_view.path = None;
}
pub fn file_history_move_up(&mut self) {
self.file_history_view.nav.move_up();
}
pub fn file_history_move_down(&mut self) {
if let Some(ref history) = self.file_history_view.cache {
self.file_history_view.nav.move_down(history.len());
}
}
pub fn file_history_adjust_scroll(&mut self, visible_lines: usize) {
self.file_history_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_file_history(&self) -> Option<&FileHistoryEntry> {
self.file_history_view
.cache
.as_ref()
.and_then(|h| h.get(self.file_history_view.nav.selected_index))
}
pub fn start_timeline_view(&mut self, timeline: ActivityTimeline) {
self.timeline_cache = Some(timeline);
self.input_mode = InputMode::TimelineView;
}
pub fn end_timeline_view(&mut self) {
self.input_mode = InputMode::Normal;
self.timeline_cache = None;
}
pub fn export_timeline(&mut self) -> Option<String> {
if let Some(ref timeline) = self.timeline_cache {
let csv_path = Path::new("gitstack_timeline.csv");
let json_path = Path::new("gitstack_timeline.json");
let mut results = Vec::new();
if let Err(e) = export_timeline_csv(timeline, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_timeline_json(timeline, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn start_blame_view(&mut self, path: String, blame: Vec<BlameLine>) {
self.blame_view.path = Some(path);
self.blame_view.cache = Some(blame);
self.blame_view.nav.reset();
self.input_mode = InputMode::BlameView;
}
pub fn end_blame_view(&mut self) {
self.input_mode = InputMode::Normal;
self.blame_view.cache = None;
self.blame_view.path = None;
}
pub fn blame_move_up(&mut self) {
self.blame_view.nav.move_up();
}
pub fn blame_move_down(&mut self) {
if let Some(ref blame) = self.blame_view.cache {
self.blame_view.nav.move_down(blame.len());
}
}
pub fn blame_adjust_scroll(&mut self, visible_lines: usize) {
self.blame_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_blame_line(&self) -> Option<&BlameLine> {
self.blame_view
.cache
.as_ref()
.and_then(|b| b.get(self.blame_view.nav.selected_index))
}
pub fn start_ownership_view(&mut self, ownership: CodeOwnership) {
self.ownership_view.cache = Some(ownership);
self.ownership_view.nav.reset();
self.input_mode = InputMode::OwnershipView;
}
pub fn end_ownership_view(&mut self) {
self.input_mode = InputMode::Normal;
self.ownership_view.cache = None;
}
pub fn export_ownership(&mut self) -> Option<String> {
if let Some(ref ownership) = self.ownership_view.cache {
let csv_path = Path::new("gitstack_ownership.csv");
let json_path = Path::new("gitstack_ownership.json");
let mut results = Vec::new();
if let Err(e) = export_ownership_csv(ownership, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_ownership_json(ownership, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn ownership_move_up(&mut self) {
self.ownership_view.nav.move_up();
}
pub fn ownership_move_down(&mut self) {
if let Some(ref ownership) = self.ownership_view.cache {
self.ownership_view.nav.move_down(ownership.entries.len());
}
}
pub fn ownership_adjust_scroll(&mut self, visible_lines: usize) {
self.ownership_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_ownership_entry(&self) -> Option<&crate::stats::CodeOwnershipEntry> {
self.ownership_view
.cache
.as_ref()
.and_then(|o| o.entries.get(self.ownership_view.nav.selected_index))
}
pub fn start_stash_view(&mut self, stashes: Vec<StashEntry>) {
self.stash_view.cache = Some(stashes);
self.stash_view.nav.reset();
self.input_mode = InputMode::StashView;
}
pub fn end_stash_view(&mut self) {
self.input_mode = InputMode::Normal;
self.stash_view.cache = None;
}
pub fn stash_move_up(&mut self) {
self.stash_view.nav.move_up();
}
pub fn stash_move_down(&mut self) {
if let Some(ref stashes) = self.stash_view.cache {
self.stash_view.nav.move_down(stashes.len());
}
}
pub fn stash_adjust_scroll(&mut self, visible_lines: usize) {
self.stash_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_stash_entry(&self) -> Option<&StashEntry> {
self.stash_view
.cache
.as_ref()
.and_then(|s| s.get(self.stash_view.nav.selected_index))
}
pub fn update_stash_cache(&mut self, stashes: Vec<StashEntry>) {
let prev_len = self.stash_view.cache.as_ref().map(|s| s.len()).unwrap_or(0);
self.stash_view.cache = Some(stashes);
if let Some(ref stashes) = self.stash_view.cache {
if !stashes.is_empty() && self.stash_view.nav.selected_index >= stashes.len() {
self.stash_view.nav.selected_index = stashes.len() - 1;
}
if stashes.is_empty() {
self.stash_view.nav.selected_index = 0;
}
}
let _ = prev_len; }
pub fn start_patch_view(&mut self, patch: FilePatch) {
self.patch_view.cache = Some(patch);
self.patch_view.scroll_offset = 0;
self.input_mode = InputMode::PatchView;
}
pub fn end_patch_view(&mut self) {
self.input_mode = InputMode::Normal;
self.show_detail = true; self.patch_view.cache = None;
}
pub fn patch_scroll_up(&mut self) {
if self.patch_view.scroll_offset > 0 {
self.patch_view.scroll_offset -= 1;
}
}
pub fn patch_scroll_down(&mut self, visible_lines: usize) {
if let Some(ref patch) = self.patch_view.cache {
let max_scroll = patch.lines.len().saturating_sub(visible_lines);
if self.patch_view.scroll_offset < max_scroll {
self.patch_view.scroll_offset += 1;
}
}
}
pub fn apply_preset(&mut self, slot: usize) {
if let Some(preset) = self.filter_presets.get(slot) {
let query = preset.query.clone();
let name = preset.name.clone();
self.filter_text = query;
self.apply_filter();
self.set_status_message(format!("Preset {}: {}", slot, name));
}
}
pub fn start_preset_save(&mut self) {
if self.filter_text.is_empty() {
self.set_status_message("No filter to save".to_string());
return;
}
self.preset_save_slot = self.filter_presets.next_empty_slot().unwrap_or(1);
self.preset_save_name.clear();
self.input_mode = InputMode::PresetSave;
}
pub fn end_preset_save(&mut self) {
self.input_mode = InputMode::Filter;
self.preset_save_name.clear();
}
pub fn preset_slot_up(&mut self) {
if self.preset_save_slot > 1 {
self.preset_save_slot -= 1;
}
}
pub fn preset_slot_down(&mut self) {
if self.preset_save_slot < 5 {
self.preset_save_slot += 1;
}
}
pub fn preset_name_push(&mut self, c: char) {
self.preset_save_name.push(c);
}
pub fn preset_name_pop(&mut self) {
self.preset_save_name.pop();
}
pub fn save_preset(&mut self) -> bool {
if self.filter_text.is_empty() {
return false;
}
let name = if self.preset_save_name.is_empty() {
format!("Preset {}", self.preset_save_slot)
} else {
self.preset_save_name.clone()
};
self.filter_presets.set(
self.preset_save_slot,
FilterPreset {
name: name.clone(),
query: self.filter_text.clone(),
},
);
if self.filter_presets.save().is_ok() {
self.set_status_message(format!("Saved to slot {}: {}", self.preset_save_slot, name));
true
} else {
self.set_status_message("Failed to save preset".to_string());
false
}
}
pub fn delete_preset(&mut self, slot: usize) {
match slot {
1 => self.filter_presets.slot1 = None,
2 => self.filter_presets.slot2 = None,
3 => self.filter_presets.slot3 = None,
4 => self.filter_presets.slot4 = None,
5 => self.filter_presets.slot5 = None,
_ => return,
}
if self.filter_presets.save().is_ok() {
self.set_status_message(format!("Deleted preset {}", slot));
}
}
pub fn start_branch_compare_view(&mut self, compare: BranchCompare) {
self.branch_compare_view.cache = Some(compare);
self.branch_compare_view.tab = CompareTab::Ahead;
self.branch_compare_view.nav.reset();
self.input_mode = InputMode::BranchCompareView;
}
pub fn end_branch_compare_view(&mut self) {
self.branch_compare_view.cache = None;
self.input_mode = InputMode::TopologyView;
}
pub fn branch_compare_toggle_tab(&mut self) {
self.branch_compare_view.tab = self.branch_compare_view.tab.toggle();
self.branch_compare_view.nav.reset();
}
pub fn branch_compare_move_up(&mut self) {
self.branch_compare_view.nav.move_up();
}
pub fn branch_compare_move_down(&mut self) {
if let Some(ref compare) = self.branch_compare_view.cache {
let count = match self.branch_compare_view.tab {
CompareTab::Ahead => compare.ahead_commits.len(),
CompareTab::Behind => compare.behind_commits.len(),
};
self.branch_compare_view.nav.move_down(count);
}
}
pub fn branch_compare_adjust_scroll(&mut self, visible_lines: usize) {
self.branch_compare_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_branch_compare_commit(&self) -> Option<&crate::compare::CompareCommit> {
let compare = self.branch_compare_view.cache.as_ref()?;
let commits = match self.branch_compare_view.tab {
CompareTab::Ahead => &compare.ahead_commits,
CompareTab::Behind => &compare.behind_commits,
};
commits.get(self.branch_compare_view.nav.selected_index)
}
pub fn toggle_relevance_mode(&mut self) {
self.relevance_mode = !self.relevance_mode;
if self.relevance_mode {
self.apply_relevance_sort();
} else {
self.reapply_filter();
}
}
pub fn set_working_files(&mut self, files: Vec<String>) {
self.working_files = files;
if self.relevance_mode {
self.apply_relevance_sort();
}
}
pub fn apply_relevance_sort(&mut self) {
if self.working_files.is_empty() {
self.set_status_message("No working files to compare".to_string());
self.relevance_mode = false;
return;
}
let events: Vec<&crate::event::GitEvent> = self.all_events.iter().collect();
let scores = crate::relevance::calculate_relevance(&self.working_files, &events, |hash| {
self.file_cache.get(hash).cloned()
});
self.relevance_scores.clear();
for score in &scores {
self.relevance_scores
.insert(score.hash.clone(), score.score);
}
self.filtered_indices.sort_by(|&a, &b| {
let score_a = self
.all_events
.get(a)
.and_then(|e| self.relevance_scores.get(&e.short_hash))
.copied()
.unwrap_or(-1.0);
let score_b = self
.all_events
.get(b)
.and_then(|e| self.relevance_scores.get(&e.short_hash))
.copied()
.unwrap_or(-1.0);
score_b
.partial_cmp(&score_a)
.unwrap_or(std::cmp::Ordering::Equal)
});
self.selected_index = 0;
let related_count = scores.len();
self.set_status_message(format!("Relevance mode: {} related commits", related_count));
}
pub fn get_relevance_score(&self, hash: &str) -> Option<f32> {
self.relevance_scores.get(hash).copied()
}
pub fn is_relevance_mode(&self) -> bool {
self.relevance_mode
}
pub fn start_related_files_view(&mut self, related_files: RelatedFiles) {
self.related_files_view.cache = Some(related_files);
self.related_files_view.nav.reset();
self.input_mode = InputMode::RelatedFilesView;
}
pub fn close_related_files_view(&mut self) {
self.related_files_view.cache = None;
self.input_mode = InputMode::Normal;
self.open_detail(); }
pub fn related_files_move_down(&mut self) {
if let Some(ref related) = self.related_files_view.cache {
self.related_files_view.nav.move_down(related.related.len());
}
}
pub fn related_files_move_up(&mut self) {
self.related_files_view.nav.move_up();
}
pub fn related_files_adjust_scroll(&mut self, visible_lines: usize) {
self.related_files_view.nav.adjust_scroll(visible_lines);
}
pub fn start_impact_score_view(&mut self, analysis: CommitImpactAnalysis) {
self.impact_score_view.cache = Some(analysis);
self.impact_score_view.nav.reset();
self.input_mode = InputMode::ImpactScoreView;
}
pub fn end_impact_score_view(&mut self) {
self.input_mode = InputMode::Normal;
self.impact_score_view.cache = None;
}
pub fn impact_score_move_up(&mut self) {
self.impact_score_view.nav.move_up();
}
pub fn impact_score_move_down(&mut self) {
if let Some(ref analysis) = self.impact_score_view.cache {
self.impact_score_view.nav.move_down(analysis.commits.len());
}
}
pub fn impact_score_adjust_scroll(&mut self, visible_lines: usize) {
self.impact_score_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_impact_score(&self) -> Option<&crate::stats::CommitImpactScore> {
self.impact_score_view
.cache
.as_ref()
.and_then(|a| a.commits.get(self.impact_score_view.nav.selected_index))
}
pub fn export_impact_scores(&mut self) -> Option<String> {
use crate::export::{export_impact_csv, export_impact_json};
if let Some(ref analysis) = self.impact_score_view.cache {
let csv_path = Path::new("gitstack_impact.csv");
let json_path = Path::new("gitstack_impact.json");
let mut results = Vec::new();
if let Err(e) = export_impact_csv(analysis, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_impact_json(analysis, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn start_change_coupling_view(&mut self, analysis: ChangeCouplingAnalysis) {
self.change_coupling_view.cache = Some(analysis);
self.change_coupling_view.nav.reset();
self.input_mode = InputMode::ChangeCouplingView;
}
pub fn end_change_coupling_view(&mut self) {
self.input_mode = InputMode::Normal;
self.change_coupling_view.cache = None;
}
pub fn change_coupling_move_up(&mut self) {
self.change_coupling_view.nav.move_up();
}
pub fn change_coupling_move_down(&mut self) {
if let Some(ref analysis) = self.change_coupling_view.cache {
self.change_coupling_view
.nav
.move_down(analysis.couplings.len());
}
}
pub fn change_coupling_adjust_scroll(&mut self, visible_lines: usize) {
self.change_coupling_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_change_coupling(&self) -> Option<&crate::stats::FileCoupling> {
self.change_coupling_view.cache.as_ref().and_then(|a| {
a.couplings
.get(self.change_coupling_view.nav.selected_index)
})
}
pub fn export_change_coupling(&mut self) -> Option<String> {
use crate::export::{export_coupling_csv, export_coupling_json};
if let Some(ref analysis) = self.change_coupling_view.cache {
let csv_path = Path::new("gitstack_coupling.csv");
let json_path = Path::new("gitstack_coupling.json");
let mut results = Vec::new();
if let Err(e) = export_coupling_csv(analysis, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_coupling_json(analysis, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn start_quality_score_view(&mut self, analysis: CommitQualityAnalysis) {
self.quality_score_view.cache = Some(analysis);
self.quality_score_view.nav.reset();
self.input_mode = InputMode::QualityScoreView;
}
pub fn end_quality_score_view(&mut self) {
self.input_mode = InputMode::Normal;
self.quality_score_view.cache = None;
}
pub fn quality_score_move_up(&mut self) {
self.quality_score_view.nav.move_up();
}
pub fn quality_score_move_down(&mut self) {
if let Some(ref analysis) = self.quality_score_view.cache {
self.quality_score_view
.nav
.move_down(analysis.commits.len());
}
}
pub fn quality_score_adjust_scroll(&mut self, visible_lines: usize) {
self.quality_score_view.nav.adjust_scroll(visible_lines);
}
pub fn selected_quality_score(&self) -> Option<&crate::stats::CommitQualityScore> {
self.quality_score_view
.cache
.as_ref()
.and_then(|a| a.commits.get(self.quality_score_view.nav.selected_index))
}
pub fn export_quality_scores(&mut self) -> Option<String> {
use crate::export::{export_quality_csv, export_quality_json};
if let Some(ref analysis) = self.quality_score_view.cache {
let csv_path = Path::new("gitstack_quality.csv");
let json_path = Path::new("gitstack_quality.json");
let mut results = Vec::new();
if let Err(e) = export_quality_csv(analysis, csv_path) {
results.push(format!("CSV error: {}", e));
} else {
results.push(format!("CSV: {}", csv_path.display()));
}
if let Err(e) = export_quality_json(analysis, json_path) {
results.push(format!("JSON error: {}", e));
} else {
results.push(format!("JSON: {}", json_path.display()));
}
let message = format!("Exported: {}", results.join(", "));
self.status_message = Some(message.clone());
Some(message)
} else {
None
}
}
pub fn commit_detail_file_move_up(&mut self) {
if self.commit_detail.selected_file > 0 {
self.commit_detail.selected_file -= 1;
}
}
pub fn commit_detail_file_move_down(&mut self) {
if let Some(ref diff) = self.detail_diff_cache {
if !diff.files.is_empty() && self.commit_detail.selected_file < diff.files.len() - 1 {
self.commit_detail.selected_file += 1;
}
}
}
pub fn commit_detail_toggle_file_diff(&mut self) {
let idx = self.commit_detail.selected_file;
if !self.commit_detail.expanded_files.remove(&idx) {
self.commit_detail.expanded_files.insert(idx);
}
}
pub fn commit_detail_expand_all(&mut self) {
if let Some(ref diff) = self.detail_diff_cache {
for i in 0..diff.files.len() {
self.commit_detail.expanded_files.insert(i);
}
}
}
pub fn commit_detail_collapse_all(&mut self) {
self.commit_detail.expanded_files.clear();
}
pub fn reset_commit_detail_file_state(&mut self) {
self.commit_detail.selected_file = 0;
self.commit_detail.expanded_files.clear();
self.commit_detail.scroll = 0;
self.commit_detail.h_scroll = 0;
}
pub fn commit_detail_auto_scroll(&mut self, visible_height: usize) {
if self.commit_detail.selected_file == 0 {
self.commit_detail.scroll = 0;
return;
}
let header_lines: usize = 10;
let mut line_count = header_lines;
if let Some(ref diff) = self.detail_diff_cache {
for i in 0..self.commit_detail.selected_file {
line_count += 1; if self.commit_detail.expanded_files.contains(&i) {
if let Some(patch) = diff.patches.iter().find(|p| p.path == diff.files[i].path)
{
line_count += patch.lines.len();
}
}
line_count += 1; }
}
if line_count < self.commit_detail.scroll {
self.commit_detail.scroll = line_count;
} else if line_count >= self.commit_detail.scroll + visible_height {
self.commit_detail.scroll = line_count.saturating_sub(visible_height / 2);
}
}
pub fn start_review_queue_view(&mut self) {
let queue = crate::review_queue::ReviewQueue::load();
self.review_queue_view.cache = Some(queue);
self.review_queue_view.nav.reset();
self.input_mode = InputMode::ReviewQueueView;
}
pub fn end_review_queue_view(&mut self) {
self.input_mode = InputMode::Normal;
self.review_queue_view.cache = None;
}
pub fn review_queue_move_up(&mut self) {
self.review_queue_view.nav.move_up();
}
pub fn review_queue_move_down(&mut self) {
if let Some(ref queue) = self.review_queue_view.cache {
self.review_queue_view.nav.move_down(queue.items.len());
}
}
pub fn start_pr_create(&mut self) {
self.pr_create_state = crate::pr::PrCreateState {
gh_available: crate::pr::check_gh_available(),
base_branch: self.branch_name().to_string(),
..Default::default()
};
self.input_mode = InputMode::PrCreate;
}
pub fn end_pr_create(&mut self) {
self.input_mode = InputMode::Normal;
}
pub fn start_review_pack_view(&mut self, pack: ReviewPack, verdict: Option<serde_json::Value>) {
self.review_pack_view.cache = Some(pack);
self.review_pack_view.verdict = verdict;
self.review_pack_view.nav.reset();
self.input_mode = InputMode::ReviewPackView;
}
pub fn end_review_pack_view(&mut self) {
self.input_mode = InputMode::Normal;
self.review_pack_view.cache = None;
self.review_pack_view.verdict = None;
}
pub fn review_pack_move_up(&mut self) {
self.review_pack_view.nav.move_up();
}
pub fn review_pack_move_down(&mut self) {
if let Some(ref pack) = self.review_pack_view.cache {
let total = pack.top_risks.len()
+ pack.test_gaps.len()
+ pack.recommended_actions.len()
+ pack.owner_candidates.len();
self.review_pack_view.nav.move_down(total);
}
}
pub fn review_pack_adjust_scroll(&mut self, visible_lines: usize) {
self.review_pack_view.nav.adjust_scroll(visible_lines);
}
pub fn start_next_actions_view(&mut self, actions: Vec<ActionRecommendation>) {
self.next_actions_view.cache = Some(actions);
self.next_actions_view.nav.reset();
self.input_mode = InputMode::NextActionsView;
}
pub fn end_next_actions_view(&mut self) {
self.input_mode = InputMode::Normal;
self.next_actions_view.cache = None;
}
pub fn next_actions_move_up(&mut self) {
self.next_actions_view.nav.move_up();
}
pub fn next_actions_move_down(&mut self) {
if let Some(ref actions) = self.next_actions_view.cache {
self.next_actions_view.nav.move_down(actions.len());
}
}
pub fn next_actions_adjust_scroll(&mut self, visible_lines: usize) {
self.next_actions_view.nav.adjust_scroll(visible_lines);
}
pub fn start_handoff_view(&mut self, context: HandoffContext) {
self.handoff_view.cache = Some(context);
self.handoff_view.nav.reset();
self.input_mode = InputMode::HandoffView;
}
pub fn end_handoff_view(&mut self) {
self.input_mode = InputMode::Normal;
self.handoff_view.cache = None;
}
pub fn handoff_move_up(&mut self) {
self.handoff_view.nav.move_up();
}
pub fn handoff_move_down(&mut self) {
if let Some(ref ctx) = self.handoff_view.cache {
let total = ctx.next_actions.len() + 1; self.handoff_view.nav.move_down(total);
}
}
pub fn handoff_adjust_scroll(&mut self, visible_lines: usize) {
self.handoff_view.nav.adjust_scroll(visible_lines);
}
}