teletype 0.0.0 → 1.0.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/Rakefile +5 -4
- data/bin/teletype +29 -1
- data/lib/teletype/key.rb +64 -0
- data/lib/teletype/page.rb +103 -0
- data/lib/teletype/pager.rb +33 -0
- data/lib/teletype/practice.rb +28 -0
- data/lib/teletype/screen.rb +64 -0
- data/lib/teletype/stats.rb +99 -0
- data/lib/teletype.rb +8 -0
- metadata +11 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f3d17c040e59cad3900c7fd183a9458d640fc00e22d9e513f0cf2a6ad7420747
|
4
|
+
data.tar.gz: aae9d399a93ba481d3baee7090faade55d3285e4f6307cd62329760332b71d22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55907ec80e180e4393de75b6a4d3f09bebf8f9d70517b13f3ff88d62fa62e177d9b97d551087b820b46930ffece9f1a96a476122f53db34decf0fb4221995d64
|
7
|
+
data.tar.gz: 94ffe5bbbdf66706350ca3d999e751a0e49681c7e4ededcf50852ffeff30bd6c4376cbf437a6fcca3760ac9dcc5a5905735e6bbe44513126016c3e1631efda47
|
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,31 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
|
4
|
+
require_relative '../lib/teletype'
|
5
|
+
|
6
|
+
exercises = File.join(File.dirname(__FILE__), '../', 'exercise')
|
7
|
+
|
8
|
+
if ARGV.first == '--list'
|
9
|
+
Dir.glob(File.join(exercises, '**', '*.*')).each do |path|
|
10
|
+
puts path.sub(File.join(exercises, ''), '')
|
11
|
+
end
|
12
|
+
exit
|
13
|
+
end
|
14
|
+
|
15
|
+
text = ''
|
16
|
+
paths = []
|
17
|
+
paths << 'base' if ARGV.length.zero?
|
18
|
+
|
19
|
+
paths.each do |path|
|
20
|
+
path = File.join(exercises, path)
|
21
|
+
if File.directory?(path)
|
22
|
+
Dir.glob(File.join(path, '*.*')).each do |file|
|
23
|
+
text += File.read(file)
|
24
|
+
end
|
25
|
+
elsif File.exist?(path)
|
26
|
+
text += File.read(path)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
practice = Teletype::Practice.new(text)
|
31
|
+
practice.start
|
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,103 @@
|
|
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
|
+
end
|
11
|
+
|
12
|
+
def fetch
|
13
|
+
loop do
|
14
|
+
@line = @lines.shift
|
15
|
+
if @y
|
16
|
+
@y += 1
|
17
|
+
else
|
18
|
+
@y = 0
|
19
|
+
end
|
20
|
+
break if @line.nil? || @line.strip.length.positive?
|
21
|
+
end
|
22
|
+
|
23
|
+
@x = if (spaces = @line&.match(/\A\ +/))
|
24
|
+
spaces[0].length
|
25
|
+
else
|
26
|
+
0
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
@screen.fill(@lines.map { |line| default(line) })
|
32
|
+
|
33
|
+
fetch
|
34
|
+
to(@x, @y)
|
35
|
+
|
36
|
+
loop do
|
37
|
+
break if @lines.empty? && @line.nil?
|
38
|
+
|
39
|
+
char = Key.read
|
40
|
+
|
41
|
+
case char
|
42
|
+
when '⏻'
|
43
|
+
exit
|
44
|
+
when '⌫'
|
45
|
+
erase
|
46
|
+
else
|
47
|
+
advance(char)
|
48
|
+
end
|
49
|
+
|
50
|
+
@screen.log(@stats.rankings)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def advance(char)
|
55
|
+
at = @line[@x]
|
56
|
+
if char == at
|
57
|
+
@stats.hit!(at)
|
58
|
+
print(correct(char))
|
59
|
+
else
|
60
|
+
@stats.miss!(at)
|
61
|
+
print(wrong(visible(char)))
|
62
|
+
end
|
63
|
+
@x += 1
|
64
|
+
fetch if @x == @line.length
|
65
|
+
to(@x, @y)
|
66
|
+
end
|
67
|
+
|
68
|
+
def erase
|
69
|
+
return unless @x.positive?
|
70
|
+
|
71
|
+
@x -= 1
|
72
|
+
to(@x, @y)
|
73
|
+
print default(@line[@x])
|
74
|
+
to(@x, @y)
|
75
|
+
end
|
76
|
+
|
77
|
+
def to(x, y)
|
78
|
+
@screen.to(x, y)
|
79
|
+
end
|
80
|
+
|
81
|
+
def visible(char)
|
82
|
+
case char
|
83
|
+
when ' ' then '␣'
|
84
|
+
when "\n" then '↵'
|
85
|
+
when ("\u0001".."\u001A") then '⌃' # Ctrl[a-z]
|
86
|
+
when /^\e/ then '�'
|
87
|
+
else; char
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def default(str)
|
92
|
+
"\e[94m#{str}\e[0m"
|
93
|
+
end
|
94
|
+
|
95
|
+
def correct(str)
|
96
|
+
"\e[2;32m#{str}\e[0m"
|
97
|
+
end
|
98
|
+
|
99
|
+
def wrong(str)
|
100
|
+
"\e[91m#{str}\e[0m"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teletype
|
4
|
+
# Pager divides lines of text into proper screen size and provides page suggestion
|
5
|
+
# based on statistics of click accuracy.
|
6
|
+
class Pager
|
7
|
+
SUGGEST = 1
|
8
|
+
|
9
|
+
def initialize(lines, stats, height)
|
10
|
+
@lines = lines
|
11
|
+
@stats = stats
|
12
|
+
@height = height
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
@lines.each_slice(@height).map do |lines|
|
17
|
+
yield lines
|
18
|
+
|
19
|
+
loop do
|
20
|
+
suggestions = @stats.suggestions
|
21
|
+
break if suggestions.empty?
|
22
|
+
|
23
|
+
yield pick(suggestions)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def pick(suggestions)
|
29
|
+
index = 0.0
|
30
|
+
@lines.sort_by { |line| suggestions.map { |keys| -line.scan(keys).count }.sum + (index += 0.0001) }.first(SUGGEST)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
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(text, height: 5, width: 120)
|
7
|
+
@screen = Screen.new(height: height, width: width)
|
8
|
+
@stats = Stats.new
|
9
|
+
|
10
|
+
@lines = []
|
11
|
+
text.each_line do |line|
|
12
|
+
line.chars.each_slice(@screen.width) do |slice|
|
13
|
+
@lines << slice.join
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
@pager = Pager.new(@lines, @stats, @screen.height)
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
at_exit { @stats.save }
|
22
|
+
|
23
|
+
@pager.each do |lines|
|
24
|
+
Page.new(lines, @screen, @stats).run
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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:)
|
9
|
+
@maxh, @maxw = $stdout.winsize
|
10
|
+
|
11
|
+
if @maxh > height
|
12
|
+
@height = height
|
13
|
+
@top = (@maxh - height) / 2
|
14
|
+
else
|
15
|
+
@height = @maxh
|
16
|
+
@top = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
if @maxw > width
|
20
|
+
@width = width
|
21
|
+
@left = (@maxw - width) / 2
|
22
|
+
else
|
23
|
+
@width = @maxw
|
24
|
+
@left = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
at_exit { $stdout.clear_screen }
|
28
|
+
end
|
29
|
+
|
30
|
+
def to(x, y)
|
31
|
+
$stdout.goto(@top + y, @left + x)
|
32
|
+
end
|
33
|
+
|
34
|
+
def fill(lines)
|
35
|
+
invisible do
|
36
|
+
$stdout.clear_screen
|
37
|
+
|
38
|
+
lines.each_with_index do |line, index|
|
39
|
+
$stdout.goto(@top + index, @left)
|
40
|
+
$stdout.puts line
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def log(*lines)
|
46
|
+
original = $stdin.cursor
|
47
|
+
invisible do
|
48
|
+
lines.each_with_index do |line, index|
|
49
|
+
$stdout.goto(@top + @height + 5 + index, @left)
|
50
|
+
$stdout.print "\e[90;104m#{line}\e[0m"
|
51
|
+
$stdout.erase_line(0)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
$stdout.cursor = original if original
|
55
|
+
end
|
56
|
+
|
57
|
+
def invisible
|
58
|
+
$stdout.print "\e[?25l"
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
$stdout.print "\e[?25h"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Teletype
|
4
|
+
# Stats keep track of hit/miss rate for each key and suggests a key that needs more practice.
|
5
|
+
class Stats
|
6
|
+
def initialize
|
7
|
+
@previous = nil
|
8
|
+
@pairs = {}
|
9
|
+
load
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
return unless File.exist?(file)
|
14
|
+
|
15
|
+
File.readlines(file).each do |line|
|
16
|
+
keys, hit, miss = line.split("\t")
|
17
|
+
@pairs[keys] = Pair.new(keys, hit: Integer(hit), miss: Integer(miss))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def save
|
22
|
+
File.write(file, @pairs.map {|k, p| [k, p.hit, p.miss].join("\t")}.join("\n"))
|
23
|
+
end
|
24
|
+
|
25
|
+
def file
|
26
|
+
File.join(Dir.home, '.teletype-stats')
|
27
|
+
end
|
28
|
+
|
29
|
+
def hit!(key)
|
30
|
+
lookup(key)&.hit!
|
31
|
+
end
|
32
|
+
|
33
|
+
def miss!(key)
|
34
|
+
lookup(key)&.miss!
|
35
|
+
end
|
36
|
+
|
37
|
+
def lookup(key)
|
38
|
+
current = key.downcase
|
39
|
+
keys = "#{@previous}#{current}"
|
40
|
+
@previous = current
|
41
|
+
return if keys.strip.length < 2
|
42
|
+
|
43
|
+
@pairs[keys] ||= Pair.new(keys)
|
44
|
+
end
|
45
|
+
|
46
|
+
def suggestions
|
47
|
+
@pairs.values.select(&:inefficient?).sort.map(&:keys)
|
48
|
+
end
|
49
|
+
|
50
|
+
def rankings
|
51
|
+
@pairs.values.sort.first(10).map(&:to_s).join(' ')
|
52
|
+
end
|
53
|
+
|
54
|
+
# It implements hit/miss rate for second keystroke.
|
55
|
+
# Sometimes a key tends be a miss only when preceded by a certain key.
|
56
|
+
class Pair
|
57
|
+
THRESHOLD = 0.8
|
58
|
+
|
59
|
+
# hit/miss is for the second key
|
60
|
+
attr_accessor :keys, :hit, :miss
|
61
|
+
|
62
|
+
def initialize(keys, hit: 0, miss: 0)
|
63
|
+
@keys = keys
|
64
|
+
@hit = hit
|
65
|
+
@miss = miss
|
66
|
+
end
|
67
|
+
|
68
|
+
def inefficient?
|
69
|
+
rate < THRESHOLD
|
70
|
+
end
|
71
|
+
|
72
|
+
def hit!
|
73
|
+
@hit += 1
|
74
|
+
end
|
75
|
+
|
76
|
+
def miss!
|
77
|
+
@miss += 1
|
78
|
+
end
|
79
|
+
|
80
|
+
def rate
|
81
|
+
return 0 if total.zero?
|
82
|
+
|
83
|
+
hit / total.to_f
|
84
|
+
end
|
85
|
+
|
86
|
+
def total
|
87
|
+
hit + miss
|
88
|
+
end
|
89
|
+
|
90
|
+
def <=>(other)
|
91
|
+
[rate, keys] <=> [other.rate, other.keys]
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_s
|
95
|
+
keys.gsub(/[\n\r]/, '↵').gsub(' ', '␣')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/teletype.rb
ADDED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: teletype
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.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-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -43,6 +43,13 @@ files:
|
|
43
43
|
- README.md
|
44
44
|
- Rakefile
|
45
45
|
- bin/teletype
|
46
|
+
- lib/teletype.rb
|
47
|
+
- lib/teletype/key.rb
|
48
|
+
- lib/teletype/page.rb
|
49
|
+
- lib/teletype/pager.rb
|
50
|
+
- lib/teletype/practice.rb
|
51
|
+
- lib/teletype/screen.rb
|
52
|
+
- lib/teletype/stats.rb
|
46
53
|
homepage: https://github.com/ochko/teletype
|
47
54
|
licenses:
|
48
55
|
- MIT
|
@@ -62,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
62
69
|
- !ruby/object:Gem::Version
|
63
70
|
version: '0'
|
64
71
|
requirements: []
|
65
|
-
rubygems_version: 3.
|
72
|
+
rubygems_version: 3.1.6
|
66
73
|
signing_key:
|
67
74
|
specification_version: 4
|
68
75
|
summary: Typing practice on command line.
|