Implement proper multithreading
This commit is contained in:
parent
e0047b8fdc
commit
3806865ae4
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -164,6 +164,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"static_cell",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"tokio",
|
"tokio",
|
||||||
"unicode-general-category",
|
"unicode-general-category",
|
||||||
@ -2131,6 +2132,15 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_cell"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|||||||
@ -15,6 +15,7 @@ ratatui = "0.30.0"
|
|||||||
reqwest = { version = "0.12.28", features = ["json"] }
|
reqwest = { version = "0.12.28", features = ["json"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.148"
|
serde_json = "1.0.148"
|
||||||
|
static_cell = "2.1.1"
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.2"
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
unicode-general-category = "1.1.0"
|
unicode-general-category = "1.1.0"
|
||||||
|
|||||||
701
src/app.rs
701
src/app.rs
@ -1,9 +1,17 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use ratatui::{crossterm::event::KeyCode, widgets::ListState};
|
use ratatui::{crossterm::event::KeyCode, widgets::ListState};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{self, error::SendError},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::snowballing::{Publication, get_publication_by_id};
|
use crate::snowballing::{
|
||||||
|
Publication, SnowballingHistory, SnowballingStep, get_publication_by_id,
|
||||||
|
};
|
||||||
|
|
||||||
use log::warn;
|
use log::{error, info, warn};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Serialize, Deserialize, Default, PartialEq)]
|
||||||
pub enum ActivePane {
|
pub enum ActivePane {
|
||||||
@ -12,21 +20,7 @@ pub enum ActivePane {
|
|||||||
PendingPublications,
|
PendingPublications,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum ActiveTab {
|
|
||||||
#[default]
|
|
||||||
Seeding,
|
|
||||||
Snowballing,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
|
||||||
pub enum SnowballingStep {
|
|
||||||
#[default]
|
|
||||||
Backward,
|
|
||||||
Forward,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub enum StatusMessage {
|
pub enum StatusMessage {
|
||||||
Info(String),
|
Info(String),
|
||||||
Warning(String),
|
Warning(String),
|
||||||
@ -39,15 +33,6 @@ impl Default for StatusMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for SnowballingStep {
|
|
||||||
fn to_string(&self) -> String {
|
|
||||||
match self {
|
|
||||||
SnowballingStep::Forward => String::from("forward"),
|
|
||||||
SnowballingStep::Backward => String::from("backward"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct SerializableListState {
|
struct SerializableListState {
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
@ -84,42 +69,287 @@ pub mod liststate_serde {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default, PartialEq, Copy, Clone)]
|
||||||
|
pub enum Tab {
|
||||||
|
#[default]
|
||||||
|
Seeding,
|
||||||
|
Snowballing,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SeedingAction {
|
||||||
|
EnterChar(char),
|
||||||
|
EnterBackspace,
|
||||||
|
ClearInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum SnowballingAction {
|
||||||
|
SelectLeftPane,
|
||||||
|
SelectRightPane,
|
||||||
|
SearchForSelected,
|
||||||
|
NextItem,
|
||||||
|
PrevItem,
|
||||||
|
ShowIncludedPublication(Publication),
|
||||||
|
ShowPendingPublication(Publication),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum GlobalAction {
|
||||||
|
TriggerNextSnowballingStep,
|
||||||
|
ShowStatusMessage(StatusMessage),
|
||||||
|
ClearStatusMessage,
|
||||||
|
AddIncludedPublication(Publication),
|
||||||
|
SubmitSeedLink,
|
||||||
|
NextTab,
|
||||||
|
Quit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum Action {
|
||||||
|
Snowballing(SnowballingAction),
|
||||||
|
Seeding(SeedingAction),
|
||||||
|
Global(GlobalAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GlobalAction> for Action {
|
||||||
|
fn from(action: GlobalAction) -> Self {
|
||||||
|
Action::Global(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SnowballingAction> for Action {
|
||||||
|
fn from(action: SnowballingAction) -> Self {
|
||||||
|
Action::Snowballing(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SeedingAction> for Action {
|
||||||
|
fn from(action: SeedingAction) -> Self {
|
||||||
|
Action::Seeding(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Component<T> {
|
||||||
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: T,
|
||||||
|
tx: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
pub struct App {
|
pub struct SeedingComponent {
|
||||||
/// List of Publications that have been conclusively included
|
pub input: String,
|
||||||
|
#[serde(skip)]
|
||||||
pub included_publications: Vec<Publication>,
|
pub included_publications: Vec<Publication>,
|
||||||
|
}
|
||||||
|
|
||||||
/// List of Publications pending screening
|
impl Component<SeedingAction> for SeedingComponent {
|
||||||
pub pending_publications: Vec<Publication>,
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: SeedingAction,
|
||||||
|
action_tx: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match action {
|
||||||
|
SeedingAction::ClearInput => {
|
||||||
|
self.input.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SeedingAction::EnterChar(c) => {
|
||||||
|
self.input.push(c);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SeedingAction::EnterBackspace => {
|
||||||
|
if self.input.len() > 0 {
|
||||||
|
self.input.truncate(self.input.len() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
/// List of Publications that have been conclusively excluded
|
Ok(())
|
||||||
pub excluded_publications: Vec<Publication>,
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// UI state: included publications list
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct SnowballingComponent {
|
||||||
#[serde(with = "liststate_serde")]
|
#[serde(with = "liststate_serde")]
|
||||||
pub included_list_state: ListState,
|
pub included_list_state: ListState,
|
||||||
|
|
||||||
/// UI state: pending publications list
|
|
||||||
#[serde(with = "liststate_serde")]
|
#[serde(with = "liststate_serde")]
|
||||||
pub pending_list_state: ListState,
|
pub pending_list_state: ListState,
|
||||||
|
|
||||||
/// UI state: active pane
|
|
||||||
pub active_pane: ActivePane,
|
pub active_pane: ActivePane,
|
||||||
|
/// Local component copy of the included publications list
|
||||||
|
#[serde(skip)]
|
||||||
|
pub included_publications: Vec<Publication>,
|
||||||
|
/// Local component copy of the pending publications list
|
||||||
|
#[serde(skip)]
|
||||||
|
pub pending_publications: Vec<Publication>,
|
||||||
|
}
|
||||||
|
|
||||||
/// UI state: active window
|
impl SnowballingComponent {
|
||||||
pub active_tab: ActiveTab,
|
fn next_list_item(
|
||||||
|
list_state: &mut ListState,
|
||||||
|
publications: &Vec<Publication>,
|
||||||
|
) {
|
||||||
|
let i = match list_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i >= publications.len().wrapping_sub(1) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
|
||||||
pub seeding_input: String,
|
fn prev_list_item(
|
||||||
|
list_state: &mut ListState,
|
||||||
|
publications: &Vec<Publication>,
|
||||||
|
) {
|
||||||
|
let i = match list_state.selected() {
|
||||||
|
Some(i) => {
|
||||||
|
if i == 0 {
|
||||||
|
publications.len().wrapping_sub(1)
|
||||||
|
} else {
|
||||||
|
i - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => 0,
|
||||||
|
};
|
||||||
|
list_state.select(Some(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub snowballing_iteration: usize,
|
impl Component<SnowballingAction> for SnowballingComponent {
|
||||||
|
fn handle_action(
|
||||||
|
&mut self,
|
||||||
|
action: SnowballingAction,
|
||||||
|
action_tx: &mpsc::UnboundedSender<Action>,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match action {
|
||||||
|
SnowballingAction::SelectLeftPane => {
|
||||||
|
self.active_pane = ActivePane::IncludedPublications;
|
||||||
|
|
||||||
pub snowballing_step: SnowballingStep,
|
if let None = self.included_list_state.selected() {
|
||||||
|
self.included_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SnowballingAction::SelectRightPane => {
|
||||||
|
self.active_pane = ActivePane::IncludedPublications;
|
||||||
|
|
||||||
|
if let None = self.included_list_state.selected() {
|
||||||
|
self.included_list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
SnowballingAction::SearchForSelected => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
if let Some(idx) = self.included_list_state.selected() {
|
||||||
|
open::that(&self.included_publications[idx].id)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
if let Some(idx) = self.pending_list_state.selected() {
|
||||||
|
open::that(&self.pending_publications[idx].id).unwrap();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SnowballingAction::NextItem => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
Self::next_list_item(
|
||||||
|
&mut self.included_list_state,
|
||||||
|
&self.included_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
Self::next_list_item(
|
||||||
|
&mut self.pending_list_state,
|
||||||
|
&self.pending_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SnowballingAction::PrevItem => match self.active_pane {
|
||||||
|
ActivePane::IncludedPublications => {
|
||||||
|
Self::prev_list_item(
|
||||||
|
&mut self.included_list_state,
|
||||||
|
&self.included_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
ActivePane::PendingPublications => {
|
||||||
|
Self::prev_list_item(
|
||||||
|
&mut self.pending_list_state,
|
||||||
|
&self.pending_publications,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct AppState {
|
||||||
|
// UI state
|
||||||
|
pub seeding: SeedingComponent,
|
||||||
|
pub snowballing: SnowballingComponent,
|
||||||
|
pub current_tab: Tab,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub status_message: StatusMessage,
|
pub status_message: StatusMessage,
|
||||||
|
// Internal state
|
||||||
|
pub history: SnowballingHistory,
|
||||||
|
}
|
||||||
|
|
||||||
#[serde(skip)]
|
impl AppState {
|
||||||
|
pub fn refresh_component_states(&mut self) {
|
||||||
|
self.seeding.included_publications = self.history.get_all_included();
|
||||||
|
|
||||||
|
self.snowballing.included_publications =
|
||||||
|
self.history.get_all_included();
|
||||||
|
self.snowballing.pending_publications = self.history.get_all_pending();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub state: AppState,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
|
pub action_tx: &'static mpsc::UnboundedSender<Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! status_info {
|
||||||
|
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
||||||
|
$action_tx.send(
|
||||||
|
GlobalAction::ShowStatusMessage(StatusMessage::Info(format!($text, $($args)*)))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
macro_rules! status_warn {
|
||||||
|
($action_tx:expr, $text:expr, $($args:expr)*) => {
|
||||||
|
$action_tx.send(
|
||||||
|
GlobalAction::ShowStatusMessage(StatusMessage::Warning(format!($text, $($args)*)))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
macro_rules! status_error {
|
||||||
|
($action_tx:expr, $text:expr $(, $args:expr)*) => {
|
||||||
|
$action_tx.send(
|
||||||
|
GlobalAction::ShowStatusMessage(StatusMessage::Error(format!($text, $($args)*)))
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement moving through steps and iterations (populating pending papers)
|
// TODO: Implement moving through steps and iterations (populating pending papers)
|
||||||
@ -127,229 +357,238 @@ pub struct App {
|
|||||||
// TODO: Implement possibility of pushing excluded papers back into pending
|
// TODO: Implement possibility of pushing excluded papers back into pending
|
||||||
// TODO: Implement export of included papers as csv for keywording with a spreadsheet
|
// TODO: Implement export of included papers as csv for keywording with a spreadsheet
|
||||||
// TODO: Implement export of included papers into zotero (Use RIS format somehow)
|
// TODO: Implement export of included papers into zotero (Use RIS format somehow)
|
||||||
// TODO: Implement proper multithreading
|
|
||||||
// TODO: Log everything relevant
|
|
||||||
impl App {
|
impl App {
|
||||||
pub async fn add_seed_paper(&mut self, api_link: &String) {
|
// TODO: Have status messages always last the same amount of time
|
||||||
let publ =
|
fn handle_global_action(
|
||||||
get_publication_by_id(api_link, "an.tsouchlos@gmail.com").await;
|
&mut self,
|
||||||
|
action: GlobalAction,
|
||||||
match publ {
|
action_tx: &'static mpsc::UnboundedSender<Action>,
|
||||||
Ok(publ) => self.included_publications.push(publ),
|
) -> Result<(), SendError<Action>> {
|
||||||
Err(err) => {
|
match action {
|
||||||
warn!(
|
GlobalAction::NextTab => {
|
||||||
"Failed to get publication metadata using OpenAlex API: {}",
|
self.state.current_tab = match self.state.current_tab {
|
||||||
err
|
Tab::Seeding => Tab::Snowballing,
|
||||||
);
|
Tab::Snowballing => Tab::Seeding,
|
||||||
self.set_status_message(StatusMessage::Error(format!(
|
};
|
||||||
"Failed to get publication metadata using OpenAlex API: {}",
|
Ok(())
|
||||||
err
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
GlobalAction::ShowStatusMessage(msg) => {
|
||||||
|
match &msg {
|
||||||
|
StatusMessage::Error(_) => {
|
||||||
|
error!("Status message: {:?}", msg)
|
||||||
|
}
|
||||||
|
StatusMessage::Warning(_) => {
|
||||||
|
warn!("Status message: {:?}", msg)
|
||||||
|
}
|
||||||
|
StatusMessage::Info(_) => {
|
||||||
|
info!("Status message: {:?}", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_status_message(&mut self, s: StatusMessage) {
|
self.state.status_message = msg;
|
||||||
self.status_message = s;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_key(&mut self, key: KeyCode) {
|
tokio::spawn(async move {
|
||||||
if KeyCode::Esc == key {
|
sleep(Duration::from_millis(1000)).await;
|
||||||
self.should_quit = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.active_tab {
|
if let Err(err) =
|
||||||
ActiveTab::Seeding => match key {
|
action_tx.send(GlobalAction::ClearStatusMessage.into())
|
||||||
KeyCode::Tab => {
|
|
||||||
self.active_tab = ActiveTab::Snowballing;
|
|
||||||
}
|
|
||||||
KeyCode::Enter => {
|
|
||||||
self.add_seed_paper(&self.seeding_input.clone()).await;
|
|
||||||
self.seeding_input.clear();
|
|
||||||
}
|
|
||||||
KeyCode::Char(to_insert) => self.seeding_input.push(to_insert),
|
|
||||||
KeyCode::Backspace => {
|
|
||||||
if self.seeding_input.len() > 0 {
|
|
||||||
self.seeding_input
|
|
||||||
.truncate(self.seeding_input.len() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
},
|
|
||||||
ActiveTab::Snowballing => match key {
|
|
||||||
KeyCode::Char(' ') => {
|
|
||||||
if self.pending_publications.len() > 0 {
|
|
||||||
warn!(
|
|
||||||
"The next snowballing step can only be initiated \
|
|
||||||
after screening all pending publications"
|
|
||||||
);
|
|
||||||
self.set_status_message(StatusMessage::Warning(
|
|
||||||
"The next snowballing step can only be initiated \
|
|
||||||
after screening all pending publications"
|
|
||||||
.to_string(),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.snowballing_step {
|
|
||||||
SnowballingStep::Forward => {
|
|
||||||
// TODO: Implement
|
|
||||||
}
|
|
||||||
SnowballingStep::Backward => {
|
|
||||||
self.set_status_message(StatusMessage::Info(
|
|
||||||
"Fetching references...".to_string(),
|
|
||||||
));
|
|
||||||
|
|
||||||
// TODO: Find a way to not clone the publications
|
|
||||||
for publication in
|
|
||||||
self.included_publications.clone()
|
|
||||||
{
|
{
|
||||||
// TODO: In addition to the referenced_works do
|
error!("{}", err);
|
||||||
// an API call for citations
|
}
|
||||||
for reference in &publication.referenced_works {
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
GlobalAction::ClearStatusMessage => {
|
||||||
|
self.state.status_message = StatusMessage::Info("".to_string());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
GlobalAction::TriggerNextSnowballingStep => {
|
||||||
|
// if self.pending_publications.len() > 0 {
|
||||||
|
// warn!(
|
||||||
|
// "The next snowballing step can only be initiated \
|
||||||
|
// after screening all pending publications"
|
||||||
|
// );
|
||||||
|
// self.set_status_message(StatusMessage::Warning(
|
||||||
|
// "The next snowballing step can only be initiated \
|
||||||
|
// after screening all pending publications"
|
||||||
|
// .to_string(),
|
||||||
|
// ));
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// match self.snowballing_step {
|
||||||
|
// SnowballingStep::Forward => {
|
||||||
|
// // TODO: Implement
|
||||||
|
// }
|
||||||
|
// SnowballingStep::Backward => {
|
||||||
|
// self.set_status_message(StatusMessage::Info(
|
||||||
|
// "Fetching references...".to_string(),
|
||||||
|
// ));
|
||||||
|
//
|
||||||
|
// // TODO: Find a way to not clone the publications
|
||||||
|
// for publication in self.included_publications.clone() {
|
||||||
|
// // TODO: In addition to the referenced_works do
|
||||||
|
// // an API call for citations
|
||||||
|
// for reference in &publication.referenced_works {
|
||||||
|
// let api_link = format!(
|
||||||
|
// "https://api.openalex.org/{}",
|
||||||
|
// &reference[21..]
|
||||||
|
// );
|
||||||
|
// let publ = get_publication_by_id(
|
||||||
|
// &api_link,
|
||||||
|
// "an.tsouchlos@gmail.com",
|
||||||
|
// )
|
||||||
|
// .await;
|
||||||
|
//
|
||||||
|
// match publ {
|
||||||
|
// Ok(publ) => {
|
||||||
|
// self.pending_publications.push(publ)
|
||||||
|
// }
|
||||||
|
// Err(err) => {
|
||||||
|
// warn!(
|
||||||
|
// "Failed to get publication\
|
||||||
|
// metadata using OpenAlex API: \
|
||||||
|
// {}",
|
||||||
|
// err
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// self.set_status_message(
|
||||||
|
// StatusMessage::Error(format!(
|
||||||
|
// "Failed to get publication\
|
||||||
|
// metadata using OpenAlex API: \
|
||||||
|
// {}",
|
||||||
|
// err
|
||||||
|
// )),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// self.set_status_message(StatusMessage::Info(
|
||||||
|
// "Done".to_string(),
|
||||||
|
// ));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
GlobalAction::SubmitSeedLink => {
|
||||||
|
if !self
|
||||||
|
.state
|
||||||
|
.seeding
|
||||||
|
.input
|
||||||
|
.starts_with("https://openalex.org/")
|
||||||
|
{
|
||||||
|
status_error!(
|
||||||
|
self.action_tx,
|
||||||
|
"Seed link must start with 'https://openalex.org/'"
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
status_info!(
|
||||||
|
self.action_tx,
|
||||||
|
"Submitting seed link: {}",
|
||||||
|
&self.state.seeding.input
|
||||||
|
)?;
|
||||||
|
|
||||||
let api_link = format!(
|
let api_link = format!(
|
||||||
"https://api.openalex.org/{}",
|
"https://api.openalex.org/{}",
|
||||||
&reference[21..]
|
self.state
|
||||||
|
.seeding
|
||||||
|
.input
|
||||||
|
.trim_start_matches("https://openalex.org/")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
let publ = get_publication_by_id(
|
let publ = get_publication_by_id(
|
||||||
&api_link,
|
&api_link,
|
||||||
"an.tsouchlos@gmail.com",
|
"an.tsouchlos@gmail.com",
|
||||||
)
|
);
|
||||||
.await;
|
|
||||||
|
|
||||||
match publ {
|
match publ.await {
|
||||||
Ok(publ) => {
|
Ok(publ) => {
|
||||||
self.pending_publications.push(publ)
|
let _ = status_info!(
|
||||||
}
|
action_tx,
|
||||||
Err(err) => {
|
"Seed paper obtained successfully: {}",
|
||||||
warn!(
|
publ.get_title().unwrap_or(
|
||||||
"Failed to get publication\
|
"[title unavailable]".to_string()
|
||||||
metadata using OpenAlex API: \
|
)
|
||||||
{}",
|
|
||||||
err
|
|
||||||
);
|
);
|
||||||
|
|
||||||
self.set_status_message(
|
let _ = action_tx.send(
|
||||||
StatusMessage::Error(format!(
|
GlobalAction::AddIncludedPublication(publ)
|
||||||
"Failed to get publication\
|
.into(),
|
||||||
metadata using OpenAlex API: \
|
|
||||||
{}",
|
|
||||||
err
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let _ =
|
||||||
|
status_error!(action_tx, "{}", e.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.set_status_message(StatusMessage::Info(
|
self.action_tx.send(SeedingAction::ClearInput.into())
|
||||||
"Done".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
GlobalAction::Quit => {
|
||||||
|
self.should_quit = true;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
GlobalAction::AddIncludedPublication(publ) => {
|
||||||
}
|
self.state
|
||||||
KeyCode::Enter => match self.active_pane {
|
.history
|
||||||
ActivePane::IncludedPublications => {
|
.current_iteration
|
||||||
if let Some(idx) = self.included_list_state.selected() {
|
|
||||||
open::that(&self.included_publications[idx].id)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
if let Some(idx) = self.pending_list_state.selected() {
|
|
||||||
open::that(&self.pending_publications[idx].id)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Tab => {
|
|
||||||
self.active_tab = ActiveTab::Seeding;
|
|
||||||
}
|
|
||||||
KeyCode::Char('j') => match self.active_pane {
|
|
||||||
ActivePane::IncludedPublications => {
|
|
||||||
let i = match self.included_list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= self
|
|
||||||
.included_publications
|
.included_publications
|
||||||
.len()
|
.push(publ.clone());
|
||||||
.wrapping_sub(1)
|
Ok(())
|
||||||
{
|
}
|
||||||
0
|
_ => Ok(()),
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.included_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
let i = match self.pending_list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i >= self
|
|
||||||
.pending_publications
|
|
||||||
.len()
|
|
||||||
.wrapping_sub(1)
|
|
||||||
{
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.pending_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Char('k') => match self.active_pane {
|
|
||||||
ActivePane::IncludedPublications => {
|
|
||||||
let i = match self.included_list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
self.included_publications
|
|
||||||
.len()
|
|
||||||
.wrapping_sub(1)
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.included_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
ActivePane::PendingPublications => {
|
|
||||||
let i = match self.pending_list_state.selected() {
|
|
||||||
Some(i) => {
|
|
||||||
if i == 0 {
|
|
||||||
self.pending_publications
|
|
||||||
.len()
|
|
||||||
.wrapping_sub(1)
|
|
||||||
} else {
|
|
||||||
i - 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => 0,
|
|
||||||
};
|
|
||||||
self.pending_list_state.select(Some(i));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Char('h') => {
|
|
||||||
self.active_pane = ActivePane::IncludedPublications;
|
|
||||||
|
|
||||||
if let None = self.included_list_state.selected() {
|
pub fn handle_action(
|
||||||
self.included_list_state.select(Some(0));
|
&mut self,
|
||||||
|
action: Action,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match action {
|
||||||
|
Action::Seeding(seeding_action) => self
|
||||||
|
.state
|
||||||
|
.seeding
|
||||||
|
.handle_action(seeding_action, self.action_tx),
|
||||||
|
Action::Snowballing(snowballing_action) => self
|
||||||
|
.state
|
||||||
|
.snowballing
|
||||||
|
.handle_action(snowballing_action, self.action_tx),
|
||||||
|
Action::Global(global_action) => {
|
||||||
|
self.handle_global_action(global_action, self.action_tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('l') => {
|
|
||||||
self.active_pane = ActivePane::PendingPublications;
|
|
||||||
|
|
||||||
if let None = self.pending_list_state.selected() {
|
pub fn handle_key(
|
||||||
self.pending_list_state.select(Some(0));
|
&mut self,
|
||||||
|
key: KeyCode,
|
||||||
|
) -> Result<(), SendError<Action>> {
|
||||||
|
match (self.state.current_tab, key) {
|
||||||
|
(_, KeyCode::Esc) => {
|
||||||
|
self.action_tx.send(GlobalAction::Quit.into())?;
|
||||||
}
|
}
|
||||||
|
(_, KeyCode::Tab) => {
|
||||||
|
self.action_tx.send(GlobalAction::NextTab.into())?;
|
||||||
|
}
|
||||||
|
(Tab::Seeding, KeyCode::Char(c)) => {
|
||||||
|
self.action_tx.send(SeedingAction::EnterChar(c).into())?;
|
||||||
|
}
|
||||||
|
(Tab::Seeding, KeyCode::Backspace) => {
|
||||||
|
self.action_tx.send(SeedingAction::EnterBackspace.into())?;
|
||||||
|
}
|
||||||
|
(Tab::Seeding, KeyCode::Enter) => {
|
||||||
|
self.action_tx.send(GlobalAction::SubmitSeedLink.into())?;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use std::{error::Error, io, time::Duration};
|
use std::{error::Error, io, time::Duration};
|
||||||
|
|
||||||
use log::{error};
|
use crossterm::event::KeyCode;
|
||||||
|
use log::error;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
Terminal,
|
Terminal,
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
@ -13,10 +14,14 @@ use ratatui::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||||
|
|
||||||
use crate::{app::App, ui};
|
use crate::{
|
||||||
|
app::{Action, App, AppState},
|
||||||
|
ui,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn run(app: App) -> Result<App, Box<dyn Error>> {
|
pub async fn run(app_state: AppState) -> Result<AppState, Box<dyn Error>> {
|
||||||
// setup terminal
|
// setup terminal
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
@ -25,7 +30,7 @@ pub async fn run(app: App) -> Result<App, Box<dyn Error>> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
// create app and run it
|
// create app and run it
|
||||||
let app_result = run_app(&mut terminal, app).await;
|
let app_result = run_app(&mut terminal, app_state).await;
|
||||||
|
|
||||||
// restore terminal
|
// restore terminal
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
@ -44,24 +49,49 @@ pub async fn run(app: App) -> Result<App, Box<dyn Error>> {
|
|||||||
Ok(app_result?)
|
Ok(app_result?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
use static_cell::StaticCell;
|
||||||
|
|
||||||
|
static ACTION_QUEUE_TX: StaticCell<UnboundedSender<Action>> = StaticCell::new();
|
||||||
|
static ACTION_QUEUE_RX: StaticCell<UnboundedReceiver<Action>> =
|
||||||
|
StaticCell::new();
|
||||||
|
|
||||||
async fn run_app<B: Backend>(
|
async fn run_app<B: Backend>(
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<B>,
|
||||||
mut app: App,
|
app_state: AppState,
|
||||||
) -> io::Result<App>
|
) -> Result<AppState, Box<dyn Error>>
|
||||||
where
|
where
|
||||||
io::Error: From<B::Error>,
|
<B as Backend>::Error: 'static,
|
||||||
{
|
{
|
||||||
|
let (action_tx, action_rx): (
|
||||||
|
UnboundedSender<Action>,
|
||||||
|
UnboundedReceiver<Action>,
|
||||||
|
) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let action_tx_ref = ACTION_QUEUE_TX.init(action_tx);
|
||||||
|
let action_rx_ref = ACTION_QUEUE_RX.init(action_rx);
|
||||||
|
|
||||||
|
let mut app = App {
|
||||||
|
state: app_state,
|
||||||
|
action_tx: action_tx_ref,
|
||||||
|
should_quit: false,
|
||||||
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|frame| ui::draw(frame, &mut app))?;
|
app.state.refresh_component_states(); // TODO: Is it a problem to call this every frame?
|
||||||
|
terminal.draw(|frame| ui::draw(frame, &mut app.state))?;
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(100))? {
|
if event::poll(Duration::from_millis(100))? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
app.handle_key(key.code).await;
|
app.handle_key(key.code)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while let Ok(action) = action_rx_ref.try_recv() {
|
||||||
|
app.handle_action(action)?;
|
||||||
|
}
|
||||||
|
|
||||||
if app.should_quit {
|
if app.should_quit {
|
||||||
return Ok(app);
|
return Ok(app.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/main.rs
16
src/main.rs
@ -1,9 +1,10 @@
|
|||||||
use log::{error};
|
use log::error;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod ui;
|
mod ui;
|
||||||
use crate::app::App;
|
use crate::app::{App, AppState, StatusMessage};
|
||||||
mod snowballing;
|
mod snowballing;
|
||||||
|
|
||||||
// use crate::snowballing::get_citing_papers;
|
// use crate::snowballing::get_citing_papers;
|
||||||
@ -24,15 +25,16 @@ mod snowballing;
|
|||||||
// Ok(())
|
// Ok(())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
fn deserialize_savefile(filename: &String) -> Result<App, serde_json::Error> {
|
fn deserialize_savefile(
|
||||||
|
filename: &String,
|
||||||
|
) -> Result<AppState, serde_json::Error> {
|
||||||
match std::fs::read_to_string(filename) {
|
match std::fs::read_to_string(filename) {
|
||||||
Ok(content) => {
|
Ok(content) => {
|
||||||
let mut app: App = serde_json::from_str(&content)?;
|
let app: AppState = serde_json::from_str(&content)?;
|
||||||
app.should_quit = false;
|
|
||||||
Ok(app)
|
Ok(app)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
let app = App::default();
|
let app = AppState::default();
|
||||||
if let Ok(serialized) = serde_json::to_string_pretty(&app) {
|
if let Ok(serialized) = serde_json::to_string_pretty(&app) {
|
||||||
let _ = std::fs::write(filename, serialized);
|
let _ = std::fs::write(filename, serialized);
|
||||||
}
|
}
|
||||||
@ -42,7 +44,7 @@ fn deserialize_savefile(filename: &String) -> Result<App, serde_json::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn serialize_savefile(
|
fn serialize_savefile(
|
||||||
app: &App,
|
app: &AppState,
|
||||||
filename: &String,
|
filename: &String,
|
||||||
) -> Result<(), serde_json::Error> {
|
) -> Result<(), serde_json::Error> {
|
||||||
if let Ok(serialized) = serde_json::to_string_pretty(&app) {
|
if let Ok(serialized) = serde_json::to_string_pretty(&app) {
|
||||||
|
|||||||
@ -22,6 +22,56 @@ pub struct Publication {
|
|||||||
pub referenced_works: Vec<String>,
|
pub referenced_works: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub enum SnowballingStep {
|
||||||
|
#[default]
|
||||||
|
Backward,
|
||||||
|
Forward,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for SnowballingStep {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
SnowballingStep::Forward => String::from("forward"),
|
||||||
|
SnowballingStep::Backward => String::from("backward"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Only store IDs of excluded publications?
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct SnowballingIteration {
|
||||||
|
pub included_publications: Vec<Publication>,
|
||||||
|
pub excluded_publications: Vec<Publication>,
|
||||||
|
pub step: SnowballingStep,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
pub struct SnowballingHistory {
|
||||||
|
pub seed: Vec<Publication>,
|
||||||
|
pub current_iteration: SnowballingIteration,
|
||||||
|
pub previoius_iterations: Vec<SnowballingIteration>,
|
||||||
|
pub pending_publications: Vec<Publication>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SnowballingHistory {
|
||||||
|
pub fn get_all_included(&self) -> Vec<Publication> {
|
||||||
|
vec![self.current_iteration.included_publications.clone()]
|
||||||
|
.into_iter()
|
||||||
|
.chain(
|
||||||
|
self.previoius_iterations
|
||||||
|
.iter()
|
||||||
|
.map(|iter| iter.included_publications.clone()),
|
||||||
|
)
|
||||||
|
.flatten()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_pending(&self) -> Vec<Publication> {
|
||||||
|
self.pending_publications.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Publication {
|
impl Publication {
|
||||||
pub fn get_title(&self) -> Option<String> {
|
pub fn get_title(&self) -> Option<String> {
|
||||||
self.display_name.clone()
|
self.display_name.clone()
|
||||||
|
|||||||
71
src/ui.rs
71
src/ui.rs
@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
app::{ActivePane, ActiveTab, App, StatusMessage},
|
app::{ActivePane, App, AppState, StatusMessage, Tab},
|
||||||
snowballing::Publication,
|
snowballing::Publication,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
@ -10,14 +10,14 @@ use ratatui::{
|
|||||||
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
widgets::{Block, Borders, List, ListItem, ListState, Paragraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn draw(f: &mut Frame, app: &mut App) {
|
pub fn draw(f: &mut Frame, app: &mut AppState) {
|
||||||
match app.active_tab {
|
match app.current_tab {
|
||||||
ActiveTab::Seeding => draw_seeding_tab(f, app),
|
Tab::Seeding => draw_seeding_tab(f, app),
|
||||||
ActiveTab::Snowballing => draw_snowballing_tab(f, app),
|
Tab::Snowballing => draw_snowballing_tab(f, app),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
fn draw_seeding_tab(f: &mut Frame, app: &mut AppState) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
@ -30,7 +30,7 @@ fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
|||||||
// Included publication list
|
// Included publication list
|
||||||
|
|
||||||
let items = create_publication_item_list(
|
let items = create_publication_item_list(
|
||||||
&app.included_publications,
|
&app.seeding.included_publications,
|
||||||
None,
|
None,
|
||||||
chunks[0].width.saturating_sub(4) as usize,
|
chunks[0].width.saturating_sub(4) as usize,
|
||||||
false,
|
false,
|
||||||
@ -41,7 +41,10 @@ fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
|||||||
.title_top(Line::from("Included Publications").centered())
|
.title_top(Line::from("Included Publications").centered())
|
||||||
.title_top(
|
.title_top(
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("{} entries", app.included_publications.len()),
|
format!(
|
||||||
|
"{} entries",
|
||||||
|
app.seeding.included_publications.len()
|
||||||
|
),
|
||||||
Style::default().fg(Color::Yellow),
|
Style::default().fg(Color::Yellow),
|
||||||
))
|
))
|
||||||
.right_aligned(),
|
.right_aligned(),
|
||||||
@ -53,16 +56,16 @@ fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
|||||||
list,
|
list,
|
||||||
chunks[0],
|
chunks[0],
|
||||||
&mut ListState::default().with_selected(
|
&mut ListState::default().with_selected(
|
||||||
match app.included_publications.len() {
|
match app.seeding.included_publications.len() {
|
||||||
0 => None,
|
0 => None,
|
||||||
_ => Some(app.included_publications.len() - 1),
|
_ => Some(app.seeding.included_publications.len() - 1),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Text entry
|
// Text entry
|
||||||
|
|
||||||
let input = Paragraph::new(app.seeding_input.as_str())
|
let input = Paragraph::new(app.seeding.input.as_str())
|
||||||
.block(Block::bordered().title("Input"));
|
.block(Block::bordered().title("Input"));
|
||||||
|
|
||||||
f.render_widget(input, chunks[1]);
|
f.render_widget(input, chunks[1]);
|
||||||
@ -72,7 +75,7 @@ fn draw_seeding_tab(f: &mut Frame, app: &mut App) {
|
|||||||
draw_status_line(f, app, chunks[2]);
|
draw_status_line(f, app, chunks[2]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_snowballing_tab(f: &mut Frame, app: &mut App) {
|
fn draw_snowballing_tab(f: &mut Frame, app: &mut AppState) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Min(2), Constraint::Length(1)])
|
.constraints([Constraint::Min(2), Constraint::Length(1)])
|
||||||
@ -241,7 +244,7 @@ fn create_publication_item_list(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_left_pane(frame: &mut Frame, app: &mut AppState, area: Rect) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Min(10), Constraint::Length(2)])
|
.constraints([Constraint::Min(10), Constraint::Length(2)])
|
||||||
@ -250,9 +253,9 @@ fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
// Included Publication list
|
// Included Publication list
|
||||||
|
|
||||||
let items = create_publication_item_list(
|
let items = create_publication_item_list(
|
||||||
&app.included_publications,
|
&app.snowballing.included_publications,
|
||||||
if app.active_pane == ActivePane::IncludedPublications {
|
if app.snowballing.active_pane == ActivePane::IncludedPublications {
|
||||||
app.included_list_state.selected()
|
app.snowballing.included_list_state.selected()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@ -265,7 +268,10 @@ fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
.title_top(Line::from("Included Publications").centered())
|
.title_top(Line::from("Included Publications").centered())
|
||||||
.title_top(
|
.title_top(
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("{} entries", app.included_publications.len()),
|
format!(
|
||||||
|
"{} entries",
|
||||||
|
app.snowballing.included_publications.len()
|
||||||
|
),
|
||||||
Style::default().fg(Color::Yellow),
|
Style::default().fg(Color::Yellow),
|
||||||
))
|
))
|
||||||
.right_aligned(),
|
.right_aligned(),
|
||||||
@ -273,7 +279,11 @@ fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
.borders(Borders::ALL),
|
.borders(Borders::ALL),
|
||||||
);
|
);
|
||||||
|
|
||||||
frame.render_stateful_widget(list, chunks[0], &mut app.included_list_state);
|
frame.render_stateful_widget(
|
||||||
|
list,
|
||||||
|
chunks[0],
|
||||||
|
&mut app.snowballing.included_list_state,
|
||||||
|
);
|
||||||
|
|
||||||
// Snowballing progress
|
// Snowballing progress
|
||||||
|
|
||||||
@ -281,14 +291,14 @@ fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Iteration: "),
|
Span::raw("Iteration: "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
app.snowballing_iteration.to_string(),
|
app.history.current_iteration.step.to_string(),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(Color::Cyan),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Step: "),
|
Span::raw("Step: "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
app.snowballing_step.to_string(),
|
app.history.previoius_iterations.len().to_string(),
|
||||||
Style::default().fg(Color::Cyan),
|
Style::default().fg(Color::Cyan),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@ -299,11 +309,11 @@ fn draw_left_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
frame.render_widget(progress_widget, chunks[1]);
|
frame.render_widget(progress_widget, chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_right_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
fn draw_right_pane(frame: &mut Frame, app: &mut AppState, area: Rect) {
|
||||||
let items = create_publication_item_list(
|
let items = create_publication_item_list(
|
||||||
&app.pending_publications,
|
&app.snowballing.pending_publications,
|
||||||
if app.active_pane == ActivePane::PendingPublications {
|
if app.snowballing.active_pane == ActivePane::PendingPublications {
|
||||||
app.pending_list_state.selected()
|
app.snowballing.pending_list_state.selected()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@ -316,7 +326,10 @@ fn draw_right_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
.title_top(Line::from("Publications Pending Screening").centered())
|
.title_top(Line::from("Publications Pending Screening").centered())
|
||||||
.title_top(
|
.title_top(
|
||||||
Line::from(Span::styled(
|
Line::from(Span::styled(
|
||||||
format!("{} entries", app.pending_publications.len()),
|
format!(
|
||||||
|
"{} entries",
|
||||||
|
app.snowballing.pending_publications.len()
|
||||||
|
),
|
||||||
Style::default().fg(Color::Yellow),
|
Style::default().fg(Color::Yellow),
|
||||||
))
|
))
|
||||||
.right_aligned(),
|
.right_aligned(),
|
||||||
@ -324,10 +337,14 @@ fn draw_right_pane(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
.borders(Borders::ALL),
|
.borders(Borders::ALL),
|
||||||
);
|
);
|
||||||
|
|
||||||
frame.render_stateful_widget(list, area, &mut app.pending_list_state);
|
frame.render_stateful_widget(
|
||||||
|
list,
|
||||||
|
area,
|
||||||
|
&mut app.snowballing.pending_list_state,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status_line(frame: &mut Frame, app: &App, area: Rect) {
|
fn draw_status_line(frame: &mut Frame, app: &AppState, area: Rect) {
|
||||||
let line = Paragraph::new(Line::from(match app.status_message.clone() {
|
let line = Paragraph::new(Line::from(match app.status_message.clone() {
|
||||||
StatusMessage::Info(s) => Span::raw(s),
|
StatusMessage::Info(s) => Span::raw(s),
|
||||||
StatusMessage::Warning(s) => {
|
StatusMessage::Warning(s) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user