lexdrill 0.17.0 → 0.19.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: 90fae028c5269ace52ee6fadc58941617f30b35a4d47f42c0654fcb88a478dd8
4
- data.tar.gz: d1744556c29c18a5cf8f3b2ed8e23e2b4660bf3b0455bbde57abb1f29f954035
3
+ metadata.gz: 2c04d80068319a1be565d64c8325ce808f242d7a71609f34796b348cc04b0571
4
+ data.tar.gz: 2b06d6b6d48cc3556c77524bce72c77e18404aceadd802cb3ae40134a29cfc3d
5
5
  SHA512:
6
- metadata.gz: c8df12b5210838e036736c62940ca427d07e490da114145fc798dc3d37b8f915c0ed70ab550e287502999ac450491c7539f68589cc37fe5790693c39fb587d66
7
- data.tar.gz: ab045c1f53749f1e0654d70a614b5a9ab5699984dd39d5578a742a42d174be74edb1119eb2404c2613e56992263a85922dbcdef69794c1fd768badb0ae057805
6
+ metadata.gz: c15dac65d6521acd98fb4a81f016466882d4397587a9de255544364cbcb0a9bc535e399c91dd7979ea4fadc5206ca86c4c2f04db62898baa298d2a82a24f908d
7
+ data.tar.gz: b8f7dd159de9b89f81cafd9a73d50d2e7db6541ed3fcd0028f4dbee6b344301f085e3be7f7c78a669bb1494845afc65eaba886f17a164355dd99521a9a6d4c6b
data/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  The `lexdrill` gem installs a `drill` command that prints a vocabulary word or
4
4
  phrase on demand, tracking how often each one has been shown.
5
5
 
6
+ ![lexdrill demo](docs/demo.gif)
7
+
6
8
  ## Installation
7
9
 
8
10
  ```bash
@@ -31,8 +33,9 @@ this — rvm keeps gem executable directories on `PATH` automatically.
31
33
 
32
34
  ## Usage
33
35
 
34
- Create a `.drill.txt` file one word or phrase per line — in your home directory,
35
- or in a specific project directory to give that project its own list:
36
+ All of lexdrill's state lives in one place: `~/.drill/` (mode `0700`). It's a
37
+ single global config no per-project lists, no environment variable
38
+ overrides. Create `~/.drill/words` yourself — one word or phrase per line:
36
39
 
37
40
  ```
38
41
  apple
@@ -40,27 +43,18 @@ banana
40
43
  cherry
41
44
  ```
42
45
 
43
- If you don't create one, `drill next` seeds `~/.drill.txt` with a default
46
+ If you don't create one, `drill next` seeds `~/.drill/words` with a default
44
47
  starter list (a set of NLP presuppositions) the first time it runs, so
45
48
  there's always something to drill.
46
49
 
47
50
  Run `drill next` to print the current word and advance to the next one.
48
- There are two output styles (`drill format simple|full`, `simple` is the
49
- default):
50
-
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:
51
+ Output is `counter/total` (the word's own 1-based position in the list)
52
+ and the drill sign, together in yellow, a space, then the word (colored by
53
+ its show count — see "Mastery" below), all on one line:
53
54
  ```
54
- ⟳ apple
55
+ 1/6⟳ apple
55
56
  ```
56
-
57
- **full** — `counter/total⟳[loop_start-loop_end]` on one line, the word on
58
- the next, colored by the word's show count:
59
- ```
60
- 1/6⟳[1-3]
61
- apple
62
- ```
63
- (word 1 of 6 total; currently in the loop spanning words 1-3)
57
+ (word 1 of 6 total)
64
58
 
65
59
  ### Shell integration (one-time setup)
66
60
 
@@ -131,9 +125,7 @@ before moving on:
131
125
  drill beat 3 2 # loop size 3, repeat each loop 2 times
132
126
  ```
133
127
 
134
- `drill beat none` turns it back off (plain word-by-word again). This is a
135
- **global** setting — it applies everywhere, independent of which project's
136
- `.drill.txt` is currently active.
128
+ `drill beat none` turns it back off (plain word-by-word again).
137
129
 
138
130
  There are also named shortcuts for common loop sizes, one word/phrase apart:
139
131
 
@@ -169,16 +161,18 @@ exits 0.
169
161
 
170
162
  ### Mastery (color by show count)
171
163
 
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.
164
+ Every time `next` shows a word it's tracked in `~/.drill/stats` (see
165
+ [`drill stats`](#commands) below). By default (`drill color default`), the
166
+ word's color follows a blue → red gradient, one step per 10 shows — so at
167
+ a glance, blue words are fresh and red words are heavily drilled. Once a
168
+ word hits 120 shows it's considered mastered and `next` stops selecting it
169
+ (it still appears in `drill list`/`drill stats`, just no longer comes up
170
+ automatically). If every word in the list has been mastered, `next`
171
+ reports that on stderr and exits 1 instead of showing anything.
172
+
173
+ Prefer a vivid random color every time instead of the gradient? Run
174
+ `drill color random` (and `drill color default` to switch back — this is
175
+ the default, so it's also the way to undo `random`).
182
176
 
183
177
  ### Google Sheets export
184
178
 
@@ -202,7 +196,7 @@ Either way, `export` always **overwrites** the named tab (creating it
202
196
  first if it doesn't exist yet) with the current word list, one phrase per
203
197
  row, so it stays an exact mirror even if the list shrinks. `import` is the
204
198
  reverse: it reads column A of the tab (ignoring any other columns, like an
205
- old export's show counts) and **replaces** your local `.drill.txt` —
199
+ old export's show counts) and **replaces** your local `~/.drill/words` —
206
200
  useful for editing the list in Sheets and pulling changes back down, or
207
201
  seeding a fresh machine from an existing sheet.
208
202
 
@@ -228,9 +222,9 @@ a real secret you must keep local.
228
222
  field in the downloaded JSON).
229
223
  5. Open your target Google Sheet → **Share** → paste that email → grant
230
224
  **Editor** access → uncheck "Notify people" → Share.
231
- 6. Save the downloaded key file to `~/.drill.gcp-service-account.json` on
232
- your machine (`drill` reads it from that fixed path and sets it to mode
233
- `0600`; it's never embedded in the gem or committed anywhere).
225
+ 6. Save the downloaded key file to `~/.drill/gcp-service-account.json` on
226
+ your machine (`drill` reads it from that fixed path; it's never embedded
227
+ in the gem or committed anywhere).
234
228
 
235
229
  ```bash
236
230
  drill remote 'https://docs.google.com/spreadsheets/d/1opBP4APL5SUvepm9qwjIYRNtDZdoY1Ee87F5PWdxaMg/edit?usp=sharing'
@@ -245,7 +239,7 @@ short-lived JWT with the local key file.
245
239
  Uses the OAuth 2.0 **Device Authorization Grant** ("visit this URL, enter
246
240
  this code," the same style of flow `gcloud auth login` uses) — you approve
247
241
  access to your own account once, and the resulting token is cached locally
248
- at `~/.drill.gcp-token.json` (mode `0600`), never published or shared.
242
+ at `~/.drill/gcp-token.json` (mode `0600`), never published or shared.
249
243
 
250
244
  The OAuth client id/secret embedded in `lib/lexdrill/google_auth.rb` are
251
245
  **not** secret for this use — Google's own docs say client credentials for
@@ -286,11 +280,11 @@ itself automatically).
286
280
  |---|---|
287
281
  | `drill next` | Print the current word and advance |
288
282
  | `drill start` / `drill stop` | Pause/resume the automatic per-prompt hook (doesn't affect manual `next`) |
289
- | `drill inspect` | Show the active `.drill.txt`/`.drill.counter`/`.drill.stats` paths, word count, counter value, toggle, beat, and rand state |
283
+ | `drill inspect` | Show the active config directory, word count, counter value, toggle, beat, rand, and color state |
290
284
  | `drill hook zsh\|bash` | Print the shell integration snippet (used above) |
291
285
  | `drill beat <2-8> <repetitions>` / `drill beat none` | Set or disable the rhythm |
292
286
  | `drill polka\|waltz\|rock\|jazz\|jiga\|balkan\|samba <repetitions>` | Shorthand for a fixed loop size (see table above) |
293
- | `drill format simple\|full` | Set the output style (`simple` is the default) |
287
+ | `drill color random\|default` | Color each shown word randomly, or by its show count (`default`) |
294
288
  | `drill add <text>` | Append a new item to the end of the list |
295
289
  | `drill list` | Show how many times each item has been shown, numbered |
296
290
  | `drill open` | Open the list file in `$EDITOR`/`$VISUAL` (falls back to `vi`) |
data/lib/lexdrill/beat.rb CHANGED
@@ -3,14 +3,13 @@
3
3
  require "fileutils"
4
4
 
5
5
  # Global rhythm config: repeats loops of `loop_size` consecutive words
6
- # `repetitions` times before advancing to the next loop. Lives at
7
- # ~/.drill.beat, independent of whichever project's .drill.txt is active.
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.
6
+ # `repetitions` times before advancing to the next loop. Disabled (plain
7
+ # word-by-word advance) unless explicitly configured. `drill beat rand`
8
+ # selects a third mode (marked by the literal contents "rand") where
9
+ # WordList.next ignores the counter/rhythm entirely and picks a uniformly
10
+ # random word each time instead.
12
11
  module Lexdrill::Beat
13
- PATH = File.join(Dir.home, ".drill.beat")
12
+ PATH = Lexdrill::Config.path("beat")
14
13
  MIN_LOOP_SIZE = 2
15
14
  MAX_LOOP_SIZE = 8
16
15
  DEFAULT_REPETITIONS = 8
data/lib/lexdrill/cli.rb CHANGED
@@ -11,7 +11,7 @@ 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_color: %w[color],
15
15
  run_add: %w[add],
16
16
  run_list: %w[list],
17
17
  run_open: %w[open],
@@ -67,7 +67,7 @@ class Lexdrill::CLI
67
67
  drill beat rand Ignore the counter/rhythm; show a random item each time
68
68
  drill polka|waltz|rock|jazz|jiga|balkan|samba [repetitions]
69
69
  Shorthand for a fixed loop size (2 through 8, in order)
70
- drill format simple|full Set the output style (simple is the default)
70
+ drill color random|default Color each word randomly, or by its show count (default)
71
71
  drill add <text> Append a new item to the end of the list
72
72
  drill list Show how many times each item has been shown
73
73
  drill open Open the list file in $EDITOR/$VISUAL (falls back to vi)
@@ -75,7 +75,7 @@ class Lexdrill::CLI
75
75
  drill rand <n> drill next shows a word ~1-in-n times (n=1 is every time)
76
76
  drill go <number> Jump so the next `next` shows item <number> (1-based, see drill list)
77
77
  drill remote <url> Set the Google Sheet used by a local service account key
78
- (~/.drill.gcp-service-account.json) — no interactive sign-in
78
+ (~/.drill/gcp-service-account.json) — no interactive sign-in
79
79
  drill oauth <url> Set the Google Sheet used by the OAuth (personal-login) flow
80
80
  drill sheet Print a link to the currently active spreadsheet
81
81
  drill export <sheet-name> Export the word list text to the given tab (overwrites its
@@ -92,7 +92,7 @@ class Lexdrill::CLI
92
92
  word = Lexdrill::WordList.next
93
93
  return print_next_failure unless word
94
94
 
95
- puts colored_line(word)
95
+ puts Lexdrill::LineFormatter.format(word)
96
96
  0
97
97
  end
98
98
 
@@ -107,14 +107,6 @@ class Lexdrill::CLI
107
107
  1
108
108
  end
109
109
 
110
- def colored_line(word)
111
- text = Lexdrill::LineFormatter.format(word)
112
- return text if Lexdrill::Format.simple?
113
-
114
- count = Lexdrill::Stats.counts.fetch(word, 0)
115
- Lexdrill::Colorizer.paint_by_count(text, count)
116
- end
117
-
118
110
  def print_no_words(path)
119
111
  warn "drill: no words found in #{path}"
120
112
  1
@@ -203,16 +195,22 @@ class Lexdrill::CLI
203
195
  1
204
196
  end
205
197
 
206
- def run_format
198
+ def run_color
207
199
  mode = argv[1]
208
- return print_format_usage unless Lexdrill::Format::VALID.include?(mode)
200
+ return print_color_usage unless mode
201
+ return print_invalid_color(mode) unless Lexdrill::Color::VALID.include?(mode)
209
202
 
210
- Lexdrill::Format.set(mode)
203
+ Lexdrill::Color.set(mode)
211
204
  0
212
205
  end
213
206
 
214
- def print_format_usage
215
- warn "usage: drill format <#{Lexdrill::Format::VALID.join('|')}>"
207
+ def print_color_usage
208
+ warn "usage: drill color <#{Lexdrill::Color::VALID.join('|')}>"
209
+ 1
210
+ end
211
+
212
+ def print_invalid_color(mode)
213
+ warn "drill: unknown color mode #{mode.inspect} (expected #{Lexdrill::Color::VALID.join(' or ')})"
216
214
  1
217
215
  end
218
216
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Global "how to color a shown word" setting: "default" colors by show
4
+ # count (see Lexdrill::Colorizer's blue -> red gradient); "random" picks a
5
+ # vivid random color every time instead. An absent file (or any
6
+ # unrecognized value) means "default".
7
+ module Lexdrill::Color
8
+ PATH = Lexdrill::Config.path("color")
9
+ RANDOM = "random"
10
+ DEFAULT = "default"
11
+ VALID = [RANDOM, DEFAULT].freeze
12
+
13
+ def self.set(mode)
14
+ File.write(PATH, mode)
15
+ end
16
+
17
+ def self.current
18
+ return DEFAULT unless File.exist?(PATH)
19
+
20
+ value = File.read(PATH).strip
21
+ VALID.include?(value) ? value : DEFAULT
22
+ end
23
+
24
+ def self.random?
25
+ current == RANDOM
26
+ end
27
+ end
@@ -3,10 +3,11 @@
3
3
  # Wraps text in an ANSI color code. `paint_by_count` maps a word's show
4
4
  # count onto a blue -> red truecolor gradient, one step per
5
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.
6
+ # times it's been drilled — unless `drill color random` is set (see
7
+ # Lexdrill::Color), in which case a vivid random color is used every time
8
+ # instead.
8
9
  module Lexdrill::Colorizer
9
- BLUE = 34
10
+ YELLOW = 33
10
11
  GRADIENT_STEPS = Lexdrill::Stats::GRADUATION_THRESHOLD / Lexdrill::Stats::BUCKET_SIZE
11
12
  HUE_DEGREES = 360
12
13
  # Which of {peak, trough, 0} each RGB channel takes in each 60-degree hue
@@ -20,12 +21,12 @@ module Lexdrill::Colorizer
20
21
  %i[peak zero trough]
21
22
  ].freeze
22
23
 
23
- def self.paint_blue(text)
24
- wrap(text, BLUE)
24
+ def self.paint_yellow(text)
25
+ wrap(text, YELLOW)
25
26
  end
26
27
 
27
28
  def self.paint_by_count(text, count)
28
- code = count.even? ? random_code : gradient_code(count)
29
+ code = Lexdrill::Color.random? ? random_code : gradient_code(count)
29
30
  wrap(text, code)
30
31
  end
31
32
 
@@ -1,24 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
4
+
5
+ # All of lexdrill's persisted state lives under this one directory in the
6
+ # user's home folder — a single global config, with no per-project or
7
+ # per-directory overrides.
3
8
  module Lexdrill::Config
4
- FILENAME = ".drill.txt"
5
- COUNTER_FILENAME = ".drill.counter"
6
- STATS_FILENAME = ".drill.stats"
9
+ DIR = File.join(Dir.home, ".drill")
7
10
 
8
- def self.dir_path
9
- cwd = Dir.pwd
10
- cwd if File.exist?(File.join(cwd, FILENAME))
11
- end
11
+ # 0700: this directory can hold real secrets (OAuth refresh token, service
12
+ # account key), same reasoning as their own individual 0600 chmods.
13
+ FileUtils.mkdir_p(DIR, mode: 0o700)
12
14
 
13
- def self.base_path
14
- ENV.fetch("LEXDRILL_PATH", nil) || dir_path || Dir.home
15
+ def self.path(filename)
16
+ File.join(DIR, filename)
15
17
  end
16
-
17
- def self.home_drill_path
18
- File.join(Dir.home, FILENAME)
19
- end
20
-
21
- DRILL_PATH = File.join(base_path, FILENAME)
22
- COUNTER_PATH = File.join(base_path, COUNTER_FILENAME)
23
- STATS_PATH = File.join(base_path, STATS_FILENAME)
24
18
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The starter list seeded into ~/.drill.txt on first run, when neither an
4
- # LEXDRILL_PATH override nor a project-local .drill.txt exists yet.
3
+ # The starter list seeded into the words file on first run.
5
4
  module Lexdrill::DefaultWords
6
5
  WORDS = [
7
6
  "The map is not the territory",
@@ -14,7 +14,7 @@ require "fileutils"
14
14
  # PATH below, obtained only after this specific user's own interactive
15
15
  # consent, which never leaves this machine and is never published.
16
16
  module Lexdrill::GoogleAuth
17
- PATH = File.join(Dir.home, ".drill.gcp-token.json")
17
+ PATH = Lexdrill::Config.path("gcp-token.json")
18
18
 
19
19
  CLIENT_ID = "608056429593-ncdgh4bnfpuf9486l2rlt7g763h1gl6g.apps.googleusercontent.com"
20
20
  CLIENT_SECRET = "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"
@@ -14,8 +14,8 @@ module Lexdrill::Inspector
14
14
  Toggle: #{toggle_summary}
15
15
  Beat: #{beat_summary}
16
16
  Rand: #{rand_summary}
17
- Format: #{Lexdrill::Format.current}
18
- LEXDRILL_PATH: #{ENV.fetch('LEXDRILL_PATH', '(not set)')}
17
+ Color: #{Lexdrill::Color.current}
18
+ Config dir: #{Lexdrill::Config::DIR}
19
19
  REPORT
20
20
  end
21
21
 
@@ -1,34 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Formats a shown word for display. In "simple" mode: the drill sign (in
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
- # "{counter}/{total}{SEPARATOR}[{loop_start}-{loop_end}]\n{word}" counter
7
- # is the word's own 1-based position in the list, re-derived through
8
- # Lexdrill::Beat so it stays meaningful even when a rhythm repeats steps.
3
+ # Formats a shown word for display, on one line:
4
+ # "{index}/{total}{SEPARATOR} {word}" index is the word's own 1-based
5
+ # position in the list, re-derived through Lexdrill::Beat so it stays
6
+ # meaningful even when a rhythm repeats steps. The counter/total and the
7
+ # separator are painted yellow, as one unit; the word is colored by its
8
+ # show count (see Lexdrill::Colorizer).
9
9
  module Lexdrill::LineFormatter
10
10
  SEPARATOR = "⟳"
11
11
 
12
12
  def self.format(word)
13
- return simple(word) if Lexdrill::Format.simple?
14
-
15
- full(word)
16
- end
17
-
18
- def self.simple(word)
19
- count = Lexdrill::Stats.counts.fetch(word, 0)
20
- "#{Lexdrill::Colorizer.paint_blue(SEPARATOR)} #{Lexdrill::Colorizer.paint_by_count(word, count)}"
21
- end
22
-
23
- def self.full(word)
24
13
  total = Lexdrill::WordList.words.size
25
- info = loop_info(total)
26
- "#{info.index + 1}/#{total}#{SEPARATOR}[#{info.chunk_start}-#{info.chunk_end}]\n#{word}"
14
+ count = Lexdrill::Stats.counts.fetch(word, 0)
15
+ prefix = Lexdrill::Colorizer.paint_yellow("#{loop_index(total) + 1}/#{total}#{SEPARATOR}")
16
+ "#{prefix} #{Lexdrill::Colorizer.paint_by_count(word, count)}"
27
17
  end
28
18
 
29
- def self.loop_info(total)
19
+ def self.loop_index(total)
30
20
  counter = Lexdrill::Counter.new(Lexdrill::WordList::COUNTER_PATH)
31
21
  step_used = counter.value - 1
32
- Lexdrill::Beat.loop_info(total, step_used)
22
+ Lexdrill::Beat.loop_info(total, step_used).index
33
23
  end
34
24
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Global "which Google Sheet to export to via the OAuth device-flow login"
4
- # setting. Lives at ~/.drill.oauth-remote as a plain spreadsheet id, parsed
5
- # out of a normal Google Sheets share URL. Set via `drill oauth <url>`.
6
- # See Lexdrill::Remote for the separate, service-account-based flow.
4
+ # setting. Stored as a plain spreadsheet id, parsed out of a normal
5
+ # Google Sheets share URL. Set via `drill oauth <url>`. See
6
+ # Lexdrill::Remote for the separate, service-account-based flow.
7
7
  module Lexdrill::OauthRemote
8
- PATH = File.join(Dir.home, ".drill.oauth-remote")
8
+ PATH = Lexdrill::Config.path("oauth-remote")
9
9
  URL_PATTERN = %r{/spreadsheets/d/([a-zA-Z0-9_-]+)}
10
10
 
11
11
  def self.set(url)
data/lib/lexdrill/rand.rb CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  # Global "how often to actually show a word" setting, applied by
4
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).
5
+ # invocations respect it equally). Stored as a plain integer; defaults to
6
+ # 1 (show every time).
7
7
  module Lexdrill::Rand
8
- PATH = File.join(Dir.home, ".drill.rand")
8
+ PATH = Lexdrill::Config.path("rand")
9
9
  DEFAULT = 1
10
10
 
11
11
  def self.value
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Global "which Google Sheet to export to via a service account" setting.
4
- # Lives at ~/.drill.remote as a plain spreadsheet id, parsed out of a normal
5
- # Google Sheets share URL. Set via `drill remote <url>` — the user shares
6
- # that spreadsheet with their service account's email directly in Google
4
+ # Stored as a plain spreadsheet id, parsed out of a normal Google Sheets
5
+ # share URL. Set via `drill remote <url>` — the user shares that
6
+ # spreadsheet with their service account's email directly in Google
7
7
  # Sheets, so no interactive Google sign-in is needed at export/import time.
8
8
  # See Lexdrill::OauthRemote for the separate, personal-login-based flow.
9
9
  module Lexdrill::Remote
10
- PATH = File.join(Dir.home, ".drill.remote")
10
+ PATH = Lexdrill::Config.path("remote")
11
11
  URL_PATTERN = %r{/spreadsheets/d/([a-zA-Z0-9_-]+)}
12
12
 
13
13
  def self.set(url)
@@ -13,7 +13,7 @@ require "base64"
13
13
  # published, and lexdrill never writes or transmits it anywhere except in
14
14
  # the signed JWT sent directly to Google's own token endpoint.
15
15
  module Lexdrill::ServiceAccountAuth
16
- PATH = File.join(Dir.home, ".drill.gcp-service-account.json")
16
+ PATH = Lexdrill::Config.path("gcp-service-account.json")
17
17
  TOKEN_URL = "https://oauth2.googleapis.com/token"
18
18
  SCOPE = "https://www.googleapis.com/auth/spreadsheets"
19
19
  GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
@@ -3,15 +3,14 @@
3
3
  require "json"
4
4
 
5
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.
6
+ # as a word => count JSON object in the stats file. Every BUCKET_SIZE shows
7
+ # moves a word into the next display-color bucket (see Lexdrill::Colorizer);
8
+ # once a word reaches GRADUATION_THRESHOLD shows it's considered mastered
9
+ # and `next` stops selecting it.
11
10
  module Lexdrill::Stats
12
- PATH = Lexdrill::Config::STATS_PATH
13
- BUCKET_SIZE = 100
14
- GRADUATION_THRESHOLD = 1200
11
+ PATH = Lexdrill::Config.path("stats")
12
+ BUCKET_SIZE = 10
13
+ GRADUATION_THRESHOLD = 120
15
14
 
16
15
  def self.record(word)
17
16
  data = load
@@ -2,11 +2,10 @@
2
2
 
3
3
  require "fileutils"
4
4
 
5
- # Global pause/resume switch for the shell hook, independent of which
6
- # project's .drill.txt is active. Enabled by default; the marker file's
7
- # presence means stopped.
5
+ # Global pause/resume switch for the shell hook. Enabled by default; the
6
+ # marker file's presence means stopped.
8
7
  module Lexdrill::Toggle
9
- PATH = File.join(Dir.home, ".drill.disabled")
8
+ PATH = Lexdrill::Config.path("disabled")
10
9
 
11
10
  def self.enabled?
12
11
  !File.exist?(PATH)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lexdrill
4
- VERSION = "0.17.0"
4
+ VERSION = "0.19.0"
5
5
  end
@@ -1,28 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Reads vocabulary words/phrases from a `.drill.txt` file, one per line, and
3
+ # Reads vocabulary words/phrases from the words file, one per line, and
4
4
  # advances a persisted counter to show "the current word" each time. On a
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. Words
7
- # that have graduated (see Lexdrill::Stats) are excluded from `.next` but
8
- # remain in `.words` for `list`/`stats` to report.
5
+ # fresh install, seeds the file with a default starter list instead of
6
+ # coming up empty. Words that have graduated (see Lexdrill::Stats) are
7
+ # excluded from `.next` but remain in `.words` for `list`/`stats` to report.
9
8
  class Lexdrill::WordList
10
- PATH = Lexdrill::Config::DRILL_PATH
11
- COUNTER_PATH = Lexdrill::Config::COUNTER_PATH
12
- HOME_PATH = Lexdrill::Config.home_drill_path
9
+ PATH = Lexdrill::Config.path("words")
10
+ COUNTER_PATH = Lexdrill::Config.path("counter")
13
11
 
14
12
  def self.words
15
- seed_default_at_home
13
+ seed_default
16
14
  @words ||= File.exist?(PATH) && File.readlines(PATH, encoding: "UTF-8").map(&:strip).reject(&:empty?)
17
15
  @words ||= []
18
16
  end
19
17
 
20
- def self.seed_default_at_home
21
- return if PATH != HOME_PATH || File.exist?(PATH)
18
+ def self.seed_default
19
+ return if File.exist?(PATH)
22
20
 
23
21
  File.write(PATH, Lexdrill::DefaultWords::TEXT, encoding: "UTF-8")
24
22
  end
25
- private_class_method :seed_default_at_home
23
+ private_class_method :seed_default
26
24
 
27
25
  def self.next
28
26
  active = active_words
data/lib/lexdrill.rb CHANGED
@@ -17,7 +17,7 @@ require_relative "lexdrill/service_account_auth"
17
17
  require_relative "lexdrill/sheets_client"
18
18
  require_relative "lexdrill/shell_snippet"
19
19
  require_relative "lexdrill/inspector"
20
+ require_relative "lexdrill/color"
20
21
  require_relative "lexdrill/colorizer"
21
- require_relative "lexdrill/format"
22
22
  require_relative "lexdrill/line_formatter"
23
23
  require_relative "lexdrill/cli"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lexdrill
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siarhei Kisliak
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-11 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: lexdrill prints a vocabulary word or phrase on demand, tracking how often
14
14
  each one has been shown.
@@ -27,11 +27,11 @@ files:
27
27
  - lib/lexdrill.rb
28
28
  - lib/lexdrill/beat.rb
29
29
  - lib/lexdrill/cli.rb
30
+ - lib/lexdrill/color.rb
30
31
  - lib/lexdrill/colorizer.rb
31
32
  - lib/lexdrill/config.rb
32
33
  - lib/lexdrill/counter.rb
33
34
  - lib/lexdrill/default_words.rb
34
- - lib/lexdrill/format.rb
35
35
  - lib/lexdrill/google_auth.rb
36
36
  - lib/lexdrill/http_client.rb
37
37
  - lib/lexdrill/inspector.rb
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Global display mode for `next`'s output: "simple" (the drill sign + word)
4
- # or "full" (position + loop info). Lives at ~/.drill.format; defaults to
5
- # "simple".
6
- module Lexdrill::Format
7
- PATH = File.join(Dir.home, ".drill.format")
8
- SIMPLE = "simple"
9
- FULL = "full"
10
- VALID = [SIMPLE, FULL].freeze
11
-
12
- def self.current
13
- return SIMPLE unless File.exist?(PATH)
14
-
15
- value = File.read(PATH).strip
16
- VALID.include?(value) ? value : SIMPLE
17
- end
18
-
19
- def self.set(mode)
20
- File.write(PATH, mode)
21
- end
22
-
23
- def self.simple?
24
- current == SIMPLE
25
- end
26
- end