tag functionality
- SHA
a85b2a53df68c6dbe9e7bf4861674735e6463840- Parents
-
4785061 - Tree
bad8248
a85b2a5
a85b2a53df68c6dbe9e7bf4861674735e64638404785061
bad8248| Status | File | + | - |
|---|---|---|---|
| M |
src/app.rs
|
86 | 0 |
| M |
src/git.rs
|
89 | 0 |
| M |
src/main.rs
|
138 | 0 |
| M |
src/types.rs
|
13 | 0 |
| M |
src/ui.rs
|
184 | 1 |
src/app.rsmodified@@ -571,6 +571,92 @@ impl App { | ||
| 571 | 571 | }; |
| 572 | 572 | } |
| 573 | 573 | |
| 574 | + /// Enter tag mode - fetches tags and shows modal | |
| 575 | + pub fn enter_tag_mode(&mut self) { | |
| 576 | + // Fetch tags from remote first (non-blocking, ignore errors) | |
| 577 | + let _ = self.repo.fetch_tags(); | |
| 578 | + | |
| 579 | + // Get existing tags | |
| 580 | + let existing_tags = self.repo.get_tags(); | |
| 581 | + | |
| 582 | + self.input_mode = InputMode::Tag { | |
| 583 | + name: String::new(), | |
| 584 | + message: String::new(), | |
| 585 | + cursor: 0, | |
| 586 | + existing_tags, | |
| 587 | + step: crate::types::TagStep::EnterName, | |
| 588 | + }; | |
| 589 | + } | |
| 590 | + | |
| 591 | + /// Create the tag with current name/message | |
| 592 | + pub fn create_tag(&mut self) -> Result<()> { | |
| 593 | + let (name, message) = if let InputMode::Tag { name, message, .. } = &self.input_mode { | |
| 594 | + (name.clone(), message.clone()) | |
| 595 | + } else { | |
| 596 | + return Ok(()); | |
| 597 | + }; | |
| 598 | + | |
| 599 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 600 | + *step = crate::types::TagStep::Creating; | |
| 601 | + } | |
| 602 | + | |
| 603 | + match self.repo.create_tag(&name, &message) { | |
| 604 | + Ok(()) => { | |
| 605 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 606 | + *step = crate::types::TagStep::AskPush; | |
| 607 | + } | |
| 608 | + Ok(()) | |
| 609 | + } | |
| 610 | + Err(e) => { | |
| 611 | + let msg = e.to_string(); | |
| 612 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 613 | + *step = crate::types::TagStep::Failed(msg); | |
| 614 | + } | |
| 615 | + Ok(()) | |
| 616 | + } | |
| 617 | + } | |
| 618 | + } | |
| 619 | + | |
| 620 | + /// Push the tag to origin | |
| 621 | + pub fn push_tag(&mut self) -> Result<()> { | |
| 622 | + let name = if let InputMode::Tag { name, .. } = &self.input_mode { | |
| 623 | + name.clone() | |
| 624 | + } else { | |
| 625 | + return Ok(()); | |
| 626 | + }; | |
| 627 | + | |
| 628 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 629 | + *step = crate::types::TagStep::Pushing; | |
| 630 | + } | |
| 631 | + | |
| 632 | + match self.repo.push_tag(&name) { | |
| 633 | + Ok(()) => { | |
| 634 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 635 | + *step = crate::types::TagStep::Success; | |
| 636 | + } | |
| 637 | + self.set_status(format!("Tag '{}' pushed to origin", name)); | |
| 638 | + Ok(()) | |
| 639 | + } | |
| 640 | + Err(e) => { | |
| 641 | + let msg = e.to_string(); | |
| 642 | + if let InputMode::Tag { step, .. } = &mut self.input_mode { | |
| 643 | + *step = crate::types::TagStep::Failed(msg); | |
| 644 | + } | |
| 645 | + Ok(()) | |
| 646 | + } | |
| 647 | + } | |
| 648 | + } | |
| 649 | + | |
| 650 | + /// Close tag modal (with success message if tag was created) | |
| 651 | + pub fn close_tag(&mut self, was_created: bool) { | |
| 652 | + if was_created { | |
| 653 | + if let InputMode::Tag { name, .. } = &self.input_mode { | |
| 654 | + self.set_status(format!("Tag '{}' created", name)); | |
| 655 | + } | |
| 656 | + } | |
| 657 | + self.input_mode = InputMode::Navigation; | |
| 658 | + } | |
| 659 | + | |
| 574 | 660 | /// Create a commit |
| 575 | 661 | pub fn commit(&mut self, message: &str) -> Result<()> { |
| 576 | 662 | self.repo.commit(message)?; |
src/git.rsmodified@@ -541,4 +541,93 @@ impl GitRepo { | ||
| 541 | 541 | Err(FussrError::Git(git2::Error::from_str("Failed to rename file"))) |
| 542 | 542 | } |
| 543 | 543 | } |
| 544 | + | |
| 545 | + /// Fetch tags from remote | |
| 546 | + pub fn fetch_tags(&self) -> Result<()> { | |
| 547 | + let output = Command::new("git") | |
| 548 | + .args(["fetch", "--tags", "--quiet"]) | |
| 549 | + .stdout(std::process::Stdio::piped()) | |
| 550 | + .stderr(std::process::Stdio::piped()) | |
| 551 | + .output()?; | |
| 552 | + | |
| 553 | + if output.status.success() { | |
| 554 | + Ok(()) | |
| 555 | + } else { | |
| 556 | + // Silently ignore fetch errors - tags list will still work | |
| 557 | + Ok(()) | |
| 558 | + } | |
| 559 | + } | |
| 560 | + | |
| 561 | + /// Get list of existing tags (sorted by version, newest first) | |
| 562 | + pub fn get_tags(&self) -> Vec<String> { | |
| 563 | + let output = Command::new("git") | |
| 564 | + .args(["tag", "--sort=-version:refname"]) | |
| 565 | + .stdout(std::process::Stdio::piped()) | |
| 566 | + .stderr(std::process::Stdio::piped()) | |
| 567 | + .output(); | |
| 568 | + | |
| 569 | + match output { | |
| 570 | + Ok(o) if o.status.success() => { | |
| 571 | + String::from_utf8_lossy(&o.stdout) | |
| 572 | + .lines() | |
| 573 | + .take(10) // Only show last 10 tags | |
| 574 | + .map(|s| s.trim().to_string()) | |
| 575 | + .filter(|s| !s.is_empty()) | |
| 576 | + .collect() | |
| 577 | + } | |
| 578 | + _ => Vec::new(), | |
| 579 | + } | |
| 580 | + } | |
| 581 | + | |
| 582 | + /// Create a new tag | |
| 583 | + pub fn create_tag(&self, name: &str, message: &str) -> Result<()> { | |
| 584 | + let output = if message.is_empty() { | |
| 585 | + // Lightweight tag | |
| 586 | + Command::new("git") | |
| 587 | + .args(["tag", name]) | |
| 588 | + .stdout(std::process::Stdio::piped()) | |
| 589 | + .stderr(std::process::Stdio::piped()) | |
| 590 | + .output()? | |
| 591 | + } else { | |
| 592 | + // Annotated tag with message | |
| 593 | + Command::new("git") | |
| 594 | + .args(["tag", "-a", name, "-m", message]) | |
| 595 | + .stdout(std::process::Stdio::piped()) | |
| 596 | + .stderr(std::process::Stdio::piped()) | |
| 597 | + .output()? | |
| 598 | + }; | |
| 599 | + | |
| 600 | + if output.status.success() { | |
| 601 | + Ok(()) | |
| 602 | + } else { | |
| 603 | + let stderr = String::from_utf8_lossy(&output.stderr); | |
| 604 | + let msg = if stderr.contains("already exists") { | |
| 605 | + format!("Tag '{}' already exists", name) | |
| 606 | + } else { | |
| 607 | + format!("Failed to create tag '{}'", name) | |
| 608 | + }; | |
| 609 | + Err(FussrError::Git(git2::Error::from_str(&msg))) | |
| 610 | + } | |
| 611 | + } | |
| 612 | + | |
| 613 | + /// Push a tag to origin | |
| 614 | + pub fn push_tag(&self, name: &str) -> Result<()> { | |
| 615 | + let output = Command::new("git") | |
| 616 | + .args(["push", "origin", name]) | |
| 617 | + .stdout(std::process::Stdio::piped()) | |
| 618 | + .stderr(std::process::Stdio::piped()) | |
| 619 | + .output()?; | |
| 620 | + | |
| 621 | + if output.status.success() { | |
| 622 | + Ok(()) | |
| 623 | + } else { | |
| 624 | + let stderr = String::from_utf8_lossy(&output.stderr); | |
| 625 | + let msg = if stderr.contains("Could not read from remote") { | |
| 626 | + "Cannot reach origin - check connection/auth".to_string() | |
| 627 | + } else { | |
| 628 | + format!("Failed to push tag '{}'", name) | |
| 629 | + }; | |
| 630 | + Err(FussrError::Git(git2::Error::from_str(&msg))) | |
| 631 | + } | |
| 632 | + } | |
| 544 | 633 | } |
src/main.rsmodified@@ -94,6 +94,7 @@ fn run_event_loop( | ||
| 94 | 94 | InputMode::Push { .. } => handle_push_key(app, key.code)?, |
| 95 | 95 | InputMode::Pull { .. } => handle_pull_key(app, key.code)?, |
| 96 | 96 | InputMode::Fetch { .. } => handle_fetch_key(app, key.code)?, |
| 97 | + InputMode::Tag { .. } => handle_tag_key(app, key.code)?, | |
| 97 | 98 | InputMode::Confirm { .. } => handle_confirm_key(app, key.code)?, |
| 98 | 99 | } |
| 99 | 100 | |
@@ -235,6 +236,9 @@ fn handle_navigation_key(app: &mut App, code: KeyCode, modifiers: KeyModifiers) | ||
| 235 | 236 | KeyCode::Char('M') if app.mode == AppMode::Git => { |
| 236 | 237 | app.enter_commit_mode(true); // amend |
| 237 | 238 | } |
| 239 | + KeyCode::Char('t') if app.mode == AppMode::Git => { | |
| 240 | + app.enter_tag_mode(); | |
| 241 | + } | |
| 238 | 242 | |
| 239 | 243 | _ => {} |
| 240 | 244 | } |
@@ -503,6 +507,140 @@ fn handle_fetch_key(app: &mut App, code: KeyCode) -> Result<()> { | ||
| 503 | 507 | Ok(()) |
| 504 | 508 | } |
| 505 | 509 | |
| 510 | +/// Handle keys in tag mode | |
| 511 | +fn handle_tag_key(app: &mut App, code: KeyCode) -> Result<()> { | |
| 512 | + use crate::types::TagStep; | |
| 513 | + | |
| 514 | + let step = if let InputMode::Tag { step, .. } = &app.input_mode { | |
| 515 | + step.clone() | |
| 516 | + } else { | |
| 517 | + return Ok(()); | |
| 518 | + }; | |
| 519 | + | |
| 520 | + match step { | |
| 521 | + TagStep::EnterName => { | |
| 522 | + match code { | |
| 523 | + KeyCode::Esc => { | |
| 524 | + app.close_tag(false); | |
| 525 | + } | |
| 526 | + KeyCode::Enter => { | |
| 527 | + // Check if name is non-empty before proceeding | |
| 528 | + let has_name = if let InputMode::Tag { name, .. } = &app.input_mode { | |
| 529 | + !name.trim().is_empty() | |
| 530 | + } else { | |
| 531 | + false | |
| 532 | + }; | |
| 533 | + | |
| 534 | + if has_name { | |
| 535 | + // Move cursor to message field | |
| 536 | + if let InputMode::Tag { step, cursor, .. } = &mut app.input_mode { | |
| 537 | + *step = TagStep::EnterMessage; | |
| 538 | + *cursor = 0; | |
| 539 | + } | |
| 540 | + } | |
| 541 | + } | |
| 542 | + KeyCode::Backspace => { | |
| 543 | + if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode { | |
| 544 | + if *cursor > 0 { | |
| 545 | + name.remove(*cursor - 1); | |
| 546 | + *cursor -= 1; | |
| 547 | + } | |
| 548 | + } | |
| 549 | + } | |
| 550 | + KeyCode::Left => { | |
| 551 | + if let InputMode::Tag { cursor, .. } = &mut app.input_mode { | |
| 552 | + if *cursor > 0 { | |
| 553 | + *cursor -= 1; | |
| 554 | + } | |
| 555 | + } | |
| 556 | + } | |
| 557 | + KeyCode::Right => { | |
| 558 | + if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode { | |
| 559 | + if *cursor < name.len() { | |
| 560 | + *cursor += 1; | |
| 561 | + } | |
| 562 | + } | |
| 563 | + } | |
| 564 | + KeyCode::Char(c) => { | |
| 565 | + if let InputMode::Tag { name, cursor, .. } = &mut app.input_mode { | |
| 566 | + name.insert(*cursor, c); | |
| 567 | + *cursor += 1; | |
| 568 | + } | |
| 569 | + } | |
| 570 | + _ => {} | |
| 571 | + } | |
| 572 | + } | |
| 573 | + TagStep::EnterMessage => { | |
| 574 | + match code { | |
| 575 | + KeyCode::Esc => { | |
| 576 | + // Go back to name entry | |
| 577 | + if let InputMode::Tag { step, name, cursor, .. } = &mut app.input_mode { | |
| 578 | + *step = TagStep::EnterName; | |
| 579 | + *cursor = name.len(); | |
| 580 | + } | |
| 581 | + } | |
| 582 | + KeyCode::Enter => { | |
| 583 | + // Create the tag | |
| 584 | + app.create_tag()?; | |
| 585 | + } | |
| 586 | + KeyCode::Backspace => { | |
| 587 | + if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode { | |
| 588 | + if *cursor > 0 { | |
| 589 | + message.remove(*cursor - 1); | |
| 590 | + *cursor -= 1; | |
| 591 | + } | |
| 592 | + } | |
| 593 | + } | |
| 594 | + KeyCode::Left => { | |
| 595 | + if let InputMode::Tag { cursor, .. } = &mut app.input_mode { | |
| 596 | + if *cursor > 0 { | |
| 597 | + *cursor -= 1; | |
| 598 | + } | |
| 599 | + } | |
| 600 | + } | |
| 601 | + KeyCode::Right => { | |
| 602 | + if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode { | |
| 603 | + if *cursor < message.len() { | |
| 604 | + *cursor += 1; | |
| 605 | + } | |
| 606 | + } | |
| 607 | + } | |
| 608 | + KeyCode::Char(c) => { | |
| 609 | + if let InputMode::Tag { message, cursor, .. } = &mut app.input_mode { | |
| 610 | + message.insert(*cursor, c); | |
| 611 | + *cursor += 1; | |
| 612 | + } | |
| 613 | + } | |
| 614 | + _ => {} | |
| 615 | + } | |
| 616 | + } | |
| 617 | + TagStep::Creating | TagStep::Pushing => { | |
| 618 | + // Don't respond to keys while creating/pushing | |
| 619 | + } | |
| 620 | + TagStep::AskPush => { | |
| 621 | + match code { | |
| 622 | + KeyCode::Char('y') | KeyCode::Char('Y') => { | |
| 623 | + app.push_tag()?; | |
| 624 | + } | |
| 625 | + KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { | |
| 626 | + app.close_tag(true); | |
| 627 | + } | |
| 628 | + _ => {} | |
| 629 | + } | |
| 630 | + } | |
| 631 | + TagStep::Success => { | |
| 632 | + // Any key closes | |
| 633 | + app.close_tag(true); | |
| 634 | + } | |
| 635 | + TagStep::Failed(_) => { | |
| 636 | + // Any key closes | |
| 637 | + app.close_tag(false); | |
| 638 | + } | |
| 639 | + } | |
| 640 | + | |
| 641 | + Ok(()) | |
| 642 | +} | |
| 643 | + | |
| 506 | 644 | /// Handle keys in search mode (legacy - not currently used) |
| 507 | 645 | fn handle_search_key(app: &mut App, code: KeyCode) -> Result<()> { |
| 508 | 646 | match code { |
src/types.rsmodified@@ -207,6 +207,18 @@ pub enum FetchStatus { | ||
| 207 | 207 | Failed(String), |
| 208 | 208 | } |
| 209 | 209 | |
| 210 | +/// Status/step of tag operation | |
| 211 | +#[derive(Debug, Clone, PartialEq, Eq)] | |
| 212 | +pub enum TagStep { | |
| 213 | + EnterName, | |
| 214 | + EnterMessage, | |
| 215 | + Creating, | |
| 216 | + AskPush, | |
| 217 | + Pushing, | |
| 218 | + Success, | |
| 219 | + Failed(String), | |
| 220 | +} | |
| 221 | + | |
| 210 | 222 | /// Input mode for special states |
| 211 | 223 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 212 | 224 | pub enum InputMode { |
@@ -217,6 +229,7 @@ pub enum InputMode { | ||
| 217 | 229 | Push { remotes: Vec<String>, selected: usize, status: PushStatus }, |
| 218 | 230 | Pull { remotes: Vec<String>, selected: usize, status: PullStatus }, |
| 219 | 231 | Fetch { remotes: Vec<String>, selected: usize, status: FetchStatus }, |
| 232 | + Tag { name: String, message: String, cursor: usize, existing_tags: Vec<String>, step: TagStep }, | |
| 220 | 233 | Confirm { message: String, action: ConfirmAction }, |
| 221 | 234 | } |
| 222 | 235 | |
src/ui.rsmodified@@ -1,5 +1,5 @@ | ||
| 1 | 1 | use crate::app::App; |
| 2 | -use crate::types::{AppMode, CommitStatus, FetchStatus, InputMode, PullStatus, PushStatus, SelectableItem}; | |
| 2 | +use crate::types::{AppMode, CommitStatus, FetchStatus, InputMode, PullStatus, PushStatus, SelectableItem, TagStep}; | |
| 3 | 3 | use ratatui::{ |
| 4 | 4 | layout::{Constraint, Direction, Layout, Rect}, |
| 5 | 5 | style::{Color, Modifier, Style}, |
@@ -43,6 +43,11 @@ pub fn draw(frame: &mut Frame, app: &App) { | ||
| 43 | 43 | draw_fetch_modal(frame, remotes, *selected, status); |
| 44 | 44 | } |
| 45 | 45 | |
| 46 | + // Draw modal overlay if in tag mode | |
| 47 | + if let InputMode::Tag { name, message, cursor, existing_tags, step } = &app.input_mode { | |
| 48 | + draw_tag_modal(frame, name, message, *cursor, existing_tags, step); | |
| 49 | + } | |
| 50 | + | |
| 46 | 51 | // Draw modal overlay if in confirm mode |
| 47 | 52 | if let InputMode::Confirm { message, .. } = &app.input_mode { |
| 48 | 53 | draw_confirm_modal(frame, message); |
@@ -386,6 +391,184 @@ fn draw_fetch_modal(frame: &mut Frame, remotes: &[String], selected: usize, stat | ||
| 386 | 391 | frame.render_widget(widget, modal_area); |
| 387 | 392 | } |
| 388 | 393 | |
| 394 | +/// Draw tag creation modal | |
| 395 | +fn draw_tag_modal( | |
| 396 | + frame: &mut Frame, | |
| 397 | + name: &str, | |
| 398 | + message: &str, | |
| 399 | + cursor: usize, | |
| 400 | + existing_tags: &[String], | |
| 401 | + step: &TagStep, | |
| 402 | +) { | |
| 403 | + let area = frame.area(); | |
| 404 | + | |
| 405 | + // Modal size based on step | |
| 406 | + let modal_height = match step { | |
| 407 | + TagStep::EnterName | TagStep::EnterMessage => { | |
| 408 | + let tags_height = existing_tags.len().min(5) as u16; | |
| 409 | + 8 + tags_height | |
| 410 | + } | |
| 411 | + _ => 5, | |
| 412 | + }; | |
| 413 | + let modal_width = 55.min(area.width.saturating_sub(4)); | |
| 414 | + let x = (area.width.saturating_sub(modal_width)) / 2; | |
| 415 | + let y = (area.height.saturating_sub(modal_height)) / 2; | |
| 416 | + | |
| 417 | + let modal_area = Rect::new(x, y, modal_width, modal_height); | |
| 418 | + | |
| 419 | + frame.render_widget(Clear, modal_area); | |
| 420 | + | |
| 421 | + let (title, border_color) = match step { | |
| 422 | + TagStep::EnterName => (" Tag - Enter Name ", Color::Cyan), | |
| 423 | + TagStep::EnterMessage => (" Tag - Enter Message ", Color::Cyan), | |
| 424 | + TagStep::Creating => (" Creating Tag... ", Color::Yellow), | |
| 425 | + TagStep::AskPush => (" Push Tag? ", Color::Yellow), | |
| 426 | + TagStep::Pushing => (" Pushing Tag... ", Color::Yellow), | |
| 427 | + TagStep::Success => (" ✓ Tag Pushed ", Color::Green), | |
| 428 | + TagStep::Failed(_) => (" ✗ Tag Failed ", Color::Red), | |
| 429 | + }; | |
| 430 | + | |
| 431 | + let block = Block::default() | |
| 432 | + .title(title) | |
| 433 | + .borders(Borders::ALL) | |
| 434 | + .border_style(Style::default().fg(border_color)); | |
| 435 | + | |
| 436 | + let content: Vec<Line> = match step { | |
| 437 | + TagStep::EnterName => { | |
| 438 | + let mut lines = vec![]; | |
| 439 | + | |
| 440 | + // Show existing tags | |
| 441 | + if !existing_tags.is_empty() { | |
| 442 | + lines.push(Line::from(Span::styled( | |
| 443 | + " Recent tags:", | |
| 444 | + Style::default().fg(Color::DarkGray), | |
| 445 | + ))); | |
| 446 | + for tag in existing_tags.iter().take(5) { | |
| 447 | + lines.push(Line::from(Span::styled( | |
| 448 | + format!(" {}", tag), | |
| 449 | + Style::default().fg(Color::DarkGray), | |
| 450 | + ))); | |
| 451 | + } | |
| 452 | + lines.push(Line::from("")); | |
| 453 | + } | |
| 454 | + | |
| 455 | + // Name input with cursor | |
| 456 | + let display_name = if cursor < name.len() { | |
| 457 | + format!( | |
| 458 | + "{}█{}", | |
| 459 | + &name[..cursor], | |
| 460 | + &name[cursor..] | |
| 461 | + ) | |
| 462 | + } else { | |
| 463 | + format!("{}█", name) | |
| 464 | + }; | |
| 465 | + lines.push(Line::from(vec![ | |
| 466 | + Span::styled(" Name: ", Style::default().fg(Color::White)), | |
| 467 | + Span::styled(display_name, Style::default().fg(Color::Yellow)), | |
| 468 | + ])); | |
| 469 | + | |
| 470 | + lines.push(Line::from("")); | |
| 471 | + lines.push(Line::from(Span::styled( | |
| 472 | + " Enter:next ESC:cancel", | |
| 473 | + Style::default().fg(Color::DarkGray), | |
| 474 | + ))); | |
| 475 | + lines | |
| 476 | + } | |
| 477 | + TagStep::EnterMessage => { | |
| 478 | + let mut lines = vec![]; | |
| 479 | + | |
| 480 | + // Show the name | |
| 481 | + lines.push(Line::from(vec![ | |
| 482 | + Span::styled(" Name: ", Style::default().fg(Color::DarkGray)), | |
| 483 | + Span::styled(name, Style::default().fg(Color::Yellow)), | |
| 484 | + ])); | |
| 485 | + lines.push(Line::from("")); | |
| 486 | + | |
| 487 | + // Message input with cursor | |
| 488 | + let display_msg = if cursor < message.len() { | |
| 489 | + format!( | |
| 490 | + "{}█{}", | |
| 491 | + &message[..cursor], | |
| 492 | + &message[cursor..] | |
| 493 | + ) | |
| 494 | + } else { | |
| 495 | + format!("{}█", message) | |
| 496 | + }; | |
| 497 | + lines.push(Line::from(vec![ | |
| 498 | + Span::styled(" Message: ", Style::default().fg(Color::White)), | |
| 499 | + Span::styled(display_msg, Style::default().fg(Color::Cyan)), | |
| 500 | + ])); | |
| 501 | + | |
| 502 | + lines.push(Line::from(Span::styled( | |
| 503 | + " (optional - leave empty for lightweight tag)", | |
| 504 | + Style::default().fg(Color::DarkGray), | |
| 505 | + ))); | |
| 506 | + lines.push(Line::from("")); | |
| 507 | + lines.push(Line::from(Span::styled( | |
| 508 | + " Enter:create ESC:back", | |
| 509 | + Style::default().fg(Color::DarkGray), | |
| 510 | + ))); | |
| 511 | + lines | |
| 512 | + } | |
| 513 | + TagStep::Creating => { | |
| 514 | + vec![ | |
| 515 | + Line::from(""), | |
| 516 | + Line::from(Span::styled( | |
| 517 | + format!(" Creating tag '{}'...", name), | |
| 518 | + Style::default().fg(Color::Yellow), | |
| 519 | + )), | |
| 520 | + ] | |
| 521 | + } | |
| 522 | + TagStep::AskPush => { | |
| 523 | + vec![ | |
| 524 | + Line::from(""), | |
| 525 | + Line::from(Span::styled( | |
| 526 | + format!(" Tag '{}' created!", name), | |
| 527 | + Style::default().fg(Color::Green), | |
| 528 | + )), | |
| 529 | + Line::from(""), | |
| 530 | + Line::from(vec![ | |
| 531 | + Span::raw(" Push to origin? "), | |
| 532 | + Span::styled("y", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), | |
| 533 | + Span::raw("es / "), | |
| 534 | + Span::styled("n", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), | |
| 535 | + Span::raw("o"), | |
| 536 | + ]), | |
| 537 | + ] | |
| 538 | + } | |
| 539 | + TagStep::Pushing => { | |
| 540 | + vec![ | |
| 541 | + Line::from(""), | |
| 542 | + Line::from(Span::styled( | |
| 543 | + format!(" Pushing tag '{}'...", name), | |
| 544 | + Style::default().fg(Color::Yellow), | |
| 545 | + )), | |
| 546 | + ] | |
| 547 | + } | |
| 548 | + TagStep::Success => { | |
| 549 | + vec![ | |
| 550 | + Line::from(""), | |
| 551 | + Line::from(Span::styled( | |
| 552 | + format!(" ✓ Tag '{}' pushed to origin!", name), | |
| 553 | + Style::default().fg(Color::Green), | |
| 554 | + )), | |
| 555 | + ] | |
| 556 | + } | |
| 557 | + TagStep::Failed(msg) => { | |
| 558 | + vec![ | |
| 559 | + Line::from(""), | |
| 560 | + Line::from(Span::styled( | |
| 561 | + format!(" ✗ {}", msg), | |
| 562 | + Style::default().fg(Color::Red), | |
| 563 | + )), | |
| 564 | + ] | |
| 565 | + } | |
| 566 | + }; | |
| 567 | + | |
| 568 | + let widget = Paragraph::new(content).block(block); | |
| 569 | + frame.render_widget(widget, modal_area); | |
| 570 | +} | |
| 571 | + | |
| 389 | 572 | /// Draw confirmation modal |
| 390 | 573 | fn draw_confirm_modal(frame: &mut Frame, message: &str) { |
| 391 | 574 | let area = frame.area(); |