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