use serde::{Deserialize, Serialize};
use crate::error::{Result, TixError};
use crate::model::Issue;
use crate::storage::{IssueSelector, Store};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SelectionSource {
Explicit,
Filter,
Search,
}
impl SelectionSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Explicit => "selector",
Self::Filter => "filter",
Self::Search => "search",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum SelectionScope {
#[default]
Active,
ActiveAndArchived,
ArchivedOnly,
}
impl SelectionScope {
pub fn as_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::ActiveAndArchived => "active+archived",
Self::ArchivedOnly => "archived",
}
}
pub fn parse(raw: &str) -> Result<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"active" => Ok(Self::Active),
"active+archived" | "all" => Ok(Self::ActiveAndArchived),
"archived" | "archived-only" => Ok(Self::ArchivedOnly),
other => Err(TixError::InvalidInput(format!(
"unknown scope '{other}' — expected one of: active, active+archived, archived"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selection {
source: SelectionSource,
selectors: Vec<String>,
query: Option<String>,
issues: Vec<Issue>,
scope: SelectionScope,
duplicates: Vec<String>,
}
impl Selection {
pub fn resolve(
store: &Store,
selectors: &[String],
query: Option<&str>,
source: SelectionSource,
scope: SelectionScope,
) -> Result<Self> {
let mut issues: Vec<Issue> = Vec::new();
let mut seen_numbers: Vec<u32> = Vec::new();
let mut seen_selectors: Vec<String> = Vec::new();
let mut duplicates: Vec<String> = Vec::new();
for raw in selectors {
let selector = crate::commands::parse_selector(store, raw)?;
let issue = store.get(selector).map_err(|_| {
TixError::NotFound(format!(
"issue matching '{raw}' by number, filename or title"
))
})?;
if seen_numbers.contains(&issue.number) {
duplicates.push(raw.clone());
continue;
}
seen_numbers.push(issue.number);
seen_selectors.push(raw.clone());
issues.push(issue);
}
if let Some(query) = query {
for issue in selection_query(store, query, source, scope)? {
if seen_numbers.contains(&issue.number) {
continue;
}
seen_numbers.push(issue.number);
issues.push(issue);
}
}
if issues.is_empty() {
return Ok(Self {
source,
selectors: seen_selectors,
query: query.map(str::to_string),
issues,
scope,
duplicates,
});
}
Ok(Self {
source,
selectors: seen_selectors,
query: query.map(str::to_string),
issues,
scope,
duplicates,
})
}
pub fn from_issues(
issues: impl IntoIterator<Item = Issue>,
source: SelectionSource,
scope: SelectionScope,
) -> Self {
let mut seen: Vec<u32> = Vec::new();
let mut kept = Vec::new();
for issue in issues {
if seen.contains(&issue.number) {
continue;
}
seen.push(issue.number);
kept.push(issue);
}
Self {
source,
selectors: Vec::new(),
query: None,
issues: kept,
scope,
duplicates: Vec::new(),
}
}
pub fn issues(&self) -> &[Issue] {
&self.issues
}
pub fn into_issues(self) -> Vec<Issue> {
self.issues
}
pub fn source(&self) -> SelectionSource {
self.source
}
pub fn query(&self) -> Option<&str> {
self.query.as_deref()
}
pub fn scope(&self) -> SelectionScope {
self.scope
}
pub fn selectors(&self) -> &[String] {
&self.selectors
}
pub fn duplicates(&self) -> &[String] {
&self.duplicates
}
pub fn len(&self) -> usize {
self.issues.len()
}
pub fn is_empty(&self) -> bool {
self.issues.is_empty()
}
pub fn numbers(&self) -> Vec<u32> {
self.issues.iter().map(|i| i.number).collect()
}
pub fn fingerprint(&self) -> Vec<u32> {
self.numbers()
}
pub fn matches_fingerprint(&self, fingerprint: &[u32]) -> bool {
let mut mine = self.numbers();
let mut theirs = fingerprint.to_vec();
mine.sort_unstable();
theirs.sort_unstable();
mine == theirs
}
}
fn selection_query(
store: &Store,
query: &str,
source: SelectionSource,
scope: SelectionScope,
) -> Result<Vec<Issue>> {
let issues = store.list()?;
let corpus = crate::search::Corpus::from_issues(issues.clone());
let _ = scope;
let out: Vec<Issue> = match source {
SelectionSource::Search => crate::search::search_substring(&corpus, query)
.into_iter()
.map(|hit| hit.issue)
.collect(),
_ => {
let (query_text, criteria) = crate::search::resolve_filter_criteria(store, query)?;
let (_, resolution) = crate::workflow::active_resolution(&store.config);
corpus
.issues
.iter()
.zip(corpus.texts().iter())
.filter(|(issue, text)| {
crate::search::matches_issue(&query_text, &criteria, issue, text, &resolution)
})
.map(|(issue, _)| issue.clone())
.collect()
}
};
Ok(out)
}
pub fn scope_metadata(scope: SelectionScope, selected: usize) -> ScopeMetadata {
ScopeMetadata {
scope,
selected,
can_write: scope == SelectionScope::Active,
delegation_pending: scope != SelectionScope::Active,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScopeMetadata {
pub scope: SelectionScope,
pub selected: usize,
pub can_write: bool,
pub delegation_pending: bool,
}
impl std::fmt::Display for ScopeMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({} selected)", self.scope.as_str(), self.selected)?;
if self.delegation_pending {
write!(f, " — read-only, the archive service is not wired yet")?;
}
Ok(())
}
}
pub fn selector_number(store: &Store, raw: &str) -> Result<u32> {
let selector: IssueSelector = crate::commands::parse_selector(store, raw)?;
Ok(store.get(selector)?.number)
}