Use terminal binary as a base for Terminal

openid
Marcin Kulik 11 years ago
parent e537c1b78a
commit 0e2c1784c6

@ -22,8 +22,8 @@ gem 'dotenv-rails', '~> 0.8'
gem 'sass-rails', '~> 4.0.0' gem 'sass-rails', '~> 4.0.0'
gem 'coffee-rails', '~> 4.0.0' gem 'coffee-rails', '~> 4.0.0'
gem 'uglifier', '>= 2.1.2' gem 'uglifier', '>= 2.1.2'
gem 'tsm', :git => 'git://github.com/sickill/tsm.git' gem 'sinatra', '~> 1.4.3', :require => false
gem 'sinatra', :require => false gem 'oj', '~> 2.1.4'
group :development do group :development do
gem 'quiet_assets', '~> 1.0.1' gem 'quiet_assets', '~> 1.0.1'

@ -1,10 +1,3 @@
GIT
remote: git://github.com/sickill/tsm.git
revision: 6959bcecdb9061607fd41e9aee34f25e65c11a2d
specs:
tsm (0.1.0)
ffi (~> 1.8)
GEM GEM
remote: http://rubygems.org/ remote: http://rubygems.org/
specs: specs:
@ -172,6 +165,7 @@ GEM
jwt (~> 0.1.4) jwt (~> 0.1.4)
multi_json (~> 1.0) multi_json (~> 1.0)
rack (~> 1.2) rack (~> 1.2)
oj (2.1.4)
omniauth (1.1.4) omniauth (1.1.4)
hashie (>= 1.2, < 3) hashie (>= 1.2, < 3)
rack rack
@ -338,6 +332,7 @@ DEPENDENCIES
jasmine-rails (~> 0.4.5) jasmine-rails (~> 0.4.5)
jquery-rails (~> 2.2.0) jquery-rails (~> 2.2.0)
kaminari (~> 0.14.1) kaminari (~> 0.14.1)
oj (~> 2.1.4)
omniauth (~> 1.1.4) omniauth (~> 1.1.4)
omniauth-github (~> 1.1.0) omniauth-github (~> 1.1.0)
omniauth-twitter (~> 0.0.16) omniauth-twitter (~> 0.0.16)
@ -357,10 +352,9 @@ DEPENDENCIES
sidekiq (~> 2.13) sidekiq (~> 2.13)
simple_form (~> 3.0.0.rc) simple_form (~> 3.0.0.rc)
simplecov (~> 0.7.1) simplecov (~> 0.7.1)
sinatra sinatra (~> 1.4.3)
slim (~> 2.0.0) slim (~> 2.0.0)
thin (~> 1.5.0) thin (~> 1.5.0)
tsm!
uglifier (>= 2.1.2) uglifier (>= 2.1.2)
unicorn (~> 4.6.2) unicorn (~> 4.6.2)
zeus (= 0.13.4.pre2) zeus (= 0.13.4.pre2)

@ -1,67 +1,59 @@
require 'open3'
class Terminal class Terminal
BINARY_PATH = "/home/kill/code/ascii.io-converter/terminal"
def initialize(width, height) def initialize(width, height)
@screen = TSM::Screen.new(width, height) @process = Process.new("#{BINARY_PATH} #{width} #{height}")
@vte = TSM::Vte.new(@screen)
end end
def feed(data) def feed(data)
vte.input(data) process.write("d\n#{data.bytesize}\n#{data}")
end end
def snapshot def snapshot
lines = [] process.write("p\n")
lines = Oj.load(process.read_line)
screen.draw do |x, y, char, screen_attribute|
assign_cell(lines, x, y, char, screen_attribute)
end
Snapshot.build(lines) Snapshot.build(lines)
end end
def cursor def cursor
Cursor.new(screen.cursor_x, screen.cursor_y, screen.cursor_visible?) process.write("c\n")
c = Oj.load(process.read_line)
Cursor.new(c['x'], c['y'], c['visible'])
end end
def release def release
screen.release process.stop
vte.release
end end
private private
attr_reader :screen, :vte attr_reader :process
def assign_cell(lines, x, y, char, screen_attribute) class Process
line = lines[y] ||= []
line[x] = [sanitize_char(char), attributes_hash(screen_attribute)]
end
def sanitize_char(char) def initialize(command)
char. @stdin, @stdout, @thread = Open3.popen2(command)
encode('UTF-16', :invalid => :replace, :undef => :replace,
:replace => "\001").
encode('UTF-8').gsub(/\001+/, '?').
first
end end
def attributes_hash(screen_attribute) def write(data)
attrs = {} raise "terminal died" unless @thread.alive?
@stdin.write(data)
[:fg, :bg, :bold?, :underline?, :inverse?, :blink?].each do |name|
assign_attr(attrs, screen_attribute, name)
end end
attrs def read_line
raise "terminal died" unless @thread.alive?
@stdout.readline.strip
end end
def assign_attr(attrs, screen_attribute, name) def stop
value = screen_attribute.public_send(name) @stdin.close
if value
key = name.to_s.sub('?', '').to_sym
attrs[key] = value
end end
end end
end end

@ -0,0 +1 @@
Oj.default_options = { mode: :strict }

@ -4,43 +4,23 @@ require 'spec_helper'
describe Terminal do describe Terminal do
let(:terminal) { Terminal.new(20, 10) } let(:terminal) { Terminal.new(4, 3) }
let(:tsm_screen) { double('tsm_screen', :draw => nil) } let(:first_line_text) { subject.as_json.first.map(&:first).join.strip }
let(:tsm_vte) { double('tsm_vte', :input => nil) }
before do before do
allow(TSM::Screen).to receive(:new).with(20, 10) { tsm_screen } data.each do |chunk|
allow(TSM::Vte).to receive(:new).with(tsm_screen) { tsm_vte } terminal.feed(chunk)
end end
describe '#feed' do
subject { terminal.feed('foo') }
it 'feeds the vte with the data' do
subject
expect(tsm_vte).to have_received(:input).with('foo')
end end
after do
terminal.release
end end
describe '#snapshot' do describe '#snapshot' do
subject { terminal.snapshot } subject { terminal.snapshot }
def make_attr(attrs = {}) let(:data) { ["f\e[31mo\e[42mo\n", "\rb\e[0ma\e[1;4;5;7ms"] }
TSM::ScreenAttribute.new.tap do |screen_attribute|
attrs.each { |name, value| screen_attribute[name] = value }
end
end
before do
allow(tsm_screen).to receive(:draw).
and_yield(0, 0, 'f', make_attr(fg: 1)).
and_yield(1, 0, 'o', make_attr(bg: 2)).
and_yield(0, 1, 'o', make_attr(bold?: true)).
and_yield(1, 1, 'ś', make_attr(fg: 2, bg: 3,
bold?: true, underline?: true,
inverse?: true, blink?: true))
end
it 'returns an instance of Snapshot' do it 'returns an instance of Snapshot' do
expect(subject).to be_kind_of(Snapshot) expect(subject).to be_kind_of(Snapshot)
@ -49,46 +29,121 @@ describe Terminal do
it "returns each screen cell with its character attributes" do it "returns each screen cell with its character attributes" do
expect(subject.as_json).to eq([ expect(subject.as_json).to eq([
[ [
['f', fg: 1], ['f', {}], ['o', fg: 1], ['o', fg: 1, bg: 2], [' ', {}],
['o', bg: 2] ],
[
['b', fg: 1, bg: 2], ['a', {}], ['s', bold: true, underline: true,
inverse: true, blink: true],
[' ', inverse: true] # <- cursor here
], ],
[ [
['o', bold: true], [' ', {}], [' ', {}], [' ', {}], [' ', {}]
['ś', fg: 2, bg: 3, bold: true, underline: true, inverse: true,
blink: true]
] ]
]) ])
end end
describe 'utf-8 characters handling' do
let(:terminal) { Terminal.new(20, 1) }
context "when polish national characters given" do
let(:data) { ['żółć'] }
it 'returns proper utf-8 string' do
expect(first_line_text).to eq('żółć')
end
end
context "when chinese national characters given" do
let(:data) { ['雞機基積'] }
it 'returns proper utf-8 string' do
expect(first_line_text).to eq('雞機基積')
end
end
end
context "when invalid utf-8 character is yielded by tsm_screen" do context "when invalid utf-8 character is yielded by tsm_screen" do
before do let(:terminal) { Terminal.new(3, 1) }
allow(tsm_screen).to receive(:draw). let(:data) { ["A\xc3\xff\xaaZ"] }
and_yield(0, 0, "\xc3\xff\xaa", make_attr(fg: 1))
it 'gets replaced with "<22>"' do
expect(first_line_text).to eq('A<>Z')
end
end
context "when double quote character ...." do
let(:terminal) { Terminal.new(6, 1) }
let(:data) { ['"a"b"'] }
it 'works' do
expect(first_line_text).to eq('"a"b"')
end
end end
it 'gets replaced with "?"' do context "when backslash character..." do
expect(subject.as_json).to eq([[['?', fg: 1]]]) let(:terminal) { Terminal.new(6, 1) }
let(:data) { ['a\\b'] }
it 'works' do
expect(first_line_text).to eq('a\\b')
end
end
describe 'with a 256-color mode foreground color' do
subject { terminal.snapshot.as_json.first.first.last[:fg] }
let(:data) { ["\x1b[38;5;#{color_code}mX"] }
(1..255).each do |n|
context "of value #{n}" do
let(:color_code) { n }
it { should eq(n) }
end end
end end
end end
describe '#cursor' do describe 'with a 256-color mode background color' do
let(:tsm_screen) { double('tsm_screen', :cursor_x => 3, :cursor_y => 5, subject { terminal.snapshot.as_json.first.first.last[:bg] }
:cursor_visible? => false) }
let(:data) { ["\x1b[48;5;#{color_code}mX"] }
(1..255).each do |n|
context "of value #{n}" do
let(:color_code) { n }
it { should eq(n) }
end
end
end
end
describe '#cursor' do
subject { terminal.cursor } subject { terminal.cursor }
let(:data) { ["foo\n\rba"] }
it 'gets its x position from the screen' do it 'gets its x position from the screen' do
expect(subject.x).to eq(3) expect(subject.x).to eq(2)
end end
it 'gets its y position from the screen' do it 'gets its y position from the screen' do
expect(subject.y).to eq(5) expect(subject.y).to eq(1)
end
it 'gets its visibility from the screen' do
expect(subject.visible).to eq(true)
end
context "when cursor was hidden" do
before do
terminal.feed("\e[?25l")
end end
it 'gets its visibility from the screen' do it 'gets its visibility from the screen' do
expect(subject.visible).to eq(false) expect(subject.visible).to eq(false)
end end
end end
end
end end

Loading…
Cancel
Save