coolline 0.0.1pre

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