teletype 0.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.