"""BIOS-style TUI for CmdForge using snack (python3-newt).""" import sys # Ensure system packages are accessible if '/usr/lib/python3/dist-packages' not in sys.path: sys.path.insert(0, '/usr/lib/python3/dist-packages') import snack from typing import Optional, List, Tuple from .tool import ( Tool, ToolArgument, PromptStep, CodeStep, Step, list_tools, load_tool, save_tool, delete_tool, tool_exists ) from .providers import Provider, load_providers, add_provider, delete_provider, get_provider class CmdForgeUI: """BIOS-style UI for CmdForge.""" def __init__(self): self.screen = None def run(self): """Run the UI.""" self.screen = snack.SnackScreen() # Enable mouse support self.screen.pushHelpLine(" Tab/Arrow:Navigate | Enter:Select | Mouse:Click | Esc:Back ") try: # Enable mouse - newt supports GPM and xterm mouse import os os.environ.setdefault('NEWT_MONO', '0') self.main_menu() finally: self.screen.finish() def main_menu(self): """Show the main menu.""" while True: items = [ ("Create New Tool", "create"), ("Edit Tool", "edit"), ("Delete Tool", "delete"), ("List Tools", "list"), ("Test Tool", "test"), ("Manage Providers", "providers"), ("Exit", "exit"), ] listbox = snack.Listbox(height=7, width=30, returnExit=1) for label, value in items: listbox.append(label, value) grid = snack.GridForm(self.screen, "CmdForge Manager", 1, 1) grid.add(listbox, 0, 0) result = grid.runOnce() selected = listbox.current() if selected == "exit" or result == "ESC": break elif selected == "create": self.tool_builder(None) elif selected == "edit": self.select_and_edit_tool() elif selected == "delete": self.select_and_delete_tool() elif selected == "list": self.show_tools_list() elif selected == "test": self.select_and_test_tool() elif selected == "providers": self.manage_providers() def message_box(self, title: str, message: str): """Show a message box.""" snack.ButtonChoiceWindow(self.screen, title, message, ["OK"]) def yes_no(self, title: str, message: str) -> bool: """Show a yes/no dialog.""" result = snack.ButtonChoiceWindow(self.screen, title, message, ["Yes", "No"]) return result == "yes" def input_box(self, title: str, prompt: str, initial: str = "", width: int = 40) -> Optional[str]: """Show an input dialog.""" entry = snack.Entry(width, initial) grid = snack.GridForm(self.screen, title, 1, 3) grid.add(snack.Label(prompt), 0, 0) grid.add(entry, 0, 1, padding=(0, 1, 0, 1)) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 2) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": return entry.value() return None def text_edit(self, title: str, initial: str = "", width: int = 60, height: int = 10) -> Optional[str]: """Show a multi-line text editor.""" text = snack.Textbox(width, height, initial, scroll=1, wrap=1) # snack doesn't have a true multi-line editor, so we use Entry for now # For multi-line, we'll use a workaround with a simple entry entry = snack.Entry(width, initial, scroll=1) grid = snack.GridForm(self.screen, title, 1, 2) grid.add(entry, 0, 0, padding=(0, 0, 0, 1)) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": return entry.value() return None def select_provider(self) -> Optional[str]: """Show provider selection dialog.""" providers = load_providers() listbox = snack.Listbox(height=min(len(providers) + 1, 8), width=50, returnExit=1) for p in providers: listbox.append(f"{p.name}: {p.command}", p.name) listbox.append("[ + Add New Provider ]", "__new__") grid = snack.GridForm(self.screen, "Select Provider", 1, 2) grid.add(listbox, 0, 0) buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "cancel" or result == "ESC": return None selected = listbox.current() if selected == "__new__": provider = self.add_provider_dialog() if provider: add_provider(provider) return provider.name return None return selected def add_provider_dialog(self) -> Optional[Provider]: """Dialog to add a new provider.""" name_entry = snack.Entry(30, "") cmd_entry = snack.Entry(40, "") desc_entry = snack.Entry(40, "") grid = snack.GridForm(self.screen, "Add Provider", 2, 4) grid.add(snack.Label("Name:"), 0, 0, anchorLeft=1) grid.add(name_entry, 1, 0, padding=(1, 0, 0, 0)) grid.add(snack.Label("Command:"), 0, 1, anchorLeft=1) grid.add(cmd_entry, 1, 1, padding=(1, 0, 0, 0)) grid.add(snack.Label("Description:"), 0, 2, anchorLeft=1) grid.add(desc_entry, 1, 2, padding=(1, 0, 0, 0)) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 3, growx=1, growy=1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": name = name_entry.value().strip() cmd = cmd_entry.value().strip() if name and cmd: return Provider(name=name, command=cmd, description=desc_entry.value().strip()) self.message_box("Error", "Name and command are required.") return None def add_argument_dialog(self, existing: Optional[ToolArgument] = None) -> Optional[ToolArgument]: """Dialog to add/edit an argument.""" flag_entry = snack.Entry(20, existing.flag if existing else "--") var_entry = snack.Entry(20, existing.variable if existing else "") default_entry = snack.Entry(20, existing.default or "" if existing else "") desc_entry = snack.Entry(40, existing.description if existing else "") title = "Edit Argument" if existing else "Add Argument" grid = snack.GridForm(self.screen, title, 2, 5) grid.add(snack.Label("Flag:"), 0, 0, anchorLeft=1) grid.add(flag_entry, 1, 0, padding=(1, 0, 0, 0)) grid.add(snack.Label("Variable:"), 0, 1, anchorLeft=1) grid.add(var_entry, 1, 1, padding=(1, 0, 0, 0)) grid.add(snack.Label("Default:"), 0, 2, anchorLeft=1) grid.add(default_entry, 1, 2, padding=(1, 0, 0, 0)) grid.add(snack.Label("Description:"), 0, 3, anchorLeft=1) grid.add(desc_entry, 1, 3, padding=(1, 0, 0, 0)) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 4, growx=1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": flag = flag_entry.value().strip() var = var_entry.value().strip() if not flag: self.message_box("Error", "Flag is required.") return None if not var: var = flag.lstrip("-").replace("-", "_") return ToolArgument( flag=flag, variable=var, default=default_entry.value().strip() or None, description=desc_entry.value().strip() ) return None def add_step_dialog(self, tool: Tool, existing_step: Optional[Step] = None, step_idx: int = -1) -> Optional[Step]: """Dialog to choose and add a step.""" if existing_step: # Edit existing step if isinstance(existing_step, PromptStep): return self.add_prompt_dialog(tool, existing_step, step_idx) else: return self.add_code_dialog(tool, existing_step, step_idx) # Choose step type listbox = snack.Listbox(height=2, width=30, returnExit=1) listbox.append("Prompt (AI call)", "prompt") listbox.append("Code (Python)", "code") grid = snack.GridForm(self.screen, "Add Step", 1, 2) grid.add(listbox, 0, 0) buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "cancel" or result == "ESC": return None step_type = listbox.current() if step_type == "prompt": return self.add_prompt_dialog(tool, None, step_idx) else: return self.add_code_dialog(tool, None, step_idx) def get_available_variables(self, tool: Tool, up_to_step: int = -1) -> List[str]: """Get available variables at a point in the tool.""" variables = ["input"] for arg in tool.arguments: variables.append(arg.variable) if up_to_step == -1: up_to_step = len(tool.steps) for i, step in enumerate(tool.steps): if i >= up_to_step: break variables.append(step.output_var) return variables def add_prompt_dialog(self, tool: Tool, existing: Optional[PromptStep] = None, step_idx: int = -1) -> Optional[PromptStep]: """Dialog to add/edit a prompt step.""" available = self.get_available_variables(tool, step_idx if step_idx >= 0 else -1) var_help = "Variables: " + ", ".join(f"{{{v}}}" for v in available) # Provider selection first provider = self.select_provider() if not provider: provider = existing.provider if existing else "mock" prompt_entry = snack.Entry(60, existing.prompt if existing else f"Process this:\n\n{{input}}", scroll=1) output_entry = snack.Entry(20, existing.output_var if existing else "result") title = "Edit Prompt Step" if existing else "Add Prompt Step" grid = snack.GridForm(self.screen, title, 2, 4) grid.add(snack.Label(f"Provider: {provider}"), 0, 0, anchorLeft=1, growx=1) grid.add(snack.Label(""), 1, 0) grid.add(snack.Label(f"Prompt ({var_help}):"), 0, 1, anchorLeft=1, growx=1) grid.add(snack.Label(""), 1, 1) grid.add(prompt_entry, 0, 2, growx=1) grid.add(snack.Label(""), 1, 2) sub_grid = snack.Grid(2, 1) sub_grid.setField(snack.Label("Output var:"), 0, 0, anchorLeft=1) sub_grid.setField(output_entry, 1, 0, padding=(1, 0, 0, 0)) grid.add(sub_grid, 0, 3, anchorLeft=1) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 1, 3) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": prompt = prompt_entry.value().strip() output_var = output_entry.value().strip() if not prompt: self.message_box("Error", "Prompt is required.") return None if not output_var: output_var = "result" return PromptStep(prompt=prompt, provider=provider, output_var=output_var) return None def add_code_dialog(self, tool: Tool, existing: Optional[CodeStep] = None, step_idx: int = -1) -> Optional[CodeStep]: """Dialog to add/edit a code step.""" available = self.get_available_variables(tool, step_idx if step_idx >= 0 else -1) var_help = "Variables: " + ", ".join(available) default_code = existing.code if existing else "# Set 'result' for output\nresult = input.upper()" code_entry = snack.Entry(60, default_code, scroll=1) output_entry = snack.Entry(20, existing.output_var if existing else "processed") title = "Edit Code Step" if existing else "Add Code Step" grid = snack.GridForm(self.screen, title, 2, 4) grid.add(snack.Label(f"Python Code ({var_help}):"), 0, 0, anchorLeft=1, growx=1) grid.add(snack.Label("Set 'result' variable for output"), 1, 0) grid.add(code_entry, 0, 1, growx=1) grid.add(snack.Label(""), 1, 1) sub_grid = snack.Grid(2, 1) sub_grid.setField(snack.Label("Output var:"), 0, 0, anchorLeft=1) sub_grid.setField(output_entry, 1, 0, padding=(1, 0, 0, 0)) grid.add(sub_grid, 0, 2, anchorLeft=1) buttons = snack.ButtonBar(self.screen, [("OK", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 1, 2) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": code = code_entry.value().strip() output_var = output_entry.value().strip() if not code: self.message_box("Error", "Code is required.") return None if not output_var: output_var = "processed" return CodeStep(code=code, output_var=output_var) return None def tool_builder(self, existing: Optional[Tool] = None) -> Optional[Tool]: """Main tool builder - BIOS-style unified form.""" is_edit = existing is not None # Initialize tool if existing: tool = Tool( name=existing.name, description=existing.description, arguments=list(existing.arguments), steps=list(existing.steps), output=existing.output ) else: tool = Tool(name="", description="", arguments=[], steps=[], output="{input}") while True: # Create form elements name_entry = snack.Entry(25, tool.name, scroll=0) desc_entry = snack.Entry(25, tool.description, scroll=1) output_entry = snack.Entry(25, tool.output, scroll=1) # Arguments listbox args_listbox = snack.Listbox(height=4, width=35, returnExit=0, scroll=1) for i, arg in enumerate(tool.arguments): args_listbox.append(f"{arg.flag} -> {{{arg.variable}}}", i) args_listbox.append("[ + Add ]", "add") # Steps listbox steps_listbox = snack.Listbox(height=5, width=35, returnExit=0, scroll=1) for i, step in enumerate(tool.steps): if isinstance(step, PromptStep): steps_listbox.append(f"P:{step.provider} -> {{{step.output_var}}}", i) else: steps_listbox.append(f"C: -> {{{step.output_var}}}", i) steps_listbox.append("[ + Add ]", "add") # Build the grid layout using nested grids for better control title = f"Edit Tool: {tool.name}" if is_edit and tool.name else "New Tool" # Left column grid: Name, Description, Output left_grid = snack.Grid(1, 6) left_grid.setField(snack.Label("Name:"), 0, 0, anchorLeft=1) left_grid.setField(name_entry, 0, 1, anchorLeft=1) left_grid.setField(snack.Label("Description:"), 0, 2, anchorLeft=1, padding=(0, 1, 0, 0)) left_grid.setField(desc_entry, 0, 3, anchorLeft=1) left_grid.setField(snack.Label("Output:"), 0, 4, anchorLeft=1, padding=(0, 1, 0, 0)) left_grid.setField(output_entry, 0, 5, anchorLeft=1) # Right column grid: Arguments and Steps right_grid = snack.Grid(1, 4) right_grid.setField(snack.Label("Arguments:"), 0, 0, anchorLeft=1) right_grid.setField(args_listbox, 0, 1, anchorLeft=1) right_grid.setField(snack.Label("Execution Steps:"), 0, 2, anchorLeft=1, padding=(0, 1, 0, 0)) right_grid.setField(steps_listbox, 0, 3, anchorLeft=1) # Main grid grid = snack.GridForm(self.screen, title, 2, 2) grid.add(left_grid, 0, 0, anchorTop=1, padding=(0, 0, 2, 0)) grid.add(right_grid, 1, 0, anchorTop=1) # Buttons at bottom buttons = snack.ButtonBar(self.screen, [("Save", "save"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1, growx=1) # Handle hotkeys for listbox interaction form = grid.form while True: result = form.run() # Update tool from entries if not is_edit: tool.name = name_entry.value().strip() tool.description = desc_entry.value().strip() tool.output = output_entry.value().strip() # Check what was activated if result == args_listbox: selected = args_listbox.current() if selected == "add": new_arg = self.add_argument_dialog() if new_arg: tool.arguments.append(new_arg) break # Refresh form elif isinstance(selected, int): # Edit/delete existing argument action = self.arg_action_menu(tool.arguments[selected]) if action == "edit": updated = self.add_argument_dialog(tool.arguments[selected]) if updated: tool.arguments[selected] = updated elif action == "delete": tool.arguments.pop(selected) break # Refresh form elif result == steps_listbox: selected = steps_listbox.current() if selected == "add": new_step = self.add_step_dialog(tool) if new_step: tool.steps.append(new_step) break # Refresh form elif isinstance(selected, int): # Edit/delete existing step action = self.step_action_menu(tool.steps[selected], selected, len(tool.steps)) if action == "edit": updated = self.add_step_dialog(tool, tool.steps[selected], selected) if updated: tool.steps[selected] = updated elif action == "delete": tool.steps.pop(selected) elif action == "move_up" and selected > 0: tool.steps[selected], tool.steps[selected-1] = tool.steps[selected-1], tool.steps[selected] elif action == "move_down" and selected < len(tool.steps) - 1: tool.steps[selected], tool.steps[selected+1] = tool.steps[selected+1], tool.steps[selected] break # Refresh form elif buttons.buttonPressed(result) == "save": if not tool.name: self.message_box("Error", "Tool name is required.") break if not is_edit and tool_exists(tool.name): if not self.yes_no("Overwrite?", f"Tool '{tool.name}' exists. Overwrite?"): break self.screen.popWindow() save_tool(tool) self.message_box("Success", f"Tool '{tool.name}' saved!") return tool elif buttons.buttonPressed(result) == "cancel" or result == "ESC": self.screen.popWindow() return None else: # Tab between fields, continue continue self.screen.popWindow() def arg_action_menu(self, arg: ToolArgument) -> Optional[str]: """Show action menu for an argument.""" listbox = snack.Listbox(height=3, width=20, returnExit=1) listbox.append("Edit", "edit") listbox.append("Delete", "delete") listbox.append("Cancel", "cancel") grid = snack.GridForm(self.screen, f"Argument: {arg.flag}", 1, 1) grid.add(listbox, 0, 0) grid.runOnce() return listbox.current() if listbox.current() != "cancel" else None def step_action_menu(self, step: Step, idx: int, total: int) -> Optional[str]: """Show action menu for a step.""" step_type = "Prompt" if isinstance(step, PromptStep) else "Code" items = [("Edit", "edit")] if idx > 0: items.append(("Move Up", "move_up")) if idx < total - 1: items.append(("Move Down", "move_down")) items.append(("Delete", "delete")) items.append(("Cancel", "cancel")) listbox = snack.Listbox(height=len(items), width=20, returnExit=1) for label, value in items: listbox.append(label, value) grid = snack.GridForm(self.screen, f"Step {idx+1}: {step_type}", 1, 1) grid.add(listbox, 0, 0) grid.runOnce() return listbox.current() if listbox.current() != "cancel" else None def select_and_edit_tool(self): """Select a tool to edit.""" tools = list_tools() if not tools: self.message_box("Edit Tool", "No tools found.") return listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1) for name in tools: tool = load_tool(name) desc = tool.description[:30] if tool and tool.description else "No description" listbox.append(f"{name}: {desc}", name) grid = snack.GridForm(self.screen, "Select Tool to Edit", 1, 2) grid.add(listbox, 0, 0) buttons = snack.ButtonBar(self.screen, [("Select", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": selected = listbox.current() tool = load_tool(selected) if tool: self.tool_builder(tool) def select_and_delete_tool(self): """Select a tool to delete.""" tools = list_tools() if not tools: self.message_box("Delete Tool", "No tools found.") return listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1) for name in tools: listbox.append(name, name) grid = snack.GridForm(self.screen, "Select Tool to Delete", 1, 2) grid.add(listbox, 0, 0) buttons = snack.ButtonBar(self.screen, [("Delete", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": selected = listbox.current() if self.yes_no("Confirm", f"Delete tool '{selected}'?"): if delete_tool(selected): self.message_box("Deleted", f"Tool '{selected}' deleted.") def show_tools_list(self): """Show list of all tools.""" tools = list_tools() if not tools: self.message_box("Tools", "No tools found.\n\nCreate one from the main menu.") return text = "" for name in tools: tool = load_tool(name) if tool: text += f"{name}\n" text += f" {tool.description or 'No description'}\n" if tool.arguments: args = ", ".join(a.flag for a in tool.arguments) text += f" Args: {args}\n" if tool.steps: steps = [] for s in tool.steps: if isinstance(s, PromptStep): steps.append(f"P:{s.provider}") else: steps.append("C") text += f" Steps: {' -> '.join(steps)}\n" text += "\n" snack.ButtonChoiceWindow(self.screen, "Available Tools", text.strip(), ["OK"]) def select_and_test_tool(self): """Select a tool to test.""" tools = list_tools() if not tools: self.message_box("Test Tool", "No tools found.") return listbox = snack.Listbox(height=min(len(tools), 10), width=40, returnExit=1, scroll=1) for name in tools: listbox.append(name, name) grid = snack.GridForm(self.screen, "Select Tool to Test", 1, 2) grid.add(listbox, 0, 0) buttons = snack.ButtonBar(self.screen, [("Test", "ok"), ("Cancel", "cancel")]) grid.add(buttons, 0, 1) result = grid.runOnce() if buttons.buttonPressed(result) == "ok": selected = listbox.current() tool = load_tool(selected) if tool: test_input = self.input_box("Test Input", "Enter test input:", "Hello world") if test_input: from .runner import run_tool output, code = run_tool( tool=tool, input_text=test_input, custom_args={}, provider_override="mock", dry_run=False, show_prompt=False, verbose=False ) result_text = f"Exit code: {code}\n\nOutput:\n{output[:500]}" self.message_box("Test Result", result_text) def manage_providers(self): """Manage providers menu.""" while True: providers = load_providers() listbox = snack.Listbox(height=min(len(providers) + 2, 10), width=50, returnExit=1, scroll=1) for p in providers: listbox.append(f"{p.name}: {p.command}", p.name) listbox.append("[ + Add Provider ]", "__add__") listbox.append("[ <- Back ]", "__back__") grid = snack.GridForm(self.screen, "Manage Providers", 1, 1) grid.add(listbox, 0, 0) grid.runOnce() selected = listbox.current() if selected == "__back__": break elif selected == "__add__": provider = self.add_provider_dialog() if provider: add_provider(provider) self.message_box("Success", f"Provider '{provider.name}' added.") else: # Edit or delete provider = get_provider(selected) if provider: action = self.provider_action_menu(provider) if action == "edit": updated = self.add_provider_dialog() # TODO: pass existing if updated: add_provider(updated) elif action == "delete": if self.yes_no("Confirm", f"Delete provider '{selected}'?"): delete_provider(selected) def provider_action_menu(self, provider: Provider) -> Optional[str]: """Show action menu for a provider.""" listbox = snack.Listbox(height=3, width=20, returnExit=1) listbox.append("Edit", "edit") listbox.append("Delete", "delete") listbox.append("Cancel", "cancel") grid = snack.GridForm(self.screen, f"Provider: {provider.name}", 1, 1) grid.add(listbox, 0, 0) grid.runOnce() return listbox.current() if listbox.current() != "cancel" else None def run_ui(): """Entry point for the snack UI.""" ui = CmdForgeUI() ui.run() if __name__ == "__main__": run_ui()