coolline 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  require 'coolline/handler'
2
2
  require 'coolline/history'
3
+ require 'coolline/ansi'
3
4
  require 'coolline/editor'
5
+ require 'coolline/menu'
4
6
 
5
7
  require 'coolline/coolline'
6
8
 
@@ -0,0 +1,98 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ class Coolline
4
+ # Mixin that allows to manipulate strings that contain ANSI color codes:
5
+ # getting their length, printing their n first characters, etc.
6
+ #
7
+ # Additionally, it can output certain commonly needed sequences — using the
8
+ # {#print} method to write to the output.
9
+ module ANSI
10
+ Code = %r{(\e\[\??\d+(?:[;\d]*)\w)}
11
+
12
+ # @return [Integer] Amount of characters within the string, disregarding
13
+ # color codes.
14
+ def ansi_length(string)
15
+ strip_ansi_codes(string).length
16
+ end
17
+
18
+ # @return [String] The initial string without ANSI codes.
19
+ def strip_ansi_codes(string)
20
+ string.gsub(Code, "")
21
+ end
22
+
23
+ # @return [Boolean] True if the beginning of the string is an ANSI code.
24
+ def start_with_ansi_code?(string)
25
+ (string =~ Code) == 0
26
+ end
27
+
28
+ # Prints a slice of a string containing ANSI color codes. This allows to
29
+ # print a string of a fixed width, while still keeping the right colors,
30
+ # etc.
31
+ #
32
+ # @param [String] string
33
+ # @param [Integer] start
34
+ # @param [Integer] stop Stop index, excluded from the range.
35
+ def ansi_print(string, start, stop)
36
+ i = 0
37
+ string.split(Code).each do |str|
38
+ if start_with_ansi_code? str
39
+ print str
40
+ else
41
+ if i >= start
42
+ print str[0..(stop - i - 1)]
43
+ elsif i < start && i + str.size >= start
44
+ print str[(start - i), stop - start - 1]
45
+ end
46
+
47
+ i += str.size
48
+ break if i >= stop
49
+ end
50
+ end
51
+ end
52
+
53
+ # Clears the current line and resets the select graphics settings.
54
+ def reset_line
55
+ print "\r\e[0m\e[0K"
56
+ end
57
+
58
+ # Clears the screen and moves the cursor to the top-left corner.
59
+ def clear_screen
60
+ print "\e[2J"
61
+ go_to(1, 1)
62
+ end
63
+
64
+ # Moves the cursor to (x, y), where x is the colmun and y the line
65
+ # number. Both are 1-indexed. The origin is the top-left corner.
66
+ #
67
+ # @param [Integer] x
68
+ # @param [Integer] y
69
+ def go_to(x, y)
70
+ print "\e[#{y};#{x}H"
71
+ end
72
+
73
+ # Moves the cursor to the given (1-indexed) column number.
74
+ def go_to_col(x)
75
+ print "\e[#{x}G"
76
+ end
77
+
78
+ # Resets the current ansi color codes.
79
+ def reset_color
80
+ print "\e[0m"
81
+ end
82
+
83
+ # Erases the current line.
84
+ def erase_line
85
+ print "\e[0K"
86
+ end
87
+
88
+ # Moves to the beginning of the next line.
89
+ def go_to_next_line
90
+ print "\e[E"
91
+ end
92
+
93
+ # Moves to the beginning of the previous line.
94
+ def go_to_previous_line
95
+ print "\e[F"
96
+ end
97
+ end
98
+ end
@@ -1,6 +1,8 @@
1
1
  require 'io/console'
2
2
 
3
3
  class Coolline
4
+ include ANSI
5
+
4
6
  if config_home = ENV["XDG_CONFIG_HOME"] and !config_home.empty?
5
7
  ConfigDir = File.join(config_home, "coolline")
6
8
  else
@@ -22,8 +24,6 @@ class Coolline
22
24
  "/dev/null"
23
25
  end
24
26
 
25
- AnsiCode = %r{(\e\[\??\d+(?:[;\d]*)\w)}
26
-
27
27
  # @return [Hash] All the defaults settings
28
28
  Settings = {
29
29
  :word_boundaries => [" ", "-", "_"],
@@ -34,7 +34,7 @@ class Coolline
34
34
  Handler.new(?\C-a, &:beginning_of_line),
35
35
  Handler.new(?\C-e, &:end_of_line),
36
36
  Handler.new(?\C-k, &:kill_line),
37
- Handler.new(?\C-u, &:clear_line),
37
+ Handler.new(?\C-u, &:kill_beginning_of_line),
38
38
  Handler.new(?\C-f, &:forward_char),
39
39
  Handler.new(?\C-b, &:backward_char),
40
40
  Handler.new(?\C-d, &:kill_current_char_or_leave),
@@ -73,7 +73,7 @@ class Coolline
73
73
  ],
74
74
 
75
75
  :unknown_char_proc => :insert_string.to_proc,
76
- :transform_proc => :line.to_proc,
76
+ :transform_proc => proc { |line| line },
77
77
  :completion_proc => proc { |cool| [] },
78
78
 
79
79
  :history_file => HistoryFile,
@@ -127,6 +127,8 @@ class Coolline
127
127
  yield self if block_given?
128
128
 
129
129
  @history ||= History.new(@history_file, @history_size)
130
+
131
+ @menu = Menu.new(@input, @output)
130
132
  end
131
133
 
132
134
  # @return [IO]
@@ -173,6 +175,9 @@ class Coolline
173
175
  # @return [String] Current prompt
174
176
  attr_accessor :prompt
175
177
 
178
+ # @return [Menu]
179
+ attr_accessor :menu
180
+
176
181
  # Reads a line from the terminal
177
182
  # @param [String] prompt Characters to print before each line
178
183
  def readline(prompt = ">> ")
@@ -188,13 +193,15 @@ class Coolline
188
193
 
189
194
  @should_exit = false
190
195
 
191
- print "\r\e[0m\e[0K"
196
+ reset_line
192
197
  print @prompt
193
198
 
194
199
  @history.index = @history.size - 1
195
200
  @history << @line
196
201
 
197
202
  until (char = @input.getch) == "\r"
203
+ @menu.erase
204
+
198
205
  handle(char)
199
206
  return if @should_exit
200
207
 
@@ -202,49 +209,11 @@ class Coolline
202
209
  @history_moved = false
203
210
  end
204
211
 
205
- width = @input.winsize[1]
206
- prompt_size = strip_ansi_codes(@prompt).size
207
- line = transform(@line)
208
-
209
- stripped_line_width = strip_ansi_codes(line).size
210
- line << " " * [width - stripped_line_width - prompt_size, 0].max
211
-
212
- # reset the color, and kill the line
213
- print "\r\e[0m\e[0K"
214
-
215
- if strip_ansi_codes(@prompt + line).size <= width
216
- print @prompt + line
217
- print "\e[#{prompt_size + @pos + 1}G"
218
- else
219
- print @prompt
220
-
221
- left_width = width - prompt_size
222
-
223
- start_index = [@pos - left_width + 1, 0].max
224
- end_index = start_index + left_width - 1
225
-
226
- i = 0
227
- line.split(AnsiCode).each do |str|
228
- if start_with_ansi_code? str
229
- # always print ansi codes to ensure the color is right
230
- print str
231
- else
232
- if i >= start_index
233
- print str[0..(end_index - i)]
234
- elsif i < start_index && i + str.size >= start_index
235
- print str[(start_index - i), left_width]
236
- end
237
-
238
- i += str.size
239
- break if i >= end_index
240
- end
241
- end
242
- if @pos < left_width + 1
243
- print "\e[#{prompt_size + @pos + 1}G"
244
- end
245
- end
212
+ render
246
213
  end
247
214
 
215
+ @menu.erase
216
+
248
217
  print "\n"
249
218
 
250
219
  @history[-1] = @line if @history.size != 0
@@ -252,7 +221,35 @@ class Coolline
252
221
 
253
222
  @history.save_line
254
223
 
255
- @line + "\n"
224
+ @line
225
+ end
226
+
227
+ # Displays the current code on the terminal
228
+ def render
229
+ width = @input.winsize[1]
230
+ prompt_size = ansi_length(@prompt)
231
+ line = transform(@line)
232
+
233
+ stripped_line_width = ansi_length(line)
234
+ line += " " * [width - stripped_line_width - prompt_size, 0].max
235
+
236
+ reset_line
237
+
238
+ if ansi_length(@prompt + line) <= width
239
+ print @prompt + line
240
+ else
241
+ print @prompt
242
+
243
+ left_width = width - prompt_size
244
+
245
+ start_index = [@pos - left_width + 1, 0].max
246
+ end_index = start_index + left_width
247
+
248
+ ansi_print(line, start_index, end_index)
249
+ end
250
+
251
+ @menu.display
252
+ go_to_col [prompt_size + @pos + 1, width].min
256
253
  end
257
254
 
258
255
  # Reads a line with no prompt
@@ -367,40 +364,40 @@ class Coolline
367
364
  line[word_beginning_before(pos)...pos]
368
365
  end
369
366
 
370
- # Tries to complete the current word
371
- def complete
372
- return if word_boundary? line[pos - 1]
373
-
374
- completions = @completion_proc.call(self)
375
- return if completions.empty?
367
+ # Sets the word at point, and moves the cursor to after the end of said word.
368
+ def completed_word=(string)
369
+ beg = word_beginning_before(pos)
370
+ line[beg...pos] = string
371
+ self.pos = beg + string.size
372
+ end
376
373
 
377
- result = completions.inject do |common, el|
374
+ # @param [Array<String>] candidates
375
+ # @return [String] The common part between all completion candidates
376
+ def common_beginning(candidates)
377
+ candidates.inject do |common, el|
378
378
  i = 0
379
379
  i += 1 while common[i] == el[i]
380
380
 
381
381
  el[0...i]
382
382
  end
383
-
384
- beg = word_beginning_before(pos)
385
- line[beg...pos] = result
386
- self.pos = beg + result.size
387
383
  end
388
384
 
389
- def word_boundary?(char)
390
- char =~ word_boundaries_regexp
391
- end
385
+ # Tries to complete the current word
386
+ def complete
387
+ return if word_boundary? line[pos - 1]
392
388
 
393
- def strip_ansi_codes(string)
394
- string.gsub(AnsiCode, "")
395
- end
389
+ completions = @completion_proc.call(self)
396
390
 
397
- def start_with_ansi_code?(string)
398
- (string =~ AnsiCode) == 0
391
+ if completions.empty?
392
+ menu.string = "(No completions found)"
393
+ else
394
+ menu.list = completions
395
+ self.completed_word = common_beginning(completions)
396
+ end
399
397
  end
400
398
 
401
- def clear_screen
402
- print "\e[2J" # clear
403
- print "\e[0;0H" # goto 0, 0
399
+ def word_boundary?(char)
400
+ char =~ word_boundaries_regexp
404
401
  end
405
402
 
406
403
  private
@@ -128,6 +128,12 @@ class Coolline
128
128
  line[pos..-1] = ""
129
129
  end
130
130
 
131
+ # Removes everything up to the current character
132
+ def kill_beginning_of_line
133
+ line[0...pos] = ""
134
+ self.pos = 0
135
+ end
136
+
131
137
  # Removes all the characters in the line
132
138
  def clear_line
133
139
  line.clear
@@ -0,0 +1,107 @@
1
+ class Coolline
2
+ # A menu allows to display some information on a rectangle below the cursor.
3
+ #
4
+ # It displays a string or a list of strings until the user presses another
5
+ # key.
6
+ class Menu
7
+ include ANSI
8
+
9
+ def initialize(input, output)
10
+ @input = input
11
+ @output = output
12
+
13
+ @string = ""
14
+
15
+ @last_line_count = 0
16
+ end
17
+
18
+ # @return [String] Information to be displayed
19
+ attr_accessor :string
20
+
21
+ # Sets the menu's string to a list of items, formatted in columns.
22
+ #
23
+ # @param [Array<String>] items
24
+ def list=(items)
25
+ if items.empty?
26
+ self.string = ""
27
+ else
28
+ height, width = @input.winsize
29
+
30
+ col_width = items.map { |s| ansi_length(s) }.max
31
+ col_count = width / col_width
32
+ item_count = col_count * (height - 1)
33
+
34
+ self.string = format_columns(items[0, item_count], col_count, col_width)
35
+ end
36
+ end
37
+
38
+ # Renders the menu below the current line.
39
+ #
40
+ # This will ensure not to draw to much, so that the line currently being
41
+ # edited will always stay visible.
42
+ #
43
+ # Once the menu is drawn, the cursor returns to the correct line.
44
+ def display
45
+ # An empty string shouldn't be treated like a 1-line string.
46
+ return if @string.empty?
47
+
48
+ height, width = @input.winsize
49
+
50
+ lines = @string.lines.to_a
51
+
52
+ lines[0, height - 1].each do |line|
53
+ print "\n\r"
54
+ ansi_print(line.chomp, 0, width)
55
+ end
56
+ reset_color
57
+
58
+ [lines.size, height].min.times { go_to_previous_line }
59
+
60
+ @last_line_count = [height - 1, lines.size].min
61
+ end
62
+
63
+ # Erases anything that had been written by the menu.
64
+ #
65
+ # This allows to hide the menu once it is no longer relevant.
66
+ #
67
+ # Notice it can only work by knowing how many lines the menu drew on the
68
+ # screen last time it was called, and assuming the terminal size didn't
69
+ # change.
70
+ def erase
71
+ return if @last_line_count.zero?
72
+
73
+ self.string = ""
74
+
75
+ @last_line_count.times do
76
+ go_to_next_line
77
+ erase_line
78
+ end
79
+ reset_color
80
+
81
+ @last_line_count.times { go_to_previous_line }
82
+
83
+ @last_line_count = 0
84
+ end
85
+
86
+ private
87
+
88
+ # @param [Array<String>] items Items to show
89
+ # @param [Integer] col_count Amount of columns to show
90
+ # @param [Integer] col_width Width of each column
91
+ #
92
+ # @return [String] Items formatted appropriately
93
+ def format_columns(items, col_count, col_width)
94
+ string = ""
95
+
96
+ items.each_slice(col_count) do |line|
97
+ string << line.map { |s| s.ljust(col_width - 1) } * " " << "\n"
98
+ end
99
+
100
+ string
101
+ end
102
+
103
+ def print(*objs)
104
+ @output.print(*objs)
105
+ end
106
+ end
107
+ end
@@ -1,3 +1,3 @@
1
1
  class Coolline
2
- Version = "0.3.0"
2
+ Version = "0.4.0"
3
3
  end
data/repl.rb CHANGED
@@ -5,7 +5,7 @@ require 'coderay'
5
5
  require 'pp'
6
6
 
7
7
  Coolline.bind "\C-z" do |c|
8
- puts c.object_id
8
+ c.menu.string = "Coolline object id: #{c.object_id}"
9
9
  end
10
10
 
11
11
  cool = Coolline.new do |c|
@@ -63,6 +63,13 @@ context "editor of an empty line" do
63
63
  asserts(:line).equals ""
64
64
  end
65
65
 
66
+ context "after killing the beginning of the line" do
67
+ hookup { topic.kill_beginning_of_line }
68
+
69
+ asserts(:pos).equals 0
70
+ asserts(:line).equals ""
71
+ end
72
+
66
73
  context "after transposing words" do
67
74
  hookup { topic.transpose_words }
68
75
 
@@ -142,6 +149,13 @@ context "editor before many words" do
142
149
  asserts(:line).equals ""
143
150
  end
144
151
 
152
+ context "after killing the beginning of the line" do
153
+ hookup { topic.kill_beginning_of_line }
154
+
155
+ asserts(:pos).equals 0
156
+ asserts(:line).equals "a lovely dragon"
157
+ end
158
+
145
159
  context "after transposing words" do
146
160
  hookup { topic.transpose_words }
147
161
 
@@ -216,6 +230,13 @@ context "editor between two words" do
216
230
  asserts(:line).equals "foo "
217
231
  end
218
232
 
233
+ context "after killing the beginning of the line" do
234
+ hookup { topic.kill_beginning_of_line }
235
+
236
+ asserts(:pos).equals 0
237
+ asserts(:line).equals " bar"
238
+ end
239
+
219
240
  context "after transposing words" do
220
241
  hookup { topic.transpose_words }
221
242
 
@@ -318,6 +339,13 @@ context "editor between two out of three words" do
318
339
  asserts(:line).equals "foo bar"
319
340
  end
320
341
 
342
+ context "after killing the beginning of the line" do
343
+ hookup { topic.kill_beginning_of_line }
344
+
345
+ asserts(:pos).equals 0
346
+ asserts(:line).equals " baz"
347
+ end
348
+
321
349
  context "after transposing words" do
322
350
  hookup { topic.transpose_words }
323
351
 
@@ -425,6 +453,13 @@ context "editor at the end of a line" do
425
453
  asserts(:line).equals "a lovely dragon"
426
454
  end
427
455
 
456
+ context "after killing the beginning of the line" do
457
+ hookup { topic.kill_beginning_of_line }
458
+
459
+ asserts(:pos).equals 0
460
+ asserts(:line).equals ""
461
+ end
462
+
428
463
  context "after transposing words" do
429
464
  hookup { topic.transpose_words }
430
465
 
@@ -537,6 +572,13 @@ context "editor in the middle of a sentence" do
537
572
  asserts(:line).equals "foo bar "
538
573
  end
539
574
 
575
+ context "after killing the beginning of the line" do
576
+ hookup { topic.kill_beginning_of_line }
577
+
578
+ asserts(:pos).equals 0
579
+ asserts(:line).equals "baz qux"
580
+ end
581
+
540
582
  context "after transposing words" do
541
583
  hookup { topic.transpose_words }
542
584
 
@@ -649,6 +691,13 @@ context "editor in the middle of a word" do
649
691
  asserts(:line).equals "foo bar b"
650
692
  end
651
693
 
694
+ context "after killing the beginning of the line" do
695
+ hookup { topic.kill_beginning_of_line }
696
+
697
+ asserts(:pos).equals 0
698
+ asserts(:line).equals "az qux"
699
+ end
700
+
652
701
  context "after transposing words" do
653
702
  hookup { topic.transpose_words }
654
703
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: coolline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-11 00:00:00.000000000 Z
12
+ date: 2012-12-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: riot
16
- requirement: &18648680 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,7 +21,12 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *18648680
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
25
30
  description: ! 'A readline-like library that allows to change how the input
26
31
 
27
32
  is displayed.
@@ -32,17 +37,19 @@ executables: []
32
37
  extensions: []
33
38
  extra_rdoc_files: []
34
39
  files:
40
+ - lib/coolline/editor.rb
41
+ - lib/coolline/ansi.rb
42
+ - lib/coolline/version.rb
43
+ - lib/coolline/menu.rb
35
44
  - lib/coolline/handler.rb
36
45
  - lib/coolline/history.rb
37
- - lib/coolline/editor.rb
38
46
  - lib/coolline/coolline.rb
39
- - lib/coolline/version.rb
40
47
  - lib/coolline.rb
41
48
  - test/helpers.rb
42
- - test/editor_test.rb
43
49
  - test/coolline_test.rb
44
- - test/run_all.rb
45
50
  - test/history_test.rb
51
+ - test/editor_test.rb
52
+ - test/run_all.rb
46
53
  - repl.rb
47
54
  - .gemtest
48
55
  homepage: http://github.com/Mon-Ouie/coolline
@@ -65,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
72
  version: '0'
66
73
  requirements: []
67
74
  rubyforge_project:
68
- rubygems_version: 1.8.10
75
+ rubygems_version: 1.8.23
69
76
  signing_key:
70
77
  specification_version: 3
71
78
  summary: Sounds like readline, smells like readline, but isn't readline