From 87039541a5aecc75eb703a397f151b268a03d5bd Mon Sep 17 00:00:00 2001 From: Thomas Eppers Date: Tue, 31 Jan 2023 23:51:45 +0100 Subject: [PATCH] rewrote parts to make it more async --- Cargo.lock | 152 +++++++--- Cargo.toml | 3 +- src/repository/dockerhub.rs | 10 +- src/repository/mod.rs | 13 +- src/ui/default.rs | 225 -------------- src/ui/mod.rs | 8 +- src/ui/no_yaml.rs | 161 ---------- src/ui/no_yaml_found.rs | 225 ++++++++++++++ src/ui/yaml_found.rs | 285 ++++++++++++++++++ src/widget/{tag_list.rs => async_tag_list.rs} | 48 +-- src/widget/mod.rs | 2 +- src/widget/repo_entry.rs | 2 +- 12 files changed, 661 insertions(+), 473 deletions(-) delete mode 100644 src/ui/default.rs delete mode 100644 src/ui/no_yaml.rs create mode 100644 src/ui/no_yaml_found.rs create mode 100644 src/ui/yaml_found.rs rename src/widget/{tag_list.rs => async_tag_list.rs} (83%) diff --git a/Cargo.lock b/Cargo.lock index 11eaf1c..2dc83df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,16 +32,16 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi", ] [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" @@ -184,12 +184,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" -[[package]] -name = "futures-io" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" - [[package]] name = "futures-sink" version = "0.3.16" @@ -210,12 +204,9 @@ checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" dependencies = [ "autocfg", "futures-core", - "futures-io", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -226,7 +217,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", ] [[package]] @@ -272,6 +263,15 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "http" version = "0.2.4" @@ -393,9 +393,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.99" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "log" @@ -426,24 +426,14 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.13" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] @@ -464,15 +454,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", -] - [[package]] name = "num-integer" version = "0.1.44" @@ -494,11 +475,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] @@ -691,6 +672,7 @@ dependencies = [ "structopt", "termion", "thiserror", + "tokio", "tui", ] @@ -845,9 +827,9 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "socket2" -version = "0.4.1" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -956,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -977,9 +959,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", @@ -988,7 +970,20 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "winapi", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1139,6 +1134,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.75" @@ -1239,6 +1240,63 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winreg" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index 7a751ae..6013370 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [dependencies] serde = { version = "1.0.127", features = ["derive"] } serde_json = "1.0.66" -reqwest = { version = "0.11.4", features = ["blocking", "json"] } +reqwest = { version = "0.11.4", features = ["json"] } chrono = "0.4.19" tui = "0.16" termion = "1.5" @@ -18,6 +18,7 @@ lazy_static = "1.4.0" structopt = "0.3.23" thiserror = "1.0.32" anyhow = "1.0.59" +tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] } [profile.release] lto = "yes" diff --git a/src/repository/dockerhub.rs b/src/repository/dockerhub.rs index 890fde5..6f2d331 100644 --- a/src/repository/dockerhub.rs +++ b/src/repository/dockerhub.rs @@ -46,17 +46,17 @@ pub struct DockerHub { impl DockerHub { /// fetches tag information with a repository name in the form of organization/repository or library/repository in the case of official images from docker - pub fn create_repo(repo: &str) -> Result { + pub async fn create_repo(repo: &str) -> Result { let request = format!("https://hub.docker.com/v2/repositories/{}/tags", repo); - Self::with_url(&request) + Self::with_url(&request).await } /// fetches tag information from a url - pub fn with_url(url: &str) -> Result { - let response = reqwest::blocking::get(url)?; + pub async fn with_url(url: &str) -> Result { + let response = reqwest::get(url).await?; //convert it to json - let tags = response.json::()?; + let tags = response.json::().await?; if tags.results.is_empty() { return Err(Error::NoTagsFound); } diff --git a/src/repository/mod.rs b/src/repository/mod.rs index b862944..7ba22a4 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -46,13 +46,14 @@ impl Tag { } } +#[derive(Clone)] pub struct Repo { tags: Vec, next_page: Option, } impl Repo { - pub fn new(repo: &str) -> Result { + pub async fn new(repo: &str) -> Result { use crate::repo::Repo; let (registry, repo) = match crate::repo::split_repo_without_tag(repo) { Ok(Repo::WithServer(reg, org, pro)) => (Some(reg), format!("{}/{}", org, pro)), @@ -62,7 +63,7 @@ impl Repo { }; if registry.unwrap_or_default().is_empty() { - dockerhub::DockerHub::create_repo(&repo) + dockerhub::DockerHub::create_repo(&repo).await } else { Err(Error::Converting( "This registry is not supported yet".into(), @@ -70,18 +71,18 @@ impl Repo { } } - pub fn with_url(url: &str) -> Result { + pub async fn with_url(url: &str) -> Result { //TODO fix for other registries - dockerhub::DockerHub::with_url(url) + dockerhub::DockerHub::with_url(url).await } pub fn get_tags(&self) -> &Vec { &self.tags } - pub fn next_page(&self) -> Option { + pub async fn next_page(&self) -> Option { if let Some(url) = &self.next_page { - match Self::with_url(url) { + match Self::with_url(url).await { Ok(tags) => return Some(tags), Err(e) => println!("Encountered error: {e}"), } diff --git a/src/ui/default.rs b/src/ui/default.rs deleted file mode 100644 index 472e0b2..0000000 --- a/src/ui/default.rs +++ /dev/null @@ -1,225 +0,0 @@ -use anyhow::Result; -use termion::event::Key; -use termion::raw::IntoRawMode; -use tui::backend::TermionBackend; -use tui::layout::{Constraint, Direction, Layout}; -use tui::Terminal; - -use std::{io, thread}; - -use crate::repository; -use crate::widget::info; -use crate::widget::repo_entry; -use crate::widget::service_switcher; -use crate::widget::tag_list; -use crate::Opt; - -pub struct Ui { - state: State, - repo: crate::widget::repo_entry::RepoEntry, - tags: crate::widget::tag_list::TagList, - services: crate::widget::service_switcher::ServiceSwitcher, - details: crate::widget::details::Details, - info: crate::widget::info::Info, -} - -#[derive(PartialEq, Clone)] -pub enum State { - EditRepo, - SelectTag, - SelectService, -} - -impl std::fmt::Display for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::EditRepo => write!(f, "Edit repository"), - State::SelectTag => write!(f, "Select a tag"), - State::SelectService => write!(f, "Select a image"), - } - } -} - -impl std::iter::Iterator for State { - type Item = Self; - - fn next(&mut self) -> Option { - match self { - State::EditRepo => *self = State::SelectTag, - State::SelectTag => *self = State::SelectService, - State::SelectService => *self = State::EditRepo, - } - Some(self.clone()) - } -} - -impl Ui { - pub fn run(opt: &Opt, switcher: service_switcher::ServiceSwitcher) -> Result<()> { - let repo_id = opt.repo.as_deref(); - - let mut ui = Ui { - state: State::SelectService, - repo: repo_entry::RepoEntry::new(repo_id), - tags: tag_list::TagList::with_status("Tags are empty"), - services: switcher, - details: crate::widget::details::Details::new(), - info: info::Info::new("Select image of edit Repository"), - }; - - if opt.repo.is_none() { - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - - //setup tui - let stdout = io::stdout().into_raw_mode()?; - let backend = TermionBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - //setup input thread - let receiver = super::spawn_stdin_channel(); - - //core interaction loop - 'core: loop { - //draw - terminal.draw(|rect| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(10), - Constraint::Length(3), - Constraint::Min(7), - Constraint::Length(2), - ] - .as_ref(), - ) - .split(rect.size()); - - let (list, state) = ui.services.render(ui.state == State::SelectService); - rect.render_stateful_widget(list, chunks[0], state); - rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[1]); - let (list, state) = ui.tags.render(ui.state == State::SelectTag); - let more_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(15), Constraint::Length(28)].as_ref()) - .split(chunks[2]); - rect.render_stateful_widget(list, more_chunks[0], state); - rect.render_widget(ui.details.render(), more_chunks[1]); - rect.render_widget(ui.info.render(), chunks[3]); - })?; - - //handle input - match receiver.try_recv() { - Ok(Key::Ctrl('q')) => break 'core, //quit program without saving - Ok(Key::Char('\t')) => { - ui.state.next(); - ui.info.set_info(&ui.state); - } - Ok(Key::Ctrl('s')) => match ui.services.save() { - Err(e) => { - ui.info.set_info(&format!("{}", e)); - continue; - } - Ok(_) => ui.info.set_text("Saved compose file"), - }, - Ok(Key::Ctrl('r')) => { - ui.repo.confirm(); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - Ok(Key::Char('\n')) => match ui.state { - State::EditRepo => { - ui.repo.confirm(); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - State::SelectTag => { - let mut repo = ui.repo.get(); - let tag = match ui.tags.get_selected() { - Err(tag_list::Error::NextPageSelected) => continue, - Err(e) => { - ui.info.set_info(&format!("{}", e)); - continue; - } - Ok(tag) => tag, - }; - repo.push(':'); - repo.push_str(&tag); - ui.services.change_current_line(repo); - } - _ => (), - }, - Ok(Key::Char(key)) => match ui.state { - State::SelectService => (), - State::EditRepo => { - ui.info.set_text("Editing Repository"); - ui.repo.handle_input(Key::Char(key)); - } - State::SelectTag => (), - }, - Ok(Key::Backspace) => match ui.state { - State::SelectService => (), - State::EditRepo => { - ui.info.set_text("Editing Repository"); - ui.repo.handle_input(Key::Backspace); - } - State::SelectTag => (), - }, - Ok(Key::Up) => match ui.state { - State::SelectService if ui.services.find_previous_match() => { - match ui.services.extract_repo() { - Err(e) => ui.info.set_info(&format!("{}", e)), - Ok(s) => { - let repo = match repository::check_repo(&s) { - Err(e) => { - ui.info.set_info(&format!("{}", e)); - continue; - } - Ok(s) => s, - }; - ui.repo.set(repo.to_string()); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - } - } - State::SelectService => (), - State::EditRepo => (), - State::SelectTag => { - ui.tags.handle_input(Key::Up); - ui.details = ui.tags.create_detail_widget(); - } - }, - Ok(Key::Down) => match ui.state { - State::SelectService if ui.services.find_next_match() => { - match ui.services.extract_repo() { - Err(e) => ui.info.set_info(&format!("{}", e)), - Ok(s) => { - let repo = match repository::check_repo(&s) { - Err(e) => { - ui.info.set_info(&format!("{}", e)); - continue; - } - Ok(s) => s, - }; - ui.repo.set(repo.to_string()); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - } - } - State::SelectService => (), - State::EditRepo => (), - State::SelectTag => { - ui.tags.handle_input(Key::Down); - ui.details = ui.tags.create_detail_widget(); - } - }, - _ => (), - } - - //sleep for 32ms (30 fps) - thread::sleep(std::time::Duration::from_millis(32)); - } - - terminal.clear()?; - - Ok(()) - } -} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6e91e89..ed59a03 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,5 @@ -mod default; -mod no_yaml; +mod no_yaml_found; +mod yaml_found; use anyhow::Result; use termion::input::TermRead; @@ -13,8 +13,8 @@ use std::{io, thread}; pub fn create_ui(opt: &Opt) -> Result<()> { let service_result = service_switcher::ServiceSwitcher::new(&opt.file); match service_result { - None => no_yaml::NoYaml::run(opt), - Some(switcher) => default::Ui::run(opt, switcher), + Some(switcher) => yaml_found::Ui::run(opt, switcher), + _ => no_yaml_found::Ui::run(opt), }?; Ok(()) diff --git a/src/ui/no_yaml.rs b/src/ui/no_yaml.rs deleted file mode 100644 index 4826102..0000000 --- a/src/ui/no_yaml.rs +++ /dev/null @@ -1,161 +0,0 @@ -use anyhow::Result; -use termion::event::Key; -use termion::raw::IntoRawMode; -use tui::backend::TermionBackend; -use tui::layout::{Constraint, Direction, Layout}; -use tui::Terminal; - -use std::{io, thread}; - -use crate::widget::details; -use crate::widget::info; -use crate::widget::repo_entry; -use crate::widget::tag_list; -use crate::Opt; - -#[derive(PartialEq, Clone)] -pub enum State { - EditRepo, - SelectTag, -} - -impl std::fmt::Display for State { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - State::EditRepo => write!(f, "Edit repository"), - State::SelectTag => write!(f, "Select a tag"), - } - } -} - -impl std::iter::Iterator for State { - type Item = Self; - - fn next(&mut self) -> Option { - match self { - State::EditRepo => *self = State::SelectTag, - State::SelectTag => *self = State::EditRepo, - } - Some(self.clone()) - } -} - -pub struct NoYaml { - state: State, - repo: repo_entry::RepoEntry, - tags: tag_list::TagList, - details: details::Details, - info: info::Info, -} - -impl NoYaml { - pub fn run(opt: &Opt) -> Result<()> { - let repo_id = opt.repo.as_deref(); - - let mut ui = NoYaml { - state: State::EditRepo, - repo: repo_entry::RepoEntry::new(repo_id), - tags: tag_list::TagList::with_status("Tags are empty"), - details: details::Details::new(), - info: info::Info::new("could not find a docker-compose file"), - }; - - // load tags if a repository was given thorugh paramter - if opt.repo.is_none() { - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - - //setup tui - let stdout = io::stdout().into_raw_mode()?; - let backend = TermionBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - //setup input thread - let receiver = super::spawn_stdin_channel(); - - //core interaction loop - 'core: loop { - //draw - terminal.draw(|rect| { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(3), - Constraint::Min(7), - Constraint::Length(2), - ] - .as_ref(), - ) - .split(rect.size()); - - rect.render_widget(ui.repo.render(ui.state == State::EditRepo), chunks[0]); - let (list, state) = ui.tags.render(ui.state == State::SelectTag); - let more_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Min(15), Constraint::Length(28)].as_ref()) - .split(chunks[1]); - rect.render_stateful_widget(list, more_chunks[0], state); - rect.render_widget(ui.details.render(), more_chunks[1]); - rect.render_widget(ui.info.render(), chunks[2]); - })?; - - //handle input - match receiver.try_recv() { - Ok(Key::Ctrl('q')) => break 'core, - Ok(Key::Char('\t')) => { - ui.state.next(); - ui.info.set_info(&ui.state); - } - Ok(Key::Ctrl('r')) => { - ui.repo.confirm(); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - Ok(Key::Char('\n')) => match ui.state { - State::EditRepo => { - ui.repo.confirm(); - ui.tags = tag_list::TagList::with_repo_name(ui.repo.get()); - } - State::SelectTag => ui.tags.handle_input(Key::Char('\n')), - }, - Ok(Key::Char(key)) => match ui.state { - State::EditRepo => { - ui.info.set_text("Editing Repository"); - ui.repo.handle_input(Key::Char(key)); - } - State::SelectTag => { - ui.tags.handle_input(Key::Char(key)); - } - }, - Ok(Key::Backspace) => match ui.state { - State::EditRepo => { - ui.info.set_text("Editing Repository"); - ui.repo.handle_input(Key::Backspace); - } - State::SelectTag => (), - }, - Ok(Key::Up) => match ui.state { - State::EditRepo => (), - State::SelectTag => { - ui.tags.handle_input(Key::Up); - ui.details = ui.tags.create_detail_widget(); - } - }, - Ok(Key::Down) => match ui.state { - State::EditRepo => (), - State::SelectTag => { - ui.tags.handle_input(Key::Down); - ui.details = ui.tags.create_detail_widget(); - } - }, - _ => (), - } - - //sleep for 32ms (30 fps) - thread::sleep(std::time::Duration::from_millis(32)); - } - - terminal.clear()?; - Ok(()) - } -} diff --git a/src/ui/no_yaml_found.rs b/src/ui/no_yaml_found.rs new file mode 100644 index 0000000..e307cd6 --- /dev/null +++ b/src/ui/no_yaml_found.rs @@ -0,0 +1,225 @@ +use anyhow::Result; +use termion::event::Key; +use termion::raw::IntoRawMode; +use tui::backend::TermionBackend; +use tui::layout::{Constraint, Direction, Layout}; +use tui::Terminal; + +use std::sync::{Arc, Mutex}; +use std::{io, thread}; + +use crate::widget::async_tag_list; +use crate::widget::info; +use crate::widget::repo_entry; +use crate::Opt; + +pub struct Ui { + state: State, + repo: repo_entry::RepoEntry, + tags: async_tag_list::TagList, + details: crate::widget::details::Details, + info: info::Info, +} + +#[derive(PartialEq, Clone)] +pub enum State { + EditRepo, + SelectTag, +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::EditRepo => write!(f, "Edit repository"), + State::SelectTag => write!(f, "Select a tag"), + } + } +} + +impl std::iter::Iterator for State { + type Item = Self; + + fn next(&mut self) -> Option { + match self { + State::EditRepo => *self = State::SelectTag, + State::SelectTag => *self = State::EditRepo, + } + Some(self.clone()) + } +} + +pub enum UiEvent { + NewRepo(String), + TagInput(termion::event::Key), + Quit, +} + +impl Ui { + #[tokio::main] + pub async fn work_requests(ui: Arc>, event: std::sync::mpsc::Receiver) { + loop { + match event.recv() { + Ok(UiEvent::Quit) => break, + Ok(UiEvent::NewRepo(name)) => { + let list = async_tag_list::TagList::with_repo_name(name).await; + let mut ui = ui.lock().unwrap(); + ui.tags = list; + } + Ok(UiEvent::TagInput(key)) => { + let mut tags = { + let ui_data = ui.lock().unwrap(); + ui_data.tags.clone() + }; + tags.handle_input(key).await; + let mut ui = ui.lock().unwrap(); + ui.tags = tags; + } + Err(e) => { + let mut ui = ui.lock().unwrap(); + ui.info.set_info(&e); + } + }; + } + } + + pub fn run(opt: &Opt) -> Result<()> { + let repo_id = opt.repo.as_deref(); + + let ui = Arc::new(Mutex::new(Ui { + state: State::EditRepo, + repo: repo_entry::RepoEntry::new(repo_id), + tags: async_tag_list::TagList::with_status("no tags"), + details: crate::widget::details::Details::new(), + info: info::Info::new("Select image or edit Repository"), + })); + + // spawn new thread that fetches information async + let (sender, receiver) = std::sync::mpsc::channel(); + let ui_clone = ui.clone(); + std::thread::spawn(move || { + Self::work_requests(ui_clone, receiver); + }); + + //setup tui + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + //setup input thread + let receiver = super::spawn_stdin_channel(); + + //core interaction loop + 'core: loop { + let mut ui_data = ui.lock().unwrap(); + //draw + terminal.draw(|rect| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(3), + Constraint::Min(7), + Constraint::Length(2), + ] + .as_ref(), + ) + .split(rect.size()); + rect.render_widget( + ui_data.repo.render(ui_data.state == State::EditRepo), + chunks[0], + ); + let render_state = ui_data.state == State::SelectTag; + let (tags, state) = ui_data.tags.render(render_state); + let more_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(15), Constraint::Length(28)].as_ref()) + .split(chunks[1]); + rect.render_stateful_widget(tags, more_chunks[0], state); + rect.render_widget(ui_data.details.render(), more_chunks[1]); + rect.render_widget(ui_data.info.render(), chunks[2]); + })?; + + //handle input + match receiver.try_recv() { + Ok(Key::Ctrl('q')) => { + sender.send(UiEvent::Quit)?; + break 'core; //quit program without saving + } + Ok(Key::Char('\t')) => { + ui_data.state.next(); + let state = ui_data.state.clone(); + ui_data.info.set_info(&state); + } + Ok(Key::Ctrl('r')) => { + ui_data.repo.confirm(); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + Ok(Key::Char('\n')) => match ui_data.state { + State::EditRepo => { + ui_data.repo.confirm(); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + State::SelectTag => {} // { + // let mut repo = ui_data.repo.get(); + // let tag = match ui_data.tags.get_selected() { + // Err(async_tag_list::Error::NextPageSelected) => continue, + // Err(e) => { + // ui_data.info.set_info(&format!("{}", e)); + // continue; + // } + // Ok(tag) => tag, + // }; + // repo.push(':'); + // repo.push_str(&tag); + // ui_data.services.change_current_line(repo); + // } + }, + Ok(Key::Char(key)) => match ui_data.state { + State::EditRepo => { + ui_data.info.set_text("Editing Repository"); + ui_data.repo.handle_input(Key::Char(key)); + } + State::SelectTag => {} + }, + Ok(Key::Backspace) => match ui_data.state { + State::EditRepo => { + ui_data.info.set_text("Editing Repository"); + ui_data.repo.handle_input(Key::Backspace); + } + State::SelectTag => {} + }, + Ok(Key::Up) => { + let state = ui_data.state.clone(); + match state { + State::EditRepo => {} + State::SelectTag => { + sender.send(UiEvent::TagInput(Key::Up)).unwrap(); + ui_data.details = ui_data.tags.create_detail_widget(); + } + } + } + Ok(Key::Down) => { + let state = ui_data.state.clone(); + match state { + State::EditRepo => {} + State::SelectTag => { + sender.send(UiEvent::TagInput(Key::Down)).unwrap(); + ui_data.details = ui_data.tags.create_detail_widget(); + } + } + } + _ => {} + } + + // 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()?; + + Ok(()) + } +} diff --git a/src/ui/yaml_found.rs b/src/ui/yaml_found.rs new file mode 100644 index 0000000..82b925c --- /dev/null +++ b/src/ui/yaml_found.rs @@ -0,0 +1,285 @@ +use anyhow::Result; +use termion::event::Key; +use termion::raw::IntoRawMode; +use tui::backend::TermionBackend; +use tui::layout::{Constraint, Direction, Layout}; +use tui::Terminal; + +use std::sync::{Arc, Mutex}; +use std::{io, thread}; + +use crate::repository; +use crate::widget::async_tag_list; +use crate::widget::info; +use crate::widget::repo_entry; +use crate::widget::service_switcher; +use crate::Opt; + +pub struct Ui { + state: State, + repo: repo_entry::RepoEntry, + tags: async_tag_list::TagList, + services: service_switcher::ServiceSwitcher, + details: crate::widget::details::Details, + info: info::Info, +} + +#[derive(PartialEq, Clone)] +pub enum State { + EditRepo, + SelectTag, + SelectService, +} + +impl std::fmt::Display for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + State::EditRepo => write!(f, "Edit repository"), + State::SelectTag => write!(f, "Select a tag"), + State::SelectService => write!(f, "Select a image"), + } + } +} + +impl std::iter::Iterator for State { + type Item = Self; + + fn next(&mut self) -> Option { + match self { + State::EditRepo => *self = State::SelectTag, + State::SelectTag => *self = State::SelectService, + State::SelectService => *self = State::EditRepo, + } + Some(self.clone()) + } +} + +pub enum UiEvent { + NewRepo(String), + TagInput(termion::event::Key), + Quit, +} + +impl Ui { + #[tokio::main] + pub async fn work_requests(ui: Arc>, event: std::sync::mpsc::Receiver) { + loop { + match event.recv() { + Ok(UiEvent::Quit) => break, + Ok(UiEvent::NewRepo(name)) => { + let list = async_tag_list::TagList::with_repo_name(name).await; + let mut ui = ui.lock().unwrap(); + ui.tags = list; + } + Ok(UiEvent::TagInput(key)) => { + let mut tags = { + let ui_data = ui.lock().unwrap(); + ui_data.tags.clone() + }; + tags.handle_input(key).await; + let mut ui = ui.lock().unwrap(); + ui.tags = tags; + } + Err(e) => { + let mut ui = ui.lock().unwrap(); + ui.info.set_info(&e); + } + }; + } + } + + pub fn run(opt: &Opt, switcher: service_switcher::ServiceSwitcher) -> Result<()> { + let repo_id = opt.repo.as_deref(); + + let ui = Arc::new(Mutex::new(Ui { + state: State::SelectService, + repo: repo_entry::RepoEntry::new(repo_id), + tags: async_tag_list::TagList::with_status("no tags"), + services: switcher, + details: crate::widget::details::Details::new(), + info: info::Info::new("Select image or edit Repository"), + })); + + // spawn new thread that fetches information async + let (sender, receiver) = std::sync::mpsc::channel(); + let ui_clone = ui.clone(); + std::thread::spawn(move || { + Self::work_requests(ui_clone, receiver); + }); + + //setup tui + let stdout = io::stdout().into_raw_mode()?; + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + //setup input thread + let receiver = super::spawn_stdin_channel(); + + //core interaction loop + 'core: loop { + let mut ui_data = ui.lock().unwrap(); + //draw + terminal.draw(|rect| { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Length(10), + Constraint::Min(7), + Constraint::Length(2), + ] + .as_ref(), + ) + .split(rect.size()); + + let render_state = ui_data.state == State::SelectService; + let (file, state) = ui_data.services.render(render_state); + rect.render_stateful_widget(file, chunks[0], state); + let more_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + Constraint::Ratio(1, 3), + ] + .as_ref(), + ) + .split(chunks[1]); + rect.render_widget( + ui_data.repo.render(ui_data.state == State::EditRepo), + more_chunks[0], + ); + let render_state = ui_data.state == State::SelectTag; + let (tags, state) = ui_data.tags.render(render_state); + rect.render_stateful_widget(tags, more_chunks[1], state); + rect.render_widget(ui_data.details.render(), more_chunks[2]); + rect.render_widget(ui_data.info.render(), chunks[2]); + })?; + + //handle input + match receiver.try_recv() { + Ok(Key::Ctrl('q')) => { + sender.send(UiEvent::Quit)?; + break 'core; //quit program without saving + } + Ok(Key::Char('\t')) => { + ui_data.state.next(); + let state = ui_data.state.clone(); + ui_data.info.set_info(&state); + } + Ok(Key::Ctrl('s')) => match ui_data.services.save() { + Err(e) => { + ui_data.info.set_info(&format!("{}", e)); + continue; + } + Ok(_) => ui_data.info.set_text("Saved compose file"), + }, + Ok(Key::Ctrl('r')) => { + ui_data.repo.confirm(); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + Ok(Key::Char('\n')) => match ui_data.state { + State::EditRepo => { + ui_data.repo.confirm(); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + State::SelectTag => { + let mut repo = ui_data.repo.get(); + let tag = match ui_data.tags.get_selected() { + Err(async_tag_list::Error::NextPageSelected) => continue, + Err(e) => { + ui_data.info.set_info(&format!("{}", e)); + continue; + } + Ok(tag) => tag, + }; + repo.push(':'); + repo.push_str(&tag); + ui_data.services.change_current_line(repo); + } + _ => (), + }, + Ok(Key::Char(key)) => match ui_data.state { + State::SelectService => (), + State::EditRepo => { + ui_data.info.set_text("Editing Repository"); + ui_data.repo.handle_input(Key::Char(key)); + } + State::SelectTag => (), + }, + Ok(Key::Backspace) => match ui_data.state { + State::SelectService => (), + State::EditRepo => { + ui_data.info.set_text("Editing Repository"); + ui_data.repo.handle_input(Key::Backspace); + } + State::SelectTag => (), + }, + Ok(Key::Up) => { + let state = ui_data.state.clone(); + match state { + State::SelectService if ui_data.services.find_previous_match() => { + match ui_data.services.extract_repo() { + Err(e) => ui_data.info.set_info(&format!("{}", e)), + Ok(s) => { + let repo = match repository::check_repo(&s) { + Err(e) => { + ui_data.info.set_info(&format!("{}", e)); + continue; + } + Ok(s) => s, + }; + ui_data.repo.set(repo.to_string()); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + } + } + State::SelectService | State::EditRepo => (), + State::SelectTag => { + sender.send(UiEvent::TagInput(Key::Up)).unwrap(); + ui_data.details = ui_data.tags.create_detail_widget(); + } + } + } + Ok(Key::Down) => { + let state = ui_data.state.clone(); + match state { + State::SelectService if ui_data.services.find_next_match() => { + match ui_data.services.extract_repo() { + Err(e) => ui_data.info.set_info(&format!("{}", e)), + Ok(s) => { + let repo = match repository::check_repo(&s) { + Err(e) => { + ui_data.info.set_info(&format!("{}", e)); + continue; + } + Ok(s) => s, + }; + ui_data.repo.set(repo.to_string()); + sender.send(UiEvent::NewRepo(ui_data.repo.get())).unwrap(); + } + } + } + State::SelectService => (), + State::EditRepo => (), + State::SelectTag => { + sender.send(UiEvent::TagInput(Key::Down)).unwrap(); + ui_data.details = ui_data.tags.create_detail_widget(); + } + } + } + _ => (), + } + + drop(ui_data); + + //sleep for 32ms (30 fps) + thread::sleep(std::time::Duration::from_millis(32)); + } + + terminal.clear()?; + + Ok(()) + } +} diff --git a/src/widget/tag_list.rs b/src/widget/async_tag_list.rs similarity index 83% rename from src/widget/tag_list.rs rename to src/widget/async_tag_list.rs index 9019300..8e0d280 100644 --- a/src/widget/tag_list.rs +++ b/src/widget/async_tag_list.rs @@ -22,6 +22,7 @@ impl fmt::Display for Error { } } +#[derive(Clone)] enum Line { Status(String), Image(repository::Tag), @@ -38,6 +39,7 @@ impl fmt::Display for Line { } } +#[derive(Clone)] pub struct TagList { lines: Vec, state: ListState, @@ -55,22 +57,22 @@ impl TagList { } /// list the tags of the repository if the input is valid - pub fn with_repo_name(repo: String) -> Self { - match repository::Repo::new(&repo) { - Ok(tags) => Self::with_tags(tags), + pub async fn with_repo_name(repo: String) -> Self { + match repository::Repo::new(&repo).await { + Ok(tags) => Self::with_tags(tags).await, Err(_) => Self::with_status("input repo was not found"), } } /// list the tags of the input - fn with_tags(mut tags: repository::Repo) -> Self { + async fn with_tags(mut tags: repository::Repo) -> Self { let mut lines: Vec = tags .get_tags() .iter() .map(|r| Line::Image(r.clone())) .collect(); - match tags.next_page() { + match tags.next_page().await { None => (), Some(new_tags) => { lines.push(Line::NextPage(String::from("load more tags"))); @@ -128,20 +130,20 @@ impl TagList { } } - pub fn handle_input(&mut self, key: termion::event::Key) { + pub async fn handle_input(&mut self, key: termion::event::Key) { match key { - Key::Down => self.next(), + Key::Down => self.next().await, Key::Up => self.previous(), - Key::Char('\n') => self.select(), + Key::Char('\n') => self.select().await, _ => (), } } /// loads new tags when matching line is selected - fn select(&mut self) { + async fn select(&mut self) { if let Some(i) = self.state.selected() { if let Line::NextPage(_) = &self.lines[i] { - self.load_next_page() + self.load_next_page().await } } } @@ -152,18 +154,15 @@ impl TagList { Some(i) => match &self.lines[i] { Line::Status(_) => Err(Error::SelectedStatus), Line::Image(i) => Ok(i.get_name().to_string()), - Line::NextPage(_) => { - self.load_next_page(); - Err(Error::NextPageSelected) - } + Line::NextPage(_) => Err(Error::NextPageSelected), }, } } /// load new tags from the next page - fn load_next_page(&mut self) { + async fn load_next_page(&mut self) { match &self.tags { - Some(tags) => match tags.next_page() { + Some(tags) => match tags.next_page().await { None => (), Some(new_tags) => { //load new tags object @@ -183,7 +182,7 @@ impl TagList { } //readd next page - match self.tags.as_ref().unwrap().next_page() { + match self.tags.as_ref().unwrap().next_page().await { None => (), Some(_) => self.lines.push(next_page.unwrap()), } @@ -194,21 +193,26 @@ impl TagList { } /// select next tag - fn next(&mut self) { + async fn next(&mut self) { + if let Some(Line::Status(_)) = self.lines.get(0) { + return; + } match self.state.selected() { None if !self.lines.is_empty() => self.state.select(Some(0)), None => (), - Some(i) if i == self.lines.len() - 1 => self.state.select(Some(0)), + Some(i) if i == self.lines.len() - 2 => self.load_next_page().await, Some(i) => self.state.select(Some(i + 1)), } } /// select previous tag fn previous(&mut self) { + if let Some(Line::Status(_)) = self.lines.get(0) { + return; + } match self.state.selected() { - None if !self.lines.is_empty() => self.state.select(Some(self.lines.len())), - None => (), - Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 1)), + None => self.state.select(Some(0)), + Some(i) if i == 0 => self.state.select(Some(self.lines.len() - 2)), Some(i) => self.state.select(Some(i - 1)), } } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 802872b..a197195 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,5 +1,5 @@ +pub mod async_tag_list; pub mod details; pub mod info; pub mod repo_entry; pub mod service_switcher; -pub mod tag_list; diff --git a/src/widget/repo_entry.rs b/src/widget/repo_entry.rs index 14f9828..c7cdbec 100644 --- a/src/widget/repo_entry.rs +++ b/src/widget/repo_entry.rs @@ -12,7 +12,7 @@ pub struct RepoEntry { impl RepoEntry { pub fn new(text: Option<&str>) -> Self { - let default_text = "enter a repository here or select one from file widget"; + let default_text = "edit me or select a repository"; Self { text: String::from(text.unwrap_or(default_text)), old_text: String::from(text.unwrap_or(default_text)),