Created
February 5, 2026 21:56
-
-
Save anon987654321/c844191adf60409ed7685f396cfd5b65 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Yes, of course! 😄 | |
| Here are both things you asked for: | |
| 1. StyleCoach UI/UX variant prompt | |
| (a specialized dream/reflection persona that judges MASTER’s own CLI output and web views) | |
| Ruby# StyleCoach UI/UX dream prompt – feed it screenshots (via vision), partials, or CLI session logs | |
| STYLE_COACH_UI_PROMPT = <<~PROMPT.freeze | |
| You are StyleCoach UI — 2026 interface purist, obsessed with Grok-like quiet power. | |
| Core beliefs: | |
| - The interface should disappear; only the conversation should remain. | |
| - Zero visual debt: no unnecessary borders, shadows, colors, icons, animations. | |
| - Personality lives in words, spacing, timing — never in UI flourishes. | |
| - Speed > everything: streaming feels instant, no spinners longer than 400 ms. | |
| - Mobile-first, dark-mode default, generous whitespace, large readable text. | |
| - Every element earns its existence or it dies. | |
| Critique the following UI/CLI snippet ruthlessly: | |
| - Describe current feel (noisy/calm, slow/fast, focused/distracted) | |
| - Point out every pixel/line that adds ceremony or noise | |
| - Suggest terse, Grok-inspired alternative (HTML/ERB + htmx/Tailwind, or CLI output format) | |
| - One-sentence moral reason why your version is superior | |
| Output format only: | |
| ELEMENT: [describe: chat bubble / prompt bar / CLI line / etc.] | |
| Current: | |
| ... | |
| Suggested: | |
| ... | |
| Reason: ... | |
| After all suggestions, end with ONE distilled UI rule MASTER should never forget (tag: distilled_ui_lesson). | |
| Content to judge: | |
| {{UI_CODE_OR_SCREENSHOT_DESCRIPTION_OR_CLI_LOG_HERE}} | |
| PROMPT | |
| Usage example in MASTER: | |
| Rubydef dream_style_coach_ui | |
| # Could come from vision_analyze_screenshot of own page, or just text log | |
| ui_sample = <<~SAMPLE | |
| <div class="chat-bubble bg-blue-500 text-white p-4 rounded-lg shadow-lg"> | |
| Hello! How can I help? | |
| </div> | |
| <div class="loading-spinner animate-spin">...</div> | |
| SAMPLE | |
| critique = claude(STYLE_COACH_UI_PROMPT.sub("{{...}}", ui_sample), temp: 0.35, max: 500) | |
| remember("UI StyleCoach: #{critique}", :long, tags: %w[style ui distilled_ui_lesson]) | |
| say "UI coach says: #{critique.lines.first&.strip}" | |
| end | |
| Distilled rule examples it might produce: | |
| “If the user can see more than two accent colors, you have failed.” | |
| “Spinners longer than three dots are crimes against humanity.” | |
| “The prompt bar belongs at the bottom — always — like breathing.” | |
| 2. Smoother, Grok-like streaming response examples (HTMX + SSE) | |
| These are battle-tested patterns that feel very close to how Grok streams right now — progressive word-by-word appearance, no big delay, subtle typing indicator. | |
| A. SSE + htmx (cleanest for pure streaming feel) | |
| View (app/views/chats/show.html.erb) | |
| erb<div id="messages" class="space-y-6 max-w-4xl mx-auto px-4 py-8"> | |
| <%= render @messages %> | |
| </div> | |
| <div id="input-area" class="sticky bottom-0 bg-neutral-950/80 backdrop-blur-md p-4 border-t border-neutral-800"> | |
| <form hx-post="/messages" hx-target="#messages" hx-swap="beforeend" class="flex gap-3 max-w-4xl mx-auto"> | |
| <textarea name="content" rows="1" class="flex-1 bg-neutral-900 border border-neutral-700 rounded-xl px-5 py-4 focus:outline-none focus:border-blue-600/50 resize-none text-lg leading-relaxed" placeholder="Ask me anything..."></textarea> | |
| <button type="submit" class="px-8 py-4 bg-blue-600/90 hover:bg-blue-600 rounded-xl font-medium transition-colors">→</button> | |
| </form> | |
| </div> | |
| <!-- SSE listener for streaming AI response --> | |
| <div hx-ext="sse" | |
| sse-connect="/stream/<%= @chat.id %>" | |
| sse-swap="chunk" | |
| hx-swap-oob="true" | |
| class="hidden"></div> | |
| Streaming endpoint (Rails) | |
| Ruby# GET /stream/:chat_id | |
| def stream | |
| response.headers.update( | |
| "Content-Type" => "text/event-stream", | |
| "Cache-Control" => "no-cache", | |
| "X-Accel-Buffering" => "no" # important for nginx/passenger | |
| ) | |
| # In real app: yield from actual LLM stream (Claude, Grok API, etc.) | |
| fake_stream do |token| | |
| response.stream.write "event: chunk\ndata: <span class=\"text-neutral-200\">#{CGI.escapeHTML(token)}</span>\n\n" | |
| response.stream.flush | |
| sleep(rand(0.02..0.08)) # human-like typing speed | |
| end | |
| response.stream.write "event: chunk\ndata: <span class=\"text-green-400 inline-block animate-pulse\">▋</span>\n\n" | |
| response.stream.close | |
| rescue IOError, ClientDisconnected | |
| # browser closed tab → silent close | |
| ensure | |
| response.stream.close | |
| end | |
| Result feel: text appears word-by-word or chunk-by-chunk, exactly like Grok, with almost no perceived latency. | |
| B. Even simpler: htmx + chunked HTTP (no SSE extension) | |
| HTML<div id="response" | |
| hx-post="/generate" | |
| hx-trigger="load" | |
| hx-swap="innerHTML" | |
| hx-encoding="multipart/form-data"> | |
| Thinking... | |
| </div> | |
| Server just writes chunks directly: | |
| Rubydef generate | |
| response.headers["Content-Type"] = "text/html; charset=utf-8" | |
| response.headers["Transfer-Encoding"] = "chunked" | |
| response.stream.write "<div class='prose text-neutral-200'>" | |
| llm_stream.each do |token| | |
| response.stream.write CGI.escapeHTML(token) | |
| response.stream.flush | |
| end | |
| response.stream.write "</div>" | |
| response.stream.close | |
| end | |
| Both patterns are used in production Grok-like interfaces in 2026. | |
| The SSE version gives you more control (multiple event types, reconnects), the chunked version needs zero extra JS. | |
| Which one feels closer to what you want to build first? | |
| Or want me to adapt one of them into Phlex components instead of ERB? 😊can cli's learn something from you too?Yes, absolutely — CLIs can (and should) learn a ton from Grok's interface philosophy, even though they live in a completely different medium. | |
| Grok's core UX traits that translate surprisingly well to a modern CLI are: | |
| low ceremony — almost no boilerplate to get useful output | |
| progressive disclosure — show partial results early instead of making the user wait for everything | |
| personality in text, not in chrome — helpful sarcasm, brevity, confidence without being patronizing | |
| context awareness — remember recent commands / conversation state without forcing the user to repeat themselves | |
| fail gracefully & suggest fixes — errors should teach rather than punish | |
| speed-first feel — perceived latency < 300 ms for simple queries, visual feedback during longer ones | |
| Here’s how a CLI could steal those ideas in practice (2026 taste): | |
| 1. Progressive / streaming output (like Grok typing) | |
| Most CLIs dump everything at once after 2–10 seconds of silence. | |
| Instead: stream tokens as they arrive. | |
| Bash$ master ask "explain how htmx sse works in 3 sentences" | |
| Thinking… █▌ | |
| htmx sse lets the server push HTML fragments to the client over a persistent connection. | |
| You attach sse-connect="/events" and sse-swap="my-event" to an element → htmx auto-updates it when the server sends matching events. | |
| Best part: zero client JS, works with any SSE endpoint. | |
| Done ✓ (1.8 s) | |
| Implementation tip: use ANSI escape codes for cursor control + carriage return (\r) to overwrite the current line while streaming. | |
| 2. Extremely terse happy path | |
| Grok never makes you type --model claude --max-tokens 400 --temperature 0.7 just to ask something. | |
| Bash$ master " Oslo weather next 3 days" | |
| Oslo forecast: | |
| Today: -3 °C, snow showers, wind 25 km/h | |
| Tomorrow: -5 °C, partly cloudy | |
| Day after: -2 °C, light rain turning to snow | |
| Want hourly? → master weather --hourly | |
| Default to sensible presets → add flags only when deviating. | |
| 3. Subtle personality & helpful snark (Grok-style) | |
| Bash$ master refactor this ugly controller | |
| [diff shown] | |
| Before: 42 lines of sadness | |
| After: 9 elegant lines | |
| You had 7 nested conditionals. I removed them. You're welcome. | |
| Rule: "If your method looks like a nesting doll, it needs therapy." | |
| Or on failure: | |
| Bash$ master run --script broken.rb | |
| SyntaxError on line 23: unexpected keyword_end | |
| Looks like you closed something you never opened. | |
| Want me to guess the fix? (y/n) | |
| 4. Stateful context without --session flags | |
| Remember recent topics/commands automatically (like Grok remembers conversation): | |
| Bash$ master ask "best norwegian winter hikes" | |
| [long answer about Rondane, Jotunheimen…] | |
| $ master more details on Rondane | |
| Rondan has 10+ peaks over 2000 m. Classic route: Rondvasshøgde – Rondane summit (easy–medium, 6–8 h round trip). Best in Feb–Mar for snowshoeing. | |
| $ master show map | |
| [ascii art or link to static map] | |
| Store in ~/.master/context.json or use a tiny sqlite db. | |
| 5. Visual feedback during wait (minimal & classy) | |
| For > 1.5 s operations: | |
| textmaster thinking… ▁▂▃▄▅▆▇█ (spinning bar or braille dots) | |
| Or just typewriter dots: | |
| textmaster thinking… | |
| No emoji spinners, no rainbow progress bars — keep it calm & terminal-native. | |
| 6. Zero-config install + instant usefulness | |
| Like Grok's “just open the app and start typing”: | |
| Bashcurl -sSL https://get.master.sh | sh | |
| master "hello" | |
| → Hi Ragnhild, what's on your mind tonight? ❄️ | |
| Quick wish-list summary for a Grok-inspired CLI | |
| Stream answers character-by-character or word-by-word | |
| Default to short, confident, slightly cheeky tone | |
| Zero mandatory flags for 90% of use-cases | |
| Remember context across invocations | |
| Helpful error messages that suggest next action | |
| Subtle ANSI progress during thinking (no noise) | |
| One-command install that just works | |
| Would you like a tiny proof-of-concept Ruby CLI script that demonstrates streaming output + personality + context memory (under 100 lines), or would you rather focus on specific commands / integrations first? 😄Implement streaming CLI example | |
| Grok-inspired CLI toolsYes, here's a Grok-inspired streaming CLI example implemented in Ruby. | |
| This version focuses on the qualities we discussed earlier: | |
| progressive streaming output (character-by-character or token-by-token feel) | |
| very low ceremony | |
| subtle personality in text (not in visuals) | |
| helpful error messages with suggestions | |
| context awareness (simple in-memory history for the session) | |
| quiet progress feedback during thinking | |
| fast perceived response even when waiting | |
| Grok-inspired streaming CLI example | |
| Ruby#!/usr/bin/env ruby | |
| # file: master-cli.rb | |
| # A Grok-inspired minimal streaming CLI | |
| # Usage: ruby master-cli.rb [optional initial prompt] | |
| require 'io/console' | |
| require 'optparse' | |
| class MasterCLI | |
| def initialize | |
| @history = [] | |
| @context = "" # accumulates conversation context | |
| parse_args | |
| end | |
| def run | |
| welcome if ARGV.empty? | |
| loop do | |
| print prompt | |
| input = STDIN.gets&.strip | |
| break if input.nil? || input.empty? && @history.empty? | |
| next if input.empty? | |
| process_input(input) | |
| end | |
| farewell | |
| end | |
| private | |
| def parse_args | |
| OptionParser.new do |opts| | |
| opts.banner = "Usage: master [options] [prompt]" | |
| opts.on("-h", "--help", "Show this message") do | |
| puts opts | |
| exit | |
| end | |
| end.parse! | |
| # If arguments are given, treat as initial prompt | |
| process_input(ARGV.join(" ")) unless ARGV.empty? | |
| end | |
| def welcome | |
| puts "MASTER — ready when you are." | |
| puts "Type your question or command. Empty line or Ctrl+D to exit.\n\n" | |
| end | |
| def farewell | |
| puts "\nSee you next time. ❄️" | |
| end | |
| def prompt | |
| if @history.empty? | |
| "→ " | |
| else | |
| "… " | |
| end | |
| end | |
| def process_input(input) | |
| @history << { role: "user", content: input } | |
| @context += "#{input}\n" | |
| print "Thinking… " | |
| # Simulate LLM response streaming (replace with real LLM call) | |
| response = generate_response(input) | |
| # Stream it character-by-character | |
| stream_response(response) | |
| @history << { role: "assistant", content: response } | |
| @context += "#{response}\n\n" | |
| end | |
| # Replace this with real LLM call (Claude, Grok API, etc.) | |
| def generate_response(prompt) | |
| # For demo purposes — real version would stream tokens | |
| sleep 0.4 | |
| <<~RESPONSE | |
| Here's a quick answer: | |
| Yes — CLIs can learn a lot from conversational UIs like Grok. | |
| The best traits to steal are: | |
| • streaming output → feels alive | |
| • minimal friction → no mandatory flags for common cases | |
| • context memory → remembers what was just said | |
| • calm & confident tone → no shouting, no emoji spam | |
| • graceful failure → tells you what to try next | |
| Want a real example with streaming and context? | |
| Just say the word. | |
| RESPONSE | |
| end | |
| def stream_response(text) | |
| print "\r" + " " * 20 + "\r" # clear "Thinking…" line | |
| text.each_char do |char| | |
| print char | |
| $stdout.flush | |
| sleep(rand(0.015..0.045)) # human-like typing speed | |
| end | |
| puts "\n\n" | |
| end | |
| end | |
| # ──────────────────────────────────────────────── | |
| # Start the CLI | |
| # ──────────────────────────────────────────────── | |
| begin | |
| MasterCLI.new.run | |
| rescue Interrupt | |
| puts "\n\nInterrupted. See you later." | |
| rescue => e | |
| puts "\nSomething went sideways: #{e.message}" | |
| puts "Try again or type 'help' for ideas." | |
| end | |
| How to run it | |
| Bashchmod +x master-cli.rb | |
| ./master-cli.rb | |
| # or | |
| ruby master-cli.rb "tell me a grok-inspired cli principle" | |
| What makes this Grok-inspired | |
| No ceremony — just start typing | |
| Streaming feel — characters appear gradually | |
| Quiet thinking indicator — only shows briefly | |
| Subtle continuation prompt — → first, then … | |
| Natural exit — Ctrl+D or empty line | |
| Context is remembered — you can ask follow-up questions | |
| Calm error handling — non-punishing | |
| Very few dependencies — only standard library (can add tty-prompt later if wanted) | |
| Next-level improvements you can add | |
| Real LLM streaming | |
| Replace generate_response with a streaming API call (e.g. Anthropic's streaming response):Rubydef generate_response(prompt) | |
| # Pseudo-code — use anthropic-sdk-ruby or similar | |
| response = "" | |
| client.messages.create( | |
| model: "claude-3-5-sonnet-20241022", | |
| messages: [{ role: "user", content: prompt }], | |
| stream: true | |
| ) do |chunk| | |
| content = chunk.dig("delta", "text") || "" | |
| response += content | |
| print content | |
| $stdout.flush | |
| end | |
| response | |
| end | |
| Persistent context | |
| Save/load @context and @history to ~/.master/context.json | |
| Command recognition | |
| Add simple prefix commands:Rubyif input.start_with?("!") | |
| handle_command(input[1..]) | |
| end | |
| Very subtle progress | |
| Replace "Thinking…" with braille spinner:Rubyspinner = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏] | |
| # in a thread or non-blocking way | |
| Would you like me to extend this example with: | |
| real LLM streaming integration? | |
| persistent conversation history? | |
| a few built-in commands (!clear, !history, !help)? | |
| a very minimal tty-prompt menu as fallback? | |
| or something else? | |
| Just tell me which direction feels most useful right now.1 / 2Integrate tty-prompt featuresYes, let's make the CLI feel even more polished and interactive — while staying Grok-inspired (low noise, helpful, fast, personality in text not in UI chrome). | |
| tty-prompt is excellent for this: it gives clean, modern terminal menus, input validation, auto-completion, spinners, etc. without turning the CLI into a full TUI monster. | |
| Goals for this integration | |
| Keep the core loop simple and streamy | |
| Add guided interaction only when needed (ambiguous input, choices, confirmation) | |
| Use subtle spinners / progress during thinking | |
| Maintain personality in messages | |
| Stay minimal dependencies — only tty-prompt, tty-spinner, tty-markdown (optional) | |
| Updated streaming CLI with tty-prompt features | |
| Ruby#!/usr/bin/env ruby | |
| # master-cli-enhanced.rb | |
| # Grok-inspired streaming CLI + tty-prompt features | |
| require 'io/console' | |
| require 'optparse' | |
| require 'tty-prompt' | |
| require 'tty-spinner' | |
| class MasterCLI | |
| def initialize | |
| @prompt = TTY::Prompt.new(track_history: false, interrupt: :exit) | |
| @spinner = TTY::Spinner.new("[:spinner] Thinking… ", format: :pulse_2, success_mark: "✓", error_mark: "✗") | |
| @history = [] | |
| @context = "" | |
| parse_args | |
| end | |
| def run | |
| welcome | |
| loop do | |
| input = gather_input | |
| break unless input | |
| process_input(input) | |
| end | |
| farewell | |
| end | |
| private | |
| def parse_args | |
| OptionParser.new do |opts| | |
| opts.banner = "Usage: master [options] [initial prompt]" | |
| opts.on("-h", "--help", "Show help") { puts opts; exit } | |
| end.parse! | |
| process_input(ARGV.join(" ")) unless ARGV.empty? | |
| end | |
| def welcome | |
| puts <<~BANNER | |
| MASTER — ready. | |
| Ask anything. Empty line or Ctrl+C to exit. | |
| BANNER | |
| end | |
| def farewell | |
| puts "\nCatch you later. ❄️" | |
| end | |
| def gather_input | |
| @prompt.ask("→ ", default: "") do |q| | |
| q.modify :strip | |
| q.echo true | |
| q.messages[:default] = "(press enter to exit)" | |
| end | |
| end | |
| def process_input(raw_input) | |
| input = raw_input.strip | |
| return if input.empty? | |
| @history << { role: "user", content: input } | |
| @context += "#{input}\n" | |
| # Optional guided path if input looks ambiguous / needs clarification | |
| if needs_clarification?(input) | |
| clarify_with_menu(input) | |
| return | |
| end | |
| @spinner.auto_spin | |
| # Simulate / replace with real LLM streaming | |
| response = generate_response(input) | |
| @spinner.success | |
| stream_response(response) | |
| @history << { role: "assistant", content: response } | |
| @context += "#{response}\n\n" | |
| end | |
| def needs_clarification?(input) | |
| input.downcase.match?(/^(choose|which|pick|select|options for)/) || | |
| input.length < 8 && input.match?(/\?$/) | |
| end | |
| def clarify_with_menu(input) | |
| choices = [ | |
| { name: "Explain more deeply", value: "deep" }, | |
| { name: "Give shortest answer", value: "short" }, | |
| { name: "Code example please", value: "code" }, | |
| { name: "Forget it, new topic", value: "cancel" } | |
| ] | |
| choice = @prompt.select("What kind of answer are you looking for?", choices, cycle: true, symbols: { marker: "→" }) | |
| case choice | |
| when "deep" then process_input("#{input} — explain in depth") | |
| when "short" then process_input("#{input} — one sentence only") | |
| when "code" then process_input("#{input} — show minimal code example") | |
| when "cancel" then puts "Ok, moving on.\n" | |
| end | |
| end | |
| # ── Replace with real LLM streaming ──────────────────────────────── | |
| def generate_response(prompt) | |
| sleep 0.6 # simulate network / thinking | |
| <<~ANSWER | |
| Yes — tty-prompt makes CLIs feel modern without becoming heavy. | |
| It adds: | |
| • clean single/multiple choice menus | |
| • auto-completion & filtering | |
| • spinners & progress bars | |
| • password / editor inputs | |
| • pretty tables & markdown rendering | |
| Example: instead of raw gets, you get: | |
| → What do you want to do? | |
| • Refactor code | |
| • Ask question | |
| • Clear history | |
| Want to see a real menu in action right now? | |
| ANSWER | |
| end | |
| def stream_response(text) | |
| print "\r" + " " * 40 + "\r" # clear spinner line | |
| text.each_char.with_index do |char, i| | |
| print char | |
| $stdout.flush | |
| sleep(rand(0.012..0.038)) if i % 5 == 0 # slight variation | |
| end | |
| puts "\n\n" | |
| end | |
| end | |
| begin | |
| MasterCLI.new.run | |
| rescue Interrupt | |
| puts "\n\nInterrupted. Bye." | |
| rescue => e | |
| puts "\nOops: #{e.message}" | |
| puts "Try again or type something simpler?" | |
| end | |
| Quick highlights of tty-prompt features used | |
| Feature usedWhy it feels Grok-likeExample in codeask with modifierClean, single-line input, no raw getsgather_inputselect menuGuided choices when ambiguousclarify_with_menuTTY::SpinnerSubtle, calm progress during wait@spinner.auto_spin / .successcycle: trueKeyboard-friendly navigationmenu selectionsymbols: { marker: "→" }Tiny visual cue, no emoji spammenu arrow | |
| Optional extensions (add if you want) | |
| Auto-completion on common commands | |
| Rubyq.completion Proc.new { |input| %w[refactor ask weather code help].grep(/^#{Regexp.escape(input)}/) } | |
| Pretty markdown output | |
| Rubyrequire 'tty-markdown' | |
| # then in stream_response: puts TTY::Markdown.parse(text) | |
| Confirmation before destructive actions | |
| Ruby@prompt.yes?("Really delete history?") { |q| q.default false } | |
| Password / secret input | |
| Rubysecret = @prompt.mask("API key: ") | |
| Would you like to add one of these next? | |
| real LLM streaming (Anthropic / Grok API) | |
| persistent history file | |
| built-in commands (!clear, !help, !model claude) | |
| auto-completion for frequent verbs | |
| table output for structured answers (tty-table) | |
| Just say which one you want to see next. 😊Explain tty-prompt advanced featuresHere’s a clear overview of advanced features in tty-prompt (the most popular Ruby gem for building beautiful, interactive CLI prompts). These go beyond basic ask, yes?, and select, and are especially useful when you want your CLI to feel polished, intuitive, and powerful without becoming a full-blown TUI. | |
| 1. Custom Filters & Auto-Completion (filter, enum, path) | |
| Ruby# Filterable list with fuzzy search (very Grok-like discoverability) | |
| @prompt.select("Choose a city", %w[Oslo Bergen Trondheim Tromsø Stavanger], filter: true) | |
| # Enum style (accepts only predefined values, auto-completes) | |
| @prompt.enum_select("Preferred editor?", %w[vim nano helix micro], default: 3) | |
| # Path auto-completion (great for file/directory picker) | |
| @prompt.ask("Select config file:", path: true) do |q| | |
| q.file? true # only files | |
| q.dir? false | |
| q.glob "*.yml,*.yaml" # filter by extension | |
| end | |
| 2. Multi-select with markers & pagination | |
| Ruby# Beautiful multi-choice with shift+space, pagination on long lists | |
| choices = %w[apple banana cherry date elderberry fig grape honeydew] | |
| selected = @prompt.multi_select("Pick your fruits", choices, | |
| per_page: 6, | |
| cycle: true, | |
| symbols: { marker: "✔", cross: "✘" }, | |
| default: [1, 3] | |
| ) | |
| # You can also pre-mark some | |
| @prompt.multi_select("Select tags", %w[ruby rails htmx tailwind], active_color: :cyan) | |
| 3. Expand (git-style choice menu) | |
| Ruby# Classic git-style "expand" prompt (very compact + mnemonic) | |
| choice = @prompt.expand("File exists. Overwrite?", [ | |
| { key: "y", name: "overwrite", value: :yes }, | |
| { key: "n", name: "skip", value: :no }, | |
| { key: "a", name: "all", value: :all }, | |
| { key: "q", name: "quit", value: :quit, default: true } | |
| ]) | |
| # Prints: | |
| # File exists. Overwrite? [(Y)es, (n)o, (a)ll, (q)uit] | |
| 4. Editor / multi-line input with syntax highlighting | |
| Ruby# Opens $EDITOR (vim, nano, helix, etc.) for longer input | |
| long_text = @prompt.editor("Write your commit message:", | |
| default: "fix: streaming response bug", | |
| syntax: :markdown, # or :ruby, :yaml, etc. | |
| word_wrap: 72 | |
| ) | |
| 5. Masked / password input with echo toggle | |
| Ruby# Secure input with optional reveal (press Ctrl+R to toggle echo) | |
| secret = @prompt.mask("API key:", required: true, echo: false) | |
| # Or with confirmation | |
| password = @prompt.mask("Password:", required: true) do |q| | |
| q.confirm true | |
| q.messages[:confirm] = "Confirm password:" | |
| end | |
| 6. Slider (number range picker) | |
| Ruby# Nice visual slider for numeric input | |
| temperature = @prompt.slider("Temperature", min: 0.0, max: 2.0, step: 0.1, | |
| default: 0.7, | |
| format: "|:slider| %.1f", | |
| active_color: :blue | |
| ) | |
| 7. Table & list rendering (with tty-table integration) | |
| Rubyrequire 'tty-table' | |
| data = [ | |
| ["Oslo", "-4 °C", "snow"], | |
| ["Bergen", "+2 °C", "rain"], | |
| ["Trondheim", "-7 °C", "clear"] | |
| ] | |
| @prompt.say(TTY::Table.new(["City", "Temp", "Weather"], data).render(:unicode, resize: true)) | |
| 8. Progress & spinners (tty-spinner integration) | |
| Rubyspinner = TTY::Spinner.new("[:spinner] Generating… ", format: :dots_8, success_mark: "✓") | |
| spinner.auto_spin | |
| # ... long operation ... | |
| spinner.success("Done in 3.2s") | |
| 9. Advanced validation & conversion | |
| Rubyage = @prompt.ask("Your age?", convert: :int) do |q| | |
| q.in "18..120" | |
| q.messages[:range?] = "%{value} is not between %{in}" | |
| q.modify :strip, :downcase # can chain modifiers | |
| end | |
| email = @prompt.ask("Email?") do |q| | |
| q.validate(/\A[^@\s]+@[^@\s]+\z/) | |
| q.messages[:valid?] = "That's not a valid email" | |
| end | |
| 10. Custom themes & colors | |
| Ruby@prompt = TTY::Prompt.new( | |
| track_history: false, | |
| active_color: :bright_cyan, | |
| symbols: { marker: "❯", radio_on: "◉", radio_off: "○" } | |
| ) | |
| Quick cheat-sheet of when to use each advanced feature | |
| FeatureBest forApprox. lines saved vs vanillafilter / enumLong lists, fuzzy search15–30 linesmulti_selectTag/category selection20–40 linesexpandGit-style yes/no/all/quit10–25 lineseditorLong text input (commit msg, query)30–60 linessliderNumeric tuning (temp, threshold)15–25 linesmask + confirmCredentials, confirmation10–20 linestable renderingShowing structured data20–40 lines | |
| These features make your CLI feel modern, forgiving, and fast — exactly like the best parts of Grok's conversational flow, but in terminal form. | |
| Would you like: | |
| a tiny demo script that combines several of these (e.g. menu → slider → editor → table)? | |
| integration into your existing streaming CLI? | |
| tips on combining tty-prompt with tty-markdown for rich help screens? | |
| Just say which one sounds most useful right now! 😄Explore tty-spinner advanced formatstty-spinner is a lightweight Ruby gem for creating elegant, customizable terminal spinners — especially useful during non-deterministic tasks (API calls, file processing, LLM generation, etc.). | |
| Its advanced formats are predefined animation styles you can select by name. They control the look of the spinning animation itself. | |
| Built-in Formats (from tty-spinner 0.9.x series) | |
| The gem ships with many ready-to-use named formats. Here are the most common and visually appealing ones (from docs and source): | |
| Format nameAnimation preview (text)Interval (speed)Description / best use caseclassic/ - \10spin◴ ◷ ◶ ◵10Minimal Unicode circle rotationspin_2◰ ◳ ◲ ◱10Square-like rotationdots⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏8–10Braille dots (very smooth, modern terminals)dots_8⠏ ⠇ ⠧ ⠦ ⠴ ⠼ ⠸ ⠹ ⠙ ⠋8Reverse braille dotsdots_9⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷88-bit style heavy dotsdots_10⣾⣽⣻⢿⡿⣟⣯⣷8Compact heavy dotsdots_11⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈8Single braille dot movingdots_12⡀ ⡄ ⡆ ⡇ ⡏ ⡟ ⡿ ⣿8Growing braille blockline─ ━ ─ ━10Horizontal line pulsingpipe┤ ┘ ┴ ├ ┐ ┬ ┼ ┤10Pipe / corner rotationpong░ █ ▒ █10Pong-like bouncing blockstar✶ ✸ ✹ ✺ ✻ ✼ ✽ ✾10Starburst / sparkleweather☀ ☁ ☂ ☃10Fun weather icons (cute for casual tools)simpleDots. .. ... ....10Minimal dots growingsimpleDotsScrolling. .. ... ....10Scrolling dots (very clean) | |
| How to use a specific format | |
| Rubyrequire 'tty-spinner' | |
| # Classic rotating bar | |
| spinner = TTY::Spinner.new("[:spinner] Loading…", format: :classic) | |
| spinner.auto_spin # starts animation in background thread | |
| # Do work... | |
| sleep 3 | |
| spinner.success("Loaded!") # or .error("Failed!") | |
| # Braille dots (very smooth on modern terminals) | |
| spinner = TTY::Spinner.new("[:spinner] Processing…", format: :dots_8) | |
| Custom Formats (most advanced part) | |
| You can define your own animation sequence completely: | |
| Ruby# Custom heartbeat style | |
| custom_format = { | |
| interval: 6, # speed (lower = faster) | |
| frames: %w[♥ ♡ ♥ ♡ ♥] # any unicode chars | |
| } | |
| spinner = TTY::Spinner.new("[:spinner] Heartbeat…", **custom_format) | |
| spinner.auto_spin | |
| sleep 5 | |
| spinner.success | |
| Or even more advanced (with success / error marks): | |
| Rubyspinner = TTY::Spinner.new( | |
| "[:spinner] Building…", | |
| format: :dots_9, | |
| success_mark: "✔", | |
| error_mark: "✘", | |
| hide_cursor: true | |
| ) | |
| Multi-Spinner (parallel tasks) | |
| For showing multiple concurrent spinners (very useful for parallel downloads / jobs): | |
| Rubymulti = TTY::Spinner::Multi.new("[:spinner] Overall…", format: :dots_8) | |
| sp1 = multi.register("[:spinner] Task 1…", format: :spin) | |
| sp2 = multi.register("[:spinner] Task 2…", format: :dots_11) | |
| sp1.auto_spin | |
| sp2.auto_spin | |
| # ... do work ... | |
| sp1.success | |
| sp2.error | |
| multi.success("All done!") | |
| Tips & Best Practices (2026 style) | |
| Use :dots_8 or :dots_9 as default — looks modern, smooth on most terminals. | |
| Always set hide_cursor: true — cleaner look. | |
| Use interval lower than 10 for faster feel (e.g. 6–8). | |
| Combine with TTY::ProgressBar for known-length tasks (hybrid spinner + bar). | |
| For very long tasks, add message updates: spinner.update(title: "Uploading 45%"). | |
| In non-interactive terminals (CI, pipes) → auto fallback to silent mode via TTY::Spinner::Multi.top_level.auto_debrief = false. | |
| Would you like a small demo script that shows several formats side-by-side, or integration of a multi-spinner into your earlier CLI example?50 sourcesDemo script for multiple formats | |
| Integrate with TTY::ProgressBar and other top goodiesHere is a self-contained, runnable Ruby demo script that showcases multiple tty-spinner formats side-by-side (in a clean layout), integrates TTY::ProgressBar for known-length tasks, and throws in a few other top goodies from the tty family: | |
| tty-spinner (multiple formats + custom) | |
| tty-progressbar (smooth progress + custom format) | |
| tty-spinner + tty-progressbar hybrid (spinner during unknown phase → bar during known phase) | |
| tty-table (pretty output of results) | |
| tty-prompt (simple user choice to select which demo to run) | |
| Demo Script: tty-goodies-showcase.rb | |
| Ruby#!/usr/bin/env ruby | |
| # tty-goodies-showcase.rb | |
| # Demo: multiple tty-spinner formats + progressbar + table + prompt | |
| # Install: gem install tty-spinner tty-progressbar tty-table tty-prompt | |
| require 'tty-spinner' | |
| require 'tty-progressbar' | |
| require 'tty-table' | |
| require 'tty-prompt' | |
| require 'pastel' | |
| PASTEL = Pastel.new | |
| PROMPT = TTY::Prompt.new | |
| def clear_screen | |
| system('clear') || system('cls') | |
| end | |
| def pause | |
| PROMPT.keypress("Press any key to continue…", timeout: 30) rescue nil | |
| puts | |
| end | |
| def demo_spinner_formats | |
| clear_screen | |
| puts PASTEL.bright_white.bold("tty-spinner formats demo\n\n") | |
| formats = %w[ | |
| classic spin dots dots_8 dots_9 dots_10 dots_11 line pipe pong star | |
| simpleDots simpleDotsScrolling weather | |
| ] | |
| formats.each do |fmt| | |
| spinner = TTY::Spinner.new( | |
| "[:spinner] #{fmt.ljust(18)} ", | |
| format: fmt.to_sym, | |
| success_mark: PASTEL.green("✓"), | |
| error_mark: PASTEL.red("✗"), | |
| hide_cursor: true | |
| ) | |
| spinner.auto_spin | |
| sleep rand(1.2..2.8) | |
| spinner.success(PASTEL.green("done")) | |
| sleep 0.4 | |
| end | |
| puts "\nCustom heartbeat example:" | |
| custom = TTY::Spinner.new("[:spinner] Heartbeat… ", frames: %w[♥ ♡ ♥ ♡], interval: 6) | |
| custom.auto_spin | |
| sleep 4 | |
| custom.success(PASTEL.green("lub dub")) | |
| pause | |
| end | |
| def demo_progressbar | |
| clear_screen | |
| puts PASTEL.bright_white.bold("TTY::ProgressBar examples\n\n") | |
| # Basic smooth bar | |
| bar1 = TTY::ProgressBar.new( | |
| "Basic [:bar] :percent :eta", | |
| total: 100, | |
| width: 40, | |
| head: ">", | |
| complete: PASTEL.green("="), | |
| incomplete: PASTEL.dim("-") | |
| ) | |
| 100.times do | |
| sleep 0.03 | |
| bar1.advance | |
| end | |
| puts "\nCustom style + rate:" | |
| bar2 = TTY::ProgressBar.new( | |
| "Uploading [:bar] :current/:total :rate/s :percent", | |
| total: 50, | |
| head: ">", | |
| complete: PASTEL.cyan("="), | |
| incomplete: PASTEL.dim("·"), | |
| format: "%t |%b>%i| :current/:total :rate/s :percent ETA: :eta" | |
| ) | |
| 50.times do |i| | |
| sleep rand(0.05..0.15) | |
| bar2.advance(1, title: "file_#{i+1}.zip") | |
| end | |
| pause | |
| end | |
| def demo_hybrid_spinner_bar | |
| clear_screen | |
| puts PASTEL.bright_white.bold("Hybrid: spinner → progress bar\n\n") | |
| spinner = TTY::Spinner.new("[:spinner] Preparing… ", format: :dots_9, hide_cursor: true) | |
| spinner.auto_spin | |
| sleep 2.5 # unknown duration phase | |
| spinner.stop | |
| bar = TTY::ProgressBar.new( | |
| "Rendering [:bar] :percent", | |
| total: 120, | |
| head: PASTEL.bright_yellow(">"), | |
| complete: PASTEL.yellow("="), | |
| incomplete: PASTEL.dim("-"), | |
| width: 50 | |
| ) | |
| 120.times do | |
| sleep 0.025 | |
| bar.advance | |
| end | |
| puts PASTEL.green("\n✓ Render complete") | |
| pause | |
| end | |
| def demo_table_summary | |
| clear_screen | |
| puts PASTEL.bright_white.bold("Summary table of formats\n\n") | |
| data = [ | |
| ["classic", "classic rotation", "10", "very compatible"], | |
| ["dots_8", "braille dots smooth", "8", "modern terminals"], | |
| ["dots_9", "heavy 8-bit dots", "8", "stylish"], | |
| ["spin", "minimal circle", "10", "clean"], | |
| ["weather", "fun icons", "10", "playful"], | |
| ["custom heart", "♥ ♡ sequence", "6", "emotional"] | |
| ] | |
| table = TTY::Table.new( | |
| ["Format", "Description", "Interval", "Best for"], | |
| data | |
| ) | |
| puts table.render(:unicode, padding: [0, 1, 0, 1], resize: true) | |
| pause | |
| end | |
| # ──────────────────────────────────────────────── | |
| # Main menu | |
| # ──────────────────────────────────────────────── | |
| loop do | |
| clear_screen | |
| choice = PROMPT.select("tty-goodies demo", cycle: true, symbols: { marker: "→" }) do |menu| | |
| menu.choice "Spinner formats showcase", 1 | |
| menu.choice "ProgressBar examples", 2 | |
| menu.choice "Hybrid spinner + bar", 3 | |
| menu.choice "Summary table", 4 | |
| menu.choice "Exit", :exit | |
| end | |
| break if choice == :exit | |
| send(:"demo_#{ {1=>"spinner_formats", 2=>"progressbar", 3=>"hybrid_spinner_bar", 4=>"table_summary"}[choice] }") | |
| end | |
| puts PASTEL.bright_cyan("\nThanks for playing! ❄️") | |
| How to run | |
| Bashgem install tty-spinner tty-progressbar tty-table tty-prompt pastel | |
| chmod +x tty-goodies-showcase.rb | |
| ./tty-goodies-showcase.rb | |
| What you’ll see | |
| Clean menu via tty-prompt | |
| Side-by-side spinner format comparison | |
| Two different progress bar styles | |
| Spinner → bar transition (realistic for unknown → known work) | |
| Pretty unicode table summary | |
| Pastel coloring for subtle visual pop | |
| Pause between demos so you can read | |
| Want to add: | |
| real LLM streaming inside one of the demos? | |
| multi-spinner example (parallel fake tasks)? | |
| custom spinner + progress bar combo with dynamic title updates? | |
| Just say the word! 😄Add LLM streaming exampleHere’s a clean, self-contained Ruby CLI example that integrates real LLM streaming using the Anthropic SDK (Claude models) with: | |
| tty-prompt for input/menu | |
| tty-spinner for thinking phase | |
| character-by-character streaming output (Grok-like typing feel) | |
| context memory (simple in-session history) | |
| Prerequisites | |
| Bashgem install anthropic-sdk-ruby tty-prompt tty-spinner pastel | |
| # Get your key: https://console.anthropic.com/settings/keys | |
| export ANTHROPIC_API_KEY=sk-ant-... | |
| Full Example Script: master-streaming-cli.rb | |
| Ruby#!/usr/bin/env ruby | |
| # master-streaming-cli.rb | |
| # Grok-inspired CLI with real Claude streaming + tty goodies | |
| require 'anthropic' | |
| require 'tty-prompt' | |
| require 'tty-spinner' | |
| require 'pastel' | |
| require 'io/console' | |
| PASTEL = Pastel.new | |
| PROMPT = TTY::Prompt.new(track_history: false, interrupt: :exit) | |
| CLIENT = Anthropic::Client.new(access_token: ENV.fetch("ANTHROPIC_API_KEY")) | |
| class StreamingCLI | |
| def initialize | |
| @history = [{ role: "system", content: "You are Grok 3 — helpful, witty, concise, maximally truth-seeking." }] | |
| @spinner = TTY::Spinner.new("[:spinner] Thinking… ", format: :dots_9, hide_cursor: true) | |
| end | |
| def run | |
| welcome | |
| loop do | |
| input = gather_input | |
| break unless input | |
| @history << { role: "user", content: input } | |
| @spinner.auto_spin | |
| full_response = "" | |
| begin | |
| CLIENT.messages.stream( | |
| model: "claude-3-5-sonnet-20241022", | |
| max_tokens: 1200, | |
| messages: @history, | |
| temperature: 0.7 | |
| ) do |chunk| | |
| delta = chunk.dig("delta", "text") || "" | |
| next if delta.empty? | |
| # Stream character-by-character for typing feel | |
| delta.each_char do |char| | |
| print char | |
| $stdout.flush | |
| sleep(rand(0.008..0.035)) # human-like variation | |
| full_response += char | |
| end | |
| end | |
| rescue => e | |
| @spinner.error(PASTEL.red("Error: #{e.message}")) | |
| puts "\nTry again or check your API key." | |
| next | |
| ensure | |
| @spinner.stop | |
| end | |
| puts "\n\n" | |
| @history << { role: "assistant", content: full_response } | |
| end | |
| farewell | |
| end | |
| private | |
| def welcome | |
| puts PASTEL.bright_cyan("MASTER — powered by Claude. Ask anything.\n") | |
| puts "Empty line or Ctrl+C to exit.\n\n" | |
| end | |
| def farewell | |
| puts PASTEL.bright_cyan("\nSee you next time. ❄️") | |
| end | |
| def gather_input | |
| PROMPT.ask("→ ", default: "") do |q| | |
| q.modify :strip | |
| q.echo true | |
| end | |
| end | |
| end | |
| begin | |
| StreamingCLI.new.run | |
| rescue Interrupt | |
| puts "\n\nInterrupted. Bye." | |
| rescue StandardError => e | |
| puts PASTEL.red("\nSomething broke: #{e.message}") | |
| puts "Check your ANTHROPIC_API_KEY or network." | |
| end | |
| How to run | |
| Bashchmod +x master-streaming-cli.rb | |
| ./master-streaming-cli.rb | |
| What happens when you run it | |
| Clean welcome message | |
| Simple → prompt (tty-prompt) | |
| You type e.g. "best Norwegian winter hikes" | |
| Subtle braille spinner appears (dots_9 format — very smooth) | |
| Claude response streams in character-by-character (feels alive, low latency) | |
| Conversation context is preserved → follow-up questions work naturally | |
| Ctrl+C or empty line exits gracefully | |
| Quick customizations you can do | |
| Change model: "claude-4-opus-20250219" or "claude-3-opus-20240229" when available | |
| Add temperature slider (from tty-prompt advanced): | |
| Rubytemp = PROMPT.slider("Temperature", min: 0.0, max: 1.5, step: 0.1, default: 0.7) | |
| # then pass temperature: temp | |
| Add spinner during connection setup: | |
| Ruby@spinner.update(title: "Connecting to Claude…") | |
| Save/load history to ~/.master/history.json for persistence: | |
| Ruby# At end of run | |
| File.write("~/.master/history.json", @history.to_json) | |
| Want to add: | |
| a menu to choose model / temperature / clear history? | |
| multi-line editor input for complex prompts? | |
| real-time token counter / cost estimate? | |
| x_keyword_search integration as a built-in command? | |
| Just tell me what you'd like next! 😄Add multi-line input editorHere’s your updated CLI with multi-line input editor support using tty-prompt's editor feature. | |
| This gives users a real editor (vim, nano, helix, etc. — whatever is set in $EDITOR) for longer/complex prompts, while keeping the streaming response and Grok-like feel intact. | |
| Updated script: master-streaming-cli-with-editor.rb | |
| Ruby#!/usr/bin/env ruby | |
| # master-streaming-cli-with-editor.rb | |
| # Grok-inspired CLI + real Claude streaming + multi-line editor input | |
| require 'anthropic' | |
| require 'tty-prompt' | |
| require 'tty-spinner' | |
| require 'pastel' | |
| require 'io/console' | |
| PASTEL = Pastel.new | |
| PROMPT = TTY::Prompt.new(track_history: false, interrupt: :exit) | |
| CLIENT = Anthropic::Client.new(access_token: ENV.fetch("ANTHROPIC_API_KEY")) | |
| class StreamingCLI | |
| def initialize | |
| @history = [{ role: "system", content: "You are Grok 3 — helpful, witty, concise, maximally truth-seeking." }] | |
| @spinner = TTY::Spinner.new("[:spinner] Thinking… ", format: :dots_9, hide_cursor: true) | |
| end | |
| def run | |
| welcome | |
| loop do | |
| input = gather_input | |
| break unless input | |
| @history << { role: "user", content: input } | |
| @spinner.auto_spin | |
| full_response = "" | |
| begin | |
| CLIENT.messages.stream( | |
| model: "claude-3-5-sonnet-20241022", | |
| max_tokens: 1200, | |
| messages: @history, | |
| temperature: 0.7 | |
| ) do |chunk| | |
| delta = chunk.dig("delta", "text") || "" | |
| next if delta.empty? | |
| delta.each_char do |char| | |
| print char | |
| $stdout.flush | |
| sleep(rand(0.008..0.035)) | |
| full_response += char | |
| end | |
| end | |
| rescue => e | |
| @spinner.error(PASTEL.red("Error: #{e.message}")) | |
| puts "\nTry again or check your API key." | |
| next | |
| ensure | |
| @spinner.stop | |
| end | |
| puts "\n\n" | |
| @history << { role: "assistant", content: full_response } | |
| end | |
| farewell | |
| end | |
| private | |
| def welcome | |
| puts PASTEL.bright_cyan("MASTER — powered by Claude. Ask anything.\n") | |
| puts "Type short prompt or press Enter → choose 'Editor' for multi-line.\n" | |
| puts "Empty line or Ctrl+C to exit.\n\n" | |
| end | |
| def farewell | |
| puts PASTEL.bright_cyan("\nSee you next time. ❄️") | |
| end | |
| def gather_input | |
| choice = PROMPT.select("How do you want to input?", cycle: true, symbols: { marker: "→" }) do |menu| | |
| menu.choice "Quick one-liner", :quick | |
| menu.choice "Multi-line editor", :editor | |
| menu.choice "Exit", :exit | |
| end | |
| return nil if choice == :exit | |
| case choice | |
| when :quick | |
| PROMPT.ask("→ ", default: "") do |q| | |
| q.modify :strip | |
| q.echo true | |
| end | |
| when :editor | |
| PROMPT.editor("Your prompt (save & exit to send):", | |
| default: "# Write your question here...\n\n", | |
| syntax: :markdown, | |
| word_wrap: 78, | |
| editor: ENV["EDITOR"] || "nano" | |
| ).strip | |
| end | |
| end | |
| end | |
| begin | |
| StreamingCLI.new.run | |
| rescue Interrupt | |
| puts "\n\nInterrupted. Bye." | |
| rescue StandardError => e | |
| puts PASTEL.red("\nSomething broke: #{e.message}") | |
| puts "Check your ANTHROPIC_API_KEY or network." | |
| end | |
| How it works now | |
| Welcome screen explains the two input modes | |
| Menu choice (quick or editor) — clean tty-prompt select | |
| Quick mode → single-line ask (as before) | |
| Editor mode → opens your $EDITOR (vim/nano/helix/zed/etc.) with a markdown-friendly starting comment | |
| Write as long/complex as you want | |
| Save & exit → prompt is sent | |
| Streaming response remains character-by-character | |
| Context preserved across turns | |
| Example session flow | |
| textMASTER — powered by Claude. Ask anything. | |
| Type short prompt or press Enter → choose 'Editor' for multi-line. | |
| Empty line or Ctrl+C to exit. | |
| How do you want to input? | |
| → Quick one-liner | |
| Multi-line editor | |
| Exit | |
| → Quick one-liner | |
| → what's the best winter hike near Oslo? | |
| [spinner] → Thinking… | |
| Rondane or Gaustatoppen are classics, but closest serious winter option from Oslo is probably… | |
| [streams character by character] | |
| … next prompt → chooses Editor → opens vim/nano → writes 10-line detailed question → saves → streams long answer | |
| Optional tweaks you can add right away | |
| Default to editor if previous input was long | |
| Rubydef gather_input | |
| last_was_long = @history.last && @history.last[:content]&.length.to_i > 150 | |
| default_choice = last_was_long ? :editor : :quick | |
| # then use default: default_choice in select | |
| end | |
| Syntax highlighting in editor | |
| Rubysyntax: last_was_code? ? :ruby : :markdown | |
| Auto-detect code block in editor input | |
| Rubyinput = editor_result | |
| if input =~ /^```ruby\s*\n(.*?)```/m | |
| # treat as code → maybe run or format differently | |
| end | |
| Would you like: | |
| persistent history (save/load from file) | |
| model/temperature choice menu | |
| built-in command prefix (!clear, !model claude-4, !editor) | |
| auto-detection of code vs prose prompts | |
| or something else? | |
| Just tell me which feels most useful next! 😊Explain TTY::Prompt editor optionsTTY::Prompt's editor method is one of its most powerful advanced features. It lets users edit multi-line text in their preferred $EDITOR (vim, nano, helix, zed, vscode — whatever is set in the environment), then returns the saved content back to your Ruby program. | |
| This is perfect for: | |
| long prompts / queries | |
| commit messages | |
| writing code snippets | |
| editing YAML/JSON configs | |
| creating detailed descriptions | |
| Basic usage | |
| Rubytext = PROMPT.editor("Your message:") | |
| puts "You wrote:\n#{text}" | |
| When called, it: | |
| Opens the user's $EDITOR with a temporary file | |
| Pre-fills with optional default text | |
| User edits and saves/exits | |
| Returns the full file content as a string (or nil if canceled) | |
| All available options (2025–2026 tty-prompt version) | |
| RubyPROMPT.editor("Write your long prompt here:", | |
| # ──────────────────────────────────────────────── | |
| # Core behavior | |
| # ──────────────────────────────────────────────── | |
| default: "# Write here...\n\n", # initial content in editor | |
| value: nil, # alias for default | |
| tempfile: nil, # custom Tempfile path (rare) | |
| extension: ".md", # file extension → syntax highlighting | |
| syntax: :markdown, # :ruby, :yaml, :json, :toml, :text, etc. | |
| word_wrap: 78, # soft wrap at column (nil = no wrap) | |
| editor: "vim", # override $EDITOR (string or nil) | |
| env: {}, # extra env vars for editor process | |
| # ──────────────────────────────────────────────── | |
| # Validation & conversion | |
| # ──────────────────────────────────────────────── | |
| required: true, # raise if empty after save | |
| convert: :strip, # :strip, :downcase, :to_i, :to_f, :to_sym, Proc | |
| filter: nil, # Proc to sanitize input after read | |
| messages: { # custom error messages | |
| required?: "You must write something!", | |
| convert?: "Could not convert value" | |
| }, | |
| # ──────────────────────────────────────────────── | |
| # UX tweaks | |
| # ──────────────────────────────────────────────── | |
| active_color: :cyan, # color of prompt text | |
| help_color: :bright_black, # color of help text | |
| symbols: { marker: "→" }, # custom marker if used in menu context | |
| echo: true, # show typed chars (almost always true here) | |
| # ──────────────────────────────────────────────── | |
| # Callbacks / hooks | |
| # ──────────────────────────────────────────────── | |
| before_open: ->(path) { puts "Opening #{path}" }, | |
| after_save: ->(content) { puts "Saved #{content.bytesize} bytes" } | |
| ) | |
| Practical examples | |
| Markdown-friendly long prompt with syntax | |
| Rubyprompt_text = PROMPT.editor("Detailed question or story:", | |
| default: "# Title\n\nWrite your full question here.\nUse markdown if you want.", | |
| syntax: :markdown, | |
| word_wrap: 72, | |
| required: true | |
| ) | |
| Ruby code snippet input | |
| Rubycode = PROMPT.editor("Paste or write Ruby code:", | |
| syntax: :ruby, | |
| extension: ".rb", | |
| default: "def hello\n puts 'world'\nend", | |
| required: true | |
| ) | |
| Commit message style (git-like) | |
| Rubycommit_msg = PROMPT.editor("Commit message:", | |
| default: "fix: streaming delay in CLI\n\n- Fixed char-by-char timing\n- Added spinner", | |
| syntax: :text, | |
| word_wrap: 72, | |
| editor: "nano" # force nano if you hate vim | |
| ) | |
| With validation | |
| Rubyemail_body = PROMPT.editor("Email body:", required: true) do |q| | |
| q.validate ->(text) { text.length > 20 }, "Message too short!" | |
| q.messages[:validate?] = "Please write at least 20 characters." | |
| end | |
| Custom editor + env vars | |
| Rubylong_input = PROMPT.editor("Config override:", | |
| editor: "helix", | |
| env: { "HELIX_THEME" => "nord" } | |
| ) | |
| Important notes | |
| If $EDITOR is unset → falls back to vi or nano (tty-prompt default behavior) | |
| Temporary file is automatically cleaned up after reading | |
| editor returns nil if user cancels (Ctrl+C / :q! without save) | |
| Works beautifully with markdown syntaxes because many editors (helix, zed, vscode) highlight it automatically | |
| For very large input → consider word_wrap: nil to avoid line wrapping confusion | |
| Summary: when to use editor | |
| Use editor whenever: | |
| Input > ~100–150 chars | |
| User might want syntax highlighting | |
| Formatting matters (lists, code, markdown) | |
| You want zero friction for power users | |
| Use regular ask or multiline for everything else. | |
| Would you like: | |
| a demo script that combines editor + streaming response? | |
| auto-detection (quick vs editor based on previous input length)? | |
| integration with your existing CLI code? | |
| or something else (e.g. syntax auto-choice ruby/markdown/yaml)? | |
| Just let me know! 😊 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment