refreshing the ui pnly on input or when new data is available

This commit is contained in:
Thomas Eppers 2023-02-19 11:40:40 +01:00
parent 017bafa19b
commit 1c72d4cd24
3 changed files with 139 additions and 86 deletions

View File

@ -2,14 +2,10 @@ mod no_yaml_found;
mod yaml_found; mod yaml_found;
use anyhow::Result; use anyhow::Result;
use termion::input::TermRead;
use crate::widget::service_switcher; use crate::widget::service_switcher;
use crate::Opt; use crate::Opt;
use std::sync::mpsc;
use std::{io, thread};
pub fn create_ui(opt: &Opt) -> Result<()> { pub fn create_ui(opt: &Opt) -> Result<()> {
let service_result = service_switcher::ServiceSwitcher::new(&opt.file); let service_result = service_switcher::ServiceSwitcher::new(&opt.file);
match service_result { match service_result {
@ -19,17 +15,3 @@ pub fn create_ui(opt: &Opt) -> Result<()> {
Ok(()) Ok(())
} }
/// create a thread for catching input and send them to core loop
pub fn spawn_stdin_channel() -> mpsc::Receiver<termion::event::Key> {
let (tx, rx) = mpsc::channel::<termion::event::Key>();
thread::spawn(move || loop {
let stdin = io::stdin();
for c in stdin.keys() {
tx.send(c.unwrap()).unwrap();
}
});
thread::sleep(std::time::Duration::from_millis(64));
rx
}

View File

@ -1,10 +1,12 @@
use anyhow::Result; use anyhow::Result;
use termion::event::Key; use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal; use tui::Terminal;
use std::sync::mpsc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{io, thread}; use std::{io, thread};
@ -49,37 +51,58 @@ impl std::iter::Iterator for State {
} }
pub enum UiEvent { pub enum UiEvent {
Input(Key),
RefreshOnNewData,
}
pub enum DeferredEvent {
Quit,
NewRepo(String), NewRepo(String),
TagPrevious, TagPrevious,
TagNext, TagNext,
Quit,
} }
impl Ui { impl Ui {
/// create a thread for catching input and send them to core loop
pub fn spawn_input_channel(sender: mpsc::Sender<UiEvent>) {
thread::spawn(move || loop {
let stdin = io::stdin();
for c in stdin.keys() {
sender.send(UiEvent::Input(c.unwrap())).unwrap();
}
});
}
#[tokio::main] #[tokio::main]
pub async fn work_requests(ui: Arc<Mutex<Ui>>, event: std::sync::mpsc::Receiver<UiEvent>) { pub async fn work_requests(
ui: Arc<Mutex<Ui>>,
events: mpsc::Receiver<DeferredEvent>,
sender: mpsc::Sender<UiEvent>,
) {
loop { loop {
match event.recv() { match events.recv() {
Ok(UiEvent::Quit) => break, Ok(DeferredEvent::Quit) => break,
Ok(UiEvent::NewRepo(name)) => { Ok(DeferredEvent::NewRepo(name)) => {
{ {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags = TagList::with_status("fetching new tags..."); ui.tags = TagList::with_status("fetching new tags...");
sender.send(UiEvent::RefreshOnNewData).unwrap();
} }
let list = async_tag_list::TagList::with_repo_name(name).await; let list = async_tag_list::TagList::with_repo_name(name).await;
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags = list; ui.tags = list;
} }
Ok(UiEvent::TagPrevious) => { Ok(DeferredEvent::TagPrevious) => {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags.previous(); ui.tags.previous();
ui.details = ui.tags.create_detail_widget(); ui.details = ui.tags.create_detail_widget();
} }
Ok(UiEvent::TagNext) => { Ok(DeferredEvent::TagNext) => {
let (fetched_new_tags, mut tags) = { let (fetched_new_tags, mut tags) = {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
if ui.tags.at_end_of_list() { if ui.tags.at_end_of_list() {
ui.info.set_text("Fetching more tags..."); ui.info.set_text("Fetching more tags...");
sender.send(UiEvent::RefreshOnNewData).unwrap();
(true, ui.tags.clone()) (true, ui.tags.clone())
} else { } else {
(false, ui.tags.clone()) (false, ui.tags.clone())
@ -98,6 +121,7 @@ impl Ui {
ui.info.set_info(&e); ui.info.set_info(&e);
} }
}; };
sender.send(UiEvent::RefreshOnNewData).unwrap();
} }
} }
@ -113,10 +137,12 @@ impl Ui {
})); }));
// spawn new thread that fetches information async // spawn new thread that fetches information async
let (sender, receiver) = std::sync::mpsc::channel(); let (sender, receiver) = mpsc::channel();
let (deferred_sender, deferred_receiver) = mpsc::channel();
let ui_clone = ui.clone(); let ui_clone = ui.clone();
let sender2 = sender.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
Self::work_requests(ui_clone, receiver); Self::work_requests(ui_clone, deferred_receiver, sender2);
}); });
//setup tui //setup tui
@ -125,7 +151,7 @@ impl Ui {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
//setup input thread //setup input thread
let receiver = super::spawn_stdin_channel(); Self::spawn_input_channel(sender);
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
@ -157,50 +183,56 @@ impl Ui {
rect.render_widget(ui_data.details.render(), more_chunks[1]); rect.render_widget(ui_data.details.render(), more_chunks[1]);
rect.render_widget(ui_data.info.render(), chunks[2]); rect.render_widget(ui_data.info.render(), chunks[2]);
})?; })?;
drop(ui_data);
//handle input //handle input
match receiver.try_recv() { //wait before locking
Ok(Key::Ctrl('q')) => { let event = receiver.recv();
sender.send(UiEvent::Quit)?; let mut ui_data = ui.lock().unwrap();
match event {
Ok(UiEvent::Input(Key::Ctrl('q'))) | Ok(UiEvent::Input(Key::Ctrl('c'))) => {
deferred_sender.send(DeferredEvent::Quit)?;
break 'core; //quit program without saving break 'core; //quit program without saving
} }
Ok(Key::Char('\t')) => { Ok(UiEvent::Input(Key::Char('\t'))) => {
ui_data.state.next(); ui_data.state.next();
let state = ui_data.state.clone(); let state = ui_data.state.clone();
ui_data.info.set_info(&state); ui_data.info.set_info(&state);
} }
Ok(Key::Ctrl('r')) => { Ok(UiEvent::Input(Key::Ctrl('r'))) => {
ui_data.repo.confirm(); ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
Ok(Key::Char('\n')) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Char('\n'))) if ui_data.state == State::EditRepo => {
ui_data.repo.confirm(); ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
Ok(Key::Backspace) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Backspace)) if ui_data.state == State::EditRepo => {
ui_data.info.set_text("Editing Repository"); ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Backspace); ui_data.repo.handle_input(Key::Backspace);
} }
Ok(Key::Up) | Ok(Key::Char('k')) if ui_data.state == State::SelectTag => { Ok(UiEvent::Input(Key::Up)) | Ok(UiEvent::Input(Key::Char('k')))
sender.send(UiEvent::TagPrevious).unwrap(); if ui_data.state == State::SelectTag =>
{
deferred_sender.send(DeferredEvent::TagPrevious).unwrap();
ui_data.details = ui_data.tags.create_detail_widget(); ui_data.details = ui_data.tags.create_detail_widget();
} }
Ok(Key::Down) | Ok(Key::Char('j')) if ui_data.state == State::SelectTag => { Ok(UiEvent::Input(Key::Down)) | Ok(UiEvent::Input(Key::Char('j')))
sender.send(UiEvent::TagNext).unwrap(); if ui_data.state == State::SelectTag =>
{
deferred_sender.send(DeferredEvent::TagNext).unwrap();
ui_data.details = ui_data.tags.create_detail_widget(); ui_data.details = ui_data.tags.create_detail_widget();
} }
Ok(Key::Char(key)) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Char(key))) if ui_data.state == State::EditRepo => {
ui_data.info.set_text("Editing Repository"); ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Char(key)); ui_data.repo.handle_input(Key::Char(key));
} }
_ => {} _ => {}
} }
// release lock of ui for other threads
drop(ui_data);
//sleep for 32ms (30 fps)
thread::sleep(std::time::Duration::from_millis(32));
} }
terminal.clear()?; terminal.clear()?;

View File

@ -1,10 +1,12 @@
use anyhow::Result; use anyhow::Result;
use termion::event::Key; use termion::event::Key;
use termion::input::TermRead;
use termion::raw::IntoRawMode; use termion::raw::IntoRawMode;
use tui::backend::TermionBackend; use tui::backend::TermionBackend;
use tui::layout::{Constraint, Direction, Layout}; use tui::layout::{Constraint, Direction, Layout};
use tui::Terminal; use tui::Terminal;
use std::sync::mpsc;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::{io, thread}; use std::{io, thread};
@ -55,37 +57,58 @@ impl std::iter::Iterator for State {
} }
pub enum UiEvent { pub enum UiEvent {
Input(Key),
RefreshOnNewData,
}
pub enum DeferredEvent {
Quit,
NewRepo(String), NewRepo(String),
TagPrevious, TagPrevious,
TagNext, TagNext,
Quit,
} }
impl Ui { impl Ui {
/// create a thread for catching input and send them to core loop
pub fn spawn_input_channel(sender: mpsc::Sender<UiEvent>) {
thread::spawn(move || loop {
let stdin = io::stdin();
for c in stdin.keys() {
sender.send(UiEvent::Input(c.unwrap())).unwrap();
}
});
}
#[tokio::main] #[tokio::main]
pub async fn work_requests(ui: Arc<Mutex<Ui>>, event: std::sync::mpsc::Receiver<UiEvent>) { pub async fn work_requests(
ui: Arc<Mutex<Ui>>,
events: mpsc::Receiver<DeferredEvent>,
sender: mpsc::Sender<UiEvent>,
) {
loop { loop {
match event.recv() { match events.recv() {
Ok(UiEvent::Quit) => break, Ok(DeferredEvent::Quit) => break,
Ok(UiEvent::NewRepo(name)) => { Ok(DeferredEvent::NewRepo(name)) => {
{ {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags = TagList::with_status("Fetching new tags..."); ui.tags = TagList::with_status("Fetching new tags...");
sender.send(UiEvent::RefreshOnNewData).unwrap();
} }
let list = TagList::with_repo_name(name).await; let list = TagList::with_repo_name(name).await;
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags = list; ui.tags = list;
} }
Ok(UiEvent::TagPrevious) => { Ok(DeferredEvent::TagPrevious) => {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
ui.tags.previous(); ui.tags.previous();
ui.details = ui.tags.create_detail_widget(); ui.details = ui.tags.create_detail_widget();
} }
Ok(UiEvent::TagNext) => { Ok(DeferredEvent::TagNext) => {
let (fetched_new_tags, mut tags) = { let (fetched_new_tags, mut tags) = {
let mut ui = ui.lock().unwrap(); let mut ui = ui.lock().unwrap();
if ui.tags.at_end_of_list() { if ui.tags.at_end_of_list() {
ui.info.set_text("Fetching more tags..."); ui.info.set_text("Fetching more tags...");
sender.send(UiEvent::RefreshOnNewData).unwrap();
(true, ui.tags.clone()) (true, ui.tags.clone())
} else { } else {
(false, ui.tags.clone()) (false, ui.tags.clone())
@ -104,6 +127,7 @@ impl Ui {
ui.info.set_info(&e); ui.info.set_info(&e);
} }
}; };
sender.send(UiEvent::RefreshOnNewData).unwrap();
} }
} }
@ -120,10 +144,12 @@ impl Ui {
})); }));
// spawn new thread that fetches information async // spawn new thread that fetches information async
let (sender, receiver) = std::sync::mpsc::channel(); let (sender, receiver) = mpsc::channel();
let (deferred_sender, deferred_receiver) = mpsc::channel();
let ui_clone = ui.clone(); let ui_clone = ui.clone();
let sender2 = sender.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
Self::work_requests(ui_clone, receiver); Self::work_requests(ui_clone, deferred_receiver, sender2);
}); });
//setup tui //setup tui
@ -132,12 +158,12 @@ impl Ui {
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
//setup input thread //setup input thread
let receiver = super::spawn_stdin_channel(); Self::spawn_input_channel(sender);
//core interaction loop //core interaction loop
'core: loop { 'core: loop {
let mut ui_data = ui.lock().unwrap();
//draw //draw
let mut ui_data = ui.lock().unwrap();
terminal.draw(|rect| { terminal.draw(|rect| {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@ -175,30 +201,36 @@ impl Ui {
rect.render_widget(ui_data.details.render(), more_chunks[2]); rect.render_widget(ui_data.details.render(), more_chunks[2]);
rect.render_widget(ui_data.info.render(), chunks[2]); rect.render_widget(ui_data.info.render(), chunks[2]);
})?; })?;
drop(ui_data);
//handle input //handle events
match receiver.try_recv() { //wait before locking
Ok(Key::Ctrl('q')) => { let event = receiver.recv();
sender.send(UiEvent::Quit)?; let mut ui_data = ui.lock().unwrap();
match event {
Ok(UiEvent::Input(Key::Ctrl('q'))) | Ok(UiEvent::Input(Key::Ctrl('c'))) => {
deferred_sender.send(DeferredEvent::Quit)?;
break 'core; //quit program without saving break 'core; //quit program without saving
} }
Ok(Key::Char('\t')) => { Ok(UiEvent::Input(Key::Char('\t'))) => {
ui_data.state.next(); ui_data.state.next();
let state = ui_data.state.clone(); let state = ui_data.state.clone();
ui_data.info.set_info(&state); ui_data.info.set_info(&state);
} }
Ok(Key::Ctrl('s')) => match ui_data.services.save() { Ok(UiEvent::Input(Key::Ctrl('s'))) => match ui_data.services.save() {
Err(e) => { Err(e) => {
ui_data.info.set_info(&format!("{}", e)); ui_data.info.set_info(&format!("{}", e));
continue; continue;
} }
Ok(_) => ui_data.info.set_text("Saved compose file"), Ok(_) => ui_data.info.set_text("Saved compose file"),
}, },
Ok(Key::Ctrl('r')) => { Ok(UiEvent::Input(Key::Ctrl('r'))) => {
ui_data.repo.confirm(); ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
Ok(Key::Char('\n')) if ui_data.state == State::SelectTag => { Ok(UiEvent::Input(Key::Char('\n'))) if ui_data.state == State::SelectTag => {
let mut repo = ui_data.repo.get(); let mut repo = ui_data.repo.get();
let tag = match ui_data.tags.get_selected() { let tag = match ui_data.tags.get_selected() {
Err(async_tag_list::Error::NextPageSelected) => continue, Err(async_tag_list::Error::NextPageSelected) => continue,
@ -212,15 +244,17 @@ impl Ui {
repo.push_str(&tag); repo.push_str(&tag);
ui_data.services.change_current_line(repo); ui_data.services.change_current_line(repo);
} }
Ok(Key::Char('\n')) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Char('\n'))) if ui_data.state == State::EditRepo => {
ui_data.repo.confirm(); ui_data.repo.confirm();
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
Ok(Key::Backspace) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Backspace)) if ui_data.state == State::EditRepo => {
ui_data.info.set_text("Editing Repository"); ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Backspace); ui_data.repo.handle_input(Key::Backspace);
} }
Ok(Key::Up) | Ok(Key::Char('k')) Ok(UiEvent::Input(Key::Up)) | Ok(UiEvent::Input(Key::Char('k')))
if ui_data.state == State::SelectService if ui_data.state == State::SelectService
&& ui_data.services.find_previous_match() => && ui_data.services.find_previous_match() =>
{ {
@ -235,11 +269,13 @@ impl Ui {
Ok(s) => s, Ok(s) => s,
}; };
ui_data.repo.set(repo.to_string()); ui_data.repo.set(repo.to_string());
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
} }
} }
Ok(Key::Down) | Ok(Key::Char('j')) Ok(UiEvent::Input(Key::Down)) | Ok(UiEvent::Input(Key::Char('j')))
if ui_data.state == State::SelectService if ui_data.state == State::SelectService
&& ui_data.services.find_next_match() => && ui_data.services.find_next_match() =>
{ {
@ -254,27 +290,30 @@ impl Ui {
Ok(s) => s, Ok(s) => s,
}; };
ui_data.repo.set(repo.to_string()); ui_data.repo.set(repo.to_string());
sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); deferred_sender
.send(DeferredEvent::NewRepo(ui_data.repo.get()))
.unwrap();
} }
} }
} }
Ok(Key::Up) | Ok(Key::Char('k')) if ui_data.state == State::SelectTag => { Ok(UiEvent::Input(Key::Up)) | Ok(UiEvent::Input(Key::Char('k')))
sender.send(UiEvent::TagPrevious).unwrap(); if ui_data.state == State::SelectTag =>
{
deferred_sender.send(DeferredEvent::TagPrevious).unwrap();
} }
Ok(Key::Down) | Ok(Key::Char('j')) if ui_data.state == State::SelectTag => { Ok(UiEvent::Input(Key::Down)) | Ok(UiEvent::Input(Key::Char('j')))
sender.send(UiEvent::TagNext).unwrap(); if ui_data.state == State::SelectTag =>
{
deferred_sender.send(DeferredEvent::TagNext).unwrap();
} }
Ok(Key::Char(key)) if ui_data.state == State::EditRepo => { Ok(UiEvent::Input(Key::Char(key))) if ui_data.state == State::EditRepo => {
ui_data.info.set_text("Editing Repository"); ui_data.info.set_text("Editing Repository");
ui_data.repo.handle_input(Key::Char(key)); ui_data.repo.handle_input(Key::Char(key));
} }
_ => (), Ok(UiEvent::Input(_)) => {}
Ok(UiEvent::RefreshOnNewData) => {}
Err(_) => {}
} }
drop(ui_data);
//sleep for 32ms (30 fps)
thread::sleep(std::time::Duration::from_millis(32));
} }
terminal.clear()?; terminal.clear()?;