lexdrill 0.6.0 → 0.11.0
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 +4 -4
- data/README.md +38 -4
- data/lib/lexdrill/beat.rb +13 -1
- data/lib/lexdrill/cli.rb +86 -4
- data/lib/lexdrill/colorizer.rb +57 -6
- data/lib/lexdrill/config.rb +2 -0
- data/lib/lexdrill/inspector.rb +22 -1
- data/lib/lexdrill/line_formatter.rb +4 -3
- data/lib/lexdrill/rand.rb +29 -0
- data/lib/lexdrill/shell_snippet.rb +3 -0
- data/lib/lexdrill/stats.rb +43 -0
- data/lib/lexdrill/version.rb +1 -1
- data/lib/lexdrill/word_list.rb +12 -3
- data/lib/lexdrill.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 160443af001778081e3f6599911f3f3ab828b19fae27606ab7e9d472d5af85f3
|
|
4
|
+
data.tar.gz: b5400da446623b84d04ab9f27df807c3bb52ff6e2c857af365407318aa4732a5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7375919dc3b144de3feeb5f53ae8868b1aead949d85c5193833e12a428387b20e862e65ea2bc32caf176b2c0737ee5e5769b9e20998396cd1aba78f730802790
|
|
7
|
+
data.tar.gz: f588b965a909cb7c97e05dd22e4ce61a0170ce5e15803d386a942bfb39777f209038829fd43da4c585c51f99f79f5b836afb84928c150103648454e4d59f9867
|
data/README.md
CHANGED
|
@@ -48,14 +48,14 @@ Run `drill next` to print the current word and advance to the next one.
|
|
|
48
48
|
There are two output styles (`drill format simple|full`, `simple` is the
|
|
49
49
|
default):
|
|
50
50
|
|
|
51
|
-
**simple** — the drill sign (always blue), a space, then the word (
|
|
52
|
-
|
|
51
|
+
**simple** — the drill sign (always blue), a space, then the word (colored
|
|
52
|
+
by its show count — see "Mastery" below), all on one line:
|
|
53
53
|
```
|
|
54
54
|
⟳ apple
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
**full** — `counter/total⟳[loop_start-loop_end]` on one line, the word on
|
|
58
|
-
the next,
|
|
58
|
+
the next, colored by the word's show count:
|
|
59
59
|
```
|
|
60
60
|
1/6⟳[1-3]
|
|
61
61
|
apple
|
|
@@ -151,14 +151,48 @@ There are also named shortcuts for common loop sizes, one word/phrase apart:
|
|
|
151
151
|
`8` if you leave them off — `drill jazz` alone is `drill beat 5 8`, and
|
|
152
152
|
`drill beat 4` alone is `drill beat 4 8`.
|
|
153
153
|
|
|
154
|
+
### Frequency (`rand`)
|
|
155
|
+
|
|
156
|
+
`next` fires on every single prompt by default via the shell hook, which can
|
|
157
|
+
feel noisy in a busy session. `drill rand <n>` makes it actually show a word
|
|
158
|
+
only approximately 1-in-`n` times instead:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
drill rand 10 # roughly one in every ten calls
|
|
162
|
+
drill rand 1 # back to every time (the default)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
This is a **global** setting applied inside `drill next` itself — unlike
|
|
166
|
+
`start`/`stop`, it affects *every* call, hook-triggered or manually typed.
|
|
167
|
+
On a "skip", `next` silently does nothing (no output, nothing advances) and
|
|
168
|
+
exits 0.
|
|
169
|
+
|
|
170
|
+
### Mastery (color by show count)
|
|
171
|
+
|
|
172
|
+
Every time `next` shows a word it's tracked in `.drill.stats` (see
|
|
173
|
+
[`drill stats`](#commands) below). On an **odd** show count, the word's
|
|
174
|
+
color follows a blue → red gradient, one step per 100 shows — so at a
|
|
175
|
+
glance, blue words are fresh and red words are heavily drilled. On an
|
|
176
|
+
**even** show count, it's a vivid random color instead, just for visual
|
|
177
|
+
variety. Once a word hits 1200 shows it's considered mastered and `next`
|
|
178
|
+
stops selecting it (it still appears in `drill list`/`drill stats`, just no
|
|
179
|
+
longer comes up automatically). If every word in the list has been
|
|
180
|
+
mastered, `next` reports that on stderr and exits 1 instead of showing
|
|
181
|
+
anything.
|
|
182
|
+
|
|
154
183
|
### Commands
|
|
155
184
|
|
|
156
185
|
| Command | What it does |
|
|
157
186
|
|---|---|
|
|
158
187
|
| `drill next` | Print the current word and advance |
|
|
159
188
|
| `drill start` / `drill stop` | Pause/resume the automatic per-prompt hook (doesn't affect manual `next`) |
|
|
160
|
-
| `drill inspect` | Show the active `.drill.txt`/`.drill.counter` paths, word count, counter value, toggle, and
|
|
189
|
+
| `drill inspect` | Show the active `.drill.txt`/`.drill.counter`/`.drill.stats` paths, word count, counter value, toggle, beat, and rand state |
|
|
161
190
|
| `drill hook zsh\|bash` | Print the shell integration snippet (used above) |
|
|
162
191
|
| `drill beat <2-8> <repetitions>` / `drill beat none` | Set or disable the rhythm |
|
|
163
192
|
| `drill polka\|waltz\|rock\|jazz\|jiga\|balkan\|samba <repetitions>` | Shorthand for a fixed loop size (see table above) |
|
|
164
193
|
| `drill format simple\|full` | Set the output style (`simple` is the default) |
|
|
194
|
+
| `drill add <text>` | Append a new item to the end of the list |
|
|
195
|
+
| `drill list` | Print all items as `<count>\t<phrase>` (tab-separated), sorted by show count, highest first |
|
|
196
|
+
| `drill open` | Open the list file in `$EDITOR`/`$VISUAL` (falls back to `vi`) |
|
|
197
|
+
| `drill stats` | Show how many times each item has been shown, numbered |
|
|
198
|
+
| `drill rand <n>` | Shell hook shows a word ~1-in-`n` times (`n=1` is every time, the default) |
|
data/lib/lexdrill/beat.rb
CHANGED
|
@@ -6,11 +6,15 @@ require "fileutils"
|
|
|
6
6
|
# `repetitions` times before advancing to the next loop. Lives at
|
|
7
7
|
# ~/.drill.beat, independent of whichever project's .drill.txt is active.
|
|
8
8
|
# Disabled (plain word-by-word advance) unless explicitly configured.
|
|
9
|
+
# `drill beat rand` selects a third mode (marked by the literal contents
|
|
10
|
+
# "rand") where WordList.next ignores the counter/rhythm entirely and picks
|
|
11
|
+
# a uniformly random word each time instead.
|
|
9
12
|
module Lexdrill::Beat
|
|
10
13
|
PATH = File.join(Dir.home, ".drill.beat")
|
|
11
14
|
MIN_LOOP_SIZE = 2
|
|
12
15
|
MAX_LOOP_SIZE = 8
|
|
13
16
|
DEFAULT_REPETITIONS = 8
|
|
17
|
+
RAND_MARKER = "rand"
|
|
14
18
|
|
|
15
19
|
ALIASES = {
|
|
16
20
|
"polka" => 2,
|
|
@@ -28,13 +32,21 @@ module Lexdrill::Beat
|
|
|
28
32
|
LoopInfo = Struct.new(:index, :chunk_start, :chunk_end, :loop_number, :total_loops, keyword_init: true)
|
|
29
33
|
|
|
30
34
|
def self.configured?
|
|
31
|
-
File.exist?(PATH)
|
|
35
|
+
File.exist?(PATH) && !rand?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.rand?
|
|
39
|
+
File.exist?(PATH) && File.read(PATH).strip == RAND_MARKER
|
|
32
40
|
end
|
|
33
41
|
|
|
34
42
|
def self.set(loop_size, repetitions)
|
|
35
43
|
File.write(PATH, "#{loop_size} #{repetitions}")
|
|
36
44
|
end
|
|
37
45
|
|
|
46
|
+
def self.set_rand
|
|
47
|
+
File.write(PATH, RAND_MARKER)
|
|
48
|
+
end
|
|
49
|
+
|
|
38
50
|
def self.clear
|
|
39
51
|
FileUtils.rm_f(PATH)
|
|
40
52
|
end
|
data/lib/lexdrill/cli.rb
CHANGED
|
@@ -11,7 +11,12 @@ class Lexdrill::CLI
|
|
|
11
11
|
run_inspect: %w[inspect],
|
|
12
12
|
run_beat: %w[beat],
|
|
13
13
|
run_beat_alias: %w[polka waltz rock jazz jiga balkan samba],
|
|
14
|
-
run_format: %w[format]
|
|
14
|
+
run_format: %w[format],
|
|
15
|
+
run_add: %w[add],
|
|
16
|
+
run_list: %w[list],
|
|
17
|
+
run_open: %w[open],
|
|
18
|
+
run_stats: %w[stats],
|
|
19
|
+
run_rand: %w[rand]
|
|
15
20
|
}.freeze
|
|
16
21
|
|
|
17
22
|
def self.start(argv = ARGV)
|
|
@@ -53,26 +58,46 @@ class Lexdrill::CLI
|
|
|
53
58
|
drill inspect Show the active config/counter/toggle state
|
|
54
59
|
drill beat <2-8> [repetitions] Set the rhythm (repetitions defaults to 8)
|
|
55
60
|
drill beat none Disable the rhythm
|
|
61
|
+
drill beat rand Ignore the counter/rhythm; show a random item each time
|
|
56
62
|
drill polka|waltz|rock|jazz|jiga|balkan|samba [repetitions]
|
|
57
63
|
Shorthand for a fixed loop size (2 through 8, in order)
|
|
58
64
|
drill format simple|full Set the output style (simple is the default)
|
|
65
|
+
drill add <text> Append a new item to the end of the list
|
|
66
|
+
drill list Print items as <count>\t<phrase>, tab-separated, highest count first
|
|
67
|
+
drill open Open the list file in $EDITOR/$VISUAL (falls back to vi)
|
|
68
|
+
drill stats Show how many times each item has been shown
|
|
69
|
+
drill rand <n> drill next shows a word ~1-in-n times (n=1 is every time)
|
|
59
70
|
HELP
|
|
60
71
|
0
|
|
61
72
|
end
|
|
62
73
|
|
|
63
74
|
def run_next
|
|
75
|
+
return 0 if Lexdrill::Rand.skip?
|
|
76
|
+
|
|
64
77
|
word = Lexdrill::WordList.next
|
|
65
|
-
return
|
|
78
|
+
return print_next_failure unless word
|
|
66
79
|
|
|
67
80
|
puts colored_line(word)
|
|
68
81
|
0
|
|
69
82
|
end
|
|
70
83
|
|
|
84
|
+
def print_next_failure
|
|
85
|
+
return print_no_words(Lexdrill::WordList::PATH) if Lexdrill::WordList.words.empty?
|
|
86
|
+
|
|
87
|
+
print_all_graduated
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def print_all_graduated
|
|
91
|
+
warn "drill: every item has reached #{Lexdrill::Stats::GRADUATION_THRESHOLD} shows — nothing left to drill"
|
|
92
|
+
1
|
|
93
|
+
end
|
|
94
|
+
|
|
71
95
|
def colored_line(word)
|
|
72
96
|
text = Lexdrill::LineFormatter.format(word)
|
|
73
97
|
return text if Lexdrill::Format.simple?
|
|
74
98
|
|
|
75
|
-
Lexdrill::
|
|
99
|
+
count = Lexdrill::Stats.counts.fetch(word, 0)
|
|
100
|
+
Lexdrill::Colorizer.paint_by_count(text, count)
|
|
76
101
|
end
|
|
77
102
|
|
|
78
103
|
def print_no_words(path)
|
|
@@ -119,10 +144,16 @@ class Lexdrill::CLI
|
|
|
119
144
|
arg = argv[1]
|
|
120
145
|
return print_beat_usage unless arg
|
|
121
146
|
return clear_beat if arg == "none"
|
|
147
|
+
return set_rand_beat if arg == "rand"
|
|
122
148
|
|
|
123
149
|
set_beat(arg.to_i, Lexdrill::Beat.repetitions_or_default(argv[2]))
|
|
124
150
|
end
|
|
125
151
|
|
|
152
|
+
def set_rand_beat
|
|
153
|
+
Lexdrill::Beat.set_rand
|
|
154
|
+
0
|
|
155
|
+
end
|
|
156
|
+
|
|
126
157
|
def run_beat_alias
|
|
127
158
|
loop_size = Lexdrill::Beat::ALIASES.fetch(argv.first)
|
|
128
159
|
set_beat(loop_size, Lexdrill::Beat.repetitions_or_default(argv[1]))
|
|
@@ -143,7 +174,7 @@ class Lexdrill::CLI
|
|
|
143
174
|
|
|
144
175
|
def print_beat_usage
|
|
145
176
|
warn "usage: drill beat <#{Lexdrill::Beat::MIN_LOOP_SIZE}-#{Lexdrill::Beat::MAX_LOOP_SIZE}> " \
|
|
146
|
-
"<repetitions> | drill beat none"
|
|
177
|
+
"<repetitions> | drill beat none | drill beat rand"
|
|
147
178
|
1
|
|
148
179
|
end
|
|
149
180
|
|
|
@@ -170,6 +201,57 @@ class Lexdrill::CLI
|
|
|
170
201
|
1
|
|
171
202
|
end
|
|
172
203
|
|
|
204
|
+
def run_add
|
|
205
|
+
text = argv[1..].join(" ")
|
|
206
|
+
return print_add_usage if text.empty?
|
|
207
|
+
|
|
208
|
+
File.open(Lexdrill::WordList::PATH, "a", encoding: "UTF-8") { |file| file.puts(text) }
|
|
209
|
+
puts "added: #{text}"
|
|
210
|
+
0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def print_add_usage
|
|
214
|
+
warn "usage: drill add <text>"
|
|
215
|
+
1
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def run_list
|
|
219
|
+
words = Lexdrill::WordList.words
|
|
220
|
+
return print_no_words(Lexdrill::WordList::PATH) if words.empty?
|
|
221
|
+
|
|
222
|
+
counts = Lexdrill::Stats.counts
|
|
223
|
+
pairs = words.map { |word| [counts.fetch(word, 0), word] }.sort_by { |count, _word| -count }
|
|
224
|
+
pairs.each { |count, word| puts "#{count}\t#{word}" }
|
|
225
|
+
0
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def run_open
|
|
229
|
+
editor_cmd = (ENV["VISUAL"] || ENV["EDITOR"] || "vi").split
|
|
230
|
+
system(*editor_cmd, Lexdrill::WordList::PATH) ? 0 : 1
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def run_stats
|
|
234
|
+
words = Lexdrill::WordList.words
|
|
235
|
+
return print_no_words(Lexdrill::WordList::PATH) if words.empty?
|
|
236
|
+
|
|
237
|
+
counts = Lexdrill::Stats.counts
|
|
238
|
+
words.each_with_index { |word, index| puts "#{index + 1}. #{word} (#{counts.fetch(word, 0)})" }
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def run_rand
|
|
243
|
+
denominator = argv[1].to_s.to_i
|
|
244
|
+
return print_rand_usage unless denominator.positive?
|
|
245
|
+
|
|
246
|
+
Lexdrill::Rand.set(denominator)
|
|
247
|
+
0
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def print_rand_usage
|
|
251
|
+
warn "usage: drill rand <n> (drill next shows a word ~1-in-n times; n=1 is every time)"
|
|
252
|
+
1
|
|
253
|
+
end
|
|
254
|
+
|
|
173
255
|
def print_unknown_command(command)
|
|
174
256
|
warn "drill: unknown command #{command.inspect}"
|
|
175
257
|
print_help
|
data/lib/lexdrill/colorizer.rb
CHANGED
|
@@ -1,18 +1,69 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Wraps text in an ANSI color code.
|
|
3
|
+
# Wraps text in an ANSI color code. `paint_by_count` maps a word's show
|
|
4
|
+
# count onto a blue -> red truecolor gradient, one step per
|
|
5
|
+
# Lexdrill::Stats::BUCKET_SIZE shows, so a word's color reflects how many
|
|
6
|
+
# times it's been drilled — except on an even count, where a random vivid
|
|
7
|
+
# color is used instead, for a bit of visual variety.
|
|
4
8
|
module Lexdrill::Colorizer
|
|
5
|
-
CODES = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96].freeze
|
|
6
9
|
BLUE = 34
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
GRADIENT_STEPS = Lexdrill::Stats::GRADUATION_THRESHOLD / Lexdrill::Stats::BUCKET_SIZE
|
|
11
|
+
HUE_DEGREES = 360
|
|
12
|
+
# Which of {peak, trough, 0} each RGB channel takes in each 60-degree hue
|
|
13
|
+
# sextant, walking red -> yellow -> green -> cyan -> blue -> magenta -> red.
|
|
14
|
+
SEXTANT_CHANNELS = [
|
|
15
|
+
%i[peak trough zero],
|
|
16
|
+
%i[trough peak zero],
|
|
17
|
+
%i[zero peak trough],
|
|
18
|
+
%i[zero trough peak],
|
|
19
|
+
%i[trough zero peak],
|
|
20
|
+
%i[peak zero trough]
|
|
21
|
+
].freeze
|
|
11
22
|
|
|
12
23
|
def self.paint_blue(text)
|
|
13
24
|
wrap(text, BLUE)
|
|
14
25
|
end
|
|
15
26
|
|
|
27
|
+
def self.paint_by_count(text, count)
|
|
28
|
+
code = count.even? ? random_code : gradient_code(count)
|
|
29
|
+
wrap(text, code)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.gradient_code(count)
|
|
33
|
+
last_bucket = GRADIENT_STEPS - 1
|
|
34
|
+
bucket = [count / Lexdrill::Stats::BUCKET_SIZE, last_bucket].min
|
|
35
|
+
fraction = bucket.fdiv(last_bucket)
|
|
36
|
+
red = (fraction * 255).round
|
|
37
|
+
blue = ((1 - fraction) * 255).round
|
|
38
|
+
"38;2;#{red};0;#{blue}"
|
|
39
|
+
end
|
|
40
|
+
private_class_method :gradient_code
|
|
41
|
+
|
|
42
|
+
def self.random_code
|
|
43
|
+
red, green, blue = hue_to_rgb(Kernel.rand(HUE_DEGREES))
|
|
44
|
+
"38;2;#{red};#{green};#{blue}"
|
|
45
|
+
end
|
|
46
|
+
private_class_method :random_code
|
|
47
|
+
|
|
48
|
+
# Full-saturation, full-value HSV -> RGB, for a vivid random color.
|
|
49
|
+
def self.hue_to_rgb(hue)
|
|
50
|
+
peak = 255
|
|
51
|
+
trough = hue_trough(hue, peak)
|
|
52
|
+
rgb_for_sextant(hue / 60, peak, trough)
|
|
53
|
+
end
|
|
54
|
+
private_class_method :hue_to_rgb
|
|
55
|
+
|
|
56
|
+
def self.hue_trough(hue, peak)
|
|
57
|
+
(peak * (1 - (((hue / 60.0) % 2) - 1).abs)).round
|
|
58
|
+
end
|
|
59
|
+
private_class_method :hue_trough
|
|
60
|
+
|
|
61
|
+
def self.rgb_for_sextant(sextant, peak, trough)
|
|
62
|
+
channel_values = { peak: peak, trough: trough, zero: 0 }
|
|
63
|
+
SEXTANT_CHANNELS[sextant % SEXTANT_CHANNELS.size].map { |channel| channel_values.fetch(channel) }
|
|
64
|
+
end
|
|
65
|
+
private_class_method :rgb_for_sextant
|
|
66
|
+
|
|
16
67
|
def self.wrap(text, code)
|
|
17
68
|
"\e[#{code}m#{text}\e[0m"
|
|
18
69
|
end
|
data/lib/lexdrill/config.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Lexdrill::Config
|
|
4
4
|
FILENAME = ".drill.txt"
|
|
5
5
|
COUNTER_FILENAME = ".drill.counter"
|
|
6
|
+
STATS_FILENAME = ".drill.stats"
|
|
6
7
|
|
|
7
8
|
def self.dir_path
|
|
8
9
|
cwd = Dir.pwd
|
|
@@ -19,4 +20,5 @@ module Lexdrill::Config
|
|
|
19
20
|
|
|
20
21
|
DRILL_PATH = File.join(base_path, FILENAME)
|
|
21
22
|
COUNTER_PATH = File.join(base_path, COUNTER_FILENAME)
|
|
23
|
+
STATS_PATH = File.join(base_path, STATS_FILENAME)
|
|
22
24
|
end
|
data/lib/lexdrill/inspector.rb
CHANGED
|
@@ -10,8 +10,10 @@ module Lexdrill::Inspector
|
|
|
10
10
|
|
|
11
11
|
Words file: #{Lexdrill::WordList::PATH} (#{words_summary})
|
|
12
12
|
Counter file: #{Lexdrill::WordList::COUNTER_PATH} (value: #{counter_value})
|
|
13
|
+
Stats file: #{Lexdrill::Stats::PATH} (#{stats_summary})
|
|
13
14
|
Toggle: #{toggle_summary}
|
|
14
15
|
Beat: #{beat_summary}
|
|
16
|
+
Rand: #{rand_summary}
|
|
15
17
|
Format: #{Lexdrill::Format.current}
|
|
16
18
|
LEXDRILL_PATH: #{ENV.fetch('LEXDRILL_PATH', '(not set)')}
|
|
17
19
|
REPORT
|
|
@@ -20,13 +22,24 @@ module Lexdrill::Inspector
|
|
|
20
22
|
def self.words_summary
|
|
21
23
|
return "missing" unless File.exist?(Lexdrill::WordList::PATH)
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
words = Lexdrill::WordList.words
|
|
26
|
+
total = words.size
|
|
27
|
+
graduated = words.count { |word| Lexdrill::Stats.graduated?(word) }
|
|
28
|
+
return "#{total} word(s)" if graduated.zero?
|
|
29
|
+
|
|
30
|
+
"#{total} word(s), #{graduated} graduated"
|
|
24
31
|
end
|
|
25
32
|
|
|
26
33
|
def self.counter_value
|
|
27
34
|
Lexdrill::Counter.new(Lexdrill::WordList::COUNTER_PATH).value
|
|
28
35
|
end
|
|
29
36
|
|
|
37
|
+
def self.stats_summary
|
|
38
|
+
return "no data yet" unless File.exist?(Lexdrill::Stats::PATH)
|
|
39
|
+
|
|
40
|
+
"#{Lexdrill::Stats.counts.size} item(s) tracked"
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
def self.toggle_summary
|
|
31
44
|
return "enabled" if Lexdrill::Toggle.enabled?
|
|
32
45
|
|
|
@@ -34,8 +47,16 @@ module Lexdrill::Inspector
|
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
def self.beat_summary
|
|
50
|
+
return "random (ignores rhythm/counter)" if Lexdrill::Beat.rand?
|
|
37
51
|
return "not set (plain word-by-word)" unless Lexdrill::Beat.configured?
|
|
38
52
|
|
|
39
53
|
"loop #{Lexdrill::Beat.loop_size}, repeat #{Lexdrill::Beat.repetitions}"
|
|
40
54
|
end
|
|
55
|
+
|
|
56
|
+
def self.rand_summary
|
|
57
|
+
denominator = Lexdrill::Rand.value
|
|
58
|
+
return "every time (default)" if denominator == 1
|
|
59
|
+
|
|
60
|
+
"approximately 1-in-#{denominator}"
|
|
61
|
+
end
|
|
41
62
|
end
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Formats a shown word for display. In "simple" mode: the drill sign (in
|
|
4
|
-
# blue), a space, then the word (
|
|
5
|
-
# on one line. In "full" mode (the default):
|
|
4
|
+
# blue), a space, then the word (colored by its show count, blue -> red as
|
|
5
|
+
# it's drilled more), all on one line. In "full" mode (the default):
|
|
6
6
|
# "{counter}/{total}{SEPARATOR}[{loop_start}-{loop_end}]\n{word}" — counter
|
|
7
7
|
# is the word's own 1-based position in the list, re-derived through
|
|
8
8
|
# Lexdrill::Beat so it stays meaningful even when a rhythm repeats steps.
|
|
@@ -16,7 +16,8 @@ module Lexdrill::LineFormatter
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def self.simple(word)
|
|
19
|
-
|
|
19
|
+
count = Lexdrill::Stats.counts.fetch(word, 0)
|
|
20
|
+
"#{Lexdrill::Colorizer.paint_blue(SEPARATOR)} #{Lexdrill::Colorizer.paint_by_count(word, count)}"
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def self.full(word)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Global "how often to actually show a word" setting, applied by
|
|
4
|
+
# `drill next` itself (both automatic hook-triggered calls and manual
|
|
5
|
+
# invocations respect it equally). Lives at ~/.drill.rand as a plain
|
|
6
|
+
# integer; defaults to 1 (show every time).
|
|
7
|
+
module Lexdrill::Rand
|
|
8
|
+
PATH = File.join(Dir.home, ".drill.rand")
|
|
9
|
+
DEFAULT = 1
|
|
10
|
+
|
|
11
|
+
def self.value
|
|
12
|
+
return DEFAULT unless File.exist?(PATH)
|
|
13
|
+
|
|
14
|
+
denominator = File.read(PATH).strip.to_i
|
|
15
|
+
denominator.positive? ? denominator : DEFAULT
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.set(denominator)
|
|
19
|
+
File.write(PATH, denominator.to_s)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# True with probability (n-1)/n: "skip this show".
|
|
23
|
+
def self.skip?
|
|
24
|
+
denominator = value
|
|
25
|
+
return false if denominator <= 1
|
|
26
|
+
|
|
27
|
+
Kernel.rand(denominator) != 0
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Generates the zsh/bash shell integration snippet for `drill hook`.
|
|
4
|
+
# Show-frequency throttling (`drill rand <n>`) lives entirely in `drill
|
|
5
|
+
# next` itself, not in these snippets, so it applies equally to
|
|
6
|
+
# hook-triggered and manually-run calls.
|
|
4
7
|
module Lexdrill::ShellSnippet
|
|
5
8
|
ZSH = <<~SNIPPET
|
|
6
9
|
drill_precmd() {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# Tracks how many times each word/phrase has been shown by `next`, persisted
|
|
6
|
+
# as a word => count JSON object at `.drill.stats` (same base path as
|
|
7
|
+
# `.drill.txt`/`.drill.counter`). Every BUCKET_SIZE shows moves a word into
|
|
8
|
+
# the next display-color bucket (see Lexdrill::Colorizer); once a word
|
|
9
|
+
# reaches GRADUATION_THRESHOLD shows it's considered mastered and `next`
|
|
10
|
+
# stops selecting it.
|
|
11
|
+
module Lexdrill::Stats
|
|
12
|
+
PATH = Lexdrill::Config::STATS_PATH
|
|
13
|
+
BUCKET_SIZE = 100
|
|
14
|
+
GRADUATION_THRESHOLD = 1200
|
|
15
|
+
|
|
16
|
+
def self.record(word)
|
|
17
|
+
data = load
|
|
18
|
+
data[word] = data.fetch(word, 0) + 1
|
|
19
|
+
save(data)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.counts
|
|
23
|
+
load
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.graduated?(word)
|
|
27
|
+
counts.fetch(word, 0) >= GRADUATION_THRESHOLD
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.load
|
|
31
|
+
return {} unless File.exist?(PATH)
|
|
32
|
+
|
|
33
|
+
JSON.parse(File.read(PATH, encoding: "UTF-8"))
|
|
34
|
+
rescue JSON::ParserError
|
|
35
|
+
{}
|
|
36
|
+
end
|
|
37
|
+
private_class_method :load
|
|
38
|
+
|
|
39
|
+
def self.save(data)
|
|
40
|
+
File.write(PATH, JSON.generate(data), encoding: "UTF-8")
|
|
41
|
+
end
|
|
42
|
+
private_class_method :save
|
|
43
|
+
end
|
data/lib/lexdrill/version.rb
CHANGED
data/lib/lexdrill/word_list.rb
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
# Reads vocabulary words/phrases from a `.drill.txt` file, one per line, and
|
|
4
4
|
# advances a persisted counter to show "the current word" each time. On a
|
|
5
5
|
# fresh install with no project-local or LEXDRILL_PATH-overridden list, seeds
|
|
6
|
-
# ~/.drill.txt with a default starter list instead of coming up empty.
|
|
6
|
+
# ~/.drill.txt with a default starter list instead of coming up empty. Words
|
|
7
|
+
# that have graduated (see Lexdrill::Stats) are excluded from `.next` but
|
|
8
|
+
# remain in `.words` for `list`/`stats` to report.
|
|
7
9
|
class Lexdrill::WordList
|
|
8
10
|
PATH = Lexdrill::Config::DRILL_PATH
|
|
9
11
|
COUNTER_PATH = Lexdrill::Config::COUNTER_PATH
|
|
@@ -23,9 +25,16 @@ class Lexdrill::WordList
|
|
|
23
25
|
private_class_method :seed_default_at_home
|
|
24
26
|
|
|
25
27
|
def self.next
|
|
26
|
-
|
|
28
|
+
active = active_words
|
|
29
|
+
return nil if active.empty?
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
word = Lexdrill::Beat.rand? ? active.sample : active[take_index(active.size)]
|
|
32
|
+
Lexdrill::Stats.record(word)
|
|
33
|
+
word
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.active_words
|
|
37
|
+
words.reject { |word| Lexdrill::Stats.graduated?(word) }
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def self.take_index(size)
|
data/lib/lexdrill.rb
CHANGED
|
@@ -5,8 +5,10 @@ require_relative "lexdrill/config"
|
|
|
5
5
|
require_relative "lexdrill/counter"
|
|
6
6
|
require_relative "lexdrill/beat"
|
|
7
7
|
require_relative "lexdrill/default_words"
|
|
8
|
+
require_relative "lexdrill/stats"
|
|
8
9
|
require_relative "lexdrill/word_list"
|
|
9
10
|
require_relative "lexdrill/toggle"
|
|
11
|
+
require_relative "lexdrill/rand"
|
|
10
12
|
require_relative "lexdrill/shell_snippet"
|
|
11
13
|
require_relative "lexdrill/inspector"
|
|
12
14
|
require_relative "lexdrill/colorizer"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lexdrill
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Siarhei Kisliak
|
|
@@ -34,7 +34,9 @@ files:
|
|
|
34
34
|
- lib/lexdrill/format.rb
|
|
35
35
|
- lib/lexdrill/inspector.rb
|
|
36
36
|
- lib/lexdrill/line_formatter.rb
|
|
37
|
+
- lib/lexdrill/rand.rb
|
|
37
38
|
- lib/lexdrill/shell_snippet.rb
|
|
39
|
+
- lib/lexdrill/stats.rb
|
|
38
40
|
- lib/lexdrill/toggle.rb
|
|
39
41
|
- lib/lexdrill/version.rb
|
|
40
42
|
- lib/lexdrill/word_list.rb
|