summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/app.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-20 17:23:34 +0000
committersoryu <soryu@soryu.co>2026-01-20 17:23:46 +0000
commit055e2c4a72e3b2331a18fdc9f8132ef990af7e38 (patch)
tree75b92637b9132594b76a5a86f5f854bca1ddee49 /makima/src/daemon/tui/app.rs
parent54c6d409e1d5667f4ab7f63a43e1459e68575c94 (diff)
downloadsoryu-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.rs264
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,
}
}