coolline 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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