refined-refinements 0.0.1 → 0.0.1.1

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.
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: ''