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 +4 -4
- data/README.md +9 -1
- data/Rakefile +5 -4
- data/bin/teletype +81 -1
- data/lib/teletype/key.rb +64 -0
- data/lib/teletype/page.rb +97 -0
- data/lib/teletype/paginator.rb +57 -0
- data/lib/teletype/practice.rb +27 -0
- data/lib/teletype/screen.rb +67 -0
- data/lib/teletype/stats.rb +101 -0
- data/lib/teletype.rb +8 -0
- metadata +26 -6
- data/LICENSE.txt +0 -21
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 40a3e0c38deacc68555c1440a9fe679afc623e93decc137836a3066aa6710521
         | 
| 4 | 
            +
              data.tar.gz: 640e87ce91e95e40b37e5f768e70ae0e355f68f810c51b1862e2fcdf6dd1ddd5
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 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  | 
| 8 | 
            -
            task : | 
| 9 | 
            -
             | 
| 9 | 
            +
            desc 'Run tests'
         | 
| 10 | 
            +
            task default: :test
         | 
| 10 11 |  | 
| 11 | 
            -
            desc  | 
| 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 | 
            -
             | 
| 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
         | 
    
        data/lib/teletype/key.rb
    ADDED
    
    | @@ -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
    
    
    
        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:  | 
| 4 | 
            +
              version: 1.2.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 | 
            -
            - Ochirkhuyag | 
| 7 | 
            +
            - Ochirkhuyag.L
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 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. | 
| 85 | 
            +
            rubygems_version: 3.1.6
         | 
| 66 86 | 
             
            signing_key:
         | 
| 67 87 | 
             
            specification_version: 4
         | 
| 68 | 
            -
            summary: Typing practice on  | 
| 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.
         |