landescape 0.0.1
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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +6 -0
- data/examples/shell_on_curses.rb +32 -0
- data/landescape.gemspec +25 -0
- data/lib/landescape.rb +38 -0
- data/lib/landescape/converter.rb +124 -0
- data/lib/landescape/evaluator.rb +50 -0
- data/lib/landescape/evaluator/curses.rb +131 -0
- data/lib/landescape/tokenizer.rb +122 -0
- data/lib/landescape/version.rb +3 -0
- data/spec/converter_spec.rb +34 -0
- data/spec/evaluator_spec.rb +4 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/example_helper.rb +14 -0
- data/spec/tokenizer_spec.rb +31 -0
- metadata +111 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 22dbf4f4d4ebb551c726f65d7894706fd8e8cb7c
|
4
|
+
data.tar.gz: 5fad1bddcbfb7860b9eff86c48808e99fa934d7b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cc43b68028b01c3d3229b75008430cc0d8ef2ed2e6733212bdbe31266214fe029d331142b6ab2f96b435cb2e1e9c22402bcdb18d3fe9921d80e71cd4bb51f54a
|
7
|
+
data.tar.gz: 2c498dc00f67c24cd5c92a8f722e6c6bacb0dd11d9b7f0da8c6b2a9387fc0b99e13be151b4635522e7fad85515be195a72af122205c7c01847ca112667db25c5
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 hibariya
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# Landescape
|
2
|
+
|
3
|
+
Landscape is a library for handling escape sequence.
|
4
|
+
|
5
|
+
## Is It Good?
|
6
|
+
|
7
|
+
Yes. But Landescape has some problems for now.
|
8
|
+
**API will changed in future versions.**
|
9
|
+
|
10
|
+
Landescape doesn't support followings now:
|
11
|
+
|
12
|
+
* Coloring
|
13
|
+
* Any terminals except vt100
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
gem 'landescape'
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install landescape
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Tokenize, And convert to human readable statements
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
source = StringIO.new("\e[1mHello\e[m.") # IO like object
|
35
|
+
|
36
|
+
# tokenize
|
37
|
+
tokenizer = Landescape::Tokenizer.start(source)
|
38
|
+
tokens = tokenizer.result # => #<Queue:0x007ff0bc08f098 @que=["\e[1m", "H", "e", "l", "l", "o", "\e[m", "."], (snip...)>
|
39
|
+
|
40
|
+
# convert
|
41
|
+
converter = Landescape::Converter::VT100.start(tokens, non_block: true)
|
42
|
+
converter.result # => #<Queue:0x007ff0bc1cdc70 @que=[[:set_attributes, 1], [:print, "H"], [:print, "e"], [:print, "l"], [:print, "l"], [:print, "o"], [:exit_attribute_mode], [:print, "."]], (snip...)>
|
43
|
+
```
|
44
|
+
|
45
|
+
### Invoke shell on Curses window
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'curses'
|
49
|
+
require 'pty'
|
50
|
+
require 'landescape'
|
51
|
+
|
52
|
+
Curses.noecho
|
53
|
+
|
54
|
+
window = Curses.stdscr
|
55
|
+
window.addstr 'Landescape example for curses'
|
56
|
+
window.refresh
|
57
|
+
|
58
|
+
terminal = window.subwin(27, 102, 10, 10)
|
59
|
+
terminal.scrollok true
|
60
|
+
|
61
|
+
PTY.getpty 'TERM=vt100 bash --noprofile' do |stdout, stdin, pid|
|
62
|
+
stdin.puts <<-SHELL
|
63
|
+
tput cols 100
|
64
|
+
tput lines 25
|
65
|
+
clear
|
66
|
+
SHELL
|
67
|
+
|
68
|
+
Thread.fork do
|
69
|
+
while char = terminal.getch
|
70
|
+
stdin.putc char
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Landescape.run stdout, terminal
|
75
|
+
|
76
|
+
Curses.close_screen if Process.detach(pid).join
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
## Contributing
|
81
|
+
|
82
|
+
1. Fork it
|
83
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
84
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
85
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
86
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pty'
|
4
|
+
require 'curses'
|
5
|
+
require 'landescape'
|
6
|
+
|
7
|
+
Curses.noecho
|
8
|
+
|
9
|
+
window = Curses.stdscr
|
10
|
+
window.addstr 'Landescape example for curses'
|
11
|
+
window.refresh
|
12
|
+
|
13
|
+
terminal = window.subwin(27, 102, 10, 10)
|
14
|
+
terminal.scrollok true
|
15
|
+
|
16
|
+
PTY.getpty 'TERM=vt100 bash --noprofile' do |stdout, stdin, pid|
|
17
|
+
stdin.puts <<-SHELL
|
18
|
+
tput cols 100
|
19
|
+
tput lines 25
|
20
|
+
clear
|
21
|
+
SHELL
|
22
|
+
|
23
|
+
Thread.fork do
|
24
|
+
while char = terminal.getch
|
25
|
+
stdin.putc char
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Landescape.run stdout, terminal
|
30
|
+
|
31
|
+
Curses.close_screen if Process.detach(pid).join
|
32
|
+
end
|
data/landescape.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
require 'landescape/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'landescape'
|
9
|
+
spec.version = Landescape::VERSION
|
10
|
+
spec.authors = %w(hibariya)
|
11
|
+
spec.email = %w(celluloid.key@gmail.com)
|
12
|
+
spec.description = %(A library for handling escape sequence)
|
13
|
+
spec.summary = %(Handle escape sequence)
|
14
|
+
spec.homepage = 'https://github.com/hibariya/landescape'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = %w(lib examples)
|
21
|
+
|
22
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
end
|
data/lib/landescape.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'landescape/version'
|
3
|
+
|
4
|
+
module Landescape
|
5
|
+
autoload :Tokenizer, 'landescape/tokenizer'
|
6
|
+
autoload :Converter, 'landescape/converter'
|
7
|
+
autoload :Evaluator, 'landescape/evaluator'
|
8
|
+
|
9
|
+
autoload :Curses, 'landescape/evaluator/curses'
|
10
|
+
|
11
|
+
class Nullogger
|
12
|
+
def method_missing(*); end
|
13
|
+
end
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_writer :logger
|
17
|
+
|
18
|
+
def logger
|
19
|
+
@logger ||= Nullogger.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def run(source, window, options = {})
|
23
|
+
options = {non_block: false, converter: Converter::VT100, evaluator: Curses}.merge(options)
|
24
|
+
converter_klass, evaluator_klass = [:converter, :evaluator].map {|n|
|
25
|
+
options.delete(n)
|
26
|
+
}
|
27
|
+
|
28
|
+
tokenizer = Tokenizer.start(source)
|
29
|
+
converter = converter_klass.start(tokenizer.result, options)
|
30
|
+
|
31
|
+
evaluator_klass.start(converter.result, window, options).tap {|evaluator|
|
32
|
+
at_exit do
|
33
|
+
[tokenizer, converter, evaluator].each &:stop
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Landescape
|
2
|
+
# Convert: ['\e[1;2f', 'H', 'i']] to [[:cursor_address, 1, 2], [:print, 'H'], [:print, 'i']]
|
3
|
+
module Converter
|
4
|
+
class Base
|
5
|
+
CODES = {}
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def start(tokens, options = {})
|
9
|
+
new(tokens, options).tap(&:start)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :tokens, :result
|
14
|
+
|
15
|
+
def initialize(tokens, options = {})
|
16
|
+
@options = {non_block: false}.merge(options)
|
17
|
+
@result = Queue.new
|
18
|
+
@tokens = tokens
|
19
|
+
end
|
20
|
+
|
21
|
+
def start
|
22
|
+
@convert_thread = Thread.fork {
|
23
|
+
loop do
|
24
|
+
token =
|
25
|
+
begin
|
26
|
+
tokens.shift(@options[:non_block])
|
27
|
+
rescue ThreadError
|
28
|
+
Thread.exit
|
29
|
+
end
|
30
|
+
|
31
|
+
_, detected = self.class::CODES.detect {|pattern, _| pattern === token }
|
32
|
+
match = Regexp.last_match
|
33
|
+
statement =
|
34
|
+
if detected
|
35
|
+
name, args_proc = detected.values_at(:name, :args)
|
36
|
+
args = args_proc ? args_proc.call(match.captures) : nil
|
37
|
+
|
38
|
+
Landescape.logger.debug %([statement] #{name}(#{Array(args).join(', ')}))
|
39
|
+
[name, *args]
|
40
|
+
else
|
41
|
+
Landescape.logger.debug %([statement] print(#{token.inspect}))
|
42
|
+
[:print, *token]
|
43
|
+
end
|
44
|
+
|
45
|
+
result.push statement
|
46
|
+
end
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
@convert_thread.exit if @convert_thread.alive?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class VT100 < Base
|
56
|
+
CODES = {
|
57
|
+
/^\e\[(\d+)?(?:;(\d+))?[Hf]$/ => {
|
58
|
+
name: :cursor_address,
|
59
|
+
args: ->(captures) { captures.map(&:to_i) }
|
60
|
+
},
|
61
|
+
|
62
|
+
/^\e\[(\d+)?A$/ => {
|
63
|
+
name: :cursor_up,
|
64
|
+
args: ->(captures) { captures.first.to_i }
|
65
|
+
},
|
66
|
+
|
67
|
+
/^\e\[(\d+)?B$/ => {
|
68
|
+
name: :cursor_down,
|
69
|
+
args: ->(captures) { captures.first.to_i }
|
70
|
+
},
|
71
|
+
|
72
|
+
/^\e\[(\d+)?C$/ => {
|
73
|
+
name: :cursor_right,
|
74
|
+
args: ->(captures) { captures.first.to_i }
|
75
|
+
},
|
76
|
+
|
77
|
+
/^\e\[(\d+)?D$/ => {
|
78
|
+
name: :cursor_left,
|
79
|
+
args: ->(captures) { captures.first.to_i }
|
80
|
+
},
|
81
|
+
|
82
|
+
/^\e\[0?m$/ => {name: :exit_attribute_mode},
|
83
|
+
|
84
|
+
/^\e\[([\d;]+)m$/ => {
|
85
|
+
name: :set_attributes,
|
86
|
+
args: ->(captures) { captures.join.split(/;/).map(&:to_i) }
|
87
|
+
},
|
88
|
+
|
89
|
+
/^\e\[(\d+);(\d+)r$/ => {
|
90
|
+
name: :change_scroll_region,
|
91
|
+
args: ->(captures) { captures.map(&:to_i) }
|
92
|
+
},
|
93
|
+
|
94
|
+
/^\e\[s$/ => {name: :save_cursor},
|
95
|
+
/^\e\[u$/ => {name: :restore_cursor},
|
96
|
+
/^\e\[2?J$/ => {name: :clear_screen},
|
97
|
+
/^\e\[K$/ => {name: :clear_eol},
|
98
|
+
|
99
|
+
/^\eD$/ => {name: :scroll_forward},
|
100
|
+
/^\eM$/ => {name: :scroll_reverse},
|
101
|
+
|
102
|
+
/^\e\[\?1h$/ => {name: :keypad_xmit},
|
103
|
+
/^\e\[\?1l$/ => {name: :keypad_local},
|
104
|
+
|
105
|
+
# cursor_normal is probably not available on vt100
|
106
|
+
/^\e\[\?25h$/ => {name: :cursor_visible},
|
107
|
+
/^\e\[\?25l$/ => {name: :cursor_invisible},
|
108
|
+
|
109
|
+
# backslashes
|
110
|
+
/^\n$/ => {name: :newline},
|
111
|
+
/^\r$/ => {name: :carriage_return},
|
112
|
+
/^(\a)$/ => {name: :bell},
|
113
|
+
|
114
|
+
# really...? maybe doubt!
|
115
|
+
/^\e\\$/ => {name: :carriage_return},
|
116
|
+
|
117
|
+
/^(\u000F|\e.+)$/ => {
|
118
|
+
name: :unknown,
|
119
|
+
args: ->(captures) { captures.first }
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Landescape
|
2
|
+
module Evaluator
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
def start(statements, window, options = {})
|
6
|
+
new(statements, window, options).tap(&:start)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :statements
|
11
|
+
|
12
|
+
def initialize(statements, window, options = {})
|
13
|
+
@options = {non_block: false}.merge(options)
|
14
|
+
@window = window
|
15
|
+
@statements = statements
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
@eval_thread = Thread.fork {
|
20
|
+
loop do
|
21
|
+
name, *args =
|
22
|
+
begin
|
23
|
+
statements.shift(@options[:non_block])
|
24
|
+
rescue ThreadError
|
25
|
+
Thread.kill
|
26
|
+
end
|
27
|
+
|
28
|
+
if self.class.method_defined?(name)
|
29
|
+
__send__ name, *args
|
30
|
+
else
|
31
|
+
not_supported name, *args
|
32
|
+
end
|
33
|
+
end
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
def stop
|
38
|
+
@eval_thread.exit if @eval_thread.alive?
|
39
|
+
end
|
40
|
+
|
41
|
+
def unknown(token)
|
42
|
+
Landescape.logger.warn %([unknown] #{token.inspect})
|
43
|
+
end
|
44
|
+
|
45
|
+
def not_supported(name, *args)
|
46
|
+
Landescape.logger.warn %([not supported] #{name}(#{args.join(', ')}))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'landescape/evaluator'
|
3
|
+
|
4
|
+
module Landescape
|
5
|
+
module Evaluator
|
6
|
+
class Curses < Base
|
7
|
+
GRAPHICS_MODES = {
|
8
|
+
1 => ::Curses::A_BOLD,
|
9
|
+
4 => ::Curses::A_UNDERLINE,
|
10
|
+
5 => ::Curses::A_BLINK,
|
11
|
+
7 => ::Curses::A_REVERSE
|
12
|
+
}
|
13
|
+
|
14
|
+
attr_reader :window
|
15
|
+
|
16
|
+
def initialize(statements, window, options = {})
|
17
|
+
super
|
18
|
+
|
19
|
+
@saved_cursor = nil
|
20
|
+
@scroll_region = [0, window.maxy]
|
21
|
+
end
|
22
|
+
|
23
|
+
def print(str)
|
24
|
+
window.addstr str
|
25
|
+
|
26
|
+
window.refresh
|
27
|
+
end
|
28
|
+
|
29
|
+
def set_attributes(*modes)
|
30
|
+
return exit_attribute_mode if modes.empty?
|
31
|
+
|
32
|
+
modes.each do |mode|
|
33
|
+
if attr = GRAPHICS_MODES[mode]
|
34
|
+
window.attron attr
|
35
|
+
else
|
36
|
+
exit_attribute_mode
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def exit_attribute_mode
|
42
|
+
window.attroff GRAPHICS_MODES.values.inject(:|)
|
43
|
+
end
|
44
|
+
|
45
|
+
def change_scroll_region(top, bottom)
|
46
|
+
@scroll_region = [top, bottom]
|
47
|
+
|
48
|
+
window.setscrreg *@scroll_region
|
49
|
+
end
|
50
|
+
|
51
|
+
def scroll_reverse
|
52
|
+
window.scrl -1
|
53
|
+
end
|
54
|
+
|
55
|
+
def scroll_forward
|
56
|
+
window.scrl 1
|
57
|
+
end
|
58
|
+
|
59
|
+
def cursor_address(line, column)
|
60
|
+
scroll_forward if line > window.cury && window.maxy.pred <= window.cury
|
61
|
+
|
62
|
+
window.setpos line, column
|
63
|
+
end
|
64
|
+
|
65
|
+
def newline
|
66
|
+
scroll_forward if window.cury == @scroll_region.last
|
67
|
+
|
68
|
+
cursor_address (window.cury.succ), 0
|
69
|
+
end
|
70
|
+
|
71
|
+
def carriage_return
|
72
|
+
cursor_address window.cury, 0
|
73
|
+
end
|
74
|
+
|
75
|
+
def cursor_up(step)
|
76
|
+
cursor_address (window.cury - step), window.curx
|
77
|
+
end
|
78
|
+
|
79
|
+
def cursor_down(step)
|
80
|
+
cursor_address (window.cury + step), window.curx
|
81
|
+
end
|
82
|
+
|
83
|
+
def cursor_right(step)
|
84
|
+
cursor_address window.cury, (window.curx + step)
|
85
|
+
end
|
86
|
+
|
87
|
+
def cursor_left(step)
|
88
|
+
cursor_address window.cury, (window.curx - step)
|
89
|
+
end
|
90
|
+
|
91
|
+
def save_cursor
|
92
|
+
@saved_cursor = [window.cury, window.curx]
|
93
|
+
end
|
94
|
+
|
95
|
+
def restore_cursor
|
96
|
+
cursor_address *@saved_cursor
|
97
|
+
end
|
98
|
+
|
99
|
+
def cursor_visible
|
100
|
+
window.curs_set 2
|
101
|
+
end
|
102
|
+
|
103
|
+
def cursor_invisible
|
104
|
+
window.curs_set 0
|
105
|
+
end
|
106
|
+
|
107
|
+
def keypad_xmit
|
108
|
+
window.keypad true
|
109
|
+
end
|
110
|
+
|
111
|
+
def keypad_local
|
112
|
+
window.keypad false
|
113
|
+
end
|
114
|
+
|
115
|
+
def clear_screen
|
116
|
+
cursor_address 0, 0
|
117
|
+
|
118
|
+
window.clear
|
119
|
+
window.refresh
|
120
|
+
end
|
121
|
+
|
122
|
+
def clear_eol
|
123
|
+
window.clrtoeol
|
124
|
+
|
125
|
+
window.refresh
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Curses = Evaluator::Curses
|
131
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Landescape
|
2
|
+
# Tokenize: "\e[1;2fHi" to ['\e[1;2f', 'H', 'i']]
|
3
|
+
class Tokenizer
|
4
|
+
class << self
|
5
|
+
def start(source)
|
6
|
+
new(source).tap(&:start)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_reader :source, :result
|
11
|
+
|
12
|
+
def initialize(source)
|
13
|
+
@result = Queue.new
|
14
|
+
@source =
|
15
|
+
if source.respond_to?(:getc)
|
16
|
+
source
|
17
|
+
else
|
18
|
+
StringIO.new(source.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
at_exit do
|
22
|
+
@source.close unless @source.closed?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
@tokenize_thread = Thread.fork {
|
28
|
+
while char = read_char
|
29
|
+
tokens =
|
30
|
+
case char
|
31
|
+
when nil then Thread.exit # StringIO#getc # => nil (EOF)
|
32
|
+
when /\e/ then escape char
|
33
|
+
else char
|
34
|
+
end
|
35
|
+
|
36
|
+
Array(tokens).each do |token|
|
37
|
+
result.push token
|
38
|
+
end
|
39
|
+
end
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop
|
44
|
+
@tokenize_thread.exit if @tokenize_thread.alive?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def escape(token)
|
50
|
+
token << read_char
|
51
|
+
|
52
|
+
case token.chars.last
|
53
|
+
when /\[/
|
54
|
+
escape_code token
|
55
|
+
when /[()]/
|
56
|
+
escape_code_parens token
|
57
|
+
when /#/
|
58
|
+
escape_code_sharp token
|
59
|
+
when /\//
|
60
|
+
escape_code_slash token
|
61
|
+
when /[\da-zA-Z<=>_\\]/
|
62
|
+
token
|
63
|
+
else
|
64
|
+
fallback token
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def escape_code(token)
|
69
|
+
token << read_char
|
70
|
+
|
71
|
+
case token.chars.last
|
72
|
+
when /[\d?;<=>]/
|
73
|
+
escape_code token
|
74
|
+
when /[a-zA-Z]/
|
75
|
+
token
|
76
|
+
else
|
77
|
+
fallback token
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def escape_code_parens(token)
|
82
|
+
token << read_char
|
83
|
+
|
84
|
+
case token.chars.last
|
85
|
+
when /[\da-zA-Z]/
|
86
|
+
token
|
87
|
+
else
|
88
|
+
fallback token
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def escape_code_sharp(token)
|
93
|
+
token << read_char
|
94
|
+
|
95
|
+
case token.chars.last
|
96
|
+
when /\d/
|
97
|
+
token
|
98
|
+
else
|
99
|
+
fallback token
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def escape_code_slash(token)
|
104
|
+
token << read_char
|
105
|
+
|
106
|
+
case token.chars.last
|
107
|
+
when /[a-zA-Z]/
|
108
|
+
token
|
109
|
+
else
|
110
|
+
fallback token
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def fallback(token)
|
115
|
+
token.chars
|
116
|
+
end
|
117
|
+
|
118
|
+
def read_char
|
119
|
+
source.getc
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Landescape::Converter::VT100 do
|
4
|
+
def convert(tokens)
|
5
|
+
converter = Landescape::Converter::VT100.start(array_to_result(tokens), non_block: true)
|
6
|
+
convert_thread = converter.instance_variable_get(:@convert_thread)
|
7
|
+
convert_thread.join if convert_thread.alive?
|
8
|
+
|
9
|
+
result_to_array(converter.result)
|
10
|
+
end
|
11
|
+
|
12
|
+
it { convert(%W(\e[4m H i \e[m .)).should == [[:set_attributes, 4], [:print, 'H'], [:print, 'i'], [:exit_attribute_mode], [:print, '.']] }
|
13
|
+
it { convert(%W(\e[50;2H)).should == [[:cursor_address, 50, 2]] }
|
14
|
+
it { convert(%W(\e[5A)).should == [[:cursor_up, 5]] }
|
15
|
+
it { convert(%W(\e[5B)).should == [[:cursor_down, 5]] }
|
16
|
+
it { convert(%W(\e[5C)).should == [[:cursor_right, 5]] }
|
17
|
+
it { convert(%W(\e[5D)).should == [[:cursor_left, 5]] }
|
18
|
+
it { convert(%W(\e[1m)).should == [[:set_attributes, 1]] }
|
19
|
+
it { convert(%W(\e[1;20r)).should == [[:change_scroll_region, 1, 20]] }
|
20
|
+
it { convert(%W(\e[s)).should == [[:save_cursor]] }
|
21
|
+
it { convert(%W(\e[u)).should == [[:restore_cursor]] }
|
22
|
+
it { convert(%W(\e[2J)).should == [[:clear_screen]] }
|
23
|
+
it { convert(%W(\e[J)).should == [[:clear_screen]] }
|
24
|
+
it { convert(%W(\e[K)).should == [[:clear_eol]] }
|
25
|
+
it { convert(%W(\e[?1h)).should == [[:keypad_xmit]] }
|
26
|
+
it { convert(%W(\e[?1l)).should == [[:keypad_local]] }
|
27
|
+
it { convert(%W(\e[?25h)).should == [[:cursor_visible]] }
|
28
|
+
it { convert(%W(\e[?25l)).should == [[:cursor_invisible]] }
|
29
|
+
it { convert(%W(\n)).should == [[:newline]] }
|
30
|
+
it { convert(%W(\r)).should == [[:carriage_return]] }
|
31
|
+
it { convert(%W(\a)).should == [[:bell]] }
|
32
|
+
it { convert(%W(\e\\)).should == [[:carriage_return]] }
|
33
|
+
it { convert(%W(\u000F)).should == [[:unknown, "\u000F"]] }
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'landescape'
|
2
|
+
require 'support/example_helper'
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
6
|
+
config.run_all_when_everything_filtered = true
|
7
|
+
config.filter_run :focus
|
8
|
+
|
9
|
+
config.order = 'random'
|
10
|
+
|
11
|
+
config.include ExampleHelper
|
12
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Landescape::Tokenizer do
|
4
|
+
def tokenize(str)
|
5
|
+
tokenizer = Landescape::Tokenizer.start(str)
|
6
|
+
tokenize_thread = tokenizer.instance_variable_get(:@tokenize_thread)
|
7
|
+
tokenize_thread.join if tokenize_thread.alive?
|
8
|
+
|
9
|
+
result_to_array(tokenizer.result)
|
10
|
+
end
|
11
|
+
|
12
|
+
it { tokenize("\e]***").should == %W(\e ] * * *) }
|
13
|
+
it { tokenize("Hi\e[32mHello\e[m").should == %W(H i \e[32m H e l l o \e[m) }
|
14
|
+
it { tokenize("\e[1;10H").should == %W(\e[1;10H) }
|
15
|
+
it { tokenize("\e[;H").should == %W(\e[;H) }
|
16
|
+
it { tokenize("\e[A").should == %W(\e[A) }
|
17
|
+
it { tokenize("\e[u").should == %W(\e[u) }
|
18
|
+
it { tokenize("\e[2J").should == %W(\e[2J) }
|
19
|
+
it { tokenize("\e[?1h").should == %W(\e[?1h) }
|
20
|
+
|
21
|
+
it { tokenize("\e(A\e)0").should == %W(\e\(A \e\)0) }
|
22
|
+
it { tokenize("\e#4").should == %W(\e#4) }
|
23
|
+
it { tokenize("\eB").should == %W(\eB) }
|
24
|
+
it { tokenize("\e<").should == %W(\e<) }
|
25
|
+
it { tokenize("\e>").should == %W(\e>) }
|
26
|
+
it { tokenize("\e=").should == %W(\e=) }
|
27
|
+
it { tokenize("\eD\eM").should == %W(\eD \eM) }
|
28
|
+
it { tokenize("\e\\").should == %W(\e\\) }
|
29
|
+
|
30
|
+
it { tokenize("\n\r").should == %W(\n \r) }
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: landescape
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- hibariya
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-11-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description: A library for handling escape sequence
|
56
|
+
email:
|
57
|
+
- celluloid.key@gmail.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .rspec
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- examples/shell_on_curses.rb
|
69
|
+
- landescape.gemspec
|
70
|
+
- lib/landescape.rb
|
71
|
+
- lib/landescape/converter.rb
|
72
|
+
- lib/landescape/evaluator.rb
|
73
|
+
- lib/landescape/evaluator/curses.rb
|
74
|
+
- lib/landescape/tokenizer.rb
|
75
|
+
- lib/landescape/version.rb
|
76
|
+
- spec/converter_spec.rb
|
77
|
+
- spec/evaluator_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- spec/support/example_helper.rb
|
80
|
+
- spec/tokenizer_spec.rb
|
81
|
+
homepage: https://github.com/hibariya/landescape
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
- examples
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
requirements:
|
97
|
+
- - '>='
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
version: '0'
|
100
|
+
requirements: []
|
101
|
+
rubyforge_project:
|
102
|
+
rubygems_version: 2.1.10
|
103
|
+
signing_key:
|
104
|
+
specification_version: 4
|
105
|
+
summary: Handle escape sequence
|
106
|
+
test_files:
|
107
|
+
- spec/converter_spec.rb
|
108
|
+
- spec/evaluator_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/example_helper.rb
|
111
|
+
- spec/tokenizer_spec.rb
|