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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 69a8c9c2bf542621e42aaefcd221d552b507a333
4
- data.tar.gz: 029d171fa3c85d36edd1312e6a1ba45b1f77c4b6
3
+ metadata.gz: c65b1156a204469a85174ffa2bb761a1f4f15593
4
+ data.tar.gz: 70c589abe13e269025404af7efe2555c2567e906
5
5
  SHA512:
6
- metadata.gz: ad0a729599af474de93c42ef4b704b326514e7515e703a1377f583b6e018185de12c0bcb87fe26dd323bf1ddb70f2429c0f4f19230737d26aff56117cfd6ee73
7
- data.tar.gz: 69d6c4ca7dac9d55941bf94404b780fdc07e8918363dc3b8b5b7124fcba9b82644d986ad249bffbd4d92526b56e807359da21ccf3a5f3135baa84ce530e0cbed
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.1.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.2)
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.10.4
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][range.begin..(range.end - location[:begins_at])], location[:end_ansi_sequence]].join
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
- elsif (location[:begins_at] <= range.begin && location[:ends_at] >= range.end) || range.cover?(location[:ends_at])
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
@@ -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
- old = @content
352
- @content = ANSIString.new(str)
353
- emit :content_changed, old, @content
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
- old = @content
407
- @content = ANSIString.new(str)
408
- emit :content_changed, old, @content
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
- @cursor_position.x = style[:x] + @cursor_offset_x
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
- move_right_n_characters cursor_position.x
455
- move_down_n_rows cursor_position.y
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
- @y = input_box.cursor_position.y
458
- end
490
+ move_down_n_rows cursor_y
491
+ move_to_beginning_of_row
492
+ move_right_n_characters cursor_x
459
493
 
460
- def render(object)
461
- dumb_render(object)
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
- @y = 0
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.lines.each do |line|
485
- move_to_beginning_of_row
486
- @output.puts line
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
- @output.print @term_info.control_string "cnorm"
554
+ @previously_printed_lines = printable_lines
499
555
  end
500
556
 
501
557
  def find_input_box(dom_node)
@@ -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")
@@ -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.1.1"
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.1.1
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: 2015-12-03 00:00:00.000000000 Z
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.6
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: