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 +5 -5
- data/lib/refined-refinements/cached_http.rb +58 -0
- data/lib/refined-refinements/cli/commander.rb +57 -0
- data/lib/refined-refinements/cli/prompt.rb +140 -0
- data/lib/refined-refinements/collection.rb +150 -0
- data/lib/refined-refinements/colours.rb +112 -10
- data/lib/refined-refinements/curses/app.rb +196 -0
- data/lib/refined-refinements/curses/colours.rb +56 -0
- data/lib/refined-refinements/curses/commander.rb +100 -0
- data/lib/refined-refinements/string.rb +8 -0
- metadata +10 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9e237cd63850813d78118fb36f6d6a2af9997aad27efd52a29558364392a4320
|
4
|
+
data.tar.gz: acb9d42dd6b27cd7ff457c1dc496e4bf315e86a01ecac2f8fc6e6d2f41e5d0c9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
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-
|
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.
|
51
|
+
rubygems_version: 2.7.3
|
45
52
|
signing_key:
|
46
53
|
specification_version: 4
|
47
54
|
summary: ''
|