Files
radicle-tui/bin/commands/patch/select.rs
T

218 lines
5.7 KiB
Rust

#[path = "select/ui.rs"]
mod ui;
use std::str::FromStr;
use anyhow::Result;
use radicle::patch::PatchId;
use radicle::storage::git::Repository;
use radicle::Profile;
use radicle_tui as tui;
use tui::cob::patch;
use tui::store;
use tui::task;
use tui::task::Interrupted;
use tui::terminal;
use tui::ui::items::{Filter, PatchItem, PatchItemFilter};
use tui::ui::Frontend;
use tui::Exit;
use tui::PageStack;
use ui::Window;
use super::common::Mode;
type Selection = tui::Selection<PatchId>;
pub struct Context {
pub profile: Profile,
pub repository: Repository,
pub mode: Mode,
pub filter: patch::Filter,
}
pub struct App {
context: Context,
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum Page {
Browse,
Help,
}
#[derive(Clone, Debug)]
pub struct BrowserState {
items: Vec<PatchItem>,
selected: Option<usize>,
filter: PatchItemFilter,
search: store::StateValue<String>,
page_size: usize,
show_search: bool,
}
impl BrowserState {
pub fn patches(&self) -> Vec<PatchItem> {
self.items
.iter()
.filter(|patch| self.filter.matches(patch))
.cloned()
.collect()
}
}
#[derive(Clone, Debug)]
pub struct HelpState {
progress: usize,
page_size: usize,
}
#[derive(Clone, Debug)]
pub struct State {
mode: Mode,
pages: PageStack<Page>,
browser: BrowserState,
help: HelpState,
}
impl TryFrom<&Context> for State {
type Error = anyhow::Error;
fn try_from(context: &Context) -> Result<Self, Self::Error> {
let patches = patch::all(&context.profile, &context.repository)?;
let search = store::StateValue::new(context.filter.to_string());
let filter = PatchItemFilter::from_str(&context.filter.to_string()).unwrap_or_default();
// Convert into UI items
let mut items = vec![];
for patch in patches {
if let Ok(item) = PatchItem::new(&context.profile, &context.repository, patch.clone()) {
items.push(item);
}
}
items.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
Ok(Self {
mode: context.mode.clone(),
pages: PageStack::new(vec![Page::Browse]),
browser: BrowserState {
items,
selected: Some(0),
filter,
search,
show_search: false,
page_size: 1,
},
help: HelpState {
progress: 0,
page_size: 1,
},
})
}
}
pub enum Action {
Exit { selection: Option<Selection> },
Select { selected: Option<usize> },
BrowserPageSize(usize),
HelpPageSize(usize),
OpenSearch,
UpdateSearch { value: String },
ApplySearch,
CloseSearch,
OpenHelp,
LeavePage,
ScrollHelp { progress: usize },
}
impl store::State<Action, Selection> for State {
fn tick(&self) {}
fn handle_action(&mut self, action: Action) -> Option<Exit<Selection>> {
match action {
Action::Exit { selection } => Some(Exit { value: selection }),
Action::Select { selected } => {
self.browser.selected = selected;
None
}
Action::BrowserPageSize(size) => {
self.browser.page_size = size;
None
}
Action::HelpPageSize(size) => {
self.help.page_size = size;
None
}
Action::OpenSearch => {
self.browser.show_search = true;
None
}
Action::UpdateSearch { value } => {
self.browser.search.write(value);
self.browser.filter =
PatchItemFilter::from_str(&self.browser.search.read()).unwrap_or_default();
None
}
Action::ApplySearch => {
self.browser.search.apply();
self.browser.show_search = false;
None
}
Action::CloseSearch => {
self.browser.search.reset();
self.browser.show_search = false;
self.browser.filter =
PatchItemFilter::from_str(&self.browser.search.read()).unwrap_or_default();
None
}
Action::OpenHelp => {
self.pages.push(Page::Help);
None
}
Action::LeavePage => {
self.pages.pop();
None
}
Action::ScrollHelp { progress } => {
self.help.progress = progress;
None
}
}
}
}
impl App {
pub fn new(context: Context) -> Self {
Self { context }
}
pub async fn run(&self) -> Result<Option<Selection>> {
let (terminator, mut interrupt_rx) = task::create_termination();
let (store, state_rx) = store::Store::<Action, State, Selection>::new();
let (frontend, action_rx) = Frontend::<Action>::new();
let state = State::try_from(&self.context)?;
tokio::try_join!(
store.main_loop(state, terminator, action_rx, interrupt_rx.resubscribe()),
frontend.main_loop::<State, Window<terminal::Backend>, Selection>(
state_rx,
interrupt_rx.resubscribe()
),
)?;
if let Ok(reason) = interrupt_rx.recv().await {
match reason {
Interrupted::User { payload } => Ok(payload),
Interrupted::OsSignal => anyhow::bail!("exited because of an os sig int"),
}
} else {
anyhow::bail!("exited because of an unexpected error");
}
}
}