terminal-layout 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/lib/ansi_string.rb +34 -11
- data/lib/terminal_layout.rb +80 -24
- data/spec/ansi_string_spec.rb +97 -0
- data/terminal-layout.gemspec +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c65b1156a204469a85174ffa2bb761a1f4f15593
|
4
|
+
data.tar.gz: 70c589abe13e269025404af7efe2555c2567e906
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 85ba780f984691a236879b5fc8754e0cf490f7df4d5b9288aa03411c735f985d335a5a454bbb72de72a16fe93b69458acf6c8dc8b2d1c2323d3eb9bc91857abc
|
7
|
+
data.tar.gz: 47c7317c78a4d5814682665fa77e27e9782b07c5af658eb8a60e73c78bb65ba90ccf490f3284240f08448a54bb38599ddb70d2e4d170574aac489e9e7620da69
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
terminal-layout (0.
|
4
|
+
terminal-layout (0.2.0)
|
5
5
|
highline
|
6
6
|
ruby-terminfo (~> 0.1.1)
|
7
7
|
ruby-termios (~> 0.9.6)
|
@@ -14,7 +14,7 @@ GEM
|
|
14
14
|
coderay (1.1.0)
|
15
15
|
columnize (0.9.0)
|
16
16
|
diff-lcs (1.2.5)
|
17
|
-
highline (1.7.
|
17
|
+
highline (1.7.8)
|
18
18
|
method_source (0.8.2)
|
19
19
|
pry (0.10.1)
|
20
20
|
coderay (~> 1.1.0)
|
@@ -57,4 +57,4 @@ DEPENDENCIES
|
|
57
57
|
terminal-layout!
|
58
58
|
|
59
59
|
BUNDLED WITH
|
60
|
-
1.
|
60
|
+
1.11.2
|
data/lib/ansi_string.rb
CHANGED
@@ -1,6 +1,14 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
1
5
|
class ANSIString
|
6
|
+
extend Forwardable
|
2
7
|
attr_reader :raw, :without_ansi
|
3
8
|
|
9
|
+
def_delegators :@without_ansi, :each_char, :each_byte, :index,
|
10
|
+
:match, :=~
|
11
|
+
|
4
12
|
def initialize(str)
|
5
13
|
process_string raw_string_for(str)
|
6
14
|
end
|
@@ -16,6 +24,10 @@ class ANSIString
|
|
16
24
|
self
|
17
25
|
end
|
18
26
|
|
27
|
+
def empty?
|
28
|
+
length == 0
|
29
|
+
end
|
30
|
+
|
19
31
|
def [](range)
|
20
32
|
# convert numeric position to a range
|
21
33
|
range = (range..range) if range.is_a?(Integer)
|
@@ -47,15 +59,6 @@ class ANSIString
|
|
47
59
|
self
|
48
60
|
end
|
49
61
|
|
50
|
-
# See String#index for arguments
|
51
|
-
def index(*args)
|
52
|
-
@without_ansi.index(*args)
|
53
|
-
end
|
54
|
-
|
55
|
-
def match(*args, &blk)
|
56
|
-
@without_ansi.match(*args, &blk)
|
57
|
-
end
|
58
|
-
|
59
62
|
# See String#rindex for arguments
|
60
63
|
def rindex(*args)
|
61
64
|
@without_ansi.rindex(*args)
|
@@ -73,6 +76,22 @@ class ANSIString
|
|
73
76
|
ANSIString.new str
|
74
77
|
end
|
75
78
|
|
79
|
+
def scan(pattern)
|
80
|
+
results = []
|
81
|
+
without_ansi.enum_for(:scan, pattern).each do
|
82
|
+
md = Regexp.last_match
|
83
|
+
if md.captures.any?
|
84
|
+
results << md.captures.map.with_index do |_, i|
|
85
|
+
# captures use 1-based indexing
|
86
|
+
self[md.begin(i+1)...md.end(i+1)]
|
87
|
+
end
|
88
|
+
else
|
89
|
+
results << self[md.begin(0)...md.end(0)]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
results
|
93
|
+
end
|
94
|
+
|
76
95
|
def slice(index, length=nil)
|
77
96
|
range = nil
|
78
97
|
index = index.without_ansi if index.is_a?(ANSIString)
|
@@ -178,6 +197,10 @@ class ANSIString
|
|
178
197
|
(other.class == self.class && other.raw == @raw) || (other.kind_of?(String) && other == @raw)
|
179
198
|
end
|
180
199
|
|
200
|
+
def <=>(other)
|
201
|
+
(other.class == self.class && @raw <=> other.raw)
|
202
|
+
end
|
203
|
+
|
181
204
|
private
|
182
205
|
|
183
206
|
def raw_string_for(str)
|
@@ -312,12 +335,12 @@ class ANSIString
|
|
312
335
|
str << [location[:start_ansi_sequence], location[:text], location[:end_ansi_sequence]].join
|
313
336
|
|
314
337
|
elsif location[:begins_at] >= range.begin && location[:begins_at] <= range.end
|
315
|
-
str << [location[:start_ansi_sequence], location[:text][
|
338
|
+
str << [location[:start_ansi_sequence], location[:text][0..(range.end - location[:begins_at])], location[:end_ansi_sequence]].join
|
316
339
|
|
317
340
|
# If the location falls within the given range then make sure we pull
|
318
341
|
# out the bits that we want, and keep ANSI escape sequenece intact while
|
319
342
|
# doing so.
|
320
|
-
|
343
|
+
elsif (location[:begins_at] <= range.begin && location[:ends_at] >= range.end) || range.cover?(location[:ends_at])
|
321
344
|
start_index = range.begin - location[:begins_at]
|
322
345
|
end_index = range.end - location[:begins_at]
|
323
346
|
str << [location[:start_ansi_sequence], location[:text][start_index..end_index], location[:end_ansi_sequence]].join
|
data/lib/terminal_layout.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'pry'
|
2
1
|
require 'ansi_string'
|
3
2
|
require 'ostruct'
|
4
3
|
|
@@ -348,9 +347,18 @@ module TerminalLayout
|
|
348
347
|
end
|
349
348
|
|
350
349
|
def content=(str)
|
351
|
-
|
352
|
-
@content
|
353
|
-
|
350
|
+
new_content = ANSIString.new(str)
|
351
|
+
if @content != new_content
|
352
|
+
old_content = @content
|
353
|
+
@content = new_content
|
354
|
+
emit :content_changed, old_content, @content
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def children=(new_children)
|
359
|
+
old_children = @children
|
360
|
+
@children = new_children
|
361
|
+
emit :child_changed, old_children, new_children
|
354
362
|
end
|
355
363
|
|
356
364
|
def position
|
@@ -398,14 +406,27 @@ module TerminalLayout
|
|
398
406
|
|
399
407
|
def initialize(*args)
|
400
408
|
super
|
409
|
+
@computed = { x: 0, y: 0 }
|
401
410
|
@cursor_offset_x = 0
|
402
411
|
@cursor_position = OpenStruct.new(x: 0, y: 0)
|
412
|
+
@position = 0
|
413
|
+
end
|
414
|
+
|
415
|
+
def cursor_off
|
416
|
+
@style.update(cursor: 'none')
|
417
|
+
end
|
418
|
+
|
419
|
+
def cursor_on
|
420
|
+
@style.update(cursor: 'auto')
|
403
421
|
end
|
404
422
|
|
405
423
|
def content=(str)
|
406
|
-
|
407
|
-
@content
|
408
|
-
|
424
|
+
new_content = ANSIString.new(str)
|
425
|
+
if @content != new_content
|
426
|
+
old_content = @content
|
427
|
+
@content = new_content
|
428
|
+
emit :content_changed, old_content, @content
|
429
|
+
end
|
409
430
|
end
|
410
431
|
|
411
432
|
def position=(position)
|
@@ -416,7 +437,11 @@ module TerminalLayout
|
|
416
437
|
|
417
438
|
def update_computed(style)
|
418
439
|
@computed.merge!(style)
|
419
|
-
|
440
|
+
if style[:y] > 0
|
441
|
+
@cursor_position.x = @computed[:width] #@computed[:width] - (style[:x] + @cursor_offset_x)
|
442
|
+
else
|
443
|
+
@cursor_position.x = style[:x] + @cursor_offset_x
|
444
|
+
end
|
420
445
|
@cursor_position.y = style[:y]
|
421
446
|
end
|
422
447
|
end
|
@@ -433,6 +458,7 @@ module TerminalLayout
|
|
433
458
|
def initialize(output: $stdout)
|
434
459
|
@output = output
|
435
460
|
@term_info = TermInfo.new ENV["TERM"], @output
|
461
|
+
@previously_printed_lines = []
|
436
462
|
@x, @y = 0, 0
|
437
463
|
end
|
438
464
|
|
@@ -445,27 +471,45 @@ module TerminalLayout
|
|
445
471
|
cursor_y = cursor_position.y
|
446
472
|
|
447
473
|
# TODO: make this work when lines wrap
|
448
|
-
if cursor_x < 0
|
474
|
+
if cursor_x < 0 && cursor_y == 0
|
449
475
|
cursor_x = terminal_width
|
450
476
|
cursor_y -= 1
|
451
477
|
elsif cursor_x >= terminal_width
|
478
|
+
cursor_y = cursor_x / terminal_width
|
479
|
+
cursor_x -= terminal_width
|
452
480
|
end
|
453
481
|
|
454
|
-
|
455
|
-
|
482
|
+
if @y < cursor_y
|
483
|
+
# moving backwards
|
484
|
+
move_up_n_rows(@y - cursor_y)
|
485
|
+
elsif @y > cursor_y
|
486
|
+
# moving forwards
|
487
|
+
move_down_n_rows(cursor_y - @y)
|
488
|
+
end
|
456
489
|
|
457
|
-
|
458
|
-
|
490
|
+
move_down_n_rows cursor_y
|
491
|
+
move_to_beginning_of_row
|
492
|
+
move_right_n_characters cursor_x
|
459
493
|
|
460
|
-
|
461
|
-
|
494
|
+
@x = cursor_x
|
495
|
+
@y = cursor_y
|
496
|
+
|
497
|
+
if input_box.style[:cursor] == 'none'
|
498
|
+
@output.print @term_info.control_string "civis"
|
499
|
+
else
|
500
|
+
@output.print @term_info.control_string "cnorm"
|
501
|
+
end
|
462
502
|
end
|
463
503
|
|
464
|
-
def reset
|
465
|
-
|
504
|
+
def render(object, reset: false)
|
505
|
+
dumb_render(object, reset: reset)
|
466
506
|
end
|
467
507
|
|
468
|
-
def dumb_render(object)
|
508
|
+
def dumb_render(object, reset: false)
|
509
|
+
if reset
|
510
|
+
@y = 0
|
511
|
+
@previously_printed_lines.clear
|
512
|
+
end
|
469
513
|
@output.print @term_info.control_string "civis"
|
470
514
|
move_up_n_rows @y
|
471
515
|
move_to_beginning_of_row
|
@@ -476,26 +520,38 @@ module TerminalLayout
|
|
476
520
|
end
|
477
521
|
|
478
522
|
object_width = object.width
|
479
|
-
clear_screen_down
|
480
523
|
|
481
524
|
rendered_content = object.render
|
482
525
|
printable_content = rendered_content.sub(/\s*\Z/m, '')
|
483
526
|
|
484
|
-
printable_content.
|
485
|
-
|
486
|
-
|
527
|
+
printable_lines = printable_content.split(/\n/).each_with_object([]) do |line, results|
|
528
|
+
if line.empty?
|
529
|
+
results << line
|
530
|
+
else
|
531
|
+
results.concat line.scan(/.{1,#{terminal_width}}/)
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
printable_lines.zip(@previously_printed_lines) do |new_line, previous_line|
|
536
|
+
if new_line != previous_line
|
537
|
+
term_info.control "el"
|
538
|
+
move_to_beginning_of_row
|
539
|
+
@output.puts new_line
|
540
|
+
else
|
541
|
+
move_down_n_rows 1
|
542
|
+
end
|
487
543
|
end
|
488
544
|
move_to_beginning_of_row
|
545
|
+
clear_screen_down
|
489
546
|
|
490
547
|
# calculate lines drawn so we know where we are
|
491
548
|
lines_drawn = (printable_content.length / object_width.to_f).ceil
|
492
549
|
@y = lines_drawn
|
493
550
|
|
494
551
|
input_box = find_input_box(object.box)
|
495
|
-
|
496
552
|
render_cursor(input_box)
|
497
553
|
|
498
|
-
@
|
554
|
+
@previously_printed_lines = printable_lines
|
499
555
|
end
|
500
556
|
|
501
557
|
def find_input_box(dom_node)
|
data/spec/ansi_string_spec.rb
CHANGED
@@ -14,6 +14,13 @@ describe 'ANSIString' do
|
|
14
14
|
ansi_string = ANSIString.new "this #{blue('is')} a string"
|
15
15
|
expect(ansi_string).to be
|
16
16
|
end
|
17
|
+
|
18
|
+
it "can be constructed with UTF-8 characters" do;
|
19
|
+
expect do
|
20
|
+
ansi_string = ANSIString.new "this #{blue('ƒ')} a string"
|
21
|
+
expect(ansi_string).to be
|
22
|
+
end.to_not raise_error
|
23
|
+
end
|
17
24
|
end
|
18
25
|
|
19
26
|
describe "redundant ANSI sequences" do
|
@@ -48,6 +55,30 @@ describe 'ANSIString' do
|
|
48
55
|
end
|
49
56
|
end
|
50
57
|
|
58
|
+
describe "#each_byte" do
|
59
|
+
let(:blue_ansi_string){ ANSIString.new blue_string }
|
60
|
+
let(:blue_string){ blue("this is blue") }
|
61
|
+
|
62
|
+
it "iterates over each character ignoring ANSI sequences" do
|
63
|
+
expected = "this is blue"
|
64
|
+
actual = ""
|
65
|
+
blue_ansi_string.each_byte { |ch| actual << ch }
|
66
|
+
expect(actual).to eq(expected)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#each_char" do
|
71
|
+
let(:blue_ansi_string){ ANSIString.new blue_string }
|
72
|
+
let(:blue_string){ blue("this is blue") }
|
73
|
+
|
74
|
+
it "iterates over each character ignoring ANSI sequences" do
|
75
|
+
expected = "this is blue"
|
76
|
+
actual = ""
|
77
|
+
blue_ansi_string.each_char { |ch| actual << ch }
|
78
|
+
expect(actual).to eq(expected)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
51
82
|
describe "#<<" do
|
52
83
|
it "appends a String onto the end of the current ANSIString" do
|
53
84
|
ansi_string = ANSIString.new ""
|
@@ -83,6 +114,22 @@ describe 'ANSIString' do
|
|
83
114
|
end
|
84
115
|
end
|
85
116
|
|
117
|
+
describe "#empty?" do
|
118
|
+
it "returns true when empty" do
|
119
|
+
expect(ANSIString.new("").empty?).to be(true)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "returns true when it only contains ANSI sequences" do
|
123
|
+
expect(ANSIString.new(blue("")).empty?).to be(true)
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns false when there are non-ANSI characters" do
|
127
|
+
expect(ANSIString.new("a").empty?).to be(false)
|
128
|
+
expect(ANSIString.new(blue("a")).empty?).to be(false)
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
86
133
|
describe "#index" do
|
87
134
|
it "returns the index of the first occurrence of the given substring" do
|
88
135
|
ansi_string = ANSIString.new("this is not blue")
|
@@ -179,6 +226,11 @@ describe 'ANSIString' do
|
|
179
226
|
expect(ansi_string[17..-5]).to eq yellow("is is ye")
|
180
227
|
end
|
181
228
|
|
229
|
+
it "returns the correct substring when location of an ANSI sequence comes before the end of the request" do
|
230
|
+
s = ANSIString.new("ABC \e[7mGemfile.lock\e[0m LICENSE.txt README.md")
|
231
|
+
expect(s[4...28]).to eq ANSIString.new("\e[7mGemfile.lock\e[0m LICENSE.txt")
|
232
|
+
end
|
233
|
+
|
182
234
|
it "returns text that is not ANSI escaped" do
|
183
235
|
expect(ansi_string[12..14]).to eq "ABC"
|
184
236
|
end
|
@@ -398,6 +450,17 @@ describe 'ANSIString' do
|
|
398
450
|
end
|
399
451
|
end
|
400
452
|
|
453
|
+
describe "<=>" do
|
454
|
+
let(:string_1){ ANSIString.new blue("abc") }
|
455
|
+
let(:string_2){ ANSIString.new blue("def") }
|
456
|
+
|
457
|
+
it "behaves the same as a normal string" do
|
458
|
+
expect(string_1 <=> string_2).to eq(-1)
|
459
|
+
expect(string_1 <=> string_1).to eq(0)
|
460
|
+
expect(string_2 <=> string_1).to eq(1)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
401
464
|
describe "#match" do
|
402
465
|
it "matches on a string pattren" do
|
403
466
|
string = "apples are bananas are they not?"
|
@@ -412,6 +475,40 @@ describe 'ANSIString' do
|
|
412
475
|
end
|
413
476
|
end
|
414
477
|
|
478
|
+
describe "#=~" do
|
479
|
+
it "matches on a regex pattren" do
|
480
|
+
string = "apples are bananas are they not?"
|
481
|
+
ansi_string = ANSIString.new("app#{red('les are bananas')} are they not?")
|
482
|
+
expect(ansi_string =~ /are/).to eq(string =~ /are/)
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
describe "#scan" do
|
487
|
+
it "scans without capture groups" do
|
488
|
+
string = "567"
|
489
|
+
ansi_string = ANSIString.new("1234#{red('5678')}90")
|
490
|
+
expect(ansi_string.scan(/.{2}/)).to eq([
|
491
|
+
ANSIString.new("12"),
|
492
|
+
ANSIString.new("34"),
|
493
|
+
ANSIString.new("#{red('56')}"),
|
494
|
+
ANSIString.new("#{red('78')}"),
|
495
|
+
ANSIString.new("90")
|
496
|
+
])
|
497
|
+
end
|
498
|
+
|
499
|
+
it "scans with capture groups" do
|
500
|
+
string = "567"
|
501
|
+
ansi_string = ANSIString.new("1234#{red('5678')}90")
|
502
|
+
expect(ansi_string.scan(/(.)./)).to eq([
|
503
|
+
[ANSIString.new("1")],
|
504
|
+
[ANSIString.new("3")],
|
505
|
+
[ANSIString.new("#{red('5')}")],
|
506
|
+
[ANSIString.new("#{red('7')}")],
|
507
|
+
[ANSIString.new("9")]
|
508
|
+
])
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
415
512
|
describe "#replace" do
|
416
513
|
it "replaces the contents of the current string with the new string" do
|
417
514
|
ansi_string = ANSIString.new("abc")
|
data/terminal-layout.gemspec
CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
6
|
spec.name = "terminal-layout"
|
7
|
-
spec.version = "0.
|
7
|
+
spec.version = "0.2.0"
|
8
8
|
spec.authors = ["Zach Dennis"]
|
9
9
|
spec.email = ["zach.dennis@gmail.com"]
|
10
10
|
spec.summary = %q{A terminal layout manager}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: terminal-layout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Zach Dennis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: ruby-terminfo
|
@@ -148,7 +148,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
148
|
version: '0'
|
149
149
|
requirements: []
|
150
150
|
rubyforge_project:
|
151
|
-
rubygems_version: 2.4.
|
151
|
+
rubygems_version: 2.4.5.1
|
152
152
|
signing_key:
|
153
153
|
specification_version: 4
|
154
154
|
summary: A terminal layout manager
|
@@ -156,3 +156,4 @@ test_files:
|
|
156
156
|
- spec/ansi_string_spec.rb
|
157
157
|
- spec/spec_helper.rb
|
158
158
|
- spec/terminal_layout_spec.rb
|
159
|
+
has_rdoc:
|