Rewrite cf as lightweight inline dropdown
- No longer fullscreen - just a small dropdown below cursor - Shows max 8 items at a time with "+N more" indicator - No curses dependency - uses simple ANSI escape codes - Minimal UI: just type to filter, arrows to navigate - Cleans up after itself when done - Much more integrated terminal feel Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
992c473939
commit
df1d3ace23
|
|
@ -1,9 +1,9 @@
|
|||
"""Interactive fuzzy tool picker for CmdForge."""
|
||||
"""Interactive fuzzy tool picker for CmdForge - inline dropdown style."""
|
||||
|
||||
import curses
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import tty
|
||||
import termios
|
||||
from typing import List, Tuple, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
|
@ -14,416 +14,307 @@ from ..tool import list_tools, load_tool
|
|||
class PickerResult:
|
||||
"""Result from the picker."""
|
||||
tool_name: str
|
||||
arguments: dict # flag -> value
|
||||
arguments: dict
|
||||
|
||||
|
||||
# ANSI escape codes
|
||||
CLEAR_LINE = "\033[2K"
|
||||
MOVE_UP = "\033[A"
|
||||
HIDE_CURSOR = "\033[?25l"
|
||||
SHOW_CURSOR = "\033[?25h"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
CYAN = "\033[36m"
|
||||
YELLOW = "\033[33m"
|
||||
GREEN = "\033[32m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
MAX_VISIBLE = 8 # Show at most 8 items
|
||||
|
||||
|
||||
def fuzzy_match(query: str, text: str) -> Tuple[bool, int]:
|
||||
"""
|
||||
Check if query fuzzy-matches text.
|
||||
Returns (matches, score) where higher score = better match.
|
||||
"""
|
||||
"""Fuzzy match with scoring."""
|
||||
if not query:
|
||||
return True, 0
|
||||
|
||||
query = query.lower()
|
||||
text = text.lower()
|
||||
|
||||
# Exact substring match gets highest score
|
||||
if query in text:
|
||||
# Bonus for matching at start
|
||||
if text.startswith(query):
|
||||
return True, 1000 + len(query)
|
||||
return True, 500 + len(query)
|
||||
|
||||
# Fuzzy match - all query chars must appear in order
|
||||
query_idx = 0
|
||||
# Character-by-character fuzzy match
|
||||
qi = 0
|
||||
score = 0
|
||||
prev_match_idx = -1
|
||||
for i, c in enumerate(text):
|
||||
if qi < len(query) and c == query[qi]:
|
||||
score += 10 if i == 0 or text[i-1] in ' -_' else 1
|
||||
qi += 1
|
||||
|
||||
for i, char in enumerate(text):
|
||||
if query_idx < len(query) and char == query[query_idx]:
|
||||
# Bonus for consecutive matches
|
||||
if prev_match_idx == i - 1:
|
||||
score += 10
|
||||
else:
|
||||
score += 1
|
||||
prev_match_idx = i
|
||||
query_idx += 1
|
||||
|
||||
if query_idx == len(query):
|
||||
return True, score
|
||||
|
||||
return False, 0
|
||||
return (qi == len(query), score) if qi == len(query) else (False, 0)
|
||||
|
||||
|
||||
def get_tools_with_info() -> List[dict]:
|
||||
"""Get all tools with their info for display."""
|
||||
def get_tools() -> List[dict]:
|
||||
"""Get all tools with info."""
|
||||
tools = []
|
||||
for name in list_tools():
|
||||
tool = load_tool(name)
|
||||
if tool:
|
||||
args = []
|
||||
for arg in tool.arguments:
|
||||
args.append({
|
||||
"flag": arg.flag,
|
||||
"variable": arg.variable,
|
||||
"default": arg.default or "",
|
||||
"description": arg.description or "",
|
||||
})
|
||||
tools.append({
|
||||
"name": name,
|
||||
"description": tool.description or "",
|
||||
"category": tool.category or "Other",
|
||||
"arguments": args,
|
||||
"desc": (tool.description or "")[:50],
|
||||
"args": [{
|
||||
"flag": a.flag,
|
||||
"default": a.default or "",
|
||||
"desc": a.description or ""
|
||||
} for a in tool.arguments]
|
||||
})
|
||||
return sorted(tools, key=lambda t: t["name"])
|
||||
|
||||
|
||||
def getch():
|
||||
"""Read a single character from stdin."""
|
||||
fd = sys.stdin.fileno()
|
||||
old = termios.tcgetattr(fd)
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
ch = sys.stdin.read(1)
|
||||
# Handle escape sequences (arrow keys)
|
||||
if ch == '\x1b':
|
||||
ch2 = sys.stdin.read(1)
|
||||
if ch2 == '[':
|
||||
ch3 = sys.stdin.read(1)
|
||||
if ch3 == 'A': return 'UP'
|
||||
if ch3 == 'B': return 'DOWN'
|
||||
if ch3 == 'C': return 'RIGHT'
|
||||
if ch3 == 'D': return 'LEFT'
|
||||
return ch
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
|
||||
|
||||
def clear_dropdown(n_lines: int):
|
||||
"""Clear the dropdown lines we drew."""
|
||||
for _ in range(n_lines):
|
||||
sys.stdout.write(MOVE_UP + CLEAR_LINE)
|
||||
sys.stdout.write('\r')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def run_picker() -> Optional[PickerResult]:
|
||||
"""
|
||||
Run the interactive fuzzy picker.
|
||||
Returns PickerResult with tool name and arguments, or None if cancelled.
|
||||
"""
|
||||
tools = get_tools_with_info()
|
||||
|
||||
"""Run inline picker."""
|
||||
tools = get_tools()
|
||||
if not tools:
|
||||
print("No tools found. Create one with: cmdforge")
|
||||
print("No tools. Create one: cmdforge")
|
||||
return None
|
||||
|
||||
return curses.wrapper(lambda stdscr: _picker_loop(stdscr, tools))
|
||||
|
||||
|
||||
def _picker_loop(stdscr, tools: List[dict]) -> Optional[PickerResult]:
|
||||
"""Main picker loop using curses."""
|
||||
curses.curs_set(1) # Show cursor
|
||||
curses.use_default_colors()
|
||||
|
||||
# Initialize colors
|
||||
curses.init_pair(1, curses.COLOR_CYAN, -1) # Selected item
|
||||
curses.init_pair(2, curses.COLOR_YELLOW, -1) # Query
|
||||
curses.init_pair(3, curses.COLOR_GREEN, -1) # Match highlight
|
||||
curses.init_pair(4, 8, -1) # Dim (gray) - description
|
||||
|
||||
query = ""
|
||||
selected_idx = 0
|
||||
scroll_offset = 0
|
||||
selected = 0
|
||||
last_drawn = 0
|
||||
|
||||
print(HIDE_CURSOR, end='', flush=True)
|
||||
|
||||
try:
|
||||
while True:
|
||||
stdscr.clear()
|
||||
height, width = stdscr.getmaxyx()
|
||||
# Filter
|
||||
matches = []
|
||||
for t in tools:
|
||||
ok, score = fuzzy_match(query, t["name"])
|
||||
if not ok:
|
||||
ok, score = fuzzy_match(query, t["desc"])
|
||||
score = score // 2
|
||||
if ok:
|
||||
matches.append((t, score))
|
||||
matches.sort(key=lambda x: -x[1])
|
||||
filtered = [m[0] for m in matches]
|
||||
|
||||
# Filter tools based on query
|
||||
filtered = []
|
||||
for tool in tools:
|
||||
# Match against name and description
|
||||
match_name, score_name = fuzzy_match(query, tool["name"])
|
||||
match_desc, score_desc = fuzzy_match(query, tool["description"])
|
||||
if selected >= len(filtered):
|
||||
selected = max(0, len(filtered) - 1)
|
||||
|
||||
if match_name or match_desc:
|
||||
score = max(score_name * 2, score_desc) # Prioritize name matches
|
||||
filtered.append((tool, score))
|
||||
# Clear previous
|
||||
if last_drawn:
|
||||
clear_dropdown(last_drawn)
|
||||
|
||||
# Sort by score (highest first)
|
||||
filtered.sort(key=lambda x: x[1], reverse=True)
|
||||
filtered_tools = [t[0] for t in filtered]
|
||||
# Draw
|
||||
visible = filtered[:MAX_VISIBLE]
|
||||
lines = []
|
||||
|
||||
# Adjust selection if out of bounds
|
||||
if selected_idx >= len(filtered_tools):
|
||||
selected_idx = max(0, len(filtered_tools) - 1)
|
||||
# Query line
|
||||
prompt = f"{DIM}>{RESET} {YELLOW}{query}{RESET}{'▌' if True else ''}"
|
||||
lines.append(prompt)
|
||||
|
||||
# Calculate visible area (leave room for header and footer)
|
||||
list_height = height - 4
|
||||
|
||||
# Adjust scroll offset
|
||||
if selected_idx < scroll_offset:
|
||||
scroll_offset = selected_idx
|
||||
elif selected_idx >= scroll_offset + list_height:
|
||||
scroll_offset = selected_idx - list_height + 1
|
||||
|
||||
# Draw header
|
||||
header = f" CmdForge Tools ({len(filtered_tools)}/{len(tools)}) "
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, 0, header[:width-1])
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
|
||||
# Draw query line
|
||||
prompt = "> "
|
||||
stdscr.addstr(1, 0, prompt)
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(1, len(prompt), query[:width-len(prompt)-1])
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
|
||||
# Draw separator
|
||||
stdscr.addstr(2, 0, "─" * min(width-1, 60))
|
||||
|
||||
# Draw tool list
|
||||
visible_tools = filtered_tools[scroll_offset:scroll_offset + list_height]
|
||||
|
||||
for i, tool in enumerate(visible_tools):
|
||||
y = 3 + i
|
||||
if y >= height - 1:
|
||||
break
|
||||
|
||||
actual_idx = scroll_offset + i
|
||||
is_selected = actual_idx == selected_idx
|
||||
|
||||
# Build display line
|
||||
name = tool["name"]
|
||||
desc = tool["description"][:width-len(name)-5] if tool["description"] else ""
|
||||
has_args = bool(tool["arguments"])
|
||||
|
||||
# Truncate if needed
|
||||
max_name_len = min(30, width // 3)
|
||||
if len(name) > max_name_len:
|
||||
name = name[:max_name_len-1] + "…"
|
||||
|
||||
if is_selected:
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
prefix = "▸ "
|
||||
# Items
|
||||
for i, t in enumerate(visible):
|
||||
if i == selected:
|
||||
prefix = f"{CYAN}{BOLD}▸ {t['name']}{RESET}"
|
||||
else:
|
||||
prefix = " "
|
||||
prefix = f" {t['name']}"
|
||||
|
||||
try:
|
||||
stdscr.addstr(y, 0, prefix)
|
||||
stdscr.addstr(y, 2, name)
|
||||
# Add arg indicator
|
||||
if t['args']:
|
||||
prefix += f" {GREEN}⚙{RESET}"
|
||||
|
||||
if is_selected:
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
# Add description
|
||||
if t['desc']:
|
||||
prefix += f" {DIM}- {t['desc']}{RESET}"
|
||||
|
||||
# Show argument indicator
|
||||
if has_args:
|
||||
stdscr.attron(curses.color_pair(3))
|
||||
stdscr.addstr(y, 2 + len(name) + 1, "⚙")
|
||||
stdscr.attroff(curses.color_pair(3))
|
||||
lines.append(prefix)
|
||||
|
||||
# Add description in gray
|
||||
if desc:
|
||||
desc_x = 2 + len(name) + (3 if has_args else 2)
|
||||
if desc_x < width - 5:
|
||||
stdscr.attron(curses.color_pair(4))
|
||||
stdscr.addstr(y, desc_x, f"- {desc}"[:width-desc_x-1])
|
||||
stdscr.attroff(curses.color_pair(4))
|
||||
except curses.error:
|
||||
pass # Ignore drawing errors at screen edge
|
||||
# Show count if more
|
||||
if len(filtered) > MAX_VISIBLE:
|
||||
lines.append(f"{DIM} ... +{len(filtered) - MAX_VISIBLE} more{RESET}")
|
||||
|
||||
# Draw footer with help - different if selected tool has args
|
||||
if filtered_tools and filtered_tools[selected_idx]["arguments"]:
|
||||
footer = " Enter:run Tab:set arguments ↑↓:navigate Esc:cancel "
|
||||
else:
|
||||
footer = " Enter:run ↑↓:navigate Esc:cancel "
|
||||
try:
|
||||
stdscr.attron(curses.A_DIM)
|
||||
stdscr.addstr(height-1, 0, footer[:width-1])
|
||||
stdscr.attroff(curses.A_DIM)
|
||||
except curses.error:
|
||||
pass
|
||||
# Print
|
||||
sys.stdout.write('\n'.join(lines) + '\n')
|
||||
sys.stdout.flush()
|
||||
last_drawn = len(lines)
|
||||
|
||||
# Position cursor at end of query
|
||||
try:
|
||||
stdscr.move(1, len(prompt) + len(query))
|
||||
except curses.error:
|
||||
pass
|
||||
# Input
|
||||
ch = getch()
|
||||
|
||||
stdscr.refresh()
|
||||
if ch in ('\r', '\n'): # Enter - run
|
||||
if filtered:
|
||||
clear_dropdown(last_drawn)
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
return PickerResult(filtered[selected]["name"], {})
|
||||
|
||||
# Handle input
|
||||
try:
|
||||
key = stdscr.getch()
|
||||
except KeyboardInterrupt:
|
||||
return None
|
||||
|
||||
if key == 27: # Escape
|
||||
return None
|
||||
elif key in (curses.KEY_ENTER, 10, 13): # Enter - run immediately
|
||||
if filtered_tools:
|
||||
return PickerResult(filtered_tools[selected_idx]["name"], {})
|
||||
elif key == ord('\t'): # Tab - open argument picker
|
||||
if filtered_tools:
|
||||
tool = filtered_tools[selected_idx]
|
||||
if tool["arguments"]:
|
||||
# Show argument picker
|
||||
args = _argument_picker(stdscr, tool)
|
||||
if args is not None: # None means cancelled
|
||||
return PickerResult(tool["name"], args)
|
||||
# If cancelled, stay in main picker
|
||||
else:
|
||||
# No arguments, just run
|
||||
elif ch == '\t': # Tab - args or run
|
||||
if filtered:
|
||||
tool = filtered[selected]
|
||||
clear_dropdown(last_drawn)
|
||||
if tool['args']:
|
||||
args = pick_args(tool)
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
return PickerResult(tool["name"], args) if args is not None else None
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
return PickerResult(tool["name"], {})
|
||||
elif key == curses.KEY_UP:
|
||||
selected_idx = max(0, selected_idx - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
selected_idx = min(len(filtered_tools) - 1, selected_idx + 1)
|
||||
elif key == curses.KEY_BACKSPACE or key == 127:
|
||||
|
||||
elif ch == '\x1b' or ch == '\x03': # Esc or Ctrl+C
|
||||
clear_dropdown(last_drawn)
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
return None
|
||||
|
||||
elif ch == 'UP':
|
||||
selected = max(0, selected - 1)
|
||||
elif ch == 'DOWN':
|
||||
selected = min(len(filtered) - 1, selected + 1)
|
||||
|
||||
elif ch == '\x7f' or ch == '\b': # Backspace
|
||||
query = query[:-1]
|
||||
selected_idx = 0
|
||||
scroll_offset = 0
|
||||
elif 32 <= key <= 126: # Printable characters
|
||||
query += chr(key)
|
||||
selected_idx = 0
|
||||
scroll_offset = 0
|
||||
selected = 0
|
||||
|
||||
elif ch.isprintable():
|
||||
query += ch
|
||||
selected = 0
|
||||
|
||||
except Exception as e:
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def _argument_picker(stdscr, tool: dict) -> Optional[dict]:
|
||||
"""
|
||||
Show argument picker/editor for a tool.
|
||||
Returns dict of flag->value, or None if cancelled.
|
||||
"""
|
||||
args = tool["arguments"]
|
||||
if not args:
|
||||
return {}
|
||||
def pick_args(tool: dict) -> Optional[dict]:
|
||||
"""Inline argument picker."""
|
||||
args = tool['args']
|
||||
values = {a['flag']: a['default'] for a in args}
|
||||
selected = 0
|
||||
editing = None
|
||||
edit_buf = ""
|
||||
last_drawn = 0
|
||||
|
||||
selected_idx = 0
|
||||
values = {arg["flag"]: arg["default"] for arg in args}
|
||||
editing = False
|
||||
edit_buffer = ""
|
||||
print(HIDE_CURSOR, end='', flush=True)
|
||||
|
||||
try:
|
||||
while True:
|
||||
stdscr.clear()
|
||||
height, width = stdscr.getmaxyx()
|
||||
if last_drawn:
|
||||
clear_dropdown(last_drawn)
|
||||
|
||||
# Header
|
||||
stdscr.attron(curses.A_BOLD)
|
||||
stdscr.addstr(0, 0, f" {tool['name']} - Arguments ")
|
||||
stdscr.attroff(curses.A_BOLD)
|
||||
lines = []
|
||||
lines.append(f"{BOLD}{tool['name']}{RESET} arguments:")
|
||||
|
||||
stdscr.addstr(1, 0, "─" * min(width-1, 60))
|
||||
|
||||
# Draw arguments
|
||||
for i, arg in enumerate(args):
|
||||
y = 2 + i
|
||||
if y >= height - 2:
|
||||
break
|
||||
flag = arg['flag']
|
||||
val = edit_buf if editing == i else values[flag]
|
||||
|
||||
is_selected = i == selected_idx
|
||||
flag = arg["flag"]
|
||||
value = values.get(flag, "")
|
||||
desc = arg["description"]
|
||||
|
||||
if is_selected:
|
||||
stdscr.attron(curses.color_pair(1) | curses.A_BOLD)
|
||||
prefix = "▸ "
|
||||
if i == selected:
|
||||
if editing == i:
|
||||
line = f"{CYAN}▸ {flag}: {YELLOW}{val}▌{RESET}"
|
||||
else:
|
||||
prefix = " "
|
||||
|
||||
try:
|
||||
stdscr.addstr(y, 0, prefix)
|
||||
stdscr.addstr(y, 2, f"{flag}: ")
|
||||
|
||||
if is_selected:
|
||||
stdscr.attroff(curses.color_pair(1) | curses.A_BOLD)
|
||||
|
||||
# Show value or edit buffer
|
||||
val_x = 2 + len(flag) + 2
|
||||
if is_selected and editing:
|
||||
stdscr.attron(curses.color_pair(2))
|
||||
stdscr.addstr(y, val_x, edit_buffer + "▌")
|
||||
stdscr.attroff(curses.color_pair(2))
|
||||
line = f"{CYAN}{BOLD}▸ {flag}:{RESET} {val or f'{DIM}(empty){RESET}'}"
|
||||
else:
|
||||
display_val = value if value else "(empty)"
|
||||
if not value:
|
||||
stdscr.attron(curses.A_DIM)
|
||||
stdscr.addstr(y, val_x, display_val[:width-val_x-1])
|
||||
if not value:
|
||||
stdscr.attroff(curses.A_DIM)
|
||||
line = f" {flag}: {val or f'{DIM}(empty){RESET}'}"
|
||||
|
||||
# Description
|
||||
if desc and not editing:
|
||||
desc_x = val_x + len(value if value else "(empty)") + 2
|
||||
if desc_x < width - 10:
|
||||
stdscr.attron(curses.color_pair(4))
|
||||
stdscr.addstr(y, desc_x, f"# {desc}"[:width-desc_x-1])
|
||||
stdscr.attroff(curses.color_pair(4))
|
||||
except curses.error:
|
||||
pass
|
||||
if arg['desc'] and editing != i:
|
||||
line += f" {DIM}# {arg['desc'][:30]}{RESET}"
|
||||
|
||||
# Footer
|
||||
if editing:
|
||||
footer = " Enter:save Esc:cancel edit "
|
||||
lines.append(line)
|
||||
|
||||
if editing is None:
|
||||
lines.append(f"{DIM}Enter:edit Tab:run Esc:back{RESET}")
|
||||
|
||||
sys.stdout.write('\n'.join(lines) + '\n')
|
||||
sys.stdout.flush()
|
||||
last_drawn = len(lines)
|
||||
|
||||
ch = getch()
|
||||
|
||||
if editing is not None:
|
||||
if ch in ('\r', '\n'): # Save
|
||||
values[args[editing]['flag']] = edit_buf
|
||||
editing = None
|
||||
edit_buf = ""
|
||||
elif ch == '\x1b': # Cancel edit
|
||||
editing = None
|
||||
edit_buf = ""
|
||||
elif ch == '\x7f' or ch == '\b':
|
||||
edit_buf = edit_buf[:-1]
|
||||
elif ch.isprintable():
|
||||
edit_buf += ch
|
||||
else:
|
||||
footer = " Enter:edit value Tab:done Esc:back Del:clear "
|
||||
try:
|
||||
stdscr.attron(curses.A_DIM)
|
||||
stdscr.addstr(height-1, 0, footer[:width-1])
|
||||
stdscr.attroff(curses.A_DIM)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
stdscr.refresh()
|
||||
|
||||
# Handle input
|
||||
try:
|
||||
key = stdscr.getch()
|
||||
except KeyboardInterrupt:
|
||||
return None
|
||||
|
||||
if editing:
|
||||
if key == 27: # Escape - cancel edit
|
||||
editing = False
|
||||
edit_buffer = ""
|
||||
elif key in (curses.KEY_ENTER, 10, 13): # Enter - save value
|
||||
values[args[selected_idx]["flag"]] = edit_buffer
|
||||
editing = False
|
||||
edit_buffer = ""
|
||||
elif key == curses.KEY_BACKSPACE or key == 127:
|
||||
edit_buffer = edit_buffer[:-1]
|
||||
elif 32 <= key <= 126:
|
||||
edit_buffer += chr(key)
|
||||
else:
|
||||
if key == 27: # Escape - go back to tool picker
|
||||
return None
|
||||
elif key == ord('\t'): # Tab - done, run with these args
|
||||
if ch == '\t': # Done
|
||||
clear_dropdown(last_drawn)
|
||||
return values
|
||||
elif key in (curses.KEY_ENTER, 10, 13): # Enter - edit current value
|
||||
editing = True
|
||||
edit_buffer = values.get(args[selected_idx]["flag"], "")
|
||||
elif key == curses.KEY_UP:
|
||||
selected_idx = max(0, selected_idx - 1)
|
||||
elif key == curses.KEY_DOWN:
|
||||
selected_idx = min(len(args) - 1, selected_idx + 1)
|
||||
elif key in (curses.KEY_DC, 330): # Delete key - clear value
|
||||
values[args[selected_idx]["flag"]] = ""
|
||||
elif ch in ('\r', '\n'): # Edit
|
||||
editing = selected
|
||||
edit_buf = values[args[selected]['flag']]
|
||||
elif ch == '\x1b' or ch == '\x03': # Back
|
||||
clear_dropdown(last_drawn)
|
||||
return None
|
||||
elif ch == 'UP':
|
||||
selected = max(0, selected - 1)
|
||||
elif ch == 'DOWN':
|
||||
selected = min(len(args) - 1, selected + 1)
|
||||
|
||||
except Exception:
|
||||
print(SHOW_CURSOR, end='', flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the cf command."""
|
||||
# Check if we're in a terminal
|
||||
"""Entry point for cf command."""
|
||||
if not sys.stdin.isatty():
|
||||
print("Error: cf requires an interactive terminal", file=sys.stderr)
|
||||
print("cf requires a terminal", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
result = run_picker()
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if result:
|
||||
# Build command with arguments
|
||||
cmd_parts = [result.tool_name]
|
||||
for flag, value in result.arguments.items():
|
||||
if value: # Only add non-empty arguments
|
||||
cmd_parts.append(flag)
|
||||
cmd_parts.append(value)
|
||||
# Build command
|
||||
cmd = [result.tool_name]
|
||||
for flag, val in result.arguments.items():
|
||||
if val:
|
||||
cmd.extend([flag, val])
|
||||
|
||||
# Print the command so user can see what was selected
|
||||
cmd_str = " ".join(cmd_parts)
|
||||
print(f"\033[1m{cmd_str}\033[0m", file=sys.stderr)
|
||||
# Show what we're running
|
||||
print(f"{BOLD}{' '.join(cmd)}{RESET}", file=sys.stderr)
|
||||
|
||||
# Run the tool
|
||||
try:
|
||||
# Use exec to replace this process with the tool
|
||||
# This way the tool can properly interact with the terminal
|
||||
# Run it
|
||||
tool_path = os.path.expanduser(f"~/.local/bin/{result.tool_name}")
|
||||
if os.path.exists(tool_path):
|
||||
os.execv(tool_path, cmd_parts)
|
||||
os.execv(tool_path, cmd)
|
||||
else:
|
||||
# Fall back to running via cmdforge runner
|
||||
os.execlp("python", "python", "-m", "cmdforge.runner", *cmd_parts)
|
||||
except Exception as e:
|
||||
# Fallback: just print the command
|
||||
print(f"Run: {cmd_str}")
|
||||
else:
|
||||
sys.exit(0)
|
||||
os.execlp("python", "python", "-m", "cmdforge.runner", *cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Reference in New Issue