teletype 0.0.0 → 1.2.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: 3f113b33716079b6147b8511771b0112ec42044981ba543abaca849211563be1
4
- data.tar.gz: 85ded73b628942847ef1f6553288afe6eff1cfc02b0ee152bbb2ac4bb3ba790a
3
+ metadata.gz: 40a3e0c38deacc68555c1440a9fe679afc623e93decc137836a3066aa6710521
4
+ data.tar.gz: 640e87ce91e95e40b37e5f768e70ae0e355f68f810c51b1862e2fcdf6dd1ddd5
5
5
  SHA512:
6
- metadata.gz: be9dad8e1a5edf3d2885b1c19527609d64684abdcdd3fdb4cbfde71641e9a0526282c8ce1b5e01c92b28506e38f41e60914c09138675c6bf52e7e17a84582ec2
7
- data.tar.gz: 221045d24b990dd28cf91b9fc51880e8d99c06c13bd38dbea18db922b28ec54517d02ded848f7d934455e34ab3f63b1a6d0b7cdc3b1410a4a14de3fe40848fc5
6
+ metadata.gz: 0b885502503555e6d5874eeb897928d37db231e72d6467438ebbeba43d810b3a0e8ad7415c98eb7330017e38fe48465732a495f96ad1f2e3ca521e8184117453
7
+ data.tar.gz: 5146ce812b50f336daa87cbaa256c1227c49481e32921e9235cc026bc7238dd8e7712cb8ddd5d869b0db64b2cef233d6291cddc9b51d1178926e4d995d7a213e
data/README.md CHANGED
@@ -12,7 +12,15 @@
12
12
  ### Usage
13
13
 
14
14
  Practice numeric keys:
15
- `teletype number`
15
+ `teletype base/number.txt`
16
+
17
+ Practice basic keys(numeric and letters):
18
+ `teletype base`
16
19
 
17
20
  Practice with ruby code:
18
21
  `teletype ruby`
22
+
23
+ ### Custom text
24
+
25
+ You can copy text files to **exercise/custom** directory and practice with:
26
+ `teletype custom` or `teletype custom/poem.txt`
data/Rakefile CHANGED
@@ -1,14 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rake/testtask'
2
4
 
3
5
  Rake::TestTask.new do |t|
4
6
  t.libs << 'test'
5
7
  end
6
8
 
7
- desc "Run tests"
8
- task :default => :test
9
-
9
+ desc 'Run tests'
10
+ task default: :test
10
11
 
11
- desc "Testing usage for fast iteration"
12
+ desc 'Testing usage for fast iteration'
12
13
  task :usage do
13
14
  puts 'gem uninstall teletype -x'
14
15
  puts `gem uninstall teletype -x`
data/bin/teletype CHANGED
@@ -1,3 +1,83 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- puts "Please use version above 0.0.0"
4
+ require 'optparse'
5
+ require_relative '../lib/teletype'
6
+
7
+ exercises = File.join(File.dirname(__FILE__), '../', 'exercise')
8
+
9
+ profile = nil
10
+ height = 5
11
+ width = 120
12
+ suggest = 3
13
+ verbose = false
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = 'Usage: teletype [options] [exercise]'
17
+
18
+ opts.on('--help', 'Prints this help') do
19
+ puts opts
20
+ exit
21
+ end
22
+
23
+ opts.on('-l', '--list', 'List exercise files') do |_v|
24
+ Dir.glob(File.join(exercises, '**', '*.*')).each do |path|
25
+ puts path.sub(File.join(exercises, ''), '')
26
+ end
27
+ exit
28
+ end
29
+
30
+ opts.on('-p', '--profile [DIR]', 'Specify profile dir. Defaults to ~/.teletype') do |f|
31
+ profile = f
32
+ end
33
+
34
+ opts.on('-s', '--suggest [LINES]', Integer,
35
+ "Suggested practice lines that needs attention. Defaults to #{suggest}") do |s|
36
+ suggest = s
37
+ end
38
+
39
+ opts.on('-h', '--height [HEIGHT]', Integer, "Height of practice window. Defaults to #{height}") do |h|
40
+ height = h
41
+ end
42
+ opts.on('-w', '--width [WIDTH]', Integer, "Width of practice window. Defaults to #{width}") do |w|
43
+ width = w
44
+ end
45
+ opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
46
+ verbose = v
47
+ end
48
+ end.parse!
49
+
50
+ profile ||= File.join(Dir.home, '.teletype')
51
+ Dir.mkdir(profile) unless File.exist?(profile)
52
+
53
+ paths = ARGV
54
+ paths << 'base' if paths.length.zero?
55
+
56
+ text = ''
57
+ paths.each do |path|
58
+ path = File.join(exercises, path)
59
+
60
+ if File.directory?(path)
61
+ Dir.glob(File.join(path, '*.*')).each do |file|
62
+ text += File.read(file)
63
+ end
64
+ elsif File.exist?(path)
65
+ text += File.read(path)
66
+ end
67
+ end
68
+
69
+ screen = Teletype::Screen.new(height, width, verbose: verbose)
70
+ stats = Teletype::Stats.new(profile, text)
71
+ paginator = Teletype::Paginator.new(
72
+ profile, text,
73
+ suggest: suggest,
74
+ screen: screen,
75
+ stats: stats
76
+ )
77
+
78
+ at_exit do
79
+ stats.save
80
+ paginator.save
81
+ end
82
+
83
+ paginator.run
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/wait'
4
+ require 'io/console'
5
+
6
+ module Teletype
7
+ # Key implements console reading for multibyte inputs and translates to unicode
8
+ # characters. The unicode characters will be useful when displaying the inputs back
9
+ # to console.
10
+ class Key
11
+ DICTIONARY = {
12
+ "\e" => '␛',
13
+ "\e[1;5A" => '↥', # ctrl-up
14
+ "\e[1;5B" => '↧', # ctrl-down
15
+ "\e[1;5C" => '↦', # ctrl-right
16
+ "\e[1;5D" => '↤', # ctrl-left
17
+
18
+ "\e[1~" => '⌂', # home
19
+ "\e[2~" => '⎀', # insert
20
+ "\e[3~" => '⌦', # delete
21
+ "\e[3;2~" => '↱', # shift+delete
22
+ "\e[3;5~" => '↪', # ctrl+delete
23
+ "\e[4~" => '↘', # end
24
+ "\e[5~" => '⇞', # page up
25
+ "\e[6~" => '⇟', # page down
26
+ "\e[7~" => '⌂', # home
27
+ "\e[8~" => '↘', # end
28
+
29
+ "\e[A" => '↑',
30
+ "\e[B" => '↓',
31
+ "\e[C" => '→',
32
+ "\e[D" => '←',
33
+ "\e[E" => '⇲', # clear
34
+ "\e[H" => '⌂', # home
35
+ "\e[F" => '↘', # end
36
+ "\e[Z" => '⇤', # backtab(shift + tab)
37
+
38
+ "\u0003" => '⏻',
39
+ "\u007F" => '⌫', # backspace
40
+ "\r" => "\n",
41
+ "\t" => '⇥'
42
+ }.freeze
43
+
44
+ def self.read
45
+ blocking = true
46
+ chars = []
47
+ $stdin.raw do
48
+ $stdin.noecho do
49
+ loop do
50
+ if blocking
51
+ chars << $stdin.getc
52
+ blocking = false
53
+ elsif $stdin.ready?
54
+ chars << $stdin.getc
55
+ else
56
+ key = chars.join
57
+ return DICTIONARY[key] || key
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teletype
4
+ # Prints lines of text to screen and handles key strokes.
5
+ class Page
6
+ def initialize(lines, screen, stats)
7
+ @lines = lines.map { |line| line.gsub("\t", '⇥') }
8
+ @screen = screen
9
+ @stats = stats
10
+ @y = -1
11
+ end
12
+
13
+ def fetch
14
+ loop do
15
+ @line = @lines.shift
16
+ @y += 1
17
+ break if @line.nil? || @line.strip.length.positive?
18
+ end
19
+
20
+ # skip white spaces at the beginning of a line
21
+ @x = @line&.match(/[[:graph:]]/)&.pre_match&.length || 0
22
+ end
23
+
24
+ def run
25
+ @screen.fill(@lines.map { |line| default(line) })
26
+
27
+ fetch
28
+ to(@x, @y)
29
+
30
+ loop do
31
+ break if @lines.empty? && @line.nil?
32
+
33
+ char = Key.read
34
+
35
+ case char
36
+ when '⏻'
37
+ exit
38
+ when '⌫'
39
+ erase
40
+ else
41
+ advance(char)
42
+ end
43
+
44
+ @screen.log(@stats.rankings)
45
+ end
46
+ end
47
+
48
+ def advance(char)
49
+ at = @line[@x]
50
+ if char == at
51
+ @stats.hit!(at)
52
+ print(correct(char))
53
+ else
54
+ @stats.miss!(at)
55
+ print(wrong(visible(char)))
56
+ end
57
+ @x += 1
58
+ fetch if @x == @line.length
59
+ to(@x, @y)
60
+ end
61
+
62
+ def erase
63
+ return unless @x.positive?
64
+
65
+ @x -= 1
66
+ to(@x, @y)
67
+ print default(@line[@x])
68
+ to(@x, @y)
69
+ end
70
+
71
+ def to(x, y)
72
+ @screen.to(x, y)
73
+ end
74
+
75
+ def visible(char)
76
+ case char
77
+ when ' ' then '␣'
78
+ when "\n" then '↵'
79
+ when ("\u0001".."\u001A") then '⌃' # Ctrl[a-z]
80
+ when /^\e/ then '�'
81
+ else; char
82
+ end
83
+ end
84
+
85
+ def default(str)
86
+ "\e[94m#{str}\e[0m"
87
+ end
88
+
89
+ def correct(str)
90
+ "\e[2;32m#{str}\e[0m"
91
+ end
92
+
93
+ def wrong(str)
94
+ "\e[91m#{str}\e[0m"
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Teletype
6
+ # Pager divides lines of text into proper screen size and provides page suggestion
7
+ # based on statistics of click accuracy.
8
+ class Paginator
9
+ def initialize(profile, text, suggest:, screen:, stats:)
10
+ @screen = screen
11
+ @stats = stats
12
+ @suggest = suggest
13
+
14
+ @lines = split(text, @screen.width)
15
+
16
+ @linenum = 0
17
+ @pagefile = File.join(profile, "page-#{Digest::MD5.hexdigest(text)}")
18
+ end
19
+
20
+ def save
21
+ File.write(@pagefile, @linenum) if @linenum.positive?
22
+ end
23
+
24
+ def run
25
+ prev = File.exist?(@pagefile) ? Integer(File.read(@pagefile).strip) : 0
26
+ @lines.each_slice(@screen.height) do |lines|
27
+ @linenum += @screen.height
28
+ next if @linenum < prev
29
+
30
+ Page.new(lines, @screen, @stats).run
31
+
32
+ loop do
33
+ suggestions = @stats.suggestions
34
+ break if suggestions.empty?
35
+
36
+ Page.new(pick(suggestions), @screen, @stats).run
37
+ end
38
+ end
39
+ end
40
+
41
+ def pick(suggestions)
42
+ index = 0.0 # preserve the original order
43
+ matches = ->(line) { suggestions.map { |keys| line.scan(keys).count }.sum - (index += 0.0001) }
44
+ @lines.sort_by { |line| matches.call(line) }.last(@suggest)
45
+ end
46
+
47
+ def split(text, length)
48
+ lines = []
49
+ text.each_line do |line|
50
+ line.chars.each_slice(length) do |slice|
51
+ lines << slice.join
52
+ end
53
+ end
54
+ lines
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teletype
4
+ # Initializes screen size and click stats, then start the practice page by page.
5
+ class Practice
6
+ def initialize(profile, text:, height:, width:, suggest:, verbose:)
7
+ @stats = Stats.new(profile, text)
8
+ @paginator = Paginator.new(
9
+ profile, text,
10
+ height: height,
11
+ width: width,
12
+ suggest: suggest,
13
+ verbose: verbose,
14
+ stats: @stats
15
+ )
16
+ end
17
+
18
+ def start
19
+ at_exit do
20
+ @stats.save
21
+ @paginator.save
22
+ end
23
+
24
+ @paginator.run
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teletype
4
+ # Screen abstracts console printing, moving to specific row and column.
5
+ class Screen
6
+ attr_accessor :height, :width, :top, :left
7
+
8
+ def initialize(height, width, verbose: false)
9
+ @verbose = verbose
10
+ @maxh, @maxw = $stdout.winsize
11
+
12
+ if @maxh > height
13
+ @height = height
14
+ @top = (@maxh - height) / 2
15
+ else
16
+ @height = @maxh
17
+ @top = 0
18
+ end
19
+
20
+ if @maxw > width
21
+ @width = width
22
+ @left = (@maxw - width) / 2
23
+ else
24
+ @width = @maxw
25
+ @left = 0
26
+ end
27
+
28
+ at_exit { $stdout.clear_screen }
29
+ end
30
+
31
+ def to(x, y)
32
+ $stdout.goto(@top + y, @left + x)
33
+ end
34
+
35
+ def fill(lines)
36
+ invisible do
37
+ $stdout.clear_screen
38
+
39
+ lines.each_with_index do |line, index|
40
+ $stdout.goto(@top + index, @left)
41
+ $stdout.puts line
42
+ end
43
+ end
44
+ end
45
+
46
+ def log(*lines)
47
+ return unless @verbose
48
+
49
+ original = $stdin.cursor
50
+ invisible do
51
+ lines.each_with_index do |line, index|
52
+ $stdout.goto(@top + @height + 5 + index, @left)
53
+ $stdout.print "\e[90;104m#{line}\e[0m"
54
+ $stdout.erase_line(0)
55
+ end
56
+ end
57
+ $stdout.cursor = original if original
58
+ end
59
+
60
+ def invisible
61
+ $stdout.print "\e[?25l"
62
+ yield
63
+ ensure
64
+ $stdout.print "\e[?25h"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teletype
4
+ # Stats keep track of hit/miss rate for pair of keys.
5
+ # It also has suggestions for keys that need more practice.
6
+ class Stats
7
+ def initialize(profile, text)
8
+ @file = File.join(profile, 'stats')
9
+ @previous = nil
10
+ @pairs = {}
11
+ load(text)
12
+ end
13
+
14
+ def load(text)
15
+ return unless File.exist?(@file)
16
+
17
+ File.readlines(@file).each do |line|
18
+ keys, hit, miss = line.split("\t")
19
+ @pairs[keys] = Pair.new(keys,
20
+ hit: Integer(hit),
21
+ miss: Integer(miss),
22
+ available: text.scan(keys).count.positive?)
23
+ end
24
+ end
25
+
26
+ def save
27
+ File.write(@file, @pairs.map { |k, p| [k, p.hit, p.miss].join("\t") }.join("\n"))
28
+ end
29
+
30
+ def hit!(key)
31
+ lookup(key)&.hit!
32
+ end
33
+
34
+ def miss!(key)
35
+ lookup(key)&.miss!
36
+ end
37
+
38
+ def lookup(key)
39
+ current = key.downcase
40
+ keys = "#{@previous}#{current}"
41
+ @previous = current
42
+ return if keys.strip.length < 2
43
+
44
+ @pairs[keys] ||= Pair.new(keys)
45
+ end
46
+
47
+ def suggestions
48
+ @pairs.values.select(&:available).select(&:inefficient?).sort.map(&:keys)
49
+ end
50
+
51
+ def rankings
52
+ @pairs.values.select(&:available).sort.first(10).map(&:to_s).join(' ')
53
+ end
54
+
55
+ # It implements hit/miss rate for second keystroke.
56
+ # Key miss tends occur when preceded by a certain key.
57
+ class Pair
58
+ THRESHOLD = 0.7
59
+
60
+ # hit/miss is for the second key
61
+ attr_accessor :keys, :hit, :miss, :available
62
+
63
+ def initialize(keys, hit: 0, miss: 0, available: true)
64
+ @keys = keys
65
+ @hit = hit
66
+ @miss = miss
67
+ @available = available
68
+ end
69
+
70
+ def inefficient?
71
+ rate < THRESHOLD
72
+ end
73
+
74
+ def hit!
75
+ @hit += 1
76
+ end
77
+
78
+ def miss!
79
+ @miss += 1
80
+ end
81
+
82
+ def rate
83
+ return 0 if total.zero?
84
+
85
+ hit / total.to_f
86
+ end
87
+
88
+ def total
89
+ hit + miss
90
+ end
91
+
92
+ def <=>(other)
93
+ [rate, keys] <=> [other.rate, other.keys]
94
+ end
95
+
96
+ def to_s
97
+ keys.gsub(/[\n\r]/, '↵').gsub(' ', '␣')
98
+ end
99
+ end
100
+ end
101
+ end
data/lib/teletype.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'teletype/key'
4
+ require_relative 'teletype/screen'
5
+ require_relative 'teletype/stats'
6
+ require_relative 'teletype/page'
7
+ require_relative 'teletype/paginator'
8
+ require_relative 'teletype/practice'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: teletype
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
- - Ochirkhuyag Lkhagva
7
+ - Ochirkhuyag.L
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-28 00:00:00.000000000 Z
11
+ date: 2022-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.16'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -39,10 +53,16 @@ executables:
39
53
  extensions: []
40
54
  extra_rdoc_files: []
41
55
  files:
42
- - LICENSE.txt
43
56
  - README.md
44
57
  - Rakefile
45
58
  - bin/teletype
59
+ - lib/teletype.rb
60
+ - lib/teletype/key.rb
61
+ - lib/teletype/page.rb
62
+ - lib/teletype/paginator.rb
63
+ - lib/teletype/practice.rb
64
+ - lib/teletype/screen.rb
65
+ - lib/teletype/stats.rb
46
66
  homepage: https://github.com/ochko/teletype
47
67
  licenses:
48
68
  - MIT
@@ -62,8 +82,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
62
82
  - !ruby/object:Gem::Version
63
83
  version: '0'
64
84
  requirements: []
65
- rubygems_version: 3.2.22
85
+ rubygems_version: 3.1.6
66
86
  signing_key:
67
87
  specification_version: 4
68
- summary: Typing practice on command line.
88
+ summary: Typing practice on terminal.
69
89
  test_files: []
data/LICENSE.txt DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) 2021 Ochirkhuyag Lkhagva
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.