use crate::api::ids::*;
use crate::api::types::*;
use crate::config::{Config, Theme};
use crate::grouping::{GroupBy, Preset, Section, group};
use crate::message::{Message, Page, Request};
pub use crate::store::{IssueSource, PerSource, Store, TeamContext};
use crate::usecase::{self, Refusal};
mod actions;
mod agents;
mod cursor;
mod frame;
mod input;
mod lists;
mod messages;
mod mouse;
mod navigation;
mod notes;
mod outbox;
mod palette;
mod popups;
mod restore;
mod sidebar;
mod snapshot;
#[cfg(test)]
mod tests;
mod view;
pub use frame::*;
pub use input::*;
pub use lists::*;
pub use navigation::*;
pub use notes::*;
pub use outbox::*;
pub use sidebar::*;
pub use view::*;
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const PREFETCH_MARGIN: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Nav {
MyIssues,
Views,
View(usize),
Team(usize, TeamSection),
Favorite(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TeamSection {
Issues,
Cycles,
Projects,
Views,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ViewKind {
#[default]
Issues,
Projects,
}
impl ViewKind {
pub fn label(&self) -> &'static str {
match self {
Self::Issues => "Issues",
Self::Projects => "Projects",
}
}
fn of(view: &CustomView) -> Self {
if view.lists_issues() {
Self::Issues
} else {
Self::Projects
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Chip {
Preset(Preset),
ViewKind(ViewKind),
}
impl Nav {
pub fn screen(&self) -> Screen {
match self {
Self::MyIssues | Self::View(_) | Self::Team(_, TeamSection::Issues) => {
Screen::IssueList
}
Self::Views | Self::Team(_, TeamSection::Views) => Screen::ViewList,
Self::Team(_, TeamSection::Cycles) => Screen::CycleList,
Self::Team(_, TeamSection::Projects) => Screen::ProjectList,
Self::Favorite(_) => Screen::ProjectDetail,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Screen {
IssueList,
IssueDetail,
ProjectList,
ProjectDetail,
CycleList,
CycleDetail,
ViewList,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum InputMode {
Normal,
Search,
Comment,
NewIssue,
Note,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Popup {
None,
Palette,
GroupBy,
TeamSelect,
Filter(FilterKind),
StatusChange(IssueId),
PriorityChange(IssueId),
AssigneeChange(IssueId),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FilterKind {
Status,
Priority,
}
pub struct App {
pub store: Store,
pub nav: Navigation,
pub view: ViewState,
pub frame: FrameState,
pub outbox: Outbox,
pub should_quit: bool,
pub herdr: bool,
pub agents: Vec<crate::herdr::AgentLink>,
restore: Option<restore::Restore>,
pub theme: Theme,
pub items_per_page: u32,
default_team: Option<String>,
}
impl App {
pub fn new(config: &Config) -> Self {
let mut app = Self {
store: Store::default(),
nav: Navigation::default(),
view: ViewState::new(config),
frame: FrameState::default(),
outbox: Outbox::default(),
should_quit: false,
herdr: crate::herdr::available(),
agents: Vec::new(),
restore: None,
theme: Theme::from_name(config.ui.theme),
items_per_page: config.ui.items_per_page,
default_team: config.ui.default_team.clone(),
};
if !config.warnings.is_empty() {
app.set_error(format!("config.toml:\n{}", config.warnings.join("\n")));
}
app.request(Request::Teams);
app.request(Request::Viewer);
app.request(Request::CustomViews);
app.request(Request::Favorites);
app
}
fn run(&mut self, outcome: Result<Request, Refusal>) -> bool {
match outcome {
Ok(request) => {
self.request(request);
true
}
Err(refusal) => {
self.set_status(refusal.to_string());
false
}
}
}
pub fn request(&mut self, req: Request) {
self.outbox.push(req);
}
pub fn loading(&self) -> bool {
self.outbox.inflight > 0
}
pub fn team_id(&self) -> Option<TeamId> {
self.current_team().map(|t| t.id.clone())
}
pub fn current_team(&self) -> Option<&Team> {
self.store.teams.get(self.nav.team)
}
pub fn quit(&mut self) {
self.should_quit = true;
}
pub fn open_help(&mut self) {
self.view.show_help = true;
self.view.help_scroll = 0;
}
pub fn close_help(&mut self) {
self.view.show_help = false;
}
pub fn scroll_help(&mut self, delta: i16) {
self.view.help_scroll = self.view.help_scroll.saturating_add_signed(delta);
}
pub fn start_goto_chord(&mut self) {
self.view.pending_chord = Some('g');
}
pub fn end_chord(&mut self) {
self.view.pending_chord = None;
}
pub fn set_status(&mut self, msg: impl Into<String>) {
self.view.status_message = Some(msg.into());
}
pub fn clear_status(&mut self) {
self.view.status_message = None;
}
pub fn set_error(&mut self, msg: impl Into<String>) {
self.view.error_popup = Some(msg.into());
}
pub fn dismiss_error(&mut self) {
self.view.error_popup = None;
}
pub fn tick_spinner(&mut self) {
self.view.spinner_frame = (self.view.spinner_frame + 1) % SPINNER_FRAMES.len();
}
pub fn spinner_symbol(&self) -> &'static str {
SPINNER_FRAMES[self.view.spinner_frame]
}
}