diff --git a/Cargo.lock b/Cargo.lock index e4f981e..0696644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -69,9 +69,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" @@ -135,9 +135,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.21" +version = "1.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "9540e661f81799159abee814118cc139a2004b3a3aa3ea37724a1b66530b90e0" dependencies = [ "shlex", ] @@ -237,6 +237,12 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -485,9 +491,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -498,7 +504,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -536,6 +541,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "instability" version = "0.3.2" @@ -548,9 +563,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c25163201be6ded9e686703e85532f8f852ea1f92ba625cb3c51f7fe6d07a4a" +checksum = "44bd7eced44cfe2cebc674adb2a7124a754a4b5269288d22e9f39f8fada3562d" dependencies = [ "memchr", "serde", @@ -597,9 +612,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "linux-raw-sys" @@ -744,9 +759,12 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] [[package]] name = "openssl-probe" @@ -831,6 +849,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -854,8 +878,10 @@ dependencies = [ "dotenv", "octocrab", "ratatui", + "serde", "thiserror", "tokio", + "toml", ] [[package]] @@ -890,9 +916,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags", ] @@ -970,9 +996,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -1086,6 +1112,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1163,18 +1198,18 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "snafu" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b835cb902660db3415a672d862905e791e54d306c6e8189168c7f3d9ae1c79d" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" dependencies = [ "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d1e02fca405f6280643174a50c942219f0bbf4dbf7d480f1dd864d6f211ae5" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ "heck", "proc-macro2", @@ -1234,9 +1269,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -1362,6 +1397,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -1700,6 +1769,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 0c61592..d2de6fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,7 @@ crossterm = "0.28.1" dotenv = "0.15.0" octocrab = "0.39.0" ratatui = "0.28.1" +serde = { version = "1.0", features = ["derive"] } thiserror = "1.0.64" tokio = { version = "1.40.0", features = ["full"] } +toml = "0.8.19" diff --git a/src/core/app.rs b/src/core/app.rs index a5c4681..ca20caa 100644 --- a/src/core/app.rs +++ b/src/core/app.rs @@ -11,10 +11,12 @@ pub struct App { pub pull_request: PullRequest, pub input_mode: InputMode, pub current_field: usize, - pub show_popup: bool, + pub show_confirm_popup: bool, + pub show_pat_popup: bool, pub repo_owner: String, pub repo_name: String, pub default_target_branch: String, + pub config_pat: String, } impl App { @@ -28,9 +30,11 @@ impl App { pull_request: PullRequest::new(current_branch.clone()), input_mode: InputMode::Normal, current_field: 0, - show_popup: false, + show_confirm_popup: false, + show_pat_popup: false, error_message: None, success_message: None, + config_pat: String::new(), repo_owner, repo_name, default_target_branch: std::env::var("GITHUB_DEFAULT_TARGET_BRANCH") @@ -40,12 +44,9 @@ impl App { pub async fn create_github_pull_request( &self, + pat: String, ) -> Result { - let github_token = std::env::var("GITHUB_TOKEN") - .map_err(|_| PullRequestError::PATNotSet("Github PAT not set".to_string()))?; - - let octocrab = Octocrab::builder().personal_token(github_token).build()?; - + let octocrab = Octocrab::builder().personal_token(pat).build()?; if self.pull_request.source_branch.is_empty() { return Err(PullRequestError::InvalidInput( "Source branch is empty".to_string(), @@ -103,14 +104,14 @@ impl App { pub fn confirm_pull_request(&mut self) { self.input_mode = InputMode::Creating; - self.show_popup = true; + self.show_confirm_popup = true; } pub fn reset(&mut self) { self.pull_request = PullRequest::new(self.pull_request.source_branch.clone()); self.input_mode = InputMode::Normal; self.current_field = 0; - self.show_popup = false; + self.show_confirm_popup = false; self.error_message = None; } diff --git a/src/core/app_test.rs b/src/core/app_test.rs index 73ab6ae..e989ea5 100644 --- a/src/core/app_test.rs +++ b/src/core/app_test.rs @@ -38,7 +38,7 @@ mod tests { assert!(app.error_message.is_none()); assert_eq!(app.input_mode, InputMode::Normal); assert_eq!(app.current_field, 0); - assert!(!app.show_popup); + assert!(!app.show_confirm_popup); } #[test] diff --git a/src/core/config.rs b/src/core/config.rs new file mode 100644 index 0000000..04d1c8d --- /dev/null +++ b/src/core/config.rs @@ -0,0 +1,35 @@ +use serde::{Deserialize, Serialize}; +use std::fs::{self, File}; +use std::io::{self, Write}; + +#[derive(Deserialize, Serialize)] +pub struct Config { + pub github: GitHubConfig, +} + +#[derive(Deserialize, Serialize)] +pub struct GitHubConfig { + pub pat: String, +} + +pub fn load_config() -> Option { + if let Ok(config_content) = fs::read_to_string("config.toml") { + let config: Config = + toml::from_str(&config_content).expect("Failed to parse configuration file"); + Some(config) + } else { + None // No existe el archivo + } +} + +pub fn save_config(pat: &str) -> Result<(), io::Error> { + let config = Config { + github: GitHubConfig { + pat: pat.to_string(), + }, + }; + let toml_str = toml::to_string(&config).expect("Failed to serialize configuration"); + let mut file = File::create("config.toml")?; + file.write_all(toml_str.as_bytes())?; + Ok(()) +} diff --git a/src/core/errors.rs b/src/core/errors.rs index 3d81d49..c4b3b47 100644 --- a/src/core/errors.rs +++ b/src/core/errors.rs @@ -10,7 +10,4 @@ pub enum PullRequestError { #[error("Invalid input: {0}")] InvalidInput(String), - - #[error("{0}")] - PATNotSet(String), } diff --git a/src/core/mod.rs b/src/core/mod.rs index daa4384..825c6c1 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,7 +1,7 @@ pub mod app; +pub mod app_test; +pub mod config; pub mod errors; +pub mod git; pub mod input_mode; pub mod pull_request; - -pub mod app_test; -pub mod git; diff --git a/src/main.rs b/src/main.rs index 9e8b7b2..1a172b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ mod core; mod ui; - +use crate::core::config::{load_config, save_config}; use crate::core::input_mode::InputMode; use crate::ui::layout::ui; use core::app::App; @@ -21,11 +21,40 @@ fn main() -> Result<(), io::Error> { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; + let config = load_config(); + let mut app = App::new(); let runtime = tokio::runtime::Runtime::new().unwrap(); + + if config.is_none() { + app.show_pat_popup = true; + } + loop { terminal.draw(|f| ui(f, &app))?; if let Event::Key(key) = event::read()? { + if app.show_pat_popup { + match key.code { + KeyCode::Char(c) => { + app.config_pat.push(c); + } + KeyCode::Backspace => { + app.config_pat.pop(); + } + KeyCode::Enter => { + if !app.config_pat.is_empty() { + save_config(&app.config_pat).expect("Failed to save the configuration"); + app.show_pat_popup = false; + } else { + app.set_error("PAT cannot be empty!".to_string()); + } + } + KeyCode::Esc => break, + _ => {} + } + continue; + } + match app.input_mode { InputMode::Normal => match key.code { KeyCode::Char('q') => break, @@ -77,8 +106,9 @@ fn main() -> Result<(), io::Error> { InputMode::Creating => match key.code { KeyCode::Enter | KeyCode::Char('y') => { app.input_mode = InputMode::Normal; - app.show_popup = false; - let result = runtime.block_on(app.create_github_pull_request()); + app.show_confirm_popup = false; + let result = runtime + .block_on(app.create_github_pull_request(app.config_pat.clone())); match result { Ok(pr) => { let url_str = match pr.html_url { @@ -98,11 +128,11 @@ fn main() -> Result<(), io::Error> { } KeyCode::Char('e') | KeyCode::Char('n') => { app.input_mode = InputMode::Editing; - app.show_popup = false; + app.show_confirm_popup = false; } KeyCode::Char('q') => { app.input_mode = InputMode::Normal; - app.show_popup = false; + app.show_confirm_popup = false; } _ => {} }, diff --git a/src/ui/layout.rs b/src/ui/layout.rs index dea1c1d..34505a4 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -2,9 +2,8 @@ use crate::ui::util::{centered_rect, inner_area}; use crate::App; use crate::InputMode; use ratatui::layout::{Constraint, Direction, Layout}; -use ratatui::style::Modifier; use ratatui::text::{Line, Span, Text}; -use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap}; +use ratatui::widgets::{Block, Borders, Clear, Padding, Paragraph, Wrap}; use ratatui::{ style::{Color, Style}, Frame, @@ -13,12 +12,12 @@ use ratatui::{ pub fn ui(f: &mut Frame, app: &App) { let chunks = Layout::default() .direction(Direction::Vertical) - .margin(1) + .margin(2) .constraints( [ Constraint::Percentage(15), - Constraint::Percentage(60), - Constraint::Percentage(20), + Constraint::Percentage(50), + Constraint::Percentage(30), Constraint::Percentage(5), ] .as_ref(), @@ -33,6 +32,7 @@ pub fn ui(f: &mut Frame, app: &App) { let repository_block = Block::default() .title("Context") + .padding(Padding::new(1, 0, 1, 0)) .borders(Borders::ALL) .border_type(ratatui::widgets::BorderType::Rounded); let text = vec![ @@ -45,7 +45,7 @@ pub fn ui(f: &mut Frame, app: &App) { ]; let paragraph = Paragraph::new(text) .block(repository_block) - .style(Style::default().add_modifier(Modifier::BOLD)); + .style(Style::default()); let repo_area = Layout::default() .direction(Direction::Vertical) @@ -58,7 +58,9 @@ pub fn ui(f: &mut Frame, app: &App) { let description_height = description_lines.min(20) + 2; let form_layout = Layout::default() .direction(Direction::Vertical) - .margin(2) + .margin(1) + .vertical_margin(2) + .horizontal_margin(2) .constraints( [ Constraint::Length(1), @@ -68,7 +70,10 @@ pub fn ui(f: &mut Frame, app: &App) { .as_ref(), ) .split(chunks[1]); - let form_block = Block::default().title("Create").borders(Borders::ALL); + let form_block = Block::default() + .title("Create") + .padding(Padding::proportional(1)) + .borders(Borders::ALL); f.render_widget(form_block, chunks[1]); let fields = vec![ ("Title", &app.pull_request.title), @@ -79,19 +84,7 @@ pub fn ui(f: &mut Frame, app: &App) { for (i, (name, value)) in fields.iter().enumerate() { let (text, style) = match app.input_mode { InputMode::Normal => ( - format!( - "{}: {}", - name, - if value.is_empty() { - if i == 3 { - "[default]" - } else { - "" - } - } else { - value - } - ), + format!("{}: {}", name, if value.is_empty() { "" } else { value }), if i == app.current_field { Style::default().fg(Color::Yellow) } else { @@ -151,7 +144,7 @@ pub fn ui(f: &mut Frame, app: &App) { let instructions_paragraph = Paragraph::new(instructions).style(Style::default()); f.render_widget(instructions_paragraph, chunks[3]); - if app.show_popup { + if app.show_confirm_popup { let popup_block = Block::default() .title("Pull Request Confirmation") .borders(Borders::ALL) @@ -176,4 +169,27 @@ pub fn ui(f: &mut Frame, app: &App) { f.render_widget(popup_paragraph, inner_area(area)); } + + if app.show_pat_popup { + let area = centered_rect(50, 15, f.area()); + f.render_widget(Clear, area); + + let popup_text = vec![ + Line::from(app.config_pat.as_str()) + .style(Style::default().bg(Color::Black).fg(Color::White)), + Line::from(""), + Line::from("Press [enter] to confirm or [esc] to cancel"), + ]; + + let pat_input = Paragraph::new(popup_text) + .block( + Block::default() + .borders(Borders::ALL) + .title("Enter you Github PAT:"), + ) + .style(Style::default().bg(Color::White).fg(Color::Black)); + + // Centrar el modal en la pantalla + f.render_widget(pat_input, inner_area(area)); + } }