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