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
def attributes_hash(screen_attribute)
attrs = {}
[:fg, :bg, :bold?, :underline?, :inverse?, :blink?].each do |name|
assign_attr(attrs, screen_attribute, name)
end end
attrs def write(data)
end raise "terminal died" unless @thread.alive?
@stdin.write(data)
end
def assign_attr(attrs, screen_attribute, name) def read_line
value = screen_attribute.public_send(name) raise "terminal died" unless @thread.alive?
@stdout.readline.strip
end
if value def stop
key = name.to_s.sub('?', '').to_sym @stdin.close
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 end
describe '#feed' do after do
subject { terminal.feed('foo') } terminal.release
it 'feeds the vte with the data' do
subject
expect(tsm_vte).to have_received(:input).with('foo')
end
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,45 +29,120 @@ 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
context "when backslash 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 describe 'with a 256-color mode foreground color' do
expect(subject.as_json).to eq([[['?', fg: 1]]]) 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
describe 'with a 256-color mode background color' do
subject { terminal.snapshot.as_json.first.first.last[:bg] }
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 end
end end
describe '#cursor' do describe '#cursor' do
let(:tsm_screen) { double('tsm_screen', :cursor_x => 3, :cursor_y => 5,
:cursor_visible? => false) }
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 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(true)
end
context "when cursor was hidden" do
before do
terminal.feed("\e[?25l")
end
it 'gets its visibility from the screen' do
expect(subject.visible).to eq(false)
end
end end
end end

Loading…
Cancel
Save