{"id":20768513,"url":"https://github.com/papierkorb/fancyline","last_synced_at":"2025-07-23T02:04:31.866Z","repository":{"id":48831842,"uuid":"85512646","full_name":"Papierkorb/fancyline","owner":"Papierkorb","description":"Readline-esque library with fancy features","archived":false,"fork":false,"pushed_at":"2021-07-10T18:38:44.000Z","size":94,"stargazers_count":79,"open_issues_count":2,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-30T12:15:18.048Z","etag":null,"topics":["cli","console","crystal","readline","repl","shell","terminal"],"latest_commit_sha":null,"homepage":null,"language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Papierkorb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-03-19T22:12:03.000Z","updated_at":"2025-02-18T19:22:27.000Z","dependencies_parsed_at":"2022-09-01T10:42:02.165Z","dependency_job_id":null,"html_url":"https://github.com/Papierkorb/fancyline","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/Papierkorb/fancyline","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Ffancyline","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Ffancyline/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Ffancyline/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Ffancyline/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Papierkorb","download_url":"https://codeload.github.com/Papierkorb/fancyline/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Papierkorb%2Ffancyline/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266604009,"owners_count":23954725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-23T02:00:09.312Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","console","crystal","readline","repl","shell","terminal"],"created_at":"2024-11-17T11:39:19.483Z","updated_at":"2025-07-23T02:04:31.841Z","avatar_url":"https://github.com/Papierkorb.png","language":"Crystal","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fancyline [![Build Status](https://travis-ci.org/Papierkorb/fancyline.svg?branch=master)](https://travis-ci.org/Papierkorb/fancyline)\n\n\u003c!-- Hello reader: If you're editing this file, please use two spaces after each\n     sentence to improve readability of the raw file - Thanks! --\u003e\n\nReadline-esque library with fancy features!\n\n## Compared with Readline\n\n|                           | Fancyline  | Readline     |\n|-------------------------- | ---------- | ------------ |\n| Uses Readline config      | No         | Yes          |\n| Code style                | OOP        | Imperative   |\n| Autocompletion            | Yes        | Yes          |\n| Input highlighting        | Yes        | No           |\n| Can show further info     | Yes        | No           |\n| Right-side prompt         | Yes        | Hacky        |\n| Multi-line prompt         | Yes        | Manually     |\n| Blocking behaviour        | Only Fiber | Whole Thread |\n| License                   | MPL-2      | GPL          |\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  fancyline:\n    github: Papierkorb/fancyline\n    version: ~\u003e 0.4.1\n```\n\n## Tutorial\n\nLet's build a simple system shell.  We want it to do syntax highlighting, do\ntab-autocompletion, and show a quicktip about the current command.  We're\nfocusing on the REPL part, so we'll stick to using `system()` to use `/bin/sh`\nto handle pipes etc..\n\nDon't want to paste all of these yourself?  Fear not, check out\n[samples/tutorial/](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial).\n\nThere are more general samples in\n[samples/](https://github.com/Papierkorb/fancyline/tree/master/samples).\n\n### Step 0: Most basic usage\n\nLet's start with something simple: A greeter.  The user is asked for a name,\nand that name is then greeted.  All we need to do is createing a `Fancyline`\ninstance and then calling `#readline` on it with our prompt.\n\n```crystal\nrequire \"fancyline\"\n\nfancy = Fancyline.new # Build a shell object\ninput = fancy.readline(\"Name: \") # Show the prompt\nputs \"Hello, #{input}!\"\n```\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step0.cr)\n\n### Step 1: The REPL skeleton\n\nThe skeleton of a REPL (**R**ead **E**valuate **P**rint **L**oop) is really\nwhat it says on the tin: A loop, which accepts input, runs it, and then prints\nthe output.  Replace the last file with the following:\n\n```crystal\nrequire \"fancyline\"\n\nfancy = Fancyline.new # Build a shell object\nputs \"Press Ctrl-C or Ctrl-D to quit.\"\n\nwhile input = fancy.readline(\"$ \") # Ask the user for input\n  system(input) # And run it\nend\n```\n\nNow we can run commands and have a command history.  Pretty decent for a few\nlines.\n\nPossible improvement: Make `cd` work by implementing it.  This series will not.\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step1.cr)\n\n### Step 2: Simple syntax-highlighting\n\nMany people seem to enjoy having their shell do some syntax-highlighting to\nshow the command, arguments, or similar.  So let's add it to our shell!  We use\nthe `display` middleware of Fancyline, which is called with the line buffer and\ncan then add colors to it.  Make sure to not change the visual size of the line.\n\nJust add this code snippet to your source file:\n\n```crystal\nfancy.display.add do |ctx, line, yielder|\n  # We underline command names\n  line = line.gsub(/^\\w+/, \u0026.colorize.mode(:underline))\n  line = line.gsub(/(\\|\\s*)(\\w+)/) do\n    \"#{$1}#{$2.colorize.mode(:underline)}\"\n  end\n\n  # And turn --arguments green\n  line = line.gsub(/--?\\w+/, \u0026.colorize(:green))\n\n  # Then we call the next middleware with the modified line\n  yielder.call ctx, line\nend\n```\n\nNow, everytime the user hits a key, Fancyline will render the line buffer, which\ncalls all `display` middlewares in order.\n\nPossible improvement: Try to add better highlighting.\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step2.cr)\n\n### Step 3: Custom key bindings\n\nOne thing that's really useful is being able to pull up the man-page of the\ncommand you're currently working on without having to type it yourself.  We add\na key-binding for `Ctrl-O` (`^O`) to do this for us:\n\n```crystal\ndef get_command(ctx)\n  line = ctx.editor.line\n  cursor = ctx.editor.cursor.clamp(0, line.size - 1)\n  pipe = line.rindex('|', cursor)\n  line = line[(pipe + 1)..-1] if pipe\n\n  line.split.first?\nend\n\nfancy.actions.set Fancyline::Key::Control::CtrlO do |ctx|\n  if command = get_command(ctx) # Figure out the current command\n    system(\"man #{command}\") # And open the man-page of it\n  end\nend\n```\n\nIf you look at line **3**, you see we're clamping the value of `ctx.editor.cursor` to\nthe range of `[0...line.size]`.  Fancyline allows the cursor to be at\n`line.size`, so just after the line buffer, allowing the user to append\ncharacters at the end of it.  But Crystal doesn't like that and may raise an\nexception if the cursor is currently at the end of the line.\n\nNow, run the program, type a command, and hit `Ctrl-H` to show the man-page.\n\nPossible improvement: Add a key-binding which saves the last line as\n`script.sh`.\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step3.cr)\n\n### Step 4: Showing information below the prompt\n\nNext we learn about using the `sub_info` middleware, which allows us to display\nadditional lines of text under the prompt.  We use this feature to give the user\na short hint about what the current command will do using the `whatis` program.\n\n```crystal\nfancy.sub_info.add do |ctx, yielder|\n  lines = yielder.call(ctx) # First run the next part of the middleware chain\n\n  if command = get_command(ctx) # Grab the command\n    help_line = `whatis #{command} 2\u003e /dev/null`.lines.first?\n    lines \u003c\u003c help_line if help_line # Display it if we got something\n  end\n\n  lines # Return the lines so far\nend\n```\n\nWhen you're writing `sub_info` middlewares, make sure that each line fits in a\nsingle line in the terminal. `Fancyline::Context#columns` can tell you how much\nspace you have. If your middleware wants to display more, just append more\nlines.\n\nPossible improvement: Create a `sub_info` middleware which shows the current\ntime or the weather.\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step4.cr)\n\n### Step 5: Tab Auto-completion\n\nFinally the moment you've been probably waiting for: Adding the most useful\nfeature a REPL can offer: Auto-completion!  For this we add autocompletion of\npaths.\n\nAlso look at the\n[sample source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step5.cr#L10)\nfor this, it offers some more explanation comments.\n\n```crystal\nfancy.autocomplete.add do |ctx, range, word, yielder|\n  completions = yielder.call(ctx, range, word)\n\n  # The `word` may not suffice for us here.  It'd be fine however for command\n  # name completion.\n\n  # Find the range of the current path name near the cursor.\n  prev_char = ctx.editor.line[ctx.editor.cursor - 1]?\n  if !word.empty? || { '/', '.' }.includes?(prev_char)\n    # Then we try to find where it begins and ends\n    arg_begin = ctx.editor.line.rindex(' ', ctx.editor.cursor - 1) || 0\n    arg_end = ctx.editor.line.index(' ', arg_begin + 1) || ctx.editor.line.size\n    range = (arg_begin + 1)...arg_end\n\n    # And using that range we just built, we can find the path the user entered\n    path = ctx.editor.line[range].strip\n  end\n\n  # Find suggestions and append them to the completions array.\n  Dir[\"#{path}*\"].each do |suggestion|\n    base = File.basename(suggestion)\n    suggestion += '/' if Dir.exists? suggestion\n    completions \u003c\u003c Fancyline::Completion.new(range, suggestion, base)\n  end\n\n  completions\nend\n```\n\nNow how does this work?  We're now using the third middleware Fancyline offers:\n`autocomplete`.  It is used whenever the user hits `TAB` to acquire completion\nsuggestions.  This is also the first time we're offering the user a new\ninteraction flow: Multiple TAB hits cycle through the list of suggestions.  You\ncan build custom flows yourself by creating a **Widget**.  See below for more\non that.\n\nPossible improvement: Add a second `autocomplete` middleware for command\ncompletion.\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step5.cr)\n\n### Step 6: Wrapping things up\n\nLet's wrap this up and add the last things we expect from a shell:\n1. Persistant history\n2. Not printing a stacktrace on Ctrl-C\n\nFor this we modify our trusty while-loop:\n\n```crystal\nHISTFILE = \"#{Dir.current}/history.log\"\n\nif File.exists? HISTFILE # Does it exist?\n  puts \"  Reading history from #{HISTFILE}\"\n  File.open(HISTFILE, \"r\") do |io| # Open a handle\n    fancy.history.load io # And load it\n  end\nend\n\nbegin # Get rid of stacktrace on ^C\n  while input = fancy.readline(\"$ \")\n    system(input)\n  end\nrescue err : Fancyline::Interrupt\n  puts \"Bye.\"\nend\n\nFile.open(HISTFILE, \"w\") do |io| # So open it writable\n  fancy.history.save io # And save.  That's it.\nend\n```\n\nNow we're done!  We built a shell (Or a front-end for a shell) which already\noffers lots of functionality expected from a modern shell, all in about 100\nlines of code.  There's more Fancyline allows you to do, but this should give\nyou a pretty good insight in how things are supposed to work.  Happy hacking!\n\n[Complete source](https://github.com/Papierkorb/fancyline/tree/master/samples/tutorial/step6.cr)\n\n## Middlewares\n\nFancyline uses [cute](https://github.com/Papierkorb/cute) middlewares to allow\nyou augmenting default behaviour.  If you're familiar with `Ruby Rack` or\n`Kemal.cr` you know already the gist of them.\n\nIf you're not: Middlewares are basically daisy-chained method calls, which allow\nyou to change their calling order or add your own calls into the chain.\n\n### `display`\n\nThis middleware lets you change how the editor shows the line from the user on\nthe screen.  This is mostly useful to add syntax-highlighting, showing while the\nuser is typing.\n\nHave a look at [input_highlighting.cr](https://github.com/Papierkorb/fancyline/tree/master/samples/input_highlighting.cr).\n\n### `autocomplete`\n\nThis middleware allows you to add auto-completion to your shell.  The middleware\nis called by `Fancyline::Widget::Completion` to present the user with the list\nof suggestion to choose from.\n\nSee [autocompletion.cr](https://github.com/Papierkorb/fancyline/tree/master/samples/autocompletion.cr).\n\n### `sub_info`\n\nDisplays additional lines of text *below* the prompt.  Used by many widgets to\nshow a small interface.\n\nSee [sub_info.cr](https://github.com/Papierkorb/fancyline/tree/master/samples/sub_info.cr).\n\n## Key Bindings\n\nThese are the default key bindings.  You can add your own in\n`Fancyline#actions`. See also [key_binding.cr](https://github.com/Papierkorb/fancyline/tree/master/samples/key_binding.cr).\n\n|     Key     | Action                                      |\n| ----------- | ------------------------------------------- |\n| `Ctrl-C`    | Raises `Fancyline::Interrupt`               |\n| `Return`    | Accepts the input                           |\n| `Ctrl-O`    | Same as `Return`                            |\n| `Backspace` | Removes the character left of the cursor    |\n| `Delete`    | Removes the character under the cursor      |\n| `Left`      | Moves the cursor left                       |\n| `Right`     | Moves the cursor right                      |\n| `Home`      | Moves the cursor to the beginning           |\n| `End`       | Moves the cursor after the last character   |\n| `Ctrl-D`    | If buffer is empty, rejects the input       |\n| `Ctrl-U`    | Clears the line buffer                      |\n| `Ctrl-L`    | Clears the screen                           |\n| `Up`        | Activates the **History** widget            |\n| `Ctrl-R`    | Activates the **HistorySearch** widget      |\n\n## Widgets\n\nFancyline uses \"Widgets\" to augment the behaviour of a running prompt\ntemporarily.  At any time, there may be up to one widget active.  If one is\nactive, all user input is first sent to it.  The widget may then choose an\naction to take, like acting upon it or continuing default operation.\n\nSome fundamental features you expect to work from a prompt are implemented as\nwidget.  If you want to create your own, have a look at `Fancyline::Widget` and\n[widget.cr](https://github.com/Papierkorb/fancyline/tree/master/samples/widget.cr).\n\n### Completion\n\nImplements TAB-autocompletion using the `autocomplete` middleware.  The original\nword can always be recovered by tabbing \"outside\" the list of suggestions.\n\n|     Key     | Action                         |\n| ----------- | ------------------------------ |\n| Activate    | Hit `Tab` while in the prompt  |\n| `Tab`       | View the next suggestion       |\n| `Shift-Tab` | View the previous suggestion   |\n| Bold letter | Select the marked suggestion   |\n| Any other   | Deactivates the widget         |\n\nIf no suggestions were found, the widget stops itself right away.  The user does\nnot get any visual feedback of this.  If exactly one suggestion was found, it\nis applied, and the user can choose between the suggestion and the original\ninput using `Tab`.\n\n### History\n\nImplements a history, which can be navigated using the Up and Down buttons.\nThe original input line is retained and can be accessed by going beyond the most\nrecent history entry.\n\n|    Key    | Action                                    |\n| --------- | ----------------------------------------- |\n| Activate  | Hit `Up` while in the prompt              |\n| `Up`      | Show previous history entry               |\n| `Down`    | Show the next (more recent) history entry |\n| Any other | Deactivates the widget                    |\n\n### HistorySearch\n\nImplements a history search, which lets you find a specific history entry.\nThe original input line is retained and can be accessed by going beyond the most\nrecent match.\n\n|    Key    | Action                             |\n| --------- | ---------------------------------- |\n| Activate  | Hit `Ctrl-R` while in the prompt   |\n| `Up`      | Show previous match                |\n| `Down`    | Show the next (more recent) match  |\n| `Ctrl-C`  | Cancels and restores original line |\n| Any other | Deactivates the widget             |\n\nShows a sub-info line in the format of `Search X/Y: NEEDLE`, where\n* **X** shows the current position in the search matches (Up/Down)\n* **Y** shows total count of matches\n* **NEEDLE** shows the current search query\n\nIf **X** is showing `0`, you're seeing the original line input.\n\nIf **NEEDLE** contains only lower-case input, the search is case-insensitive.\nIf it also contains upper-case input, the search is case-sensitive.\n\n## To Do\n\n* Long input lines, longer than the terminal can display, will break\n\n## Contributing\n\n1. Fork it ( https://github.com/Papierkorb/fancyline/fork )\n2. Create your feature branch (git checkout -b my-new-feature)\n3. Commit your changes (git commit -am 'Add some feature')\n4. Push to the branch (git push origin my-new-feature)\n5. Create a new Pull Request\n\n## License\n\nThis library is licensed under the Mozilla Public License 2.0 (\"MPL-2\").\n\nFor a copy of the full license text see the included `LICENSE` file.\n\nFor a legally non-binding explanation visit:\n[tl;drLegal](https://tldrlegal.com/license/mozilla-public-license-2.0-%28mpl-2%29)\n\n## Still looking down here?\n\nThanks for reading, now do something cool and enjoy your day!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapierkorb%2Ffancyline","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpapierkorb%2Ffancyline","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpapierkorb%2Ffancyline/lists"}