use super::*;
impl App {
pub fn open_git_view(&mut self) {
if crate::git::branch(&self.root).is_none() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NotAGitRepo).into());
return;
}
self.git_view_entries = crate::git::changed_files(&self.root);
self.git_view_sel = 0;
self.git_view = true;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn close_git_view(&mut self) {
self.git_view = false;
}
pub fn is_git_view(&self) -> bool {
self.git_view
}
pub fn git_view_entries(&self) -> &[crate::git::ChangeEntry] {
&self.git_view_entries
}
pub fn git_view_sel(&self) -> usize {
self.git_view_sel
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_move(&mut self, delta: i32) {
self.git_view_sel = clamp_cursor(self.git_view_sel, delta, self.git_view_entries.len());
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_selected(&self) -> Option<PathBuf> {
self.git_view_entries
.get(self.git_view_sel)
.map(|e| e.path.clone())
}
pub fn git_view_reload(&mut self) {
self.git_view_entries = crate::git::changed_files(&self.root);
if self.git_view_sel >= self.git_view_entries.len() {
self.git_view_sel = self.git_view_entries.len().saturating_sub(1);
}
self.git_status_for = None; }
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_stage(&mut self) {
let Some(path) = self.git_view_selected() else {
return;
};
match crate::git::stage(&self.root, &path) {
Ok(()) => {
self.git_view_reload();
self.flash = Some(format!(
"{}: {}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Staged),
self.format_path(&path)
));
}
Err(e) => {
self.flash = Some(format!(
"{}: {e}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Failed)
))
}
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_unstage(&mut self) {
let Some(path) = self.git_view_selected() else {
return;
};
match crate::git::unstage(&self.root, &path) {
Ok(()) => {
self.git_view_reload();
self.flash = Some(format!(
"{}: {}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Unstaged),
self.format_path(&path)
));
}
Err(e) => {
self.flash = Some(format!(
"{}: {e}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Failed)
))
}
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_stage_all(&mut self) {
if self.git_view_entries.is_empty() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoChanges).into());
return;
}
match crate::git::stage_all(&self.root) {
Ok(()) => {
let n = self.git_view_entries.len();
self.git_view_reload();
self.flash = Some(format!(
"{} ({n})",
crate::i18n::tr(self.lang, crate::i18n::Msg::StagedAll)
));
}
Err(e) => {
self.flash = Some(format!(
"{}: {e}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Failed)
))
}
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_unstage_all(&mut self) {
if !self.git_view_entries.iter().any(|e| e.staged) {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NothingStaged).into());
return;
}
match crate::git::unstage_all(&self.root) {
Ok(()) => {
self.git_view_reload();
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::UnstagedAll).into());
}
Err(e) => {
self.flash = Some(format!(
"{}: {e}",
crate::i18n::tr(self.lang, crate::i18n::Msg::Failed)
))
}
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_view_start_discard(&mut self) {
let Some(path) = self.git_view_selected() else {
return;
};
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string();
let message = format!(
"{} {name} ?",
crate::i18n::tr(self.lang, crate::i18n::Msg::DiscardChangesTo)
);
self.dialog = Some(Dialog {
op: PendingOp::GitDiscard { path },
kind: DialogKind::Confirm {
message,
allow_permanent: false,
},
});
}
pub fn open_git_diff(&mut self, path: &Path) {
self.preview_path = Some(path.to_path_buf());
self.preview_kind = Some(PreviewKind::GitDiff(path.to_path_buf()));
self.preview_scroll = 0;
self.preview_hscroll = 0;
self.preview_byte_top = 0;
self.preview_top_line = 0;
self.preview_win = None;
self.win_cache = None;
self.preview_total_lines = None;
self.md_cache = None;
self.diff_cache = None; self.md_links.clear();
self.focused_link = None;
self.hl_pending = false;
self.hl_warming = false;
self.came_from_git_view = self.git_view;
self.git_view = false;
self.diff_follow_scope = false;
self.mode = Mode::Preview;
}
pub fn is_git_diff_preview(&self) -> bool {
matches!(self.preview_kind, Some(PreviewKind::GitDiff(_)))
}
pub fn git_diff_lines(&mut self) -> Vec<crate::git::DiffLine> {
let Some(PreviewKind::GitDiff(p)) = self.preview_kind.clone() else {
return Vec::new();
};
let hit = matches!(&self.diff_cache, Some(c) if c.path == p);
if !hit {
let lines = crate::git::file_diff(&self.root, &p);
self.diff_cache = Some(DiffCache {
path: p.clone(),
lines,
});
}
self.diff_cache
.as_ref()
.map(|c| c.lines.clone())
.unwrap_or_default()
}
pub fn tree_open_git_diff(&mut self) {
self.refresh_git_if_needed(); let Some(e) = self.entries.get(self.selected) else {
return;
};
if e.is_dir {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NotAFile).into());
return;
}
let path = e.path.clone();
if self.git_status_of(&path).is_none() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoChanges).into());
return;
}
self.open_git_diff(&path); }
pub fn diff_is_split(&self, width: u16) -> bool {
self.diff_layout.is_split(width)
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn cycle_diff_layout(&mut self) {
self.diff_layout = self.diff_layout.next();
self.preview_hscroll = 0;
self.git_detail_hscroll = 0; let label = match self.diff_layout {
DiffLayout::Unified => crate::i18n::tr(self.lang, crate::i18n::Msg::DiffUnified),
DiffLayout::Split => crate::i18n::tr(self.lang, crate::i18n::Msg::DiffSideBySide),
DiffLayout::Auto => crate::i18n::tr(self.lang, crate::i18n::Msg::DiffAuto),
};
self.flash = Some(label.into());
}
pub fn close_git_diff(&mut self) {
let return_to_git = self.came_from_git_view;
self.back_to_tree();
if return_to_git {
self.came_from_git_view = false;
self.open_git_view();
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_diff_start_discard(&mut self) {
let Some(PreviewKind::GitDiff(path)) = self.preview_kind.clone() else {
return;
};
let name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?")
.to_string();
let message = format!(
"{} {name} ?",
crate::i18n::tr(self.lang, crate::i18n::Msg::DiscardChangesTo)
);
self.came_from_git_view = true;
self.dialog = Some(Dialog {
op: PendingOp::GitDiscard { path },
kind: DialogKind::Confirm {
message,
allow_permanent: false,
},
});
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn start_git_commit(&mut self) {
self.dialog = Some(Dialog {
op: PendingOp::GitCommit,
kind: DialogKind::Input {
title: crate::i18n::tr(self.lang, crate::i18n::Msg::CommitMessage).into(),
buffer: String::new(),
cursor: 0,
},
});
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_git_log(&mut self) {
let commits = crate::git::log(&self.root, 200);
if commits.is_empty() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoCommits).into());
return;
}
self.git_log = Some(commits);
self.git_log_sel = 0;
self.git_view = false;
}
pub fn is_git_log(&self) -> bool {
self.git_log.is_some()
}
pub fn git_log_entries(&self) -> &[crate::git::CommitInfo] {
self.git_log.as_deref().unwrap_or(&[])
}
pub fn git_log_sel(&self) -> usize {
self.git_log_sel
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_log_move(&mut self, delta: i32) {
self.git_log_sel = clamp_cursor(self.git_log_sel, delta, self.git_log_entries().len());
}
pub fn git_log_selected_id(&self) -> Option<String> {
self.git_log
.as_ref()?
.get(self.git_log_sel)
.map(|c| c.id.clone())
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn close_git_log(&mut self) {
self.git_log = None;
self.git_log_sel = 0;
self.open_git_view();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_git_branches(&mut self) {
let list = crate::git::branches(&self.root);
if list.is_empty() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoBranches).into());
return;
}
self.git_branch_filter.clear();
self.git_branch_filtering = false;
self.git_branch_sel = list.iter().position(|b| b.is_current).unwrap_or(0);
self.git_branches = Some(list);
self.git_view = false;
}
pub fn is_git_branches(&self) -> bool {
self.git_branches.is_some()
}
pub fn git_branch_view(&self) -> Vec<crate::git::BranchInfo> {
let Some(all) = &self.git_branches else {
return Vec::new();
};
let q = self.git_branch_filter.to_lowercase();
if q.is_empty() {
all.clone()
} else {
all.iter()
.filter(|b| b.name.to_lowercase().contains(&q))
.cloned()
.collect()
}
}
pub fn git_branch_sel(&self) -> usize {
self.git_branch_sel
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_branch_move(&mut self, delta: i32) {
self.git_branch_sel =
clamp_cursor(self.git_branch_sel, delta, self.git_branch_view().len());
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub(super) fn git_branch_selected(&self) -> Option<crate::git::BranchInfo> {
self.git_branch_view().into_iter().nth(self.git_branch_sel)
}
pub fn close_git_branches(&mut self) {
self.git_branches = None;
self.git_branch_sel = 0;
self.git_branch_filter.clear();
self.git_branch_filtering = false;
self.open_git_view();
}
fn git_branches_reload(&mut self) {
self.git_branches = Some(crate::git::branches(&self.root));
self.clamp_git_branch_sel();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn checkout_selected_branch(&mut self) -> Result<()> {
let Some(b) = self.git_branch_selected() else {
return Ok(());
};
let name = b.name;
match crate::git::checkout(&self.root, &name) {
Ok(()) => {
self.refresh()?;
self.refresh_git_if_needed(); self.close_git_branches();
self.flash = Some(format!(
"{}: {name}",
crate::i18n::tr(self.lang, crate::i18n::Msg::SwitchedTo)
));
}
Err(e) => self.flash = Some(format!("{e}")),
}
Ok(())
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn start_create_branch(&mut self) {
self.dialog = Some(Dialog {
op: PendingOp::GitCreateBranch,
kind: DialogKind::Input {
title: crate::i18n::tr(self.lang, crate::i18n::Msg::NewBranch).into(),
buffer: String::new(),
cursor: 0,
},
});
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn start_delete_branch(&mut self) {
let Some(b) = self.git_branch_selected() else {
return;
};
if b.is_current {
self.flash = Some(
crate::i18n::tr(self.lang, crate::i18n::Msg::CannotDeleteCurrentBranch).into(),
);
return;
}
let name = b.name;
self.dialog = Some(Dialog {
op: PendingOp::GitDeleteBranch { name: name.clone() },
kind: DialogKind::Confirm {
message: format!(
"{}: {name}",
crate::i18n::tr(self.lang, crate::i18n::Msg::DeleteBranch)
),
allow_permanent: true,
},
});
}
pub fn git_delete_branch(&mut self, name: &str, force: bool) {
match crate::git::delete_branch(&self.root, name, force) {
Ok(()) => {
self.git_branches_reload();
self.flash = Some(format!(
"{}: {name}",
crate::i18n::tr(self.lang, crate::i18n::Msg::DeletedBranch)
));
}
Err(e) => self.flash = Some(format!("{e}")),
}
}
pub fn git_branch_filtering(&self) -> bool {
self.git_branch_filtering
}
pub fn git_branch_query(&self) -> &str {
&self.git_branch_filter
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_branch_start_filter(&mut self) {
self.git_branch_filtering = true;
self.git_branch_filter.clear();
self.git_branch_sel = 0;
}
pub fn git_branch_filter_push(&mut self, c: char) {
self.git_branch_filter.push(c);
self.clamp_git_branch_sel();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_branch_filter_backspace(&mut self) {
self.git_branch_filter.pop();
self.clamp_git_branch_sel();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_branch_filter_commit(&mut self) {
self.git_branch_filtering = false;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_branch_filter_clear(&mut self) {
self.git_branch_filter.clear();
self.git_branch_filtering = false;
self.clamp_git_branch_sel();
}
fn clamp_git_branch_sel(&mut self) {
let len = self.git_branch_view().len();
if self.git_branch_sel >= len {
self.git_branch_sel = len.saturating_sub(1);
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_git_graph(&mut self) {
self.ensure_graph_order();
if self.git_graph_visible.is_empty() {
self.git_graph_visible = self.default_graph_visible();
}
if self.git_graph_base.is_none() && !self.cfg.ui.graph_base_branches.is_empty() {
if let Some((oid, label)) = self.derive_base_from_order() {
self.git_graph_base = Some(oid);
self.git_graph_base_label = Some(label);
}
}
let (rows, has_wt, legend, hidden) = self.build_git_graph_rows();
if rows.iter().all(|r| r.commit.is_none()) && !has_wt {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoCommits).into());
return;
}
self.git_graph_sel = if has_wt {
0
} else {
rows.iter().position(|r| r.commit.is_some()).unwrap_or(0)
};
self.git_graph = Some(rows);
self.git_graph_legend = legend;
self.git_graph_hidden = hidden;
self.git_view = false;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn ensure_graph_order(&mut self) {
let by_rec = crate::git::branches_by_recency(&self.root); let exists: std::collections::HashSet<&str> =
by_rec.iter().map(|(n, _, _)| n.as_str()).collect();
self.git_graph_order.retain(|n| exists.contains(n.as_str()));
if self.git_graph_order.is_empty() {
let mut order: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
for b in &self.cfg.ui.graph_base_branches {
if exists.contains(b.as_str()) && seen.insert(b.clone()) {
order.push(b.clone());
}
}
if let Some((h, _, _)) = by_rec.iter().find(|(_, c, _)| *c) {
if seen.insert(h.clone()) {
order.push(h.clone());
}
}
for (n, _, _) in &by_rec {
if seen.insert(n.clone()) {
order.push(n.clone());
}
}
self.git_graph_order = order;
} else {
let known: std::collections::HashSet<String> =
self.git_graph_order.iter().cloned().collect();
for (n, _, _) in &by_rec {
if !known.contains(n) {
self.git_graph_order.push(n.clone());
}
}
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn derive_base_from_order(&self) -> Option<(String, String)> {
for name in &self.git_graph_order {
if self.git_graph_visible.contains(name) {
if let Some(oid) = crate::git::branch_tip(&self.root, name) {
return Some((oid, name.clone()));
}
}
}
None
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn default_graph_visible(&self) -> std::collections::HashSet<String> {
let cap = self.cfg.ui.graph_max_branches;
let mut set = std::collections::HashSet::new();
for (name, is_cur, _) in crate::git::branches_by_recency(&self.root) {
if is_cur {
set.insert(name);
}
}
for name in &self.git_graph_order {
if cap != 0 && set.len() >= cap {
break;
}
set.insert(name.clone());
}
set
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn build_git_graph_rows(
&self,
) -> (
Vec<crate::git::GraphRow>,
bool,
Vec<crate::git::LegendEntry>,
usize,
) {
let total = crate::git::branches_by_recency(&self.root).len();
let visible: Vec<String> = self.git_graph_visible.iter().cloned().collect();
let use_all = visible.is_empty() || visible.len() >= total;
let refs = if use_all {
None
} else {
Some(visible.as_slice())
};
let mut rows = crate::git::graph_with_base(
&self.root,
self.git_graph_base.as_deref(),
self.lang,
refs,
);
if !use_all {
let allowed = &self.git_graph_visible;
for r in &mut rows {
if r.refs.is_empty() {
continue;
}
r.refs = r
.refs
.split(',')
.map(|t| t.trim())
.filter(|t| {
t.starts_with("HEAD -> ")
|| *t == "HEAD"
|| t.starts_with("tag:")
|| allowed.contains(*t)
})
.collect::<Vec<_>>()
.join(", ");
}
}
let has_wt = rows.iter().any(|r| r.worktree);
let mut legend =
crate::git::legend_from_rows(&rows, &self.root, self.git_graph_base_label.as_deref());
legend.sort_by_key(|e| {
self.git_graph_order
.iter()
.position(|n| n == &e.name)
.unwrap_or(usize::MAX)
});
let shown = if use_all { total } else { visible.len() };
let hidden = total.saturating_sub(shown);
(rows, has_wt, legend, hidden)
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_set_base(&mut self) {
let Some(row) = self.git_graph_selected_row() else {
return;
};
let Some(id) = row.commit.clone() else {
self.flash =
Some(crate::i18n::tr(self.lang, crate::i18n::Msg::GraphBaseNeedsCommit).into());
return;
};
let label = base_label_from(&row.refs, &row.short);
self.git_graph_base = Some(id);
self.git_graph_base_label = Some(label.clone());
self.rebuild_git_graph_keep_sel();
self.flash = Some(format!(
"{}{label}",
crate::i18n::tr(self.lang, crate::i18n::Msg::GraphBaseSet)
));
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_clear_base(&mut self) {
if self.git_graph_base.is_none() {
return;
}
self.git_graph_base = None;
self.git_graph_base_label = None;
self.rebuild_git_graph_keep_sel();
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::GraphBaseCleared).into());
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn rebuild_git_graph_keep_sel(&mut self) {
if self.git_graph.is_none() {
return;
}
let cur = self.git_graph_selected_row().and_then(|r| r.commit.clone());
let (rows, has_wt, legend, hidden) = self.build_git_graph_rows();
self.git_graph_sel = cur
.as_deref()
.and_then(|id| rows.iter().position(|r| r.commit.as_deref() == Some(id)))
.or(if has_wt { Some(0) } else { None })
.or_else(|| rows.iter().position(|r| r.commit.is_some()))
.unwrap_or(0);
self.git_graph = Some(rows);
self.git_graph_legend = legend;
self.git_graph_hidden = hidden;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_base_label(&self) -> Option<&str> {
self.git_graph_base_label.as_deref()
}
pub fn git_graph_legend(&self) -> &[crate::git::LegendEntry] {
&self.git_graph_legend
}
pub fn git_graph_hidden_count(&self) -> usize {
self.git_graph_hidden
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_open_picker(&mut self) {
if self.git_graph.is_none() {
return;
}
self.ensure_graph_order();
if self.git_graph_visible.is_empty() {
self.git_graph_visible = self.git_graph_order.iter().cloned().collect();
}
self.git_graph_picker_set = self.git_graph_visible.clone();
self.git_graph_picker_sel = 0;
self.git_graph_reordered = false;
self.git_graph_picker = true;
}
pub fn is_git_graph_picker(&self) -> bool {
self.git_graph_picker
}
pub fn git_graph_picker_sel(&self) -> usize {
self.git_graph_picker_sel
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
fn head_branch_name(&self) -> Option<String> {
crate::git::branches_by_recency(&self.root)
.into_iter()
.find(|(_, c, _)| *c)
.map(|(n, _, _)| n)
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_items(&self) -> Vec<(String, bool, bool)> {
let head = self.head_branch_name();
self.git_graph_order
.iter()
.map(|name| {
let is_cur = head.as_deref() == Some(name.as_str());
let on = self.git_graph_picker_set.contains(name);
(name.clone(), is_cur, on)
})
.collect()
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_reorder(&mut self, delta: i32) {
let n = self.git_graph_order.len();
if n < 2 {
return;
}
let i = self.git_graph_picker_sel;
let j = i as i32 + delta;
if j < 0 || j >= n as i32 {
return;
}
let j = j as usize;
self.git_graph_order.swap(i, j);
self.git_graph_picker_sel = j;
self.git_graph_reordered = true;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_move(&mut self, delta: i32) {
let n = self.git_graph_picker_items().len();
if n == 0 {
return;
}
let cur = self.git_graph_picker_sel as i32;
self.git_graph_picker_sel = (cur + delta).clamp(0, n as i32 - 1) as usize;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_jump(&mut self, top: bool) {
let n = self.git_graph_picker_items().len();
self.git_graph_picker_sel = if top { 0 } else { n.saturating_sub(1) };
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_toggle(&mut self) {
let items = self.git_graph_picker_items();
let Some((name, is_cur, _)) = items.get(self.git_graph_picker_sel).cloned() else {
return;
};
if is_cur {
self.flash =
Some(crate::i18n::tr(self.lang, crate::i18n::Msg::GraphPickerHeadLocked).into());
return; }
if self.git_graph_picker_set.contains(&name) {
self.git_graph_picker_set.remove(&name);
} else {
self.git_graph_picker_set.insert(name);
}
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_all(&mut self) {
self.git_graph_picker_set = self
.git_graph_picker_items()
.into_iter()
.map(|(n, _, _)| n)
.collect();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_current_only(&mut self) {
let mut set = std::collections::HashSet::new();
for (name, is_cur, _) in self.git_graph_picker_items() {
if is_cur {
set.insert(name);
}
}
if let Some(b) = &self.git_graph_base_label {
set.insert(b.clone());
}
self.git_graph_picker_set = set;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_apply(&mut self) {
self.git_graph_visible = self.git_graph_picker_set.clone();
if self.git_graph_reordered {
match self.derive_base_from_order() {
Some((oid, label)) => {
self.git_graph_base = Some(oid);
self.git_graph_base_label = Some(label);
}
None => {
self.git_graph_base = None;
self.git_graph_base_label = None;
}
}
}
self.git_graph_reordered = false;
self.git_graph_picker = false;
self.rebuild_git_graph_keep_sel();
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_picker_cancel(&mut self) {
self.git_graph_picker = false;
}
pub fn is_git_graph(&self) -> bool {
self.git_graph.is_some()
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn in_git_view(&self) -> bool {
self.is_git_view()
|| self.is_git_log()
|| self.is_git_graph()
|| self.is_git_branches()
|| self.is_git_detail()
|| self.is_git_diff_preview()
}
pub fn git_graph_rows(&self) -> &[crate::git::GraphRow] {
self.git_graph.as_deref().unwrap_or(&[])
}
pub fn git_graph_sel(&self) -> usize {
self.git_graph_sel
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_graph_move(&mut self, delta: i32) {
let Some(rows) = &self.git_graph else {
return;
};
let commits: Vec<usize> = rows
.iter()
.enumerate()
.filter(|(_, r)| r.commit.is_some() || r.worktree)
.map(|(i, _)| i)
.collect();
if commits.is_empty() {
return;
}
let cur = commits
.iter()
.position(|&i| i == self.git_graph_sel)
.unwrap_or(0);
let next = clamp_cursor(cur, delta, commits.len());
self.git_graph_sel = commits[next];
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn close_git_graph(&mut self) {
self.git_graph = None;
self.git_graph_sel = 0;
self.open_git_view();
}
pub fn git_graph_selected_row(&self) -> Option<&crate::git::GraphRow> {
self.git_graph
.as_ref()
.and_then(|rows| rows.get(self.git_graph_sel))
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_git_graph_detail(&mut self) {
let Some(row) = self.git_graph_selected_row().cloned() else {
return;
};
let (lines, meta) = if row.worktree {
(crate::git::worktree_diff(&self.root), None)
} else if let Some(id) = row.commit {
(
crate::git::commit_diff(&self.root, &id),
crate::git::commit_meta(&self.root, &id),
)
} else {
return;
};
self.git_detail = Some(lines);
self.git_detail_meta = meta; self.git_detail_scroll = 0;
self.git_detail_hscroll = 0;
self.git_detail_title = None; }
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_git_commit_detail(&mut self) {
let Some(id) = self.git_log_selected_id() else {
return;
};
let lines = crate::git::commit_diff(&self.root, &id);
self.git_detail = Some(lines);
self.git_detail_meta = crate::git::commit_meta(&self.root, &id);
self.git_detail_scroll = 0;
self.git_detail_hscroll = 0;
self.git_detail_title = None;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn open_worktree_detail(&mut self) {
let lines = crate::git::worktree_diff(&self.root);
if lines.is_empty() {
self.flash = Some(crate::i18n::tr(self.lang, crate::i18n::Msg::NoChanges).into());
return;
}
self.git_detail = Some(lines);
self.git_detail_meta = None; self.git_detail_scroll = 0;
self.git_detail_hscroll = 0;
self.git_detail_title =
Some(crate::i18n::tr(self.lang, crate::i18n::Msg::UncommittedChanges).into());
}
pub fn git_detail_title(&self) -> Option<&str> {
self.git_detail_title.as_deref()
}
pub fn is_git_detail(&self) -> bool {
self.git_detail.is_some()
}
pub fn git_detail_meta(&self) -> Option<&crate::git::CommitMeta> {
self.git_detail_meta.as_ref()
}
pub fn git_detail_lines(&self) -> &[crate::git::DiffLine] {
self.git_detail.as_deref().unwrap_or(&[])
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_detail_scroll_by(&mut self, delta: i32) {
let total = self.git_detail_total;
let max = total.saturating_sub(self.git_detail_viewport as usize) as i32;
let next = (self.git_detail_scroll as i32)
.saturating_add(delta)
.clamp(0, max.max(0));
self.git_detail_scroll = next as u16;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_detail_scroll_to(&mut self, end: bool) {
if end {
self.git_detail_scroll_by(i32::MAX);
} else {
self.git_detail_scroll = 0;
}
}
pub fn git_detail_scroll(&self) -> u16 {
self.git_detail_scroll
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_detail_hscroll_by(&mut self, delta: i32) {
let next = (self.git_detail_hscroll as i32 + delta).max(0);
self.git_detail_hscroll = next as u16;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_detail_hscroll_home(&mut self) {
self.git_detail_hscroll = 0;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn git_detail_hscroll_end(&mut self) {
self.git_detail_hscroll = u16::MAX;
}
pub fn git_detail_hscroll(&self) -> u16 {
self.git_detail_hscroll
}
pub fn clamp_git_detail_hscroll(&mut self, max: u16) {
self.git_detail_hscroll = self.git_detail_hscroll.min(max);
}
pub fn set_git_detail_viewport(&mut self, h: u16) {
self.git_detail_viewport = h;
}
pub fn set_git_detail_total(&mut self, n: usize) {
self.git_detail_total = n;
}
#[cfg_attr(not(feature = "git"), allow(dead_code))]
pub fn close_git_detail(&mut self) {
self.git_detail = None;
self.git_detail_meta = None;
self.git_detail_title = None;
self.git_detail_scroll = 0;
self.git_detail_hscroll = 0;
}
}