terminal-layout 0.1.1 → 0.2.0

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 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: