From fa3a2e60cd305c8e103285f49dcfc666cffacc19 Mon Sep 17 00:00:00 2001 From: jay Date: Mon, 14 Feb 2022 15:35:29 +0800 Subject: [PATCH] [feat] first version --- Cargo.lock | 204 +++++++++++++++++++++ Cargo.toml | 4 +- src/main.rs | 422 +++++++++++++++++++++++++++++++++++++++++--- src/registry/api.rs | 2 +- 4 files changed, 602 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1540606..850dbe7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -20,6 +31,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + [[package]] name = "bumpalo" version = "3.9.1" @@ -50,6 +67,58 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63edc3f163b3c71ec8aa23f9bd6070f77edbf3d1d198b164afa90ff00e4ec62" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1132dc3944b31c20dd8b906b3a9f0a5d0243e092d59171414969657ac6aa85" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.2" @@ -213,6 +282,12 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -380,6 +455,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matches" version = "0.1.9" @@ -457,6 +541,35 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.9.0" @@ -496,6 +609,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -545,6 +667,30 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -576,12 +722,15 @@ dependencies = [ name = "registry-cli" version = "0.1.0" dependencies = [ + "clap", + "clipboard", "crossterm", "reqwest", "serde", "serde_json", "tokio", "tui", + "url", ] [[package]] @@ -769,6 +918,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.86" @@ -794,6 +949,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "tinyvec" version = "1.5.1" @@ -959,6 +1129,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "want" version = "0.3.0" @@ -1061,6 +1237,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1075,3 +1260,22 @@ checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ "winapi", ] + +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] diff --git a/Cargo.toml b/Cargo.toml index c43d46f..3fc222a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,6 @@ serde = {version = "1.0.136", features = ["derive"]} serde_json = "1.0.78" tokio = { version = "1", features = ["full"] } tui = "0.17.0" - +clap = { version = "3.0.14", features = ["derive"] } +clipboard = "0.5.0" +url = "2.2.2" diff --git a/src/main.rs b/src/main.rs index 41971f3..793880a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,397 @@ +use clap::Parser; +use clipboard::{ClipboardContext, ClipboardProvider}; +use std::{ + io, thread, + time::{Duration, Instant}, +}; + +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use tui::{ + backend::{Backend, CrosstermBackend}, + layout::{Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + Frame, Terminal, +}; + mod registry; -fn main() { - println!("Hello, world!"); - - let r = registry::api::Registry { - endpoint: "https://docker.mtfos.xyz".to_string(), - user: "".to_string(), - password: "".to_string(), - }; - - // let images = r.list_images().unwrap(); - - // println!("images ::: {:?}", images); - - // let tags = r.get_image_tags("mtfos/go-bot").unwrap(); - - // println!("tags ::: {:?}", tags); - - let digest = match r.get_image_tag_manifest("demo/node-hello", "v1") { - Ok(digest) => digest, - Err(e) => panic!("digest err ::: {:?}", e), - }; - - println!("digest ::: {:?}", digest); - - match r.delete_image_tag("demo/node-hello", &digest) { - Ok(_) => println!("deleted"), - Err(e) => println!("error ::: {:?}", e), - }; +struct StatefulList { + state: ListState, + items: Vec, +} +impl StatefulList { + fn with_items(items: Vec) -> StatefulList { + StatefulList { + state: ListState::default(), + items, + } + } + + fn next(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i >= self.items.len() - 1 { + 0 + } else { + i + 1 + } + } + None => 0, + }; + self.state.select(Some(i)) + } + + fn previous(&mut self) { + let i = match self.state.selected() { + Some(i) => { + if i <= 0 { + self.items.len() - 1 + } else { + i - 1 + } + } + None => self.items.len() - 1, + }; + self.state.select(Some(i)) + } + + fn unselect(&mut self) { + self.state.select(None) + } + + fn selected(&self) -> Option<&T> { + self.state.selected().map(|i| &self.items[i]) + } +} + +enum PageState { + Images, + Tags, +} + +enum LoadingState { + Init, + Loading, + Loaded, +} + +struct App { + registry_uri: String, + username: String, + password: String, + + registry: Option, + + page_state: PageState, + loading_state: LoadingState, + + selected_image: Option, + images: Option>, + tags: Option>, +} + +impl App { + fn new(uri: String, username: String, password: String) -> App { + let mut app = App { + registry_uri: uri, + username, + password, + + registry: None, + + page_state: PageState::Images, + loading_state: LoadingState::Init, + + selected_image: None, + images: None, + tags: None, + }; + + let api = registry::api::Registry { + endpoint: app.registry_uri.to_string(), + user: app.username.to_string(), + password: app.password.to_string(), + }; + app.registry = Some(api); + + app + } +} + +#[derive(Debug, Clone, Parser)] +#[clap(version = "0.1.0")] +struct Args { + #[clap(short, long)] + registry_uri: String, + #[clap(short, long, default_value = "")] + username: String, + #[clap(short, long, default_value = "")] + password: String, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + if args.registry_uri.is_empty() { + println!("Please provide a registry URI"); + return Ok(()); + } + + enable_raw_mode()?; + + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + + let backend = CrosstermBackend::new(stdout); + + let mut terminal = Terminal::new(backend)?; + + let app = App::new(args.registry_uri, args.username, args.password); + + let res = run_app(&mut terminal, app); + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + + terminal.show_cursor()?; + + if let Err(e) = res { + println!("{}", e); + } + + Ok(()) +} + +fn run_app( + terminal: &mut Terminal, + mut app: App, +) -> Result<(), Box> { + let mut last_tick = Instant::now(); + + loop { + terminal.draw(|f| ui(f, &mut app))?; + + match app.loading_state { + LoadingState::Init => { + app.loading_state = LoadingState::Loading; + loading(&mut app); + } + LoadingState::Loading => {} + LoadingState::Loaded => {} + } + + let timeout = Duration::from_millis(250) + .checked_sub(last_tick.elapsed()) + .unwrap_or(Duration::from_millis(0)); + + if crossterm::event::poll(timeout)? { + let event = crossterm::event::read()?; + if let Event::Key(key) = event { + match key.code { + KeyCode::Char('q') => return Ok(()), + _ => {} + } + + match app.page_state { + PageState::Images => match key.code { + KeyCode::Char('r') => { + // reload list of images + app.loading_state = LoadingState::Init; + } + KeyCode::Down => { + if let Some(ref mut images) = app.images { + images.next(); + } + } + KeyCode::Up => { + if let Some(ref mut images) = app.images { + images.previous(); + } + } + KeyCode::Enter => { + app.selected_image = app.images.as_ref().unwrap().selected().cloned(); + app.page_state = PageState::Tags; + app.loading_state = LoadingState::Init; + } + _ => {} + }, + PageState::Tags => match key.code { + KeyCode::Esc => { + app.selected_image = None; + app.tags = None; + app.page_state = PageState::Images; + } + KeyCode::Down => { + if let Some(ref mut tags) = app.tags { + tags.next(); + } + } + KeyCode::Up => { + if let Some(ref mut tags) = app.tags { + tags.previous(); + } + } + KeyCode::Char('c') => { + // copy docker image uri + if let Some(ref image) = app.selected_image { + if let Some(ref tags) = app.tags { + if let Some(ref tag) = tags.selected() { + let mut u = url::Url::parse(&app.registry_uri)?; + u.set_path(image); + + let uri = u + .to_string() + .replace(&format!("{}://", u.scheme()).to_string(), ""); + + let uri_str = format!("{}:{}", uri, tag); + let mut clipboard: ClipboardContext = + ClipboardProvider::new()?; + clipboard.set_contents(uri_str)?; + } + } + } + } + _ => {} + }, + } + } + } + + if last_tick.elapsed() >= Duration::from_millis(250) { + last_tick = Instant::now(); + } + } +} +fn loading(app: &mut App) -> Result<(), Box> { + match app.page_state { + PageState::Images => { + if let Some(registry) = &app.registry { + let images = registry.list_images()?; + app.images = Some(StatefulList::with_items(images)); + app.loading_state = LoadingState::Loaded; + if let Some(ref mut images) = app.images { + images.state.select(Some(0)); + } + } + } + PageState::Tags => { + if let Some(ref image) = app.selected_image { + let image_tags = app.registry.as_ref().unwrap().get_image_tags(image)?; + app.tags = Some(StatefulList::with_items(image_tags.tags)); + app.loading_state = LoadingState::Loaded; + if let Some(ref mut tags) = app.tags { + tags.state.select(Some(0)); + } + } + } + } + + Ok(()) +} + +fn ui(f: &mut Frame, app: &mut App) { + let chunks = Layout::default() + .constraints( + [ + Constraint::Percentage(10), + Constraint::Percentage(5), + Constraint::Percentage(85), + ] + .as_ref(), + ) + .split(f.size()); + + // chunk 0 is imfomation + show_info(f, app, chunks[0]); + + // chunk 1 is the loading state + match app.loading_state { + LoadingState::Init | LoadingState::Loaded => { + f.render_widget(Block::default().borders(Borders::ALL), chunks[1]); + } + LoadingState::Loading => { + let txt = Spans::from(vec![Span::styled( + "Loading...", + Style::default().fg(Color::Yellow), + )]); + + let paragraph = Paragraph::new(txt).block(Block::default().borders(Borders::ALL)); + f.render_widget(paragraph, chunks[1]); + } + } + + match app.page_state { + PageState::Images => page_images(f, app, chunks[2]), + PageState::Tags => page_tags(f, app, chunks[2]), + } +} + +fn show_info(f: &mut Frame, app: &mut App, area: Rect) { + let chunks = Layout::default() + .margin(1) + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(40), Constraint::Percentage(60)]) + .split(area); + + let mut texts = vec![Spans::from(vec![ + Span::raw("Registry: "), + Span::raw(&app.registry_uri), + ])]; + + if let Some(ref selected_image) = app.selected_image { + texts.push(Spans::from(vec![ + Span::raw("Selected Image: "), + Span::raw(selected_image.to_owned()), + ])); + } + + let paragraph = Paragraph::new(texts).block(Block::default().borders(Borders::NONE)); + f.render_widget(paragraph, chunks[0]); +} + +fn page_images(f: &mut Frame, app: &mut App, area: Rect) { + if let Some(ref mut images) = app.images { + let items: Vec = images + .items + .iter() + .map(|i| { + let line = Spans::from(vec![Span::raw(i.as_str())]); + ListItem::new(line) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(Style::default().bg(Color::LightGreen).fg(Color::Black)); + + f.render_stateful_widget(list, area, &mut images.state); + } +} + +fn page_tags(f: &mut Frame, app: &mut App, area: Rect) { + if let Some(ref mut tags) = app.tags { + let items: Vec = tags + .items + .iter() + .map(|i| { + let line = Spans::from(vec![Span::raw(i.as_str())]); + ListItem::new(line) + }) + .collect(); + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL)) + .highlight_style(Style::default().bg(Color::LightGreen).fg(Color::Black)); + + f.render_stateful_widget(list, area, &mut tags.state); + } } diff --git a/src/registry/api.rs b/src/registry/api.rs index e4e7e23..88c3500 100644 --- a/src/registry/api.rs +++ b/src/registry/api.rs @@ -77,7 +77,7 @@ impl Registry { let headers = resp.headers().to_owned(); - println!("\n{:?}\n", headers); + // println!("\n{:?}\n", headers); if !headers .get("Content-Type")