diff options
| author | soryu <soryu@soryu.co> | 2026-01-20 17:23:34 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-20 17:23:46 +0000 |
| commit | 055e2c4a72e3b2331a18fdc9f8132ef990af7e38 (patch) | |
| tree | 75b92637b9132594b76a5a86f5f854bca1ddee49 /makima/src/daemon/tui/app.rs | |
| parent | 54c6d409e1d5667f4ab7f63a43e1459e68575c94 (diff) | |
| download | soryu-055e2c4a72e3b2331a18fdc9f8132ef990af7e38.tar.gz soryu-055e2c4a72e3b2331a18fdc9f8132ef990af7e38.zip | |
Update CLI to show log history as well
Diffstat (limited to 'makima/src/daemon/tui/app.rs')
| -rw-r--r-- | makima/src/daemon/tui/app.rs | 264 |
1 files changed, 256 insertions, 8 deletions
diff --git a/makima/src/daemon/tui/app.rs b/makima/src/daemon/tui/app.rs index 3eff998..ea431b8 100644 --- a/makima/src/daemon/tui/app.rs +++ b/makima/src/daemon/tui/app.rs @@ -63,6 +63,164 @@ pub enum InputMode { EditName, /// Edit mode - editing description/plan EditDescription, + /// Create contract - editing name + CreateName, + /// Create contract - editing description + CreateDescription, +} + +/// Create contract form field +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CreateFormField { + Name, + Description, + ContractType, + Repository, +} + +/// State for create contract form +#[derive(Debug, Clone, Default)] +pub struct CreateContractState { + /// Contract name + pub name: String, + /// Contract description + pub description: String, + /// Contract type: "simple" or "specification" + pub contract_type: String, + /// Repository URL (optional) + pub repository_url: String, + /// Currently focused field + pub focused_field: usize, + /// Cursor position in current text field + pub cursor: usize, + /// Available repository suggestions + pub repo_suggestions: Vec<String>, + /// Selected suggestion index (for repository field) + pub selected_suggestion: usize, + /// Whether suggestions popup is visible + pub show_suggestions: bool, +} + +impl CreateContractState { + pub fn new() -> Self { + Self { + name: String::new(), + description: String::new(), + contract_type: "simple".to_string(), + repository_url: String::new(), + focused_field: 0, + cursor: 0, + repo_suggestions: Vec::new(), + selected_suggestion: 0, + show_suggestions: false, + } + } + + /// Get the field at the given index + pub fn field_at(&self, index: usize) -> CreateFormField { + match index { + 0 => CreateFormField::Name, + 1 => CreateFormField::Description, + 2 => CreateFormField::ContractType, + 3 => CreateFormField::Repository, + _ => CreateFormField::Name, + } + } + + /// Get the current field + pub fn current_field(&self) -> CreateFormField { + self.field_at(self.focused_field) + } + + /// Get mutable reference to the current text field value + pub fn current_value_mut(&mut self) -> Option<&mut String> { + match self.current_field() { + CreateFormField::Name => Some(&mut self.name), + CreateFormField::Description => Some(&mut self.description), + CreateFormField::Repository => Some(&mut self.repository_url), + CreateFormField::ContractType => None, // Not a text field + } + } + + /// Get the current text field value + pub fn current_value(&self) -> Option<&str> { + match self.current_field() { + CreateFormField::Name => Some(&self.name), + CreateFormField::Description => Some(&self.description), + CreateFormField::Repository => Some(&self.repository_url), + CreateFormField::ContractType => None, + } + } + + /// Move to next field + 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; + } + + /// 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; + } + + /// Toggle contract type + pub fn toggle_contract_type(&mut self) { + self.contract_type = if self.contract_type == "simple" { + "specification".to_string() + } else { + "simple".to_string() + }; + } + + /// Insert character at cursor + pub fn insert_char(&mut self, c: char) { + let cursor = self.cursor; + match self.current_field() { + CreateFormField::Name => { + self.name.insert(cursor, c); + self.cursor += 1; + } + CreateFormField::Description => { + self.description.insert(cursor, c); + self.cursor += 1; + } + CreateFormField::Repository => { + self.repository_url.insert(cursor, c); + self.cursor += 1; + } + CreateFormField::ContractType => {} + } + } + + /// Delete character before cursor + pub fn backspace(&mut self) { + if self.cursor > 0 { + let cursor = self.cursor - 1; + match self.current_field() { + CreateFormField::Name => { + self.name.remove(cursor); + self.cursor = cursor; + } + CreateFormField::Description => { + self.description.remove(cursor); + self.cursor = cursor; + } + CreateFormField::Repository => { + self.repository_url.remove(cursor); + self.cursor = cursor; + } + CreateFormField::ContractType => {} + } + } + } + + /// Check if form is valid (name is required) + pub fn is_valid(&self) -> bool { + !self.name.trim().is_empty() + } } /// Edit state for inline editing @@ -264,6 +422,29 @@ pub enum Action { ScrollDown, /// Scroll to bottom of output ScrollToBottom, + /// Open create contract form + NewContract, + /// Add character in create form + CreateChar(char), + /// Backspace in create form + CreateBackspace, + /// Move to next field in create form + CreateNextField, + /// Move to previous field in create form + CreatePrevField, + /// Toggle value (for contract type) + CreateToggle, + /// Submit create form + CreateSubmit, + /// Cancel create form + CreateCancel, + /// Request to create contract (internal) + PerformCreateContract { + name: String, + description: String, + contract_type: String, + repository_url: Option<String>, + }, } /// A displayable item in the TUI @@ -461,6 +642,8 @@ pub struct App { pub pending_delete: Option<Uuid>, /// Edit state for inline editing pub edit_state: EditState, + /// Create contract form state + pub create_state: CreateContractState, /// Status message pub status_message: Option<String>, /// Whether the app should quit @@ -492,6 +675,7 @@ impl App { preview_visible: false, pending_delete: None, edit_state: EditState::default(), + create_state: CreateContractState::new(), status_message: None, should_quit: false, exit_action: None, @@ -636,6 +820,10 @@ impl App { if let Some(item) = self.selected_item() { let contract_id = item.id; let contract_name = item.name.clone(); + // Push view and set state before returning + self.push_view(ViewType::Tasks); + self.contract_id = Some(contract_id); + self.contract_name = Some(contract_name.clone()); return Action::LoadTasks { contract_id, contract_name }; } } @@ -644,6 +832,10 @@ impl App { if let Some(item) = self.selected_item() { let task_id = item.id; let task_name = item.name.clone(); + // Push view and set state before returning + self.push_view(ViewType::TaskOutput); + self.task_id = Some(task_id); + self.task_name = Some(task_name.clone()); return Action::LoadTaskOutput { task_id, task_name }; } } @@ -838,17 +1030,11 @@ impl App { Action::LaunchEditor(path) } Action::LoadTasks { contract_id, contract_name } => { - // Prepare for tasks view - self.push_view(ViewType::Tasks); - self.contract_id = Some(contract_id); - self.contract_name = Some(contract_name.clone()); + // Pass through to caller for data loading (view already pushed by DrillDown) Action::LoadTasks { contract_id, contract_name } } Action::LoadTaskOutput { task_id, task_name } => { - // Prepare for output view - self.push_view(ViewType::TaskOutput); - self.task_id = Some(task_id); - self.task_name = Some(task_name.clone()); + // Pass through to caller for data loading (view already pushed by DrillDown) Action::LoadTaskOutput { task_id, task_name } } Action::PerformDelete { id, item_type } => { @@ -877,6 +1063,68 @@ impl App { } Action::None } + Action::NewContract => { + // Only allow creating contracts from contracts view + if self.view_type == ViewType::Contracts { + self.create_state = CreateContractState::new(); + self.input_mode = InputMode::CreateName; + } + Action::None + } + Action::CreateChar(c) => { + self.create_state.insert_char(c); + Action::None + } + Action::CreateBackspace => { + self.create_state.backspace(); + Action::None + } + Action::CreateNextField => { + self.create_state.next_field(); + Action::None + } + Action::CreatePrevField => { + self.create_state.prev_field(); + Action::None + } + Action::CreateToggle => { + if self.create_state.current_field() == CreateFormField::ContractType { + self.create_state.toggle_contract_type(); + } + Action::None + } + Action::CreateSubmit => { + if self.create_state.is_valid() { + let name = self.create_state.name.clone(); + let description = self.create_state.description.clone(); + let contract_type = self.create_state.contract_type.clone(); + let repository_url = if self.create_state.repository_url.is_empty() { + None + } else { + Some(self.create_state.repository_url.clone()) + }; + self.input_mode = InputMode::Normal; + return Action::PerformCreateContract { + name, + description, + contract_type, + repository_url, + }; + } else { + self.status_message = Some("Name is required".to_string()); + } + Action::None + } + Action::CreateCancel => { + self.create_state = CreateContractState::new(); + self.input_mode = InputMode::Normal; + self.status_message = Some("Create cancelled".to_string()); + Action::None + } + Action::PerformCreateContract { name, description, contract_type, repository_url } => { + // Pass through to caller for API call + Action::PerformCreateContract { name, description, contract_type, repository_url } + } Action::None => Action::None, } } |
