diff options
Diffstat (limited to 'makima/src/daemon/tui')
| -rw-r--r-- | makima/src/daemon/tui/app.rs | 85 | ||||
| -rw-r--r-- | makima/src/daemon/tui/event.rs | 24 | ||||
| -rw-r--r-- | makima/src/daemon/tui/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/daemon/tui/ui.rs | 87 |
4 files changed, 184 insertions, 14 deletions
diff --git a/makima/src/daemon/tui/app.rs b/makima/src/daemon/tui/app.rs index ea431b8..cb0e8f3 100644 --- a/makima/src/daemon/tui/app.rs +++ b/makima/src/daemon/tui/app.rs @@ -78,6 +78,16 @@ pub enum CreateFormField { Repository, } +/// Repository suggestion from history +#[derive(Debug, Clone)] +pub struct RepositorySuggestion { + pub name: String, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub source_type: String, + pub use_count: i32, +} + /// State for create contract form #[derive(Debug, Clone, Default)] pub struct CreateContractState { @@ -94,11 +104,13 @@ pub struct CreateContractState { /// Cursor position in current text field pub cursor: usize, /// Available repository suggestions - pub repo_suggestions: Vec<String>, + pub repo_suggestions: Vec<RepositorySuggestion>, /// Selected suggestion index (for repository field) pub selected_suggestion: usize, /// Whether suggestions popup is visible pub show_suggestions: bool, + /// Whether suggestions have been loaded + pub suggestions_loaded: bool, } impl CreateContractState { @@ -113,6 +125,47 @@ impl CreateContractState { repo_suggestions: Vec::new(), selected_suggestion: 0, show_suggestions: false, + suggestions_loaded: false, + } + } + + /// Set repository suggestions + pub fn set_suggestions(&mut self, suggestions: Vec<RepositorySuggestion>) { + self.repo_suggestions = suggestions; + self.selected_suggestion = 0; + self.show_suggestions = !self.repo_suggestions.is_empty(); + self.suggestions_loaded = true; + } + + /// Apply the selected suggestion to the form + pub fn apply_selected_suggestion(&mut self) { + if let Some(suggestion) = self.repo_suggestions.get(self.selected_suggestion) { + // Apply the suggestion + if let Some(ref url) = suggestion.repository_url { + self.repository_url = url.clone(); + } else if let Some(ref path) = suggestion.local_path { + self.repository_url = path.clone(); + } + self.cursor = self.repository_url.len(); + self.show_suggestions = false; + } + } + + /// Navigate to next suggestion + pub fn next_suggestion(&mut self) { + if !self.repo_suggestions.is_empty() { + self.selected_suggestion = (self.selected_suggestion + 1) % self.repo_suggestions.len(); + } + } + + /// Navigate to previous suggestion + pub fn prev_suggestion(&mut self) { + if !self.repo_suggestions.is_empty() { + self.selected_suggestion = if self.selected_suggestion == 0 { + self.repo_suggestions.len() - 1 + } else { + self.selected_suggestion - 1 + }; } } @@ -156,14 +209,14 @@ impl CreateContractState { pub fn next_field(&mut self) { self.focused_field = (self.focused_field + 1) % 4; self.cursor = self.current_value().map(|v| v.len()).unwrap_or(0); - self.show_suggestions = false; + // Don't hide suggestions - they stay visible } /// Move to previous field pub fn prev_field(&mut self) { self.focused_field = if self.focused_field == 0 { 3 } else { self.focused_field - 1 }; self.cursor = self.current_value().map(|v| v.len()).unwrap_or(0); - self.show_suggestions = false; + // Don't hide suggestions - they stay visible } /// Toggle contract type @@ -445,6 +498,14 @@ pub enum Action { contract_type: String, repository_url: Option<String>, }, + /// Request to load repository suggestions (internal) + LoadRepoSuggestions, + /// Navigate to next suggestion in create form + CreateNextSuggestion, + /// Navigate to previous suggestion in create form + CreatePrevSuggestion, + /// Apply selected suggestion in create form + CreateApplySuggestion, } /// A displayable item in the TUI @@ -1068,6 +1129,8 @@ impl App { if self.view_type == ViewType::Contracts { self.create_state = CreateContractState::new(); self.input_mode = InputMode::CreateName; + // Request to load repository suggestions + return Action::LoadRepoSuggestions; } Action::None } @@ -1125,6 +1188,22 @@ impl App { // Pass through to caller for API call Action::PerformCreateContract { name, description, contract_type, repository_url } } + Action::LoadRepoSuggestions => { + // Pass through to caller for API call + Action::LoadRepoSuggestions + } + Action::CreateNextSuggestion => { + self.create_state.next_suggestion(); + Action::None + } + Action::CreatePrevSuggestion => { + self.create_state.prev_suggestion(); + Action::None + } + Action::CreateApplySuggestion => { + self.create_state.apply_selected_suggestion(); + Action::None + } Action::None => Action::None, } } diff --git a/makima/src/daemon/tui/event.rs b/makima/src/daemon/tui/event.rs index 2fed55a..d5ca569 100644 --- a/makima/src/daemon/tui/event.rs +++ b/makima/src/daemon/tui/event.rs @@ -194,6 +194,30 @@ fn handle_create_mode(app: &App, key: KeyEvent) -> Action { } let current_field = app.create_state.current_field(); + let has_suggestions = app.create_state.show_suggestions + && !app.create_state.repo_suggestions.is_empty(); + + // Allow Ctrl+N/Ctrl+P to navigate suggestions from any field + if has_suggestions && key.modifiers.contains(KeyModifiers::CONTROL) { + match key.code { + KeyCode::Char('n') => return Action::CreateNextSuggestion, + KeyCode::Char('p') => return Action::CreatePrevSuggestion, + _ => {} + } + } + + // Special handling when on Repository field with suggestions visible + let on_repo_field = current_field == CreateFormField::Repository; + if has_suggestions && on_repo_field { + match key.code { + // Up/Down navigate suggestions when on repo field + KeyCode::Up => return Action::CreatePrevSuggestion, + KeyCode::Down => return Action::CreateNextSuggestion, + // Enter applies suggestion instead of submitting form + KeyCode::Enter => return Action::CreateApplySuggestion, + _ => {} + } + } match key.code { // Submit form diff --git a/makima/src/daemon/tui/mod.rs b/makima/src/daemon/tui/mod.rs index 06cac34..e52b12a 100644 --- a/makima/src/daemon/tui/mod.rs +++ b/makima/src/daemon/tui/mod.rs @@ -16,7 +16,7 @@ pub mod fuzzy; pub mod ui; pub mod ws_client; -pub use app::{App, ListItem, ViewType, ViewState, InputMode, Action, OutputBuffer, OutputLine, OutputMessageType, WsConnectionState, CreateContractState, CreateFormField}; +pub use app::{App, ListItem, ViewType, ViewState, InputMode, Action, OutputBuffer, OutputLine, OutputMessageType, WsConnectionState, CreateContractState, CreateFormField, RepositorySuggestion}; pub use ws_client::{TuiWsClient, WsCommand, WsEvent, TaskOutputEvent}; pub use fuzzy::FuzzyMatcher; diff --git a/makima/src/daemon/tui/ui.rs b/makima/src/daemon/tui/ui.rs index de15320..2a5a6ce 100644 --- a/makima/src/daemon/tui/ui.rs +++ b/makima/src/daemon/tui/ui.rs @@ -362,10 +362,21 @@ fn render_edit_dialog(frame: &mut Frame, app: &App) { /// Render the create contract dialog fn render_create_dialog(frame: &mut Frame, app: &App) { - // Calculate popup size and position + // Calculate popup size and position - make it taller if suggestions are shown let area = frame.area(); + let state = &app.create_state; + let current_field = state.current_field(); + // Show suggestions whenever we have them (like the frontend does) + let show_suggestions = state.show_suggestions && !state.repo_suggestions.is_empty(); + let popup_width = 70.min(area.width.saturating_sub(4)); - let popup_height = 20; + let base_height = 20; + let suggestion_height = if show_suggestions { + (state.repo_suggestions.len().min(5) + 2) as u16 + } else { + 0 + }; + let popup_height = base_height + suggestion_height; let popup_x = (area.width.saturating_sub(popup_width)) / 2; let popup_y = (area.height.saturating_sub(popup_height)) / 2; @@ -380,8 +391,6 @@ fn render_create_dialog(frame: &mut Frame, app: &App) { // Clear the area behind the popup frame.render_widget(Clear, popup_area); - let state = &app.create_state; - let current_field = state.current_field(); let max_field_width = (popup_width as usize).saturating_sub(18); // Styles @@ -389,6 +398,8 @@ fn render_create_dialog(frame: &mut Frame, app: &App) { let inactive_style = Style::default().fg(Color::White); let label_style = Style::default().fg(Color::DarkGray); let hint_style = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); + let suggestion_style = Style::default().fg(Color::White); + let selected_suggestion_style = Style::default().fg(Color::Black).bg(Color::Cyan); // Helper to build text field with cursor let build_field = |value: &str, cursor: usize, is_active: bool| -> String { @@ -435,7 +446,7 @@ fn render_create_dialog(frame: &mut Frame, app: &App) { ] }; - let text = vec![ + let mut text = vec![ Line::from(""), Line::from(Span::styled( " New Contract", @@ -486,9 +497,65 @@ fn render_create_dialog(frame: &mut Frame, app: &App) { ), ]), Line::from(Span::styled(" Git repository URL (optional)", hint_style)), - Line::from(""), - // Help line - Line::from(vec![ + ]; + + // Add suggestions section + if show_suggestions { + text.push(Line::from("")); + text.push(Line::from(Span::styled( + " Recent repositories (↑/↓ to select, Enter to apply):", + Style::default().fg(Color::Cyan), + ))); + + for (i, suggestion) in state.repo_suggestions.iter().take(5).enumerate() { + let is_selected = i == state.selected_suggestion; + let url_or_path = suggestion.repository_url.as_ref() + .or(suggestion.local_path.as_ref()) + .map(|s| s.as_str()) + .unwrap_or(""); + + // Truncate if too long + let display_url = if url_or_path.len() > max_field_width - 10 { + format!("...{}", &url_or_path[url_or_path.len().saturating_sub(max_field_width - 13)..]) + } else { + url_or_path.to_string() + }; + + let prefix = if is_selected { " → " } else { " " }; + let count_suffix = format!(" ({}×)", suggestion.use_count); + + text.push(Line::from(vec![ + Span::styled( + format!("{}{}{}", prefix, display_url, count_suffix), + if is_selected { selected_suggestion_style } else { suggestion_style }, + ), + ])); + } + } else if state.suggestions_loaded && state.repo_suggestions.is_empty() { + // Show message when suggestions loaded but empty + text.push(Line::from("")); + text.push(Line::from(Span::styled( + " (No recent repositories - add repos to contracts to see suggestions here)", + hint_style, + ))); + } + + text.push(Line::from("")); + + // Help line - show different help when suggestions are visible + if show_suggestions { + text.push(Line::from(vec![ + Span::styled(" ↑/↓", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::raw(": select "), + Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::raw(": apply "), + Span::styled("Tab", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::raw(": next field "), + Span::styled("Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), + Span::raw(": cancel"), + ])); + } else { + text.push(Line::from(vec![ Span::styled(" Tab/↑↓", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), Span::raw(": switch "), Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), @@ -497,8 +564,8 @@ fn render_create_dialog(frame: &mut Frame, app: &App) { Span::raw(": toggle type "), Span::styled("Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::raw(": cancel"), - ]), - ]; + ])); + } let popup = Paragraph::new(text) .block(Block::default() |
