coolline 0.0.1pre

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.
data/.gemtest ADDED
File without changes
data/lib/coolline.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'coolline/handler'
2
+ require 'coolline/history'
3
+ require 'coolline/editor'
4
+
5
+ require 'coolline/coolline'
6
+
7
+ require 'coolline/version'
@@ -0,0 +1,369 @@
1
+ require 'io/console'
2
+
3
+ class Coolline
4
+ if ENV["XDG_CONFIG_HOME"]
5
+ ConfigDir = ENV["XDG_CONFIG_HOME"]
6
+ ConfigFile = File.join(ConfigDir, "coolline.rb")
7
+ else
8
+ ConfigDir = ENV["HOME"]
9
+ ConfigFile = File.join(ConfigDir, ".coolline.rb")
10
+ end
11
+
12
+ HistoryFile = File.join(ConfigDir, ".coolline-history")
13
+
14
+ NullFile = "/dev/null"
15
+
16
+ AnsiCode = %r{(\e\[\??\d+(?:;\d+)?\w)}
17
+
18
+ # @return [Hash] All the defaults settings
19
+ Settings = {
20
+ :word_boundaries => [" ", "-", "_"],
21
+
22
+ :handlers =>
23
+ [
24
+ Handler.new(/\A(?:\C-h|\x7F)\z/, &:kill_backward_char),
25
+ Handler.new("\C-a", &:beginning_of_line),
26
+ Handler.new("\C-e", &:end_of_line),
27
+ Handler.new("\C-k", &:kill_line),
28
+ Handler.new("\C-f", &:forward_char),
29
+ Handler.new("\C-b", &:backward_char),
30
+ Handler.new("\C-d", &:kill_current_char),
31
+ Handler.new("\C-c") { Process.kill(:INT, Process.pid) },
32
+ Handler.new("\C-w", &:kill_backward_word),
33
+ Handler.new("\C-t", &:transpose_chars),
34
+ Handler.new("\C-n", &:next_history_line),
35
+ Handler.new("\C-p", &:previous_history_line),
36
+ Handler.new("\C-r", &:interactive_search),
37
+ Handler.new("\t", &:complete),
38
+ Handler.new("\C-a".."\C-z") {},
39
+
40
+ Handler.new(/\A\e(?:\C-h|\x7F)\z/, &:kill_backward_word),
41
+ Handler.new("\eb", &:backward_word),
42
+ Handler.new("\ef", &:forward_word),
43
+ Handler.new("\e[A", &:previous_history_line),
44
+ Handler.new("\e[B", &:next_history_line),
45
+ Handler.new("\e[5~", &:previous_history_line),
46
+ Handler.new("\e[6~", &:next_history_line),
47
+ Handler.new("\e[C", &:forward_char),
48
+ Handler.new("\e[D", &:backward_char),
49
+ Handler.new("\et", &:transpose_words),
50
+ Handler.new("\ec", &:capitalize_word),
51
+ Handler.new("\eu", &:uppercase_word),
52
+ Handler.new("\el", &:lowercase_word),
53
+
54
+ Handler.new(/\e.+/) {},
55
+ ],
56
+
57
+ :unknown_char_proc => :insert_string.to_proc,
58
+ :transform_proc => :line.to_proc,
59
+ :completion_proc => proc { |cool| [] },
60
+
61
+ :history_file => HistoryFile,
62
+ :history_size => 5000,
63
+ }
64
+
65
+ include Coolline::Editor
66
+
67
+ @config_loaded = false
68
+
69
+ # Loads the config, even if it has already been loaded
70
+ def self.load_config!
71
+ if File.exist? ConfigFile
72
+ load ConfigFile
73
+ end
74
+
75
+ @config_loaded = true
76
+ end
77
+
78
+ # Loads the config, unless it has already been loaded
79
+ def self.load_config
80
+ load_config! unless @config_loaded
81
+ end
82
+
83
+ # Creates a new cool line.
84
+ #
85
+ # @yieldparam [Coolline] self
86
+ def initialize
87
+ self.class.load_config
88
+
89
+ @input = STDIN # must be the actual IO object
90
+ @output = $stdout
91
+
92
+ self.word_boundaries = Settings[:word_boundaries].dup
93
+ self.handlers = Settings[:handlers].dup
94
+ self.transform_proc = Settings[:transform_proc]
95
+ self.unknown_char_proc = Settings[:unknown_char_proc]
96
+ self.completion_proc = Settings[:completion_proc]
97
+ self.history_file = Settings[:history_file]
98
+ self.history_size = Settings[:history_size]
99
+
100
+ yield self if block_given?
101
+
102
+ @history ||= History.new(@history_file, @history_size)
103
+ end
104
+
105
+ # @return [IO]
106
+ attr_accessor :input, :output
107
+
108
+ # @return [Array<String, Regexp>] Expressions detected as word boundaries
109
+ attr_reader :word_boundaries
110
+
111
+ # @return [Regexp] Regular expression to match word boundaries
112
+ attr_reader :word_boundaries_regexp
113
+
114
+ def word_boundaries=(array)
115
+ @word_boundaries = array
116
+ @word_boundaries_regexp = /\A#{Regexp.union(*array)}\z/
117
+ end
118
+
119
+ # @return [Proc] Proc called to change the way a line is displayed
120
+ attr_accessor :transform_proc
121
+
122
+ # @return [Proc] Proc called to handle unmatched characters
123
+ attr_accessor :unknown_char_proc
124
+
125
+ # @return [Proc] Proc called to retrieve completions
126
+ attr_accessor :completion_proc
127
+
128
+ # @return [Array<Handler>]
129
+ attr_accessor :handlers
130
+
131
+ # @return [String] Name of the file containing history
132
+ attr_accessor :history_file
133
+
134
+ # @return [Integer] Size of the history
135
+ attr_accessor :history_size
136
+
137
+ # @return [History] History object
138
+ attr_accessor :history
139
+
140
+ # @return [String] Current line
141
+ attr_reader :line
142
+
143
+ # @return [Integer] Cursor position
144
+ attr_accessor :pos
145
+
146
+ # @return [String] Current prompt
147
+ attr_accessor :prompt
148
+
149
+ # Reads a line from the terminal
150
+ # @param [String] prompt Characters to print before each line
151
+ def readline(prompt = ">> ")
152
+ @prompt = prompt
153
+
154
+ @line = ""
155
+ @pos = 0
156
+ @accumulator = nil
157
+
158
+ @history_index = @history.size
159
+ @history_moved = false
160
+
161
+ print "\r\e[0m\e[0K"
162
+ print @prompt
163
+
164
+ until (char = @input.getch) == "\r"
165
+ handle(char)
166
+
167
+ if @history_moved
168
+ @history_moved = false
169
+ else
170
+ @history_index = @history.size
171
+ end
172
+
173
+ width = @input.winsize[1]
174
+ prompt_size = strip_ansi_codes(@prompt).size
175
+ line = transform(@line)
176
+
177
+ stripped_line_width = strip_ansi_codes(line).size
178
+ line << " " * [width - stripped_line_width - prompt_size, 0].max
179
+
180
+ # reset the color, and kill the line
181
+ print "\r\e[0m\e[0K"
182
+
183
+ if strip_ansi_codes(@prompt + line).size <= width
184
+ print @prompt + line
185
+ print "\e[#{prompt_size + @pos + 1}G"
186
+ else
187
+ print @prompt
188
+
189
+ left_width = width - prompt_size
190
+
191
+ start_index = [@pos - left_width + 1, 0].max
192
+ end_index = start_index + left_width - 1
193
+
194
+ i = 0
195
+ line.split(AnsiCode).each do |str|
196
+ if start_with_ansi_code? str
197
+ # always print ansi codes to ensure the color is right
198
+ print str
199
+ else
200
+ if i >= start_index
201
+ print str[0..(end_index - i)]
202
+ elsif i < start_index && i + str.size >= start_index
203
+ print str[(start_index - i), left_width]
204
+ end
205
+
206
+ i += str.size
207
+ break if i >= end_index
208
+ end
209
+ end
210
+
211
+ if @pos < left_width + 1
212
+ print "\e[#{prompt_size + @pos + 1}G"
213
+ end
214
+ end
215
+ end
216
+
217
+ print "\n"
218
+
219
+ @history << @line
220
+
221
+ @line + "\n"
222
+ end
223
+
224
+ # Reads a line with no prompt
225
+ def gets
226
+ readline ""
227
+ end
228
+
229
+ # Prints objects to the output.
230
+ def print(*objs)
231
+ @output.print(*objs)
232
+ end
233
+
234
+ # Selects the previous line in history (if any)
235
+ def previous_history_line
236
+ if @history_index - 1 >= 0
237
+ @line.replace @history[@history_index - 1]
238
+ @pos = [@line.size, @pos].min
239
+
240
+ @history_index -= 1
241
+ end
242
+
243
+ @history_moved = true
244
+ end
245
+
246
+ # Selects the next line in history (if any).
247
+ #
248
+ # When on the last line, this method replaces the current line with an empty
249
+ # string.
250
+ def next_history_line
251
+ if @history_index + 1 <= @history.size
252
+ @line.replace @history[@history_index + 1] || ""
253
+ @pos = [@line.size, @pos].min
254
+
255
+ @history_index += 1
256
+ end
257
+
258
+ @history_moved = true
259
+ end
260
+
261
+ # Prompts the user to search for a line
262
+ def interactive_search
263
+ initial_index = @history_index
264
+ found_index = @history_index
265
+
266
+ # Use another coolline instance for the search! :D
267
+ Coolline.new { |c|
268
+ # Remove the search handler (to avoid nesting confusion)
269
+ c.handlers.delete_if { |h| h.char == "\C-r" }
270
+
271
+ # search line
272
+ c.transform_proc = proc do
273
+ pattern = Regexp.new Regexp.escape(c.line)
274
+
275
+ line, found_index = @history.search(pattern, @history_index).first
276
+
277
+ if line
278
+ "#{c.line}): #{line}"
279
+ else
280
+ "#{c.line}): [pattern not found]"
281
+ end
282
+ end
283
+
284
+ # Disable history
285
+ c.history_file = NullFile
286
+ c.history_size = 0
287
+ }.readline("(search:")
288
+
289
+ @line.replace @history[found_index]
290
+ @pos = [@line.size, @pos].min
291
+
292
+ @history_index = found_index
293
+ @history_moved = true
294
+ end
295
+
296
+ # @return [String] The string to be completed (useful in the completion proc)
297
+ def completed_word
298
+ line[word_beginning_before(pos)...pos]
299
+ end
300
+
301
+ # Tries to complete the current word
302
+ def complete
303
+ return if word_boundary? line[pos - 1]
304
+
305
+ completions = @completion_proc.call(self)
306
+ return if completions.empty?
307
+
308
+ result = completions.inject do |common, el|
309
+ i = 0
310
+ i += 1 while common[i] == el[i]
311
+
312
+ el[0...i]
313
+ end
314
+
315
+ beg = word_beginning_before(pos)
316
+ line[beg...pos] = result
317
+ self.pos = beg + result.size
318
+ end
319
+
320
+ def word_boundary?(char)
321
+ char =~ word_boundaries_regexp
322
+ end
323
+
324
+ def strip_ansi_codes(string)
325
+ string.gsub(AnsiCode, "")
326
+ end
327
+
328
+ def start_with_ansi_code?(string)
329
+ (string =~ AnsiCode) == 0
330
+ end
331
+
332
+ private
333
+ def transform(line)
334
+ @transform_proc.call(line)
335
+ end
336
+
337
+ def handle(char)
338
+ input = if @accumulator
339
+ handle_escape(char)
340
+ elsif char == "\e"
341
+ @accumulator = "\e"
342
+ nil
343
+ else
344
+ char
345
+ end
346
+
347
+ if input
348
+ if handler = @handlers.find { |h| h === input }
349
+ handler.call self
350
+ else
351
+ @unknown_char_proc.call self, char
352
+ end
353
+ end
354
+ end
355
+
356
+ def handle_escape(char)
357
+ if char == "[" && @accumulator =~ /\A\e?\e\z/ or
358
+ char =~ /\d/ && @accumulator =~ /\A\e?\e\[\d*\z/ or
359
+ char == "\e" && @accumulator == "\e"
360
+ @accumulator << char
361
+ nil
362
+ else
363
+ str = @accumulator + char
364
+ @accumulator = nil
365
+
366
+ str
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,227 @@
1
+ class Coolline
2
+ module Editor
3
+ # Inserts a string at the current point in the line
4
+ #
5
+ # @param [String] string String to be inserted
6
+ def insert_string(string)
7
+ line.insert pos, string
8
+ self.pos += string.size
9
+ end
10
+
11
+ def word_boundary_after(pos)
12
+ pos += 1 # don't return initial pos
13
+ pos += 1 until pos >= line.size or word_boundary? line[pos]
14
+ pos >= line.size ? nil : pos
15
+ end
16
+
17
+ def word_boundary_before(pos)
18
+ pos -= 1
19
+ pos -= 1 until pos < 0 or word_boundary? line[pos]
20
+ pos < 0 ? nil : pos
21
+ end
22
+
23
+ def non_word_boundary_after(pos)
24
+ pos += 1
25
+ pos += 1 while pos < line.size and word_boundary? line[pos]
26
+ pos >= line.size ? nil : pos
27
+ end
28
+
29
+ def non_word_boundary_before(pos)
30
+ pos -= 1
31
+ pos -= 1 while pos >= 0 and word_boundary? line[pos]
32
+ pos < 0 ? nil : pos
33
+ end
34
+
35
+ def word_end_before(pos)
36
+ pos -= 1
37
+
38
+ if line[pos] and word_boundary? line[pos]
39
+ non_word_boundary_before(pos) || 0
40
+ else
41
+ pos
42
+ end
43
+ end
44
+
45
+ def word_beginning_before(pos)
46
+ word_end = word_end_before(pos)
47
+
48
+ if first = word_boundary_before(word_end)
49
+ first + 1
50
+ else
51
+ 0
52
+ end
53
+ end
54
+
55
+ def word_beginning_after(pos)
56
+ if line[pos] and word_boundary? line[pos]
57
+ non_word_boundary_after(pos) || line.size
58
+ else
59
+ pos
60
+ end
61
+ end
62
+
63
+ def word_end_after(pos)
64
+ word_beg = word_beginning_after(pos)
65
+
66
+ if first = word_boundary_after(word_beg)
67
+ first - 1
68
+ else
69
+ line.size - 1
70
+ end
71
+ end
72
+
73
+ # Moves the cursor to the beginning of the line
74
+ def beginning_of_line
75
+ self.pos = 0
76
+ end
77
+
78
+ # Moves to the end of the line
79
+ def end_of_line
80
+ self.pos = line.size
81
+ end
82
+
83
+ # Moves to the previous character
84
+ def backward_char
85
+ self.pos -= 1 if pos != 0
86
+ end
87
+
88
+ # Moves to the next character
89
+ def forward_char
90
+ self.pos += 1 if pos != line.size
91
+ end
92
+
93
+ # Moves to the previous word
94
+ def backward_word
95
+ self.pos = word_beginning_before(pos) if pos > 0
96
+ end
97
+
98
+ # Moves to the next word
99
+ def forward_word
100
+ self.pos = word_end_after(pos) + 1 if pos != line.size
101
+ end
102
+
103
+ # Removes the previous word
104
+ def kill_backward_word
105
+ if pos > 0
106
+ beg = word_beginning_before(pos)
107
+
108
+ line[beg...pos] = ""
109
+ self.pos = beg
110
+ end
111
+ end
112
+
113
+ # Removes the previous character
114
+ def kill_backward_char
115
+ if pos > 0
116
+ line[pos - 1] = ""
117
+ self.pos -= 1
118
+ end
119
+ end
120
+
121
+ # Removes the current character
122
+ def kill_current_char
123
+ line[pos] = "" if pos != line.size
124
+ end
125
+
126
+ # Removes all the characters beyond the current point
127
+ def kill_line
128
+ line[pos..-1] = ""
129
+ end
130
+
131
+ # Swaps the previous character with the current one
132
+ def transpose_chars
133
+ if line.size >= 2
134
+ if pos == line.size
135
+ line[pos - 2], line[pos - 1] = line[pos - 1], line[pos - 2]
136
+ else
137
+ line[pos - 1], line[pos] = line[pos], line[pos - 1]
138
+ self.pos += 1
139
+ end
140
+ end
141
+ end
142
+
143
+ # Swaps the current word with the previous word, or the two previous words
144
+ # if we're at the end of the line.
145
+ def transpose_words
146
+ if !non_word_boundary_after(pos)
147
+ last = non_word_boundary_before(pos)
148
+ return unless last
149
+
150
+ last_beg = word_beginning_before(last)
151
+ return unless word_boundary_before(last_beg)
152
+
153
+ whitespace_beg = word_end_before(last_beg)
154
+ return unless non_word_boundary_before(whitespace_beg + 1)
155
+
156
+ first_beg = word_beginning_before(last_beg)
157
+
158
+ line[first_beg..last] = line[last_beg..last] +
159
+ line[(whitespace_beg + 1)...last_beg] +
160
+ line[first_beg..whitespace_beg]
161
+
162
+ self.pos = last + 1
163
+ elsif word_boundary? line[pos] # between two words?
164
+ return unless non_word_boundary_before(pos)
165
+
166
+ last_beg = word_beginning_after(pos)
167
+ last_end = word_end_after(pos)
168
+
169
+ first_end = word_end_before(pos)
170
+ first_beg = word_beginning_before(pos)
171
+
172
+ line[first_beg..last_end] = line[last_beg..last_end] +
173
+ line[(first_end + 1)...last_beg] +
174
+ line[first_beg..first_end]
175
+ self.pos = last_end + 1
176
+ else # within a word?
177
+ return unless non_word_boundary_before(pos)
178
+ return unless word_boundary_before(pos)
179
+
180
+ last_beg = word_beginning_after(pos - 1)
181
+ last_end = word_end_after(pos)
182
+
183
+ return unless non_word_boundary_before(last_beg)
184
+
185
+ first_end = word_end_before(last_beg)
186
+ first_beg = word_beginning_before(last_beg)
187
+
188
+ line[first_beg..last_end] = line[last_beg..last_end] +
189
+ line[(first_end + 1)...last_beg] +
190
+ line[first_beg..first_end]
191
+ self.pos = last_end + 1
192
+ end
193
+ end
194
+
195
+ # Capitalizes the current word
196
+ def capitalize_word
197
+ if beg = word_beginning_after(pos) and last = word_end_after(pos)
198
+ line[beg..last] = line[beg..last].capitalize
199
+ self.pos = last + 1
200
+ end
201
+ end
202
+
203
+ # Lowercases the current word
204
+ def lowercase_word
205
+ if beg = word_beginning_after(pos) and last = word_end_after(pos)
206
+ line[beg..last] = line[beg..last].downcase
207
+ self.pos = last + 1
208
+ end
209
+ end
210
+
211
+ # Uppercases the current word
212
+ def uppercase_word
213
+ if beg = word_beginning_after(pos) and last = word_end_after(pos)
214
+ line[beg..last] = line[beg..last].upcase
215
+ self.pos = last + 1
216
+ end
217
+ end
218
+
219
+ # This method can be overriden to change characters considered as word
220
+ # boundaries.
221
+ #
222
+ # @return [Boolean] True if the string is a word boundary, false otherwise
223
+ def word_boundary?(string)
224
+ string =~ /\A[ \t]+\z/
225
+ end
226
+ end
227
+ end