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 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.