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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cc865e919348867e13b05201ac159f40f7f7fce166f44d31b98cd92d0db757b
4
- data.tar.gz: c3a93dc3474fe2cafcddb60d4cd25ee3350e66c4f487c8f99878a9d09e71f01f
3
+ metadata.gz: 160443af001778081e3f6599911f3f3ab828b19fae27606ab7e9d472d5af85f3
4
+ data.tar.gz: b5400da446623b84d04ab9f27df807c3bb52ff6e2c857af365407318aa4732a5
5
5
  SHA512:
6
- metadata.gz: df405a270f001707c86ad9e9e0d3104bb822e3eb337c7ab6afb0ad3a320aaf4fece4b1f727ccabfaf09f3fc804903876012f1ef348dd613473aeb962b2a1cbd4
7
- data.tar.gz: 6849d9d157bd2c634f5687b4ac194bf3d38b2357092d1344ebc38533708723009d52e93a4445988aafe36fcfa3a61b1ec589716588c925f9115fe9d0571f0ae0
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 (in a
52
- separately-picked random color), all on one line:
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, in a randomly-picked color:
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 beat state |
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 print_no_words(Lexdrill::WordList::PATH) unless word
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::Colorizer.paint(text)
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
@@ -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
- def self.paint(text)
9
- wrap(text, CODES.sample)
10
- end
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
@@ -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
@@ -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
- "#{Lexdrill::WordList.words.size} word(s)"
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 (in a separately-picked random color), all
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
- "#{Lexdrill::Colorizer.paint_blue(SEPARATOR)} #{Lexdrill::Colorizer.paint(word)}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lexdrill
4
- VERSION = "0.6.0"
4
+ VERSION = "0.11.0"
5
5
  end
@@ -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
- return nil if words.empty?
28
+ active = active_words
29
+ return nil if active.empty?
27
30
 
28
- words[take_index(words.size)]
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.6.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