refined-refinements 0.0.1 → 0.0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 5294d42af35dee68ae9f9d9477d83a39ec3d5de2
4
- data.tar.gz: '056769fd009f3c71f019322a53ff7577b543a585'
2
+ SHA256:
3
+ metadata.gz: 9e237cd63850813d78118fb36f6d6a2af9997aad27efd52a29558364392a4320
4
+ data.tar.gz: acb9d42dd6b27cd7ff457c1dc496e4bf315e86a01ecac2f8fc6e6d2f41e5d0c9
5
5
  SHA512:
6
- metadata.gz: d915c1737d66d960769ba4c72ddbd2022d7b326a5cf2639d46d93c87592335dd19dea33a181d012e914b28e46a4d57e9f12ea289cfb5a8b9f5842872942bc631
7
- data.tar.gz: 86d9e617849bcbc70fdfe70db3630ec6eadc04c1da25ac67965e618eb7c52bac3f318dff45a39381bf58565f996256938241e0b3c44b6d6b326f953ed58d1a6c
6
+ metadata.gz: afc0245977ea79e641818fb28b1415e6876a7da70ca673c46b982c35a3d8946ff3472ac508594966845bf51f87503f643c81616ac8216b891717b8b679e9ad33
7
+ data.tar.gz: bd488952fb4efe31047de84b29c6f222e23b2a36f39c4735802ce77b8b424216f726741eec2fbd04e07c7bc6a0940b693fb57c82b5badeca11cf8167c69b2beb
@@ -0,0 +1,58 @@
1
+ require 'open-uri'
2
+ require 'base64'
3
+ require 'socket' # undefined SocketError otherwise, it's weird.
4
+
5
+ # RR::CachedHttp.cache_dir = 'tmp'
6
+ # # RR::CachedHttp.offline = true
7
+ # RR::CachedHttp.get('http://google.com/test')
8
+ module RR
9
+ module CachedHttp
10
+ def self.cache_dir
11
+ @cache_dir || '/tmp'
12
+ end
13
+
14
+ def self.cache_dir=(cache_dir)
15
+ @cache_dir = cache_dir
16
+ end
17
+
18
+ def self.offline?
19
+ @offline
20
+ end
21
+
22
+ def self.offline=(boolean)
23
+ @offline = boolean
24
+ end
25
+
26
+ def self.cache_path(url)
27
+ File.join(self.cache_dir, Base64.encode64(url).chomp.split("\n").last)
28
+ end
29
+
30
+ def self.get(url)
31
+ if self.offline?
32
+ self.retrieve_from_cache(url)
33
+ else
34
+ self.fetch(url)
35
+ end
36
+ rescue SocketError
37
+ self.retrieve_from_cache(url, true)
38
+ rescue Exception => e
39
+ p [:e, e]; raise e
40
+ end
41
+
42
+ def self.retrieve_from_cache(url, tried_to_fetch = false)
43
+ if File.exist?(self.cache_path(url))
44
+ File.read(self.cache_path(url))
45
+ else
46
+ raise "URL #{url} is not cached#{" and can't be fetched" if tried_to_fetch}."
47
+ end
48
+ end
49
+
50
+ def self.fetch(url)
51
+ open(url).read.tap do |fetched_data|
52
+ File.open(self.cache_path(url), 'w') do |file|
53
+ file.write(fetched_data)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,57 @@
1
+ require 'refined-refinements/string'
2
+ require 'refined-refinements/colours'
3
+
4
+ module RR
5
+ class Commander
6
+ using RR::ColourExts
7
+
8
+ def self.commands
9
+ @commands ||= Hash.new
10
+ end
11
+
12
+ def self.command(command_name, command_class)
13
+ self.commands[command_name] = command_class
14
+ end
15
+
16
+ def help_template(program_name)
17
+ <<-EOF
18
+ <red.bold>:: #{program_name} ::</red.bold>
19
+
20
+ <cyan.bold>Commands</cyan.bold>
21
+ EOF
22
+ end
23
+
24
+ def help
25
+ self.commands.reduce(self.help_template) do |buffer, (command_name, command_class)|
26
+ command_help = command_class.help && command_class.help.split("\n").map { |line| line.sub(/^ {4}/, '') }.join("\n")
27
+ command_class.help ? [buffer, command_help].join("\n") : buffer
28
+ end.colourise
29
+ end
30
+
31
+ def commands
32
+ self.class.commands
33
+ end
34
+
35
+ def run(command_name, args)
36
+ command_class = self.class.commands[command_name]
37
+ command = command_class.new(args)
38
+ command.run
39
+ end
40
+ end
41
+
42
+ class Command
43
+ using RR::ColourExts
44
+ using RR::StringExts # #titlecase
45
+
46
+ class << self
47
+ attr_accessor :help
48
+ def main_command
49
+ File.basename($0)
50
+ end
51
+ end
52
+
53
+ def initialize(args)
54
+ @args = args
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,140 @@
1
+ # def prompt_type
2
+ # @prompt.prompt(:type, 'Type', options: Expense::TYPES) do
3
+ # clean_value do |raw_value|
4
+ # self.retrieve_by_index_or_self_if_on_the_list(Expense::TYPES, raw_value)
5
+ # end
6
+ #
7
+ # validate_clean_value do |clean_value|
8
+ # Expense::TYPES.include?(clean_value)
9
+ # end
10
+ # end
11
+ # end
12
+
13
+ require 'refined-refinements/colours'
14
+
15
+ module RR
16
+ class InvalidResponse < StandardError; end
17
+
18
+ class Answer
19
+ def initialize
20
+ @callbacks = Hash.new { Proc.new { true } }
21
+ end
22
+
23
+ def validate_raw_value(*regexps, allow_empty: nil)
24
+ @callbacks[:validate_raw_value] = Proc.new do |raw_value|
25
+ unless (allow_empty && raw_value.empty?) || regexps.any? { |regexp| raw_value.match(regexp) }
26
+ raise InvalidResponse.new("Doesn't match any of the regexps.")
27
+ end
28
+ end
29
+ end
30
+
31
+ def validate_clean_value(&block)
32
+ @callbacks[:validate_clean_value] = block
33
+ end
34
+
35
+ def clean_value(&block)
36
+ @callbacks[:get_clean_value] = block
37
+ end
38
+
39
+ def run(raw_value)
40
+ @callbacks[:validate_raw_value].call(raw_value)
41
+ clean_value = @callbacks[:get_clean_value].call(raw_value)
42
+
43
+ unless @callbacks[:validate_clean_value].call(clean_value)
44
+ raise InvalidResponse.new("validate_clean_value failed")
45
+ end
46
+
47
+ clean_value
48
+ end
49
+
50
+ def self_or_retrieve_by_index(list, raw_value, default_value = nil)
51
+ if raw_value.match(/^\d+$/)
52
+ list[raw_value.to_i - 1]
53
+ elsif raw_value.empty?
54
+ default_value
55
+ else
56
+ raw_value
57
+ end
58
+ end
59
+
60
+ def retrieve_by_index_or_self_if_on_the_list(list, raw_value, default_value = nil)
61
+ if raw_value.match(/^\d+$/)
62
+ list[raw_value.to_i - 1]
63
+ elsif list.include?(raw_value)
64
+ raw_value
65
+ elsif raw_value.empty? && default_value
66
+ default_value
67
+ else
68
+ raise InvalidResponse.new(raw_value)
69
+ end
70
+ end
71
+
72
+ def convert_money_to_cents(raw_value)
73
+ if raw_value.match(/\./)
74
+ raw_value.delete('.').to_i
75
+ else
76
+ "#{raw_value}00".to_i
77
+ end
78
+ end
79
+ end
80
+
81
+ class Prompt
82
+ using RR::ColourExts
83
+
84
+ attr_reader :data
85
+ def initialize(&block)
86
+ @data, @block = Hash.new, block || Proc.new do |prompt|
87
+ require 'readline'
88
+ Readline.readline(prompt, true)
89
+ end
90
+ end
91
+
92
+ # options: ['one', 'two']
93
+ # help: 'One of one, two or any integer value.'
94
+ def prompt(key, prompt_text, **options, &block)
95
+ answer = Answer.new
96
+ answer.instance_eval(&block)
97
+
98
+ help = help(**options)
99
+ prompt = "<bold>#{prompt_text}</bold>#{" (#{help})" if help}: ".colourise
100
+ @data[key] = answer.run(@block.call(prompt))
101
+ raise InvalidResponse.new if @data[key].nil? && options[:required]
102
+ @data[key]
103
+ rescue InvalidResponse => error
104
+ puts "<red>Invalid response</red> (#{error.message}), try again.".colourise
105
+ retry
106
+ end
107
+
108
+ # currency_help = " (#{self.show_label_for_self_or_retrieve_by_index(currencies)})" unless currencies.empty?
109
+ # print "Currency#{currency_help}: "
110
+ # expense_data[:currency] = self.self_or_retrieve_by_index(currencies, STDIN.readline.chomp, 'EUR')
111
+ # TODO: Say that it defaults to EUR.
112
+ # TODO: raw_value = 'EUR' vs. default: 'EUR' ???
113
+ # TODO: If it evals as nil, shall we still add it to the data?
114
+ def help(help: nil, options: Array.new, default: nil, **rest)
115
+ if help then help
116
+ elsif help.nil? && ! options.empty?
117
+ options = options.map.with_index { |key, index|
118
+ if default == key
119
+ "<green.bold>#{key}</green.bold> <bright_black>default</bright_black>"
120
+ else
121
+ "<green>#{key}</green> <magenta>#{index + 1}</magenta>"
122
+ end
123
+ }.join(' ').colourise
124
+ # default ? "#{options}; defaults to #{default}" : options
125
+ else end
126
+ end
127
+
128
+ def set_completion_proc(proc, character = ' ', &block)
129
+ return block.call unless defined?(::Readline)
130
+ original_append_character = Readline.completion_append_character
131
+ Readline.completion_append_character = ' '
132
+ Readline.completion_proc = proc
133
+ block.call
134
+ ensure
135
+ return unless defined?(::Readline)
136
+ Readline.completion_proc = nil
137
+ Readline.completion_append_character = original_append_character
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,150 @@
1
+ require 'yaml'
2
+ require 'forwardable'
3
+ require 'pathname'
4
+
5
+ # flashcards = Collection.new(Flashcard, 'es.yml')
6
+ # flashcards << flashcard
7
+ # flashcards.save
8
+ module RR
9
+ class Collection
10
+ def self.data_file_dir
11
+ Pathname.new("~/Dropbox/Data/Data/Flashcards").expand_path
12
+ end
13
+
14
+ # def initialize(item_class, basename)
15
+ # @path = self.class.data_file_dir.join("#{basename}.yml")
16
+ # @item_class, @activity_filters = item_class, Hash.new
17
+ # end
18
+ def initialize(basename)
19
+ @path = self.class.data_file_dir.join(basename)
20
+ @activity_filters = Hash.new
21
+ end
22
+
23
+ def items(&block)
24
+ @items ||= self.load_raw_collection.map do |data|
25
+ begin
26
+ block.call(data)
27
+ rescue => error
28
+ abort "Loading item #{data.inspect} failed: #{error.message}.\n\n#{error.backtrace}"
29
+ end
30
+ end
31
+ end
32
+
33
+ def active_items
34
+ return self.items if @activity_filters.empty?
35
+
36
+ @activity_filters.keys.reduce(self.items) do |items, filter_name|
37
+ self.run_filter(filter_name, items)
38
+ end
39
+ end
40
+
41
+ def run_filter(filter_name, items)
42
+ items.select { |item| @activity_filters[filter_name].call(item) }
43
+ end
44
+
45
+ def filter(filter_name, &block)
46
+ @activity_filters[filter_name] = block if block
47
+ self
48
+ end
49
+
50
+ def filter_out(filter_name, &block)
51
+ @activity_filters[filter_name] = Proc.new { |item| ! block.call(item) } if block
52
+ self
53
+ end
54
+
55
+ def filters
56
+ @activity_filters.keys
57
+ end
58
+
59
+ def has_filter?(filter_name)
60
+ @activity_filters.has_key?(filter_name)
61
+ end
62
+
63
+ def filtered_out_items(filter_name)
64
+ self.items - self.run_filter(filter_name, self.items)
65
+ end
66
+
67
+ # flashcards[:expressions, 'hacer']
68
+ # flashcards[:translations, :silent_translations, 'to be']
69
+ def [](key, value)
70
+ self.items.select do |item|
71
+ [item.send(key)].flatten.include?(value)
72
+ end
73
+ end
74
+
75
+ extend Forwardable
76
+
77
+ # Generally we want to avoid proxying methods, unless
78
+ # it is clear whether they would be performed on items or active_items.
79
+ def_delegator :items, :<<
80
+
81
+ def replace(original_item, new_item)
82
+ index = self.items.index(original_item)
83
+ self.items.delete(original_item)
84
+ self.items.insert(index, new_item)
85
+ end
86
+
87
+ def save
88
+ return if self.items.empty?
89
+
90
+ if File.mtime(@path.to_s) > @loaded_at
91
+ raise "Cannot be saved #{File.mtime(@path.to_s).inspect} vs. #{@loaded_at.inspect}"
92
+ end
93
+
94
+ self.save_to(@path) && self.save_to(self.back_up_path)
95
+
96
+ @loaded_at = Time.now # Otherwise the next save call with fail.
97
+ true
98
+ end
99
+
100
+ def save_to(path)
101
+ updated_data = self.serialise
102
+ if (! File.exist?(path)) || File.read(path) != updated_data
103
+ path.open('w') do |file|
104
+ file.write(updated_data)
105
+ end
106
+ return true
107
+ else
108
+ return false
109
+ end
110
+ end
111
+
112
+ def back_up_path
113
+ chunks = @path.basename.to_s.split('.')
114
+ timestamp = Time.now.strftime('%Y-%m-%d-%H-%M')
115
+ basename = chunks.insert(-2, timestamp).join('.')
116
+
117
+ self.class.data_file_dir.join('Backups', basename)
118
+ end
119
+
120
+ # TODO: Should we deprecate this?
121
+ # It's best to use explicit .items/.active_items calls.
122
+ include Enumerable
123
+
124
+ def each(&block)
125
+ if block
126
+ self.active_items.each(&block)
127
+ else
128
+ self.active_items.to_enum
129
+ end
130
+ end
131
+
132
+ protected
133
+ def load_raw_collection
134
+ @loaded_at = Time.now
135
+
136
+ self.deserialise(@path.to_s)
137
+ rescue Errno::ENOENT
138
+ Array.new
139
+ end
140
+
141
+ def deserialise(path)
142
+ # YAML treats an empty string as false.
143
+ YAML.load_file(path) || Array.new
144
+ end
145
+
146
+ def serialise
147
+ self.items.map(&:data).to_yaml
148
+ end
149
+ end
150
+ end
@@ -1,8 +1,15 @@
1
1
  require 'term/ansicolor'
2
+ require 'refined-refinements/string'
2
3
  require 'refined-refinements/matching'
3
4
 
4
5
  module RR
5
6
  module ColourExts
7
+ REGEXP = /
8
+ <(?<dot_separated_methods>[^>]+)>
9
+ (?<text_between_tags>.*?)
10
+ <\/\k<dot_separated_methods>>
11
+ /xm
12
+
6
13
  def self.colours
7
14
  @colours ||= Object.new.extend(Term::ANSIColor)
8
15
  end
@@ -10,33 +17,128 @@ module RR
10
17
  refine String do
11
18
  using RR::MatchingExts
12
19
 
13
- def colourise(options = Hash.new)
14
- regexp = /
15
- <(?<dot_separated_methods>[^>]+)>
16
- (?<text_between_tags>.*?)
17
- <\/\k<dot_separated_methods>>
18
- /xm
19
-
20
+ def parse_colours(options = Hash.new, &block)
20
21
  colours = RR::ColourExts.colours
22
+ # was_called = false
23
+
24
+ if options[:recursed]
25
+ string = self
26
+ else
27
+ # This solves the problem of having blank spots that never get evaluated
28
+ # i. e. in "Hello <bold>world</bold>!" it'd be "Hello " and "!".
29
+ # This is a problem with bold: true or in curses, where the text never
30
+ # get rendered.
31
+ string = options[:bold] ? "<bold>#{self}</bold>" : self
32
+
33
+ # "hey\n" -> wrapping with escape sequences after \n breaks chomp, strip and the likes.
34
+ string.gsub!(/(\s*)<\/bold>/, '</bold>\1')
35
+ end
36
+
37
+ result = string.gsub(RR::ColourExts::REGEXP) do |match|
38
+ # was_called = true
21
39
 
22
- result = self.gsub(regexp) do |match|
23
40
  # p [:m, match] ####
24
41
  methods = match[:dot_separated_methods].split('.').map(&:to_sym)
25
42
  methods.push(:bold) if options[:bold]
26
43
  # p [:i, match[:text_between_tags], methods, options] ####
27
44
 
28
- methods.reduce(inner_text = match[:text_between_tags]) do |result, method|
45
+ inner_text = match[:text_between_tags]
46
+ block.call(inner_text, methods, options)
47
+ end
48
+
49
+ # block.call(result, Array.new, options) #unless was_called
50
+
51
+ result.match(RR::ColourExts::REGEXP) ? result.colourise(options.merge(recursed: true)) : result #block.call(result, [options[:bold] ? :bold : nil].compact, options)
52
+ end
53
+
54
+ # FIXME: bold true doesn't do anything for the text that is not wrapped
55
+ # in anything, only for text that is between tags. I. e. "Hey <green>you</green>!"
56
+ def colourise(options = Hash.new)
57
+ colours = RR::ColourExts.colours
58
+
59
+ self.parse_colours(options) do |inner_text, methods, options|
60
+ methods.reduce(inner_text) do |result, method|
29
61
  # (print ' '; p [:r, result, method]) if result.match(/(<\/[^>]+>)/) ####
30
62
  result.gsub!(/(<\/[^>]+>)/, "#{colours.send(method)}\\1")
31
63
  # puts "#{result.inspect}.#{method}" ####
32
64
  "#{colours.send(method)}#{result}#{colours.reset unless options[:recursed]}"
33
65
  end
34
66
  end
67
+ end
68
+
69
+ # "Hey <green>y<bold>o</bold>u</green>!"
70
+ # 1. "Hey ", []
71
+ # 2. "y", [:green]
72
+ # 3. "o", [:green, :bold]
73
+ # 4. "u", [:green]
74
+ # 5. "!", []
75
+ def _chunks_with_colours(options = Hash.new)
76
+ # p [:call, self, options]
77
+ options[:chunks] ||= Array.new
78
+ was_called = false
79
+
80
+ self.colourise.sub(/\e\[(\d+)m/) do |match, before_match, after_match|
81
+ was_called = true
82
+
83
+ store = Term::ANSIColor::Attribute.instance_variable_get(:@__store__)
84
+ colour_name = store.find { |name, attribute| attribute.code == match[1] }[0]
85
+
86
+ options[:chunks] << before_match unless before_match.empty?
87
+ options[:chunks] << colour_name
88
+
89
+ after_match._chunks_with_colours(options)
90
+ end
91
+
92
+ options[:chunks] << self unless was_called
93
+
94
+ options[:chunks]
95
+ end
96
+
97
+ def chunks_with_colours(options = Hash.new)
98
+ buffer, result = Array.new, Array.new
99
+
100
+ # p [:s, self.colourise]
101
+ self._chunks_with_colours(options).each do |item|
102
+ # p [:x, result, buffer, item]
103
+ if item.is_a?(Symbol)
104
+ buffer << item
105
+ else
106
+ result << [item, buffer.uniq.dup]
107
+ buffer.clear
108
+ end
109
+ end
35
110
 
36
- result.match(regexp) ? result.colourise(options.merge(recursed: true)) : result
111
+ result
37
112
  end
38
113
  end
39
114
  end
115
+
116
+ class TemplateString
117
+ using RR::StringExts
118
+ using RR::ColourExts
119
+
120
+ def initialize(template_string = '')
121
+ @template_string = template_string
122
+ end
123
+
124
+ def length
125
+ self.remove_tags.length
126
+ end
127
+
128
+ def titlecase
129
+ @template_string.sub(/>(#{self.remove_tags[0]})/) do |match|
130
+ ">#{$1.upcase}"
131
+ end
132
+ end
133
+
134
+ def remove_tags
135
+ @template_string.gsub(/<[^>]+>/, '')
136
+ end
137
+
138
+ def to_s
139
+ @template_string.colourise
140
+ end
141
+ end
40
142
  end
41
143
 
42
144
  # using RR::ColourExts
@@ -0,0 +1,196 @@
1
+ require 'curses'
2
+ require 'refined-refinements/curses/colours'
3
+ require 'refined-refinements/curses/commander'
4
+
5
+ using RR::ColourExts
6
+
7
+ class QuitAppError < StandardError
8
+ end
9
+
10
+ class KeyboardInterrupt < StandardError
11
+ attr_reader :key_code
12
+ def initialize(key_code)
13
+ @key_code = key_code
14
+ end
15
+
16
+ def escape?
17
+ @key_code == 27
18
+ end
19
+
20
+ def ctrl_d?
21
+ @key_code == 4
22
+ end
23
+
24
+ def inspect
25
+ "#<#{self.class.name}: @key_code=#{@key_code.inspect}>"
26
+ end
27
+ end
28
+
29
+ class App
30
+ def initialize
31
+ @history = Array.new
32
+ end
33
+
34
+ def run(&block)
35
+ self.set_up
36
+
37
+ loop do
38
+ @current_window = window = Curses::Window.new(Curses.lines, Curses.cols, 0, 0)
39
+ window.keypad = true
40
+ block.call(self, window)
41
+ end
42
+ rescue Interrupt, QuitAppError # Ctrl+C # TODO: Add Ctrl+D into this.
43
+ ensure # TODO: Without this, there's no difference.
44
+ Curses.close_screen
45
+ end
46
+
47
+ def destroy
48
+ raise QuitAppError.new
49
+ end
50
+
51
+ def set_up
52
+ Curses.init_screen
53
+ Curses.start_color
54
+ Curses.nonl # enter is 13
55
+ end
56
+
57
+ # TODO: Ctrl+a, Ctrl+e, Ctrl+k, delete.
58
+ #
59
+ # TODO: unicode handling. Currently #getch is not unicode aware.
60
+ # Each unicode char would trigger #getch twice, with numeric values such as
61
+ # é: 195, 169 or ś: 197, 155.
62
+ def readline(prompt, window = @current_window, &unknown_key_handler)
63
+ Curses.noecho
64
+ window.write(prompt)
65
+
66
+ buffer, cursor, original_x = String.new, 0, window.curx
67
+ char_buffer = []
68
+
69
+ until (char = window.getch) == 13 || @quit
70
+ begin
71
+ if (190..200).include?(char) # Reading unicode.
72
+ char_buffer = [char]
73
+ else
74
+ char_buffer << char
75
+ buffer, cursor = process_chars(char_buffer, buffer, cursor, window, original_x)
76
+ char_buffer.clear
77
+ end
78
+ rescue KeyboardInterrupt => interrupt
79
+ begin
80
+ unknown_key_handler.call(interrupt) if unknown_key_handler
81
+ rescue QuitError
82
+ @quit = true
83
+ end
84
+ end
85
+
86
+ # begin
87
+ # sp = ' ' * (window.maxx - window.curx - buffer.length - prompt.gsub(/<[^>]+>/, '').length)
88
+ # rescue
89
+ # sp = ' ERR ' # FIXME.
90
+ # end
91
+
92
+ window.setpos(window.cury, 0)
93
+ window.deleteln
94
+ window.write("#{prompt}#{buffer}")
95
+ # window.setpos(window.cury, original_x)
96
+ # window.write(buffer + sp)
97
+
98
+ # DBG
99
+ a, b = window.cury, cursor + original_x
100
+ # window.setpos(window.cury + 1, 0)
101
+ # window.write("<blue.bold>~</blue.bold> DBG: X position <green>#{original_x}</green>, cursor <green>#{cursor}</green>, buffer <green>#{buffer.inspect}</green>, history: <green>#{@history.inspect}</green> ... writing to <green>#{a}</green> x <green>#{b}</green>")
102
+ # window.setpos(window.cury - 1, cursor + original_x)
103
+
104
+ window.setpos(window.cury, cursor + original_x)
105
+ window.refresh
106
+ end
107
+
108
+ @history << buffer
109
+ Curses.echo
110
+ window.setpos(window.cury + 1, 0)
111
+ # window.write([:input, buffer].inspect + "\n")
112
+ # window.refresh
113
+ # sleep 2.5
114
+ return buffer
115
+ end
116
+
117
+ def commander
118
+ Commander.new
119
+ end
120
+
121
+ def process_chars(chars, buffer, cursor, window, original_x)
122
+ @log ||= File.open('log', 'a')
123
+ @log.puts("#{Time.now.to_i}: #{chars.inspect}")
124
+ @log.flush
125
+ # Tes-Ctrl+a-Ctrl+k produces [1] for Ctrl+a and [1, 11] for Ctrl+k.
126
+ # Multiple presses of Ctrl+whatever alternates, for instance for Ctrl+e
127
+ # it is [5], [5, 5], [5], [5, 5] etc. Therefore only the last number is relevant.
128
+ chars = [chars[-1]] if chars.all? { |char| char.is_a?(Integer) && char < 100 }
129
+ if chars.length > 1
130
+ # TODO:
131
+ process_char('É', buffer, cursor, window, original_x)
132
+ else
133
+ process_char(chars[0], buffer, cursor, window, original_x)
134
+ end
135
+ end
136
+
137
+ def process_char(char, buffer, cursor, window, original_x)
138
+ case char
139
+ when 1 # Ctrl+a.
140
+ cursor = 0
141
+ when 5 # Ctrl+e.
142
+ cursor = buffer.length
143
+ when 11 # Ctrl+k.
144
+ # yank_register = buffer[cursor..-1] # Ctrl+y suspends it currently.
145
+ if cursor == 0
146
+ buffer = ''
147
+ else
148
+ buffer = buffer[0..(cursor - 1)]
149
+ end
150
+ when 127 # Backspace.
151
+ unless buffer.empty?
152
+ buffer = buffer[0..-2]; cursor -= 1
153
+ end
154
+ when 258 # Down arrow
155
+ # TODO
156
+ window.write("X")
157
+ window.refresh
158
+ when 259 # Up arrow.
159
+ # TODO:
160
+ @history_index ||= @history.length - 1
161
+
162
+ window.setpos(window.cury + 1, 0)
163
+ window.write("DBG: #{@history_index}, #{(0..@history.length).include?(@history_index)}")
164
+ window.setpos(window.cury - 1, cursor + original_x)
165
+ window.refresh
166
+
167
+ if (0..@history.length).include?(@history_index)
168
+ @buffer_before_calling_history = buffer
169
+ buffer = @history[@history_index - 1]
170
+ else
171
+ window.setpos(window.cury, 0)
172
+ if @history.empty?
173
+ window.write("~ The history is empty.")
174
+ else
175
+ window.write("~ Already at the first item.")
176
+ end
177
+ window.setpos(window.cury - 1, original_x)
178
+ window.refresh
179
+ end
180
+ cursor = buffer.length
181
+ when 260 # Left arrow.
182
+ cursor -= 1 unless original_x == window.curx
183
+ # window.setpos(window.cury, window.curx - 1)
184
+ when 261 # Right arrow.
185
+ cursor += 1 unless original_x + buffer.length == window.curx
186
+ # window.setpos(window.cury, window.curx + 1)
187
+ when String
188
+ # window.addch(char)
189
+ buffer.insert(cursor, char); cursor += 1
190
+ else
191
+ raise KeyboardInterrupt.new(char) # TODO: Just return it, it's not really an error.
192
+ end
193
+
194
+ return buffer, cursor
195
+ end
196
+ end
@@ -0,0 +1,56 @@
1
+ require 'refined-refinements/colours'
2
+
3
+ module RR
4
+ module ColourExts
5
+ refine Curses::Window do
6
+ def defined_colours
7
+ Curses.constants.grep(/^COLOR_/).map do |colour|
8
+ colour.to_s.sub(/^COLOR_/, '').downcase.to_sym
9
+ end
10
+ end
11
+
12
+ def defined_attributes
13
+ Curses.constants.grep(/^A_/).map do |colour|
14
+ colour.to_s.sub(/^A_/, '').downcase.to_sym
15
+ end
16
+ end
17
+
18
+ # window.write("<red>Dog's <bold>bollocks</bold>!</red>")
19
+ def write(template)
20
+ template.chunks_with_colours.each do |chunk, colours|
21
+ attributes = colours.select { |method| self.defined_attributes.include?(method) }.map do |attribute_name|
22
+ Curses.const_get(:"A_#{attribute_name.to_s.upcase}") || raise("Unknown attribute #{attribute_name}.")
23
+ end
24
+
25
+ colours.select { |method| self.defined_colours.include?(method) }.each do |colour_name|
26
+ fg_colour = get_colour(colour_name)
27
+ Curses.init_pair(fg_colour, fg_colour, Curses::COLOR_BLACK)
28
+ attributes << (Curses.color_pair(fg_colour) | Curses::COLOR_WHITE)
29
+ end
30
+
31
+ if ! attributes.empty? && ! colours.include?(:clear)
32
+ self.multi_attron(*attributes) do
33
+ self.addstr(chunk)
34
+ end
35
+ else
36
+ self.addstr(chunk)
37
+ end
38
+ end
39
+ end
40
+
41
+ def get_colour(colour_name)
42
+ Curses.const_get(:"COLOR_#{colour_name.to_s.upcase}") || raise("Unknown colour: #{colour_name}")
43
+ end
44
+
45
+ def multi_attron(*attributes, &block)
46
+ self.attron(attributes.shift) do
47
+ if attributes.empty?
48
+ block.call
49
+ else
50
+ self.multi_attron(*attributes, &block)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,100 @@
1
+ require 'refined-refinements/string'
2
+
3
+ class QuitError < StandardError
4
+ end
5
+
6
+ class Command
7
+ attr_reader :keys, :help
8
+ def initialize(keys, help = nil, &handler)
9
+ @keys, @help, @handler = keys, help, handler
10
+ end
11
+
12
+ def handles?(command_key)
13
+ @keys.include?(command_key)
14
+ end
15
+
16
+ def execute(commander_window, command_key)
17
+ @handler.call(commander_window, command_key)
18
+ end
19
+ end
20
+
21
+ class Commander
22
+ using RR::ColourExts
23
+ using RR::StringExts
24
+
25
+ def initialize
26
+ @commands = Array.new
27
+ # @commands << Command.new(['?'], "Display help.") do |commander_window|
28
+ # commander_window.write(self.help)
29
+ # commander_window.getch # Enter or any key.
30
+ # end
31
+ end
32
+
33
+ def run(&block)
34
+ Curses.noecho
35
+ Curses.curs_set(0)
36
+
37
+ commander_window = Curses::Window.new(Curses.lines, Curses.cols, 0, 0)
38
+ commander_window.keypad = true
39
+
40
+ if block
41
+ block.call(self, commander_window)
42
+ commander_window.refresh
43
+ end
44
+
45
+ commander_window.setpos(commander_window.cury + 4, 0)
46
+
47
+ command_key = commander_window.getch
48
+ if command = self.find_command(command_key)
49
+ command.execute(commander_window, command_key) # Return message.
50
+ else
51
+ # TODO: command not found, display message?
52
+ self.run(&block) # Restart.
53
+ end
54
+ ensure
55
+ Curses.echo
56
+ end
57
+
58
+ def loop(&block)
59
+ Kernel.loop { self.run(&block) }
60
+ rescue QuitError
61
+ # void
62
+ end
63
+
64
+ def command(keys_or_key, help = nil, &handler)
65
+ @commands << Command.new([keys_or_key].flatten, help, &handler) # TODO: (One) key vs. keys?
66
+ end
67
+
68
+ def default_command(&handler)
69
+ @default_command = Command.new(Array.new, nil, &handler)
70
+ end
71
+
72
+ def find_command(command_key)
73
+ @commands.find { |command| command.handles?(command_key) } || @default_command
74
+ end
75
+
76
+ def help
77
+ commands_help = @commands.reduce(Array.new) do |buffer, command|
78
+ if command.help
79
+ keys_text = command.keys.join_with_and('or') do |word|
80
+ "<yellow.bold>#{word}</yellow.bold>"
81
+ end
82
+
83
+ buffer << "#{keys_text} to #{command.help}"
84
+ else
85
+ buffer
86
+ end
87
+ end
88
+
89
+ "<red.bold>Help:</red.bold> Press #{commands_help.join_with_and('or')}."
90
+ end
91
+
92
+ def available_commands_help
93
+ commands_help = @commands.reduce(Array.new) { |buffer, command|
94
+ keys_text = command.keys.join_with_and('or') { |word| "<yellow.bold>#{word}</yellow.bold>" }
95
+ buffer << "#{keys_text}"
96
+ }.join_with_and
97
+
98
+ "<green>Available commands</green> are #{commands_help}. Press <yellow>?</yellow> for <red>help</red>."
99
+ end
100
+ end
@@ -5,5 +5,13 @@ module RR
5
5
  "#{self[0].upcase}#{self[1..-1]}"
6
6
  end
7
7
  end
8
+
9
+ refine Array do
10
+ def join_with_and(xxx = 'and', delimiter = ', ', &block)
11
+ block = Proc.new { |item| item } if block.nil?
12
+ return block.call(self[0]) if self.length == 1
13
+ "#{self[0..-2].map(&block).join(delimiter)} #{xxx} #{block.call(self[-1])}"
14
+ end
15
+ end
8
16
  end
9
17
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: refined-refinements
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James C Russell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-27 00:00:00.000000000 Z
11
+ date: 2017-12-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: "."
14
14
  email: james@101ideas.cz
@@ -17,7 +17,14 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - README.md
20
+ - lib/refined-refinements/cached_http.rb
21
+ - lib/refined-refinements/cli/commander.rb
22
+ - lib/refined-refinements/cli/prompt.rb
23
+ - lib/refined-refinements/collection.rb
20
24
  - lib/refined-refinements/colours.rb
25
+ - lib/refined-refinements/curses/app.rb
26
+ - lib/refined-refinements/curses/colours.rb
27
+ - lib/refined-refinements/curses/commander.rb
21
28
  - lib/refined-refinements/date.rb
22
29
  - lib/refined-refinements/matching.rb
23
30
  - lib/refined-refinements/string.rb
@@ -41,7 +48,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
41
48
  version: '0'
42
49
  requirements: []
43
50
  rubyforge_project:
44
- rubygems_version: 2.6.11
51
+ rubygems_version: 2.7.3
45
52
  signing_key:
46
53
  specification_version: 4
47
54
  summary: ''