lexdrill 0.3.0 → 0.5.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: 694f430036d22f39f5ee347a6220a8e977e726edb54db9f5c99d3f57bfcb8161
4
- data.tar.gz: 9deb40864be7ded7ed8eb1aff88e25e20a412acf0a678526ff5d3f84f50ad69b
3
+ metadata.gz: 9ef368252ef581d3e56aadeef48302fef93cae68d52e95a5bb199977ffdf30fd
4
+ data.tar.gz: 7fb96023b0940dbce09e0cdb60d6561cc50ab52050c73536f777ca879f6f8c8d
5
5
  SHA512:
6
- metadata.gz: 49b05b7ac7dda95a4fd9c34b9de6fbe29486e8a8e84285d8fbb0fe0a211785a9a7870551c3a80566725d4884dee317117c85781a313251c9094fa927a4ff857c
7
- data.tar.gz: 201a4d5c23a232a4cbb1ca5b2d294e629a9984e1196133511d62262e3c8ae3c238386278735f91d184db4b7214578304861caa70b641c6f0b77988cc2f4ef00f
6
+ metadata.gz: 9f0786367ac59cf02606f3c6a59dec98787bb54fc938924c90321d122e8e793bff09a357220c59f4d8a3d2e705ab621ae5fbbb46fc3cc02b787aac9e38063049
7
+ data.tar.gz: d7ec11ee093daff986f0dd1a08ac521bf6067f0e7debf682a8183cb940c17d5a58c5f5ef7aa68c257fdd184b09017734b03dfaad7c6c357e3f92ee64c09919bb
data/README.md CHANGED
@@ -40,17 +40,28 @@ banana
40
40
  cherry
41
41
  ```
42
42
 
43
- Run `drill next` to print the current word and advance to the next one. Each
44
- line is printed as `current/total:` followed by a tab and the word, in a
45
- randomly-picked color, e.g.:
43
+ If you don't create one, `drill next` seeds `~/.drill.txt` with a default
44
+ starter list (a set of NLP presuppositions) the first time it runs, so
45
+ there's always something to drill.
46
46
 
47
+ 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 (in a
52
+ separately-picked random color), all on one line:
47
53
  ```
48
- 1/3: apple
49
- 2/3: banana
50
- 3/3: cherry
51
- 1/3: apple
54
+ apple
52
55
  ```
53
56
 
57
+ **full** — `counter/total⟳[loop_start-loop_end]` on one line, the word on
58
+ the next, in a randomly-picked color:
59
+ ```
60
+ 1/6⟳[1-3]
61
+ apple
62
+ ```
63
+ (word 1 of 6 total; currently in the loop spanning words 1-3)
64
+
54
65
  ### Shell integration (one-time setup)
55
66
 
56
67
  Add one line to your shell's rc file so the current word prints automatically
@@ -99,11 +110,44 @@ everything looks correctly configured.
99
110
  hook block above, so it runs first) instead of relying on it only being in
100
111
  `~/.zlogin` / `~/.bash_profile`. It's safe to leave it in both places.
101
112
 
113
+ ### Rhythm (`beat`)
114
+
115
+ By default `next` just advances one word at a time. You can instead have it
116
+ repeat loops of consecutive words — e.g. a loop of 3 words shown twice each
117
+ before moving on:
118
+
119
+ ```bash
120
+ drill beat 3 2 # loop size 3, repeat each loop 2 times
121
+ ```
122
+
123
+ `drill beat none` turns it back off (plain word-by-word again). This is a
124
+ **global** setting — it applies everywhere, independent of which project's
125
+ `.drill.txt` is currently active.
126
+
127
+ There are also named shortcuts for common loop sizes, one word/phrase apart:
128
+
129
+ | Loop size | Alias |
130
+ |-----------|----------|
131
+ | 2 | `polka` |
132
+ | 3 | `waltz` |
133
+ | 4 | `rock` |
134
+ | 5 | `jazz` |
135
+ | 6 | `jiga` |
136
+ | 7 | `balkan` |
137
+ | 8 | `samba` |
138
+
139
+ `drill waltz 16` is shorthand for `drill beat 3 16`. Repetitions default to
140
+ `8` if you leave them off — `drill jazz` alone is `drill beat 5 8`, and
141
+ `drill beat 4` alone is `drill beat 4 8`.
142
+
102
143
  ### Commands
103
144
 
104
145
  | Command | What it does |
105
146
  |---|---|
106
147
  | `drill next` | Print the current word and advance |
107
148
  | `drill start` / `drill stop` | Pause/resume the automatic per-prompt hook (doesn't affect manual `next`) |
108
- | `drill inspect` | Show the active `.drill.txt`/`.drill.counter` paths, word count, counter value, and toggle state |
149
+ | `drill inspect` | Show the active `.drill.txt`/`.drill.counter` paths, word count, counter value, toggle, and beat state |
109
150
  | `drill hook zsh\|bash` | Print the shell integration snippet (used above) |
151
+ | `drill beat <2-8> <repetitions>` / `drill beat none` | Set or disable the rhythm |
152
+ | `drill polka\|waltz\|rock\|jazz\|jiga\|balkan\|samba <repetitions>` | Shorthand for a fixed loop size (see table above) |
153
+ | `drill format simple\|full` | Set the output style (`simple` is the default) |
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
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
+ module Lexdrill::Beat
10
+ PATH = File.join(Dir.home, ".drill.beat")
11
+ MIN_LOOP_SIZE = 2
12
+ MAX_LOOP_SIZE = 8
13
+ DEFAULT_REPETITIONS = 8
14
+
15
+ ALIASES = {
16
+ "polka" => 2,
17
+ "waltz" => 3,
18
+ "rock" => 4,
19
+ "jazz" => 5,
20
+ "jiga" => 6,
21
+ "balkan" => 7,
22
+ "samba" => 8
23
+ }.freeze
24
+
25
+ # index: 0-based word index. chunk_start/chunk_end: 1-based word positions
26
+ # spanned by the current loop. loop_number: which repeat pass (1-based)
27
+ # through the current loop. total_loops: the configured repetitions.
28
+ LoopInfo = Struct.new(:index, :chunk_start, :chunk_end, :loop_number, :total_loops, keyword_init: true)
29
+
30
+ def self.configured?
31
+ File.exist?(PATH)
32
+ end
33
+
34
+ def self.set(loop_size, repetitions)
35
+ File.write(PATH, "#{loop_size} #{repetitions}")
36
+ end
37
+
38
+ def self.clear
39
+ FileUtils.rm_f(PATH)
40
+ end
41
+
42
+ def self.valid_loop_size?(value)
43
+ (MIN_LOOP_SIZE..MAX_LOOP_SIZE).cover?(value)
44
+ end
45
+
46
+ def self.repetitions_or_default(value)
47
+ value ? value.to_i : DEFAULT_REPETITIONS
48
+ end
49
+
50
+ def self.loop_size
51
+ settings[0]
52
+ end
53
+
54
+ def self.repetitions
55
+ settings[1]
56
+ end
57
+
58
+ # Total steps in one full cycle through the word list at the current
59
+ # rhythm, or plain `word_count` when no beat is configured.
60
+ def self.cycle_length(word_count)
61
+ return word_count unless configured?
62
+
63
+ chunk_sizes(word_count).sum { |size| size * repetitions }
64
+ end
65
+
66
+ # Maps a step (already bounded within cycle_length) to a word index.
67
+ def self.index_for(word_count, step)
68
+ loop_info(word_count, step).index
69
+ end
70
+
71
+ # Full detail on where `step` falls: word index, the current loop's word
72
+ # range, which repeat pass it is, and how many total. A single "loop"
73
+ # spanning the whole list (shown once) when no beat is configured.
74
+ def self.loop_info(word_count, step)
75
+ unless configured?
76
+ return LoopInfo.new(index: step, chunk_start: 1, chunk_end: word_count, loop_number: 1, total_loops: 1)
77
+ end
78
+
79
+ locate(word_count, step)
80
+ end
81
+
82
+ def self.locate(word_count, step)
83
+ offset = 0
84
+ chunk_sizes(word_count).each do |size|
85
+ span = size * repetitions
86
+ return locate_within(offset, size, step) if step < span
87
+
88
+ step -= span
89
+ offset += size
90
+ end
91
+ LoopInfo.new(index: 0, chunk_start: 1, chunk_end: word_count, loop_number: 1, total_loops: 1)
92
+ end
93
+ private_class_method :locate
94
+
95
+ def self.locate_within(offset, size, step)
96
+ LoopInfo.new(
97
+ index: offset + (step % size),
98
+ chunk_start: offset + 1,
99
+ chunk_end: offset + size,
100
+ loop_number: (step / size) + 1,
101
+ total_loops: repetitions
102
+ )
103
+ end
104
+ private_class_method :locate_within
105
+
106
+ def self.settings
107
+ return [nil, nil] unless configured?
108
+
109
+ parts = File.read(PATH).strip.split
110
+ [parts[0].to_i, parts[1].to_i]
111
+ end
112
+ private_class_method :settings
113
+
114
+ def self.chunk_sizes(word_count)
115
+ full, remainder = word_count.divmod(loop_size)
116
+ sizes = Array.new(full, loop_size)
117
+ sizes << remainder if remainder.positive?
118
+ sizes
119
+ end
120
+ private_class_method :chunk_sizes
121
+ end
data/lib/lexdrill/cli.rb CHANGED
@@ -8,7 +8,10 @@ class Lexdrill::CLI
8
8
  run_hook: %w[hook],
9
9
  run_start: %w[start],
10
10
  run_stop: %w[stop],
11
- run_inspect: %w[inspect]
11
+ run_inspect: %w[inspect],
12
+ run_beat: %w[beat],
13
+ run_beat_alias: %w[polka waltz rock jazz jiga balkan samba],
14
+ run_format: %w[format]
12
15
  }.freeze
13
16
 
14
17
  def self.start(argv = ARGV)
@@ -48,6 +51,11 @@ class Lexdrill::CLI
48
51
  drill start Resume drilling (undoes stop)
49
52
  drill stop Pause drilling everywhere until `start`
50
53
  drill inspect Show the active config/counter/toggle state
54
+ drill beat <2-8> [repetitions] Set the rhythm (repetitions defaults to 8)
55
+ drill beat none Disable the rhythm
56
+ drill polka|waltz|rock|jazz|jiga|balkan|samba [repetitions]
57
+ Shorthand for a fixed loop size (2 through 8, in order)
58
+ drill format simple|full Set the output style (simple is the default)
51
59
  HELP
52
60
  0
53
61
  end
@@ -56,10 +64,17 @@ class Lexdrill::CLI
56
64
  word = Lexdrill::WordList.next
57
65
  return print_no_words(Lexdrill::WordList::PATH) unless word
58
66
 
59
- puts Lexdrill::Colorizer.paint(Lexdrill::LineFormatter.format(word))
67
+ puts colored_line(word)
60
68
  0
61
69
  end
62
70
 
71
+ def colored_line(word)
72
+ text = Lexdrill::LineFormatter.format(word)
73
+ return text if Lexdrill::Format.simple?
74
+
75
+ Lexdrill::Colorizer.paint(text)
76
+ end
77
+
63
78
  def print_no_words(path)
64
79
  warn "drill: no words found in #{path}"
65
80
  1
@@ -100,6 +115,61 @@ class Lexdrill::CLI
100
115
  0
101
116
  end
102
117
 
118
+ def run_beat
119
+ arg = argv[1]
120
+ return print_beat_usage unless arg
121
+ return clear_beat if arg == "none"
122
+
123
+ set_beat(arg.to_i, Lexdrill::Beat.repetitions_or_default(argv[2]))
124
+ end
125
+
126
+ def run_beat_alias
127
+ loop_size = Lexdrill::Beat::ALIASES.fetch(argv.first)
128
+ set_beat(loop_size, Lexdrill::Beat.repetitions_or_default(argv[1]))
129
+ end
130
+
131
+ def set_beat(loop_size, repetitions)
132
+ return print_invalid_loop_size unless Lexdrill::Beat.valid_loop_size?(loop_size)
133
+ return print_invalid_repetitions unless repetitions.positive?
134
+
135
+ Lexdrill::Beat.set(loop_size, repetitions)
136
+ 0
137
+ end
138
+
139
+ def clear_beat
140
+ Lexdrill::Beat.clear
141
+ 0
142
+ end
143
+
144
+ def print_beat_usage
145
+ warn "usage: drill beat <#{Lexdrill::Beat::MIN_LOOP_SIZE}-#{Lexdrill::Beat::MAX_LOOP_SIZE}> " \
146
+ "<repetitions> | drill beat none"
147
+ 1
148
+ end
149
+
150
+ def print_invalid_loop_size
151
+ warn "drill: loop size must be between #{Lexdrill::Beat::MIN_LOOP_SIZE} and #{Lexdrill::Beat::MAX_LOOP_SIZE}"
152
+ 1
153
+ end
154
+
155
+ def print_invalid_repetitions
156
+ warn "drill: repetitions must be a positive number"
157
+ 1
158
+ end
159
+
160
+ def run_format
161
+ mode = argv[1]
162
+ return print_format_usage unless Lexdrill::Format::VALID.include?(mode)
163
+
164
+ Lexdrill::Format.set(mode)
165
+ 0
166
+ end
167
+
168
+ def print_format_usage
169
+ warn "usage: drill format <#{Lexdrill::Format::VALID.join('|')}>"
170
+ 1
171
+ end
172
+
103
173
  def print_unknown_command(command)
104
174
  warn "drill: unknown command #{command.inspect}"
105
175
  print_help
@@ -1,10 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Wraps text in a randomly-picked ANSI color code from a fixed palette.
3
+ # Wraps text in an ANSI color code.
4
4
  module Lexdrill::Colorizer
5
5
  CODES = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96].freeze
6
+ BLUE = 34
6
7
 
7
8
  def self.paint(text)
8
- "\e[#{CODES.sample}m#{text}\e[0m"
9
+ wrap(text, CODES.sample)
9
10
  end
11
+
12
+ def self.paint_blue(text)
13
+ wrap(text, BLUE)
14
+ end
15
+
16
+ def self.wrap(text, code)
17
+ "\e[#{code}m#{text}\e[0m"
18
+ end
19
+ private_class_method :wrap
10
20
  end
@@ -13,6 +13,10 @@ module Lexdrill::Config
13
13
  ENV.fetch("LEXDRILL_PATH", nil) || dir_path || Dir.home
14
14
  end
15
15
 
16
+ def self.home_drill_path
17
+ File.join(Dir.home, FILENAME)
18
+ end
19
+
16
20
  DRILL_PATH = File.join(base_path, FILENAME)
17
21
  COUNTER_PATH = File.join(base_path, COUNTER_FILENAME)
18
22
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
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.
5
+ module Lexdrill::DefaultWords
6
+ WORDS = [
7
+ "The map is not the territory",
8
+ "Respect for other people's model of the world",
9
+ "Mind and body are parts of the same system",
10
+ "You cannot not communicate",
11
+ "The meaning of your communication is the response you get",
12
+ "People operate out of the best choice available to them",
13
+ "Every behavior has a positive intention",
14
+ "People have all the resources they need to succeed",
15
+ "There is no failure only feedback",
16
+ "If what you are doing isn't working do something else",
17
+ "The person with the most flexibility controls the system",
18
+ "Choice is better than no choice",
19
+ "If one person can do something anyone can learn to do it",
20
+ "Modeling successful performance leads to excellence",
21
+ "All procedures should increase wholeness and choice",
22
+ "Individuals are not their behaviors",
23
+ "Resistance in a listener is a sign of a lack of flexibility in the communicator",
24
+ "We process all information through our five senses",
25
+ "Clean feedback is the breakfast of champions",
26
+ "Possible in the world is possible for me it is only a matter of how"
27
+ ].freeze
28
+
29
+ TEXT = "#{WORDS.join("\n")}\n".freeze
30
+ end
@@ -0,0 +1,26 @@
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
@@ -11,6 +11,8 @@ module Lexdrill::Inspector
11
11
  Words file: #{Lexdrill::WordList::PATH} (#{words_summary})
12
12
  Counter file: #{Lexdrill::WordList::COUNTER_PATH} (value: #{counter_value})
13
13
  Toggle: #{toggle_summary}
14
+ Beat: #{beat_summary}
15
+ Format: #{Lexdrill::Format.current}
14
16
  LEXDRILL_PATH: #{ENV.fetch('LEXDRILL_PATH', '(not set)')}
15
17
  REPORT
16
18
  end
@@ -30,4 +32,10 @@ module Lexdrill::Inspector
30
32
 
31
33
  "stopped (#{Lexdrill::Toggle::PATH})"
32
34
  end
35
+
36
+ def self.beat_summary
37
+ return "not set (plain word-by-word)" unless Lexdrill::Beat.configured?
38
+
39
+ "loop #{Lexdrill::Beat.loop_size}, repeat #{Lexdrill::Beat.repetitions}"
40
+ end
33
41
  end
@@ -1,11 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Formats a shown word as "{line_number}/{total}:\t{word}". The just-advanced
4
- # counter value equals the 1-based position of the word that was shown.
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):
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.
5
9
  module Lexdrill::LineFormatter
10
+ SEPARATOR = "⟳"
11
+
6
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
+ "#{Lexdrill::Colorizer.paint_blue(SEPARATOR)} #{Lexdrill::Colorizer.paint(word)}"
20
+ end
21
+
22
+ def self.full(word)
7
23
  total = Lexdrill::WordList.words.size
8
- line_number = Lexdrill::Counter.new(Lexdrill::WordList::COUNTER_PATH).value
9
- "#{line_number}/#{total}:\t#{word}"
24
+ info = loop_info(total)
25
+ "#{info.index + 1}/#{total}#{SEPARATOR}[#{info.chunk_start}-#{info.chunk_end}]\n#{word}"
26
+ end
27
+
28
+ def self.loop_info(total)
29
+ counter = Lexdrill::Counter.new(Lexdrill::WordList::COUNTER_PATH)
30
+ step_used = counter.value - 1
31
+ Lexdrill::Beat.loop_info(total, step_used)
10
32
  end
11
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lexdrill
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -1,16 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Reads vocabulary words/phrases from a `.drill.txt` file, one per line, and
4
- # advances a persisted counter to show "the current word" each time.
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.
5
7
  class Lexdrill::WordList
6
8
  PATH = Lexdrill::Config::DRILL_PATH
7
9
  COUNTER_PATH = Lexdrill::Config::COUNTER_PATH
10
+ HOME_PATH = Lexdrill::Config.home_drill_path
8
11
 
9
12
  def self.words
10
- @words ||= File.exist?(PATH) && File.readlines(PATH).map(&:strip).reject(&:empty?)
13
+ seed_default_at_home
14
+ @words ||= File.exist?(PATH) && File.readlines(PATH, encoding: "UTF-8").map(&:strip).reject(&:empty?)
11
15
  @words ||= []
12
16
  end
13
17
 
18
+ def self.seed_default_at_home
19
+ return if PATH != HOME_PATH || File.exist?(PATH)
20
+
21
+ File.write(PATH, Lexdrill::DefaultWords::TEXT, encoding: "UTF-8")
22
+ end
23
+ private_class_method :seed_default_at_home
24
+
14
25
  def self.next
15
26
  return nil if words.empty?
16
27
 
@@ -19,8 +30,9 @@ class Lexdrill::WordList
19
30
 
20
31
  def self.take_index(size)
21
32
  counter = Lexdrill::Counter.new(COUNTER_PATH)
22
- index = counter.bounded_value(size)
33
+ cycle_length = Lexdrill::Beat.cycle_length(size)
34
+ step = counter.bounded_value(cycle_length)
23
35
  counter.increment
24
- index
36
+ Lexdrill::Beat.index_for(size, step)
25
37
  end
26
38
  end
data/lib/lexdrill.rb CHANGED
@@ -3,10 +3,13 @@
3
3
  require_relative "lexdrill/version"
4
4
  require_relative "lexdrill/config"
5
5
  require_relative "lexdrill/counter"
6
+ require_relative "lexdrill/beat"
7
+ require_relative "lexdrill/default_words"
6
8
  require_relative "lexdrill/word_list"
7
9
  require_relative "lexdrill/toggle"
8
10
  require_relative "lexdrill/shell_snippet"
9
11
  require_relative "lexdrill/inspector"
10
12
  require_relative "lexdrill/colorizer"
13
+ require_relative "lexdrill/format"
11
14
  require_relative "lexdrill/line_formatter"
12
15
  require_relative "lexdrill/cli"
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Siarhei Kisliak
@@ -25,10 +25,13 @@ files:
25
25
  - bin/drill
26
26
  - lexdrill.gemspec
27
27
  - lib/lexdrill.rb
28
+ - lib/lexdrill/beat.rb
28
29
  - lib/lexdrill/cli.rb
29
30
  - lib/lexdrill/colorizer.rb
30
31
  - lib/lexdrill/config.rb
31
32
  - lib/lexdrill/counter.rb
33
+ - lib/lexdrill/default_words.rb
34
+ - lib/lexdrill/format.rb
32
35
  - lib/lexdrill/inspector.rb
33
36
  - lib/lexdrill/line_formatter.rb
34
37
  - lib/lexdrill/shell_snippet.rb