use tga::core::config::Config;
use tga::core::db::correlation::{CorrelationCounts, CorrelationFilter, CorrelationRow};
use tga::core::progress::{ProgressAggregate, ProgressBus};
use super::capture::LogCapture;
pub const RESULTS_LIMIT: usize = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepoSource {
Configured,
Org,
}
impl RepoSource {
pub fn label(self) -> &'static str {
match self {
Self::Configured => "config",
Self::Org => "org",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoEntry {
pub name: String,
pub source: RepoSource,
pub detail: String,
pub config_index: Option<usize>,
pub selected: bool,
}
impl RepoEntry {
pub fn is_selectable(&self) -> bool {
self.config_index.is_some()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum View {
#[default]
Repos,
Progress,
Results,
}
impl View {
pub fn next(self) -> Self {
match self {
Self::Repos => Self::Progress,
Self::Progress => Self::Results,
Self::Results => Self::Repos,
}
}
pub fn label(self) -> &'static str {
match self {
Self::Repos => "Repos",
Self::Progress => "Progress",
Self::Results => "Results",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum WorkStatus {
#[default]
Idle,
Running,
Finished(String),
}
impl WorkStatus {
pub fn is_running(&self) -> bool {
matches!(self, Self::Running)
}
}
#[derive(Debug)]
pub struct TuiState {
pub config: Config,
pub repos: Vec<RepoEntry>,
pub repo_cursor: usize,
pub view: View,
pub progress: ProgressAggregate,
pub bus: ProgressBus,
pub logs: LogCapture,
pub status: WorkStatus,
pub counts: CorrelationCounts,
pub rows: Vec<CorrelationRow>,
pub filter: CorrelationFilter,
pub row_cursor: usize,
pub show_help: bool,
pub message: Option<String>,
pub quit: bool,
pub quit_armed: bool,
pub offline: bool,
}
impl TuiState {
pub fn new(config: Config) -> Self {
let repos = build_repo_entries(&config);
Self {
config,
repos,
repo_cursor: 0,
view: View::default(),
progress: ProgressAggregate::new(),
bus: ProgressBus::new(),
logs: LogCapture::new(),
status: WorkStatus::default(),
counts: CorrelationCounts::default(),
rows: Vec::new(),
filter: CorrelationFilter::default(),
row_cursor: 0,
show_help: false,
message: None,
quit: false,
quit_armed: false,
offline: false,
}
}
#[must_use]
pub fn offline(mut self, offline: bool) -> Self {
self.offline = offline;
self
}
#[must_use]
pub fn with_logs(mut self, logs: LogCapture) -> Self {
self.logs = logs;
self
}
pub fn pump_progress(&mut self) {
for event in self.bus.drain() {
self.progress.apply(event);
}
for line in self.logs.drain() {
self.progress.push_activity(line);
}
self.progress
.set_dropped(self.bus.dropped() + self.logs.dropped());
}
pub fn move_cursor(&mut self, delta: isize) {
let (cursor, len) = match self.view {
View::Repos => (&mut self.repo_cursor, self.repos.len()),
View::Results => (&mut self.row_cursor, self.rows.len()),
View::Progress => return,
};
if len == 0 {
*cursor = 0;
return;
}
let next = (*cursor as isize + delta).clamp(0, len as isize - 1);
*cursor = next as usize;
}
pub fn toggle_selected(&mut self) {
let Some(entry) = self.repos.get_mut(self.repo_cursor) else {
return;
};
if !entry.is_selectable() {
self.message = Some(format!(
"{} is a discovery source, not a repository — add it to repositories[] to collect it",
entry.name
));
return;
}
entry.selected = !entry.selected;
}
pub fn toggle_select_all(&mut self) {
let all_on = self
.repos
.iter()
.filter(|r| r.is_selectable())
.all(|r| r.selected);
for entry in self.repos.iter_mut().filter(|r| r.is_selectable()) {
entry.selected = !all_on;
}
}
pub fn selected_repositories(&self) -> Vec<tga::core::config::RepositoryConfig> {
let mut indices: Vec<usize> = self
.repos
.iter()
.filter(|r| r.selected)
.filter_map(|r| r.config_index)
.collect();
indices.sort_unstable();
indices
.into_iter()
.filter_map(|i| self.config.repositories.get(i).cloned())
.collect()
}
pub fn scoped_config(&self) -> Config {
let mut config = self.config.clone();
config.repositories = self.selected_repositories();
config
}
pub fn set_message(&mut self, message: impl Into<String>) {
self.message = Some(message.into());
}
pub fn set_results(&mut self, counts: CorrelationCounts, rows: Vec<CorrelationRow>) {
self.counts = counts;
self.rows = rows;
self.row_cursor = self.row_cursor.min(self.rows.len().saturating_sub(1));
}
}
fn build_repo_entries(config: &Config) -> Vec<RepoEntry> {
let mut entries: Vec<RepoEntry> = config
.repositories
.iter()
.enumerate()
.map(|(i, repo)| RepoEntry {
name: repo.name.clone().unwrap_or_else(|| {
repo.path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| repo.path.display().to_string())
}),
source: RepoSource::Configured,
detail: repo.path.display().to_string(),
config_index: Some(i),
selected: true,
})
.collect();
if let Some(gh) = config.github.as_ref() {
for org in tga::collect::github::org_discovery::effective_orgs(gh.org.as_deref(), &gh.orgs)
{
entries.push(RepoEntry {
name: org,
source: RepoSource::Org,
detail: "org-wide discovery — needs a GitHub token".to_string(),
config_index: None,
selected: false,
});
}
}
entries
}