├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── shard.yml ├── spec ├── readline_spec.cr └── spec_helper.cr └── src └── readline.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This CI job installs Crystal and shard dependencies, then executes `crystal spec` to run the test suite 2 | # More configuration options are available at https://crystal-lang.github.io/install-crystal/configurator.html 3 | name: Crystal Spec 4 | 5 | on: 6 | push: 7 | pull_request: 8 | branches: 9 | # Branches from forks have the form 'user:branch-name' so we only run 10 | # this job on pull_request events for branches that look like fork 11 | # branches. Without this we would end up running this job twice for non 12 | # forked PRs, once for the push and then once for opening the PR. 13 | - "**:**" 14 | schedule: 15 | - cron: '0 6 * * 6' # Every Saturday 6 AM 16 | 17 | jobs: 18 | test: 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | include: 23 | - os: ubuntu-latest 24 | crystal: 1.0.0 25 | - os: ubuntu-latest 26 | crystal: 1.2.0 27 | - os: ubuntu-latest 28 | - os: ubuntu-latest 29 | crystal: nightly 30 | - os: macos-latest 31 | runs-on: ${{ matrix.os }} 32 | steps: 33 | - name: Download source 34 | uses: actions/checkout@v2 35 | - name: Install Crystal 36 | uses: crystal-lang/install-crystal@v1 37 | with: 38 | crystal: ${{ matrix.crystal }} 39 | - name: Install shards 40 | run: shards install 41 | - name: Run tests 42 | run: crystal spec --order=random 43 | - name: Check formatting 44 | run: crystal tool format --check 45 | if: matrix.crystal == null && matrix.os == 'ubuntu-latest' 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | 7 | # Libraries don't need dependency lock 8 | # Dependencies will be locked in applications that use them 9 | /shard.lock 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.1.1 (2021-07-21) 2 | 3 | - Fix compatibility with Crystal >= 0.31.1 in `shard.yml` ([#5](https://github.com/crystal-lang/crystal-readline/pull/5), thanks @HashNuke) 4 | - Add Troubleshooting section to README ([#2](https://github.com/crystal-lang/crystal-readline/pull/2), thanks @ryanprior) 5 | 6 | # v0.1.0 (2019-10-22) 7 | 8 | - Extract from Crystal 0.31.1 std-lib. 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Brian J. Cardiff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # readline 2 | 3 | Crystal bindings to [GNU Readline Library](https://www.gnu.org/software/readline). 4 | 5 | ## Installation 6 | 7 | 1. Add the dependency to your `shard.yml`: 8 | 9 | ```yaml 10 | dependencies: 11 | readline: 12 | github: crystal-lang/crystal-readline 13 | ``` 14 | 15 | 2. Run `shards install` 16 | 17 | ## Usage 18 | 19 | ```crystal 20 | require "readline" 21 | 22 | user_input = Readline.readline("> ") 23 | ``` 24 | 25 | ## Troubleshooting 26 | 27 | ### Static linking 28 | 29 | If you build your binary with `crystal build --static` you might encounter errors like these: 30 | 31 | ``` 32 | (.text+0x678): undefined reference to `PC' 33 | (.text+0x687): undefined reference to `BC' 34 | (.text+0x68e): undefined reference to `UP' 35 | (.text+0x728): undefined reference to `tgetent' 36 | (.text+0x76f): undefined reference to `tgetstr' 37 | (.text+0x799): undefined reference to `PC' 38 | (.text+0x7af): undefined reference to `BC' 39 | (.text+0x7bd): undefined reference to `UP' 40 | (.text+0x7cf): undefined reference to `tgetflag' 41 | (.text+0x81e): undefined reference to `tgetflag' 42 | (.text+0x938): undefined reference to `tgetflag' 43 | ``` 44 | 45 | In this case you may need to explicitly link to `libtermcap`. Crystal offers the 46 | `--link-flags` option but it doesn't let you specify where to insert the new 47 | flags, and this is order dependent. Luckily, the linker doesn't complain about 48 | duplicate flags, so you can link `libtermcap` like so: 49 | 50 | ```sh-session 51 | $ crystal build --static --link-flags "-rdynamic -static -lreadline -ltermcap" main.cr 52 | ``` 53 | 54 | ## Contributing 55 | 56 | 1. Fork it () 57 | 2. Create your feature branch (`git checkout -b my-new-feature`) 58 | 3. Commit your changes (`git commit -am 'Add some feature'`) 59 | 4. Push to the branch (`git push origin my-new-feature`) 60 | 5. Create a new Pull Request 61 | 62 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: readline 2 | version: 0.1.1 3 | 4 | libraries: 5 | readline: "*" 6 | 7 | authors: 8 | - Brian J. Cardiff 9 | - Franciscello 10 | 11 | crystal: ">= 0.31.1" 12 | 13 | license: MIT 14 | -------------------------------------------------------------------------------- /spec/readline_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Readline do 4 | typeof(Readline.readline) 5 | typeof(Readline.readline("Hello", true)) 6 | typeof(Readline.readline(prompt: "Hello")) 7 | typeof(Readline.readline(add_history: false)) 8 | typeof(Readline.line_buffer) 9 | typeof(Readline.point) 10 | typeof(Readline.autocomplete { |s| %w(foo bar) }) 11 | 12 | it "gets prefix in bytesize between two strings" do 13 | Readline.common_prefix_bytesize("", "foo").should eq(0) 14 | Readline.common_prefix_bytesize("foo", "").should eq(0) 15 | Readline.common_prefix_bytesize("a", "a").should eq(1) 16 | Readline.common_prefix_bytesize("open", "operate").should eq(3) 17 | Readline.common_prefix_bytesize("operate", "open").should eq(3) 18 | Readline.common_prefix_bytesize(["operate", "open", "optional"]).should eq(2) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/readline" 3 | -------------------------------------------------------------------------------- /src/readline.cr: -------------------------------------------------------------------------------- 1 | @[Link("readline")] 2 | {% if flag?(:openbsd) %} 3 | @[Link("termcap")] 4 | {% end %} 5 | lib LibReadline 6 | alias Int = LibC::Int 7 | 8 | fun readline(prompt : UInt8*) : UInt8* 9 | fun add_history(line : UInt8*) 10 | fun read_history(fname : UInt8*) : Int 11 | fun rl_bind_key(key : Int, f : Int, Int -> Int) : Int 12 | fun rl_unbind_key(key : Int) : Int 13 | 14 | alias CPP = (UInt8*, Int, Int) -> UInt8** 15 | 16 | $rl_attempted_completion_function : CPP 17 | $rl_line_buffer : UInt8* 18 | $rl_point : Int 19 | $rl_done : Int 20 | end 21 | 22 | private def malloc_match(match) 23 | match_ptr = LibC.malloc(match.bytesize + 1).as(UInt8*) 24 | match_ptr.copy_from(match.to_unsafe, match.bytesize) 25 | match_ptr[match.bytesize] = 0_u8 26 | match_ptr 27 | end 28 | 29 | module Readline 30 | VERSION = "0.1.0" 31 | 32 | extend self 33 | 34 | alias CompletionProc = String -> Array(String)? 35 | 36 | alias KeyBindingProc = Int32, Int32 -> Int32 37 | KeyBindingHandler = ->(count : LibReadline::Int, key : LibReadline::Int) do 38 | if (handlers = @@key_bind_handlers) && handlers[key.to_i32]? 39 | res = handlers[key].call(count.to_i32, key.to_i32) 40 | LibReadline::Int.new(res) 41 | else 42 | LibReadline::Int.new(1) 43 | end 44 | end 45 | 46 | def readline(prompt = "", add_history = false) 47 | line = LibReadline.readline(prompt) 48 | if line 49 | LibReadline.add_history(line) if add_history 50 | String.new(line).tap { LibC.free(line.as(Void*)) } 51 | else 52 | nil 53 | end 54 | end 55 | 56 | def autocomplete(&@@completion_proc : CompletionProc) 57 | end 58 | 59 | def line_buffer 60 | line = LibReadline.rl_line_buffer 61 | return nil unless line 62 | 63 | String.new(line) 64 | end 65 | 66 | def point 67 | LibReadline.rl_point 68 | end 69 | 70 | def bind_key(c : Char, &f : KeyBindingProc) 71 | raise ArgumentError.new "Not a valid ASCII character: #{c.inspect}" unless c.ascii? 72 | 73 | handlers = (@@key_bind_handlers ||= {} of LibReadline::Int => KeyBindingProc) 74 | handlers[c.ord] = f 75 | 76 | res = LibReadline.rl_bind_key(c.ord, KeyBindingHandler).to_i32 77 | raise ArgumentError.new "Invalid key: #{c.inspect}" unless res == 0 78 | end 79 | 80 | def unbind_key(c : Char) 81 | if (handlers = @@key_bind_handlers) && handlers[c.ord]? 82 | handlers.delete(c.ord) 83 | res = LibReadline.rl_unbind_key(c.ord).to_i32 84 | raise Exception.new "Error unbinding key: #{c.inspect}" unless res == 0 85 | else 86 | raise KeyError.new "Key not bound: #{c.inspect}" 87 | end 88 | end 89 | 90 | def done 91 | LibReadline.rl_done != 0 92 | end 93 | 94 | def done=(val : Bool) 95 | LibReadline.rl_done = val.hash 96 | end 97 | 98 | # :nodoc: 99 | def common_prefix_bytesize(str1 : String, str2 : String) 100 | r1 = Char::Reader.new str1 101 | r2 = Char::Reader.new str2 102 | 103 | while r1.has_next? && r2.has_next? 104 | break if r1.current_char != r2.current_char 105 | 106 | r1.next_char 107 | r2.next_char 108 | end 109 | 110 | r1.pos 111 | end 112 | 113 | # :nodoc: 114 | def common_prefix_bytesize(strings : Array) 115 | str1 = strings[0] 116 | low = str1.bytesize 117 | 1.upto(strings.size - 1).each do |i| 118 | str2 = strings[i] 119 | low2 = common_prefix_bytesize(str1, str2) 120 | low = low2 if low2 < low 121 | end 122 | low 123 | end 124 | 125 | LibReadline.rl_attempted_completion_function = ->(text_ptr, start, finish) { 126 | completion_proc = @@completion_proc 127 | return Pointer(UInt8*).null unless completion_proc 128 | 129 | text = String.new(text_ptr) 130 | matches = completion_proc.call(text) 131 | 132 | return Pointer(UInt8*).null unless matches 133 | return Pointer(UInt8*).null if matches.empty? 134 | 135 | # We *must* to create the results using malloc (readline later frees that). 136 | # We create an extra result for the first element. 137 | result = LibC.malloc(sizeof(UInt8*) * (matches.size + 2)).as(UInt8**) 138 | matches.each_with_index do |match, i| 139 | result[i + 1] = malloc_match(match) 140 | end 141 | result[matches.size + 1] = Pointer(UInt8).null 142 | 143 | # The first element is the completion if it's oe 144 | if matches.size == 1 145 | result[0] = malloc_match(matches[0]) 146 | else 147 | # Otherwise, we compute the common prefix of all matches 148 | low = Readline.common_prefix_bytesize(matches) 149 | sub = matches[0].byte_slice(0, low) 150 | result[0] = malloc_match(sub) 151 | end 152 | result 153 | } 154 | end 155 | --------------------------------------------------------------------------------