use super::*;
use crate::git::github::{self, GhItem};
const GITHUB_COOLDOWN: Duration = Duration::from_secs(60);
const GITHUB_SELECT_DEBOUNCE: Duration = Duration::from_millis(400);
#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub(super) enum GithubStateFilter {
#[default]
Open,
All,
Closed,
}
impl GithubStateFilter {
fn next(self) -> Self {
match self {
Self::Open => Self::All,
Self::All => Self::Closed,
Self::Closed => Self::Open,
}
}
fn gh_state(self) -> &'static str {
match self {
Self::Open => "open",
Self::All => "all",
Self::Closed => "closed",
}
}
fn label(self) -> &'static str {
match self {
Self::Open => "open",
Self::All => "all",
Self::Closed => "closed",
}
}
}
#[derive(Default)]
pub(super) struct GithubState {
owner_repo: Option<(String, String)>,
loading: bool,
generation: u64,
fetched_at: Option<Instant>,
issues: Vec<GhItem>,
prs: Vec<GhItem>,
open_count: usize,
error: Option<String>,
}
impl App {
fn github_target(&self) -> Option<(RepoId, std::path::PathBuf, String)> {
if let Some(aw) = &self.active_worktree {
let idx = self.repo_list.resolve_index(&aw.repo_id)?;
let entry = &self.repo_list.repos[idx];
return Some((aw.repo_id.clone(), entry.path.clone(), entry.name.clone()));
}
let entry = self.repo_list.selected_repo()?;
Some((
RepoId(entry.path.clone()),
entry.path.clone(),
entry.name.clone(),
))
}
fn selected_github_open_count(&self) -> usize {
self.github_target()
.and_then(|(id, _, _)| self.github_cache.get(&id))
.map(|s| s.open_count)
.unwrap_or(0)
}
pub(super) fn show_github_panel(&self) -> bool {
if !self.config.github.enabled {
return false;
}
match self.github_forced {
Some(forced) => forced,
None => self.selected_github_open_count() > 0,
}
}
pub(super) fn github_touch_selection(&mut self) {
if !self.config.github.enabled {
return;
}
self.github_forced = None;
self.github_state_filter = GithubStateFilter::Open;
self.github_select_gen = self.github_select_gen.wrapping_add(1);
let generation = self.github_select_gen;
let tx = self.action_tx.clone();
tokio::spawn(async move {
tokio::time::sleep(GITHUB_SELECT_DEBOUNCE).await;
let _ = tx.send(Action::GithubSelectionSettled(generation));
});
}
pub(super) fn github_selection_settled(&mut self, generation: u64) {
if generation != self.github_select_gen {
return;
}
self.request_github(false);
self.refresh_github_panel();
}
pub(super) fn toggle_github_panel(&mut self) {
if !self.config.github.enabled {
self.error_message = Some((
"GitHub panel disabled ([github] enabled = false)".to_string(),
Instant::now(),
));
return;
}
let now_visible = self.show_github_panel();
self.github_forced = Some(!now_visible);
if self.github_forced == Some(true) {
self.request_github(true);
self.refresh_github_panel();
}
}
pub(super) fn request_github(&mut self, force: bool) {
if !self.config.github.enabled || !github::gh_available() {
return;
}
let Some((id, path, _)) = self.github_target() else {
return;
};
let state = self.github_state_filter.gh_state();
let now = Instant::now();
let entry = self.github_cache.entry(id.clone()).or_default();
if entry.loading {
return;
}
if !force
&& let Some(at) = entry.fetched_at
&& now.saturating_duration_since(at) < GITHUB_COOLDOWN
{
return;
}
if entry.owner_repo.is_none() {
entry.owner_repo = github::origin_owner_repo(&path);
}
let Some((owner, repo)) = entry.owner_repo.clone() else {
entry.fetched_at = Some(now);
return;
};
entry.loading = true;
entry.generation = entry.generation.wrapping_add(1);
let generation = entry.generation;
let tx = self.action_tx.clone();
tokio::task::spawn_blocking(move || {
let result = github::fetch(&owner, &repo, state);
let _ = tx.send(Action::GitHubFetched {
repo_id: id,
generation,
result,
});
});
}
pub(super) fn github_fetched(
&mut self,
repo_id: RepoId,
generation: u64,
result: Result<github::GithubData, String>,
) {
let is_open_filter = self.github_state_filter == GithubStateFilter::Open;
if let Some(entry) = self.github_cache.get_mut(&repo_id) {
if entry.generation != generation {
return;
}
entry.loading = false;
entry.fetched_at = Some(Instant::now());
match result {
Ok(data) => {
if is_open_filter {
entry.open_count = data.issues.len() + data.prs.len();
}
entry.issues = data.issues;
entry.prs = data.prs;
entry.error = None;
}
Err(e) => entry.error = Some(e),
}
}
if self.github_target().map(|t| t.0) == Some(repo_id) {
self.refresh_github_panel();
}
}
pub(super) fn refresh_github_panel(&mut self) {
let Some((id, _, name)) = self.github_target() else {
return;
};
if !github::gh_available() {
self.github_panel
.set_error(&name, "gh CLI not found on PATH".to_string());
return;
}
let label = self.github_state_filter.label();
match self.github_cache.get(&id) {
None => self.github_panel.set_loading(&name),
Some(s) if s.loading && s.fetched_at.is_none() => self.github_panel.set_loading(&name),
Some(s) if s.owner_repo.is_none() && s.fetched_at.is_some() => {
self.github_panel.set_not_github(&name)
}
Some(s) if s.error.is_some() => self
.github_panel
.set_error(&name, s.error.clone().unwrap_or_default()),
Some(s) => self
.github_panel
.set_data(s.issues.clone(), s.prs.clone(), &name, label),
}
}
pub(super) fn cycle_github_state_filter(&mut self) {
if !self.config.github.enabled {
return;
}
self.github_state_filter = self.github_state_filter.next();
self.request_github(true);
self.refresh_github_panel();
}
pub(super) fn prune_github_cache(&mut self, removed: &[std::path::PathBuf]) {
for path in removed {
self.github_cache.remove(&RepoId(path.clone()));
}
}
}