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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f113b33716079b6147b8511771b0112ec42044981ba543abaca849211563be1
4
- data.tar.gz: 85ded73b628942847ef1f6553288afe6eff1cfc02b0ee152bbb2ac4bb3ba790a
3
+ metadata.gz: f3d17c040e59cad3900c7fd183a9458d640fc00e22d9e513f0cf2a6ad7420747
4
+ data.tar.gz: aae9d399a93ba481d3baee7090faade55d3285e4f6307cd62329760332b71d22
5
5
  SHA512:
6
- metadata.gz: be9dad8e1a5edf3d2885b1c19527609d64684abdcdd3fdb4cbfde71641e9a0526282c8ce1b5e01c92b28506e38f41e60914c09138675c6bf52e7e17a84582ec2
7
- data.tar.gz: 221045d24b990dd28cf91b9fc51880e8d99c06c13bd38dbea18db922b28ec54517d02ded848f7d934455e34ab3f63b1a6d0b7cdc3b1410a4a14de3fe40848fc5
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 "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,31 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- puts "Please use version above 0.0.0"
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
@@ -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
@@ -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/pager'
8
+ require_relative 'teletype/practice'
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: 0.0.0
4
+ version: 1.0.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-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.2.22
72
+ rubygems_version: 3.1.6
66
73
  signing_key:
67
74
  specification_version: 4
68
75
  summary: Typing practice on command line.