ektoplayer 0.1.6 → 0.1.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -12
  3. data/lib/ektoplayer/application.rb +6 -5
  4. data/lib/ektoplayer/bindings.rb +61 -55
  5. data/lib/ektoplayer/common.rb +19 -6
  6. data/lib/ektoplayer/compat.rb +3 -3
  7. data/lib/ektoplayer/config.rb +1 -11
  8. data/lib/ektoplayer/controllers/browser.rb +7 -5
  9. data/lib/ektoplayer/controllers/help.rb +1 -1
  10. data/lib/ektoplayer/controllers/info.rb +1 -1
  11. data/lib/ektoplayer/controllers/playlist.rb +24 -12
  12. data/lib/ektoplayer/icurses.rb +21 -0
  13. data/lib/ektoplayer/icurses/curses.rb +53 -0
  14. data/lib/ektoplayer/icurses/ffi_ncurses.rb +69 -0
  15. data/lib/ektoplayer/icurses/ncurses.rb +79 -0
  16. data/lib/ektoplayer/icurses/ncursesw.rb +1 -0
  17. data/lib/ektoplayer/icurses/sugar.rb +65 -0
  18. data/lib/ektoplayer/icurses/test.rb +99 -0
  19. data/lib/ektoplayer/models/player.rb +2 -2
  20. data/lib/ektoplayer/{mp3player.rb → players/mpg_portaudio_player.rb} +3 -3
  21. data/lib/ektoplayer/players/mpg_wrapper_player.rb +107 -0
  22. data/lib/ektoplayer/theme.rb +1 -6
  23. data/lib/ektoplayer/ui.rb +100 -129
  24. data/lib/ektoplayer/ui/colors.rb +14 -14
  25. data/lib/ektoplayer/ui/widgets.rb +4 -4
  26. data/lib/ektoplayer/ui/widgets/labelwidget.rb +1 -1
  27. data/lib/ektoplayer/ui/widgets/listwidget.rb +115 -46
  28. data/lib/ektoplayer/views/help.rb +7 -10
  29. data/lib/ektoplayer/views/info.rb +29 -38
  30. data/lib/ektoplayer/views/mainwindow.rb +2 -5
  31. data/lib/ektoplayer/views/playinginfo.rb +15 -20
  32. data/lib/ektoplayer/views/progressbar.rb +30 -10
  33. data/lib/ektoplayer/views/splash.rb +24 -25
  34. data/lib/ektoplayer/views/tabbar.rb +6 -5
  35. data/lib/ektoplayer/views/trackrenderer.rb +20 -14
  36. metadata +15 -47
  37. data/lib/ektoplayer/views/volumemeter.rb +0 -76
@@ -1,5 +1,5 @@
1
1
  require_relative 'model'
2
- require_relative '../mp3player'
2
+ require_relative '../players/mpg_wrapper_player'
3
3
 
4
4
  module Ektoplayer
5
5
  module Models
@@ -7,7 +7,7 @@ module Ektoplayer
7
7
  def initialize(client)
8
8
  super()
9
9
  @client = client
10
- @player = Mp3Player.new
10
+ @player = MpgWrapperPlayer.new
11
11
  @events.register(:position_change, :track_completed, :pause, :stop, :play)
12
12
  @player.events.on_all(&@events.method(:trigger))
13
13
 
@@ -2,7 +2,7 @@ require 'thread'
2
2
  require 'mpg123'
3
3
  require 'portaudio'
4
4
 
5
- require_relative 'events'
5
+ require_relative '../events'
6
6
 
7
7
  class Mpg123
8
8
  alias :samples_per_frame :spf
@@ -38,7 +38,7 @@ class Mpg123
38
38
  end
39
39
  end
40
40
 
41
- class Mp3Player
41
+ class MpgPortaudioPlayer
42
42
  attr_reader :events
43
43
 
44
44
  def initialize(buffer_size = 2**12)
@@ -148,4 +148,4 @@ class Mp3Player
148
148
  def forward(seconds = 2)
149
149
  seek(position + seconds)
150
150
  end
151
- end
151
+ e
@@ -0,0 +1,107 @@
1
+ require 'thread'
2
+ require 'open3'
3
+ require 'scanf'
4
+
5
+ require_relative '../events'
6
+
7
+ fail 'MpgWrapperPlayer: mpg123 not found' unless system('which mpg123 >/dev/null')
8
+
9
+ class MpgWrapperPlayer
10
+ attr_reader :events, :file
11
+
12
+ STATE_STOPPED, STATE_PAUSED, STATE_PLAYING = 0, 1, 2
13
+
14
+ def initialize
15
+ @events = Events.new(:play, :pause, :stop, :position_change)
16
+ @lock = Mutex.new
17
+ @mpg123_in, @mpg123_out, @mpg123_thread = nil, nil, nil
18
+ @state = 0
19
+ @file = ''
20
+
21
+ @frames_played = @frames_remaining =
22
+ @seconds_played = @seconds_remaining = 0
23
+ end
24
+
25
+ def play(file=nil)
26
+ start_mpg123_thread
27
+ @file = file if file
28
+ write("L #{@file}")
29
+ end
30
+
31
+ def pause; write(?P) if @state == STATE_PLAYING end
32
+ def stop; write(?S) if @state != STATE_STOPPED end
33
+ def toggle; write(?P) end
34
+
35
+ def level; 30 end
36
+ def paused?; @state == STATE_PAUSED end
37
+ def stopped?; @state == STATE_STOPPED end
38
+ def playing?; @state == STATE_PLAYING end
39
+
40
+ def status
41
+ return :playing if playing?
42
+ return :paused if paused?
43
+ return :stopped if stopped?
44
+ end
45
+
46
+ def position; @seconds_played end
47
+ def length; @seconds_played + @seconds_remaining end
48
+ def position_percent; Float(@seconds_played) / length end
49
+
50
+ def seek(seconds) write("J #{seconds}s") end
51
+ def rewind(seconds = 2) write("J -#{seconds}s") end
52
+ def forward(seconds = 2) write("J +#{seconds}s") end
53
+ alias :backward :rewind
54
+
55
+ private def write(string)
56
+ @lock.synchronize do
57
+ @mpg123_in.write(string + ?\n)
58
+ end
59
+ rescue
60
+ Ektoplayer::Application.log(self, $!)
61
+ end
62
+
63
+ private def start_mpg123_thread
64
+ @lock.synchronize do
65
+ unless @mpg123_thread
66
+ Thread.new do
67
+ begin
68
+ @mpg123_in, @mpg123_out, _, @mpg123_thread =
69
+ Open3.popen3('mpg123', '-o', 'jack,pulse,alsa,oss', '--fuzzy', '-R')
70
+
71
+ while (line = @mpg123_out.readline)
72
+ if line[1] == ?F
73
+ @frames_played, @frames_remaining,
74
+ @seconds_played, @seconds_remaining =
75
+ line.scanf('@F %d %d %f %f')
76
+ @events.trigger(:position_change)
77
+ elsif line[1] == ?P
78
+ if (@state = line[3].to_i) == STATE_STOPPED
79
+ if @seconds_remaining < 3
80
+ @events.trigger(:stop, :track_completed)
81
+ else
82
+ @events.trigger(:stop)
83
+ end
84
+ elsif @state == STATE_PAUSED
85
+ @events.trigger(:pause)
86
+ elsif @state == STATE_PLAYING
87
+ @events.trigger(:play)
88
+ end
89
+ end
90
+ end
91
+ rescue
92
+ Ektoplayer::Application.log(self, $!)
93
+ ensure
94
+ # shouldn't reach here
95
+ Ektoplayer::Application.log(self, 'player closed')
96
+ @mpg123_thread.kill
97
+ @mpg123_thread = nil
98
+ @mpg123_in.close
99
+ @mpg123_out.close
100
+ end
101
+ end
102
+
103
+ sleep 0.1 while not @mpg123_thread
104
+ end
105
+ end
106
+ end
107
+ end
@@ -24,14 +24,12 @@ module Ektoplayer
24
24
  :'progressbar.progress' => [:blue ].freeze,
25
25
  :'progressbar.rest' => [:black ].freeze,
26
26
 
27
- :'volumemeter.level' => [:magenta ].freeze,
28
- :'volumemeter.rest' => [:black ].freeze,
29
-
30
27
  :'tabbar.selected' => [:blue ].freeze,
31
28
  :'tabbar.unselected' => [:none ].freeze,
32
29
 
33
30
  :'list.item_even' => [:blue ].freeze,
34
31
  :'list.item_odd' => [:blue ].freeze,
32
+ :'list.item_selection' => [:magenta ].freeze,
35
33
 
36
34
  :'playinginfo.position' => [:magenta ].freeze,
37
35
  :'playinginfo.state' => [:cyan ].freeze,
@@ -54,9 +52,6 @@ module Ektoplayer
54
52
  :'progressbar.progress' => [23 ].freeze,
55
53
  :'progressbar.rest' => [236 ].freeze,
56
54
 
57
- :'volumemeter.level' => [:magenta ].freeze,
58
- :'volumemeter.rest' => [236 ].freeze,
59
-
60
55
  :'tabbar.selected' => [75 ].freeze,
61
56
  :'tabbar.unselected' => [250 ].freeze,
62
57
 
data/lib/ektoplayer/ui.rb CHANGED
@@ -1,4 +1,4 @@
1
- require 'curses'
1
+ require_relative 'icurses'
2
2
  require 'readline'
3
3
  require 'io/console'
4
4
 
@@ -6,19 +6,17 @@ require_relative 'ui/colors'
6
6
  require_relative 'events'
7
7
 
8
8
  module UI
9
- CONDITION_SIGNALS = ConditionSignals.new
10
-
11
9
  class WidgetSizeError < Exception; end
12
10
 
13
11
  class Canvas
14
- extend Curses
12
+ extend ICurses
15
13
 
16
14
  def self.size
17
- UI::Size.new(height: Curses.lines, width: Curses.cols)
15
+ UI::Size.new(height: ICurses.lines, width: ICurses.cols)
18
16
  end
19
17
 
20
18
  def self.cursor
21
- UI::Point.new(y: Curses.cury, x: Curses.curx)
19
+ #UI::Point.new(y: ICurses.cury, x: ICurses.curx)
22
20
  end
23
21
 
24
22
  def self.pos
@@ -28,10 +26,10 @@ module UI
28
26
  def self.start
29
27
  @@widget = nil
30
28
 
31
- %w(init_screen crmode noecho start_color use_default_colors).
32
- each {|_|Curses.send(_)}
33
- Curses.mousemask(Curses::ALL_MOUSE_EVENTS)
34
- Curses.stdscr.keypad(true)
29
+ %w(initscr cbreak noecho start_color use_default_colors).
30
+ each {|_|ICurses.send(_)}
31
+ ICurses.mousemask(ICurses::ALL_MOUSE_EVENTS | ICurses::REPORT_MOUSE_POSITION)
32
+ ICurses.stdscr.keypad(true)
35
33
  UI::Colors.start
36
34
 
37
35
  self.enable_resize_detection
@@ -43,7 +41,7 @@ module UI
43
41
 
44
42
  def self.widget; @@widget end
45
43
  def self.widget=(w) @@widget = w end
46
- def self.stop; Curses.close_screen end
44
+ def self.stop; ICurses.endwin end
47
45
  def self.visible?; true end
48
46
  def self.inivsibile?; false end
49
47
 
@@ -52,36 +50,33 @@ module UI
52
50
  widget
53
51
  end
54
52
 
55
- def self.getch(timeout=-1)
56
- Curses.stdscr.timeout=(timeout)
57
- UI::Input::KEYMAP_WORKAROUND[Curses.stdscr.getch]
58
- end
53
+ #def self.getch(timeout=-1)
54
+ # ICurses.stdscr.timeout(timeout)
55
+ # UI::Input::KEYMAP_WORKAROUND[ICurses.stdscr.getch]
56
+ #end
59
57
 
60
- def self.update_screen(force_redraw=false)
58
+ def self.update_screen(force_redraw=false, force_resize=false)
61
59
  @@updating ||= Mutex.new
62
60
  @@want_resize ||= false
63
61
 
64
62
  if @@updating.try_lock
65
63
  begin
66
- if @@want_resize
64
+ if @@want_resize or force_resize
67
65
  @@want_resize = false
68
66
  h, w = IO.console.winsize()
69
- Curses.resizeterm(h, w)
70
- Curses.clear
71
- Curses.refresh
67
+ ICurses.resizeterm(h, w)
72
68
  @@widget.size=(Size.new(height: h, width: w)) if @@widget
73
69
  @@widget.display(true, true, true) if @@widget
74
70
  else
75
71
  @@widget.display(true, force_redraw) if @@widget
76
72
  end
77
73
  rescue UI::WidgetSizeError
78
- Curses.clear
79
- Curses.addstr('terminal too small!')
74
+ ICurses.stdscr.clear
75
+ ICurses.stdscr.addstr('terminal too small!')
80
76
  rescue
81
77
  Ektoplayer::Application.log(self, $!)
82
78
  end
83
79
 
84
- Curses.doupdate
85
80
  @@updating.unlock
86
81
  end
87
82
  end
@@ -96,8 +91,8 @@ module UI
96
91
 
97
92
  class Input
98
93
  KEYMAP_WORKAROUND = {
99
- 13 => Curses::KEY_ENTER,
100
- 127 => Curses::KEY_BACKSPACE
94
+ 13 => ICurses::KEY_ENTER,
95
+ 127 => ICurses::KEY_BACKSPACE
101
96
  }
102
97
  KEYMAP_WORKAROUND.default_proc = proc { |h,k| k }
103
98
  KEYMAP_WORKAROUND.freeze
@@ -111,45 +106,51 @@ module UI
111
106
 
112
107
  loop do
113
108
  unless @@readline_obj.active?
114
- Curses.curs_set(0)
115
- Curses.nonl
109
+ ICurses.curs_set(0)
110
+ ICurses.nonl
116
111
 
117
112
  begin
118
- UI::Canvas.widget.win.keypad=(true)
119
- c = KEYMAP_WORKAROUND[UI::Canvas.widget.win.getch1]
113
+ UI::Canvas.widget.win.keypad(true)
120
114
 
121
- if c == Curses::KEY_MOUSE
122
- if c = Curses.getmouse
123
- UI::Canvas.widget.mouse_click(c)
115
+ if (c = UI::Canvas.widget.win.getch1(600))
116
+ if c == ICurses::KEY_MOUSE
117
+ if c = ICurses.getmouse
118
+ UI::Canvas.widget.mouse_click(c)
119
+ end
120
+ else
121
+ c = KEYMAP_WORKAROUND[c.ord]
122
+ UI::Canvas.widget.key_press(c) if c >= 0
124
123
  end
125
- elsif c # (not nil)
126
- UI::Canvas.widget.key_press(c.is_a?(Integer) ? c : c.to_sym)
127
124
  end
125
+
126
+ ICurses.doupdate
128
127
  end while !@@readline_obj.active?
129
128
  else
130
- Curses.curs_set(1)
131
- Curses.nl
129
+ ICurses.curs_set(1)
130
+ ICurses.nl
132
131
 
133
132
  begin
134
133
  win = UI::Canvas.widget.win
135
- win.keypad=(false)
136
- next unless (c = win.getch1)
134
+ win.keypad(false)
135
+ @@readline_obj.redraw
136
+ next unless (c = (win.getch1(100).ord rescue -1)) > 0
137
137
 
138
138
  if c == 10 or c == 4
139
- @@readline_obj.feed(?\n)
139
+ @@readline_obj.feed(?\n.ord)
140
140
  else
141
- @@readline_obj.feed(c.chr)
141
+ @@readline_obj.feed(c)
142
142
 
143
143
  if c == 27 # pass 3-character escape sequence
144
- if c = win.getch1(1)
145
- @@readline_obj.feed(c.chr)
146
- if c = win.getch1(1)
147
- @@readline_obj.feed(c.chr)
144
+ win.timeout(5)
145
+ if (c = (win.getch.ord rescue -1)) > 0
146
+ @@readline_obj.feed(c)
147
+ if (c = (win.getch.ord rescue -1)) > 0
148
+ @@readline_obj.feed(c)
148
149
  end
149
150
  end
150
151
  end
151
152
  end
152
- rescue
153
+ #rescue
153
154
  # getch() returned something weird that could not be chr()d
154
155
  end while @@readline_obj.active?
155
156
  end
@@ -157,15 +158,14 @@ module UI
157
158
  end
158
159
 
159
160
  def self.readline(*args, **opts, &block)
160
- (@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts) do
161
- Canvas.class_variable_get('@@updating').synchronize { yield }
161
+ (@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts) do |result|
162
+ Canvas.class_variable_get('@@updating').synchronize { yield result }
162
163
  end
163
164
  end
164
165
  end
165
166
 
166
167
  class ReadlineWindow
167
168
  def initialize
168
- @mutex, @cond = Mutex.new, ConditionVariable.new
169
169
  Readline.input, @readline_in_write = IO.pipe
170
170
  Readline.output = File.open(File::NULL, ?w)
171
171
  @thread = nil
@@ -173,35 +173,38 @@ module UI
173
173
 
174
174
  def active?; @thread; end
175
175
 
176
+ def redraw
177
+ return unless @window
178
+ @window.erase
179
+ buffer = @prompt + Readline.line_buffer.to_s
180
+ @window.addstr(buffer[(buffer.size - @size.width).clamp(0, buffer.size)..-1])
181
+ @window.move(0, Readline.point + @prompt.size)
182
+ @window.refresh
183
+ end
184
+
176
185
  def readline(pos, size, prompt: '', add_hist: false, &block)
177
186
  @thread ||= Thread.new do
178
- window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
179
-
180
- rlt = Thread.new { Readline.readline(prompt, add_hist) }
181
- Readline.set_screen_size(size.height, size.width)
182
- Readline.delete_text
183
- @readline_in_write.read_nonblock(100) rescue nil
184
-
185
- while rlt.alive?
186
- window.erase
187
- buffer = prompt + Readline.line_buffer.to_s
188
- window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
189
- window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
190
- window.refresh
191
- CONDITION_SIGNALS.wait(:readline, 0.2)
192
- end
187
+ @size, @prompt = size, prompt
188
+ @window = ICurses.newwin(size.height, size.width, pos.y, pos.x)
193
189
 
194
- window.clear
195
- block.(Readline.line_buffer)
196
- UI::Canvas.update_screen(true)
197
- @thread = nil
190
+ begin
191
+ Readline.set_screen_size(size.height, size.width)
192
+ Readline.delete_text
193
+ @readline_in_write.read_nonblock(100) rescue nil
194
+ block.(Readline.readline(prompt, add_hist))
195
+ ensure
196
+ @window.clear
197
+ @window = @thread = nil
198
+ UI::Canvas.update_screen(true)
199
+ end
198
200
  end
199
201
  end
200
202
 
201
203
  def feed(c)
202
- @readline_in_write.write(c)
203
- CONDITION_SIGNALS.signal(:readline)
204
- @thread = nil if c == ?\n
204
+ @readline_in_write.putc(c)
205
+ Thread.pass
206
+ @thread = nil if c == ?\n.ord
207
+ redraw
205
208
  end
206
209
  end
207
210
 
@@ -251,49 +254,9 @@ module UI
251
254
  def to_s; "[(Size) height=#{height}, width=#{width}]" end
252
255
  end
253
256
 
254
- # We want to change the mouse coordinates as we pass the mouse event
255
- # through the widgets. The attributes of Curses::MouseEvent are
256
- # readonly, therefore we need to carry out our own MouseEvent class.
257
- class FakeMouseEvent
258
- attr_accessor :x, :y, :z, :bstate
259
-
260
- def initialize(mouse_event=nil)
261
- if mouse_event
262
- from_mouse_event!(mouse_event)
263
- else
264
- @x, @y, @z, @bstate = 0, 0, 0, Curses::BUTTON1_CLICKED
265
- end
266
- end
267
-
268
- def from_mouse_event!(m)
269
- @x, @y, @z, @bstate = m.x, m.y, m.z, m.bstate
270
- end
271
-
272
- def update!(x: nil, y: nil, z: nil, bstate: nil)
273
- @x, @y, @z = (x or @x), (y or @y), (z or @z)
274
- @bstate = (bstate or @bstate)
275
- end
276
-
277
- def pos
278
- Point.new(x: @x, y: @y)
279
- end
280
-
281
- def to_fake
282
- FakeMouseEvent.new(self)
283
- end
284
-
285
- def to_s
286
- name = Curses.constants.
287
- select { |c| c =~ /^BUTTON_/ }.
288
- select { |c| Curses.const_get(c) & @bstate > 0 }[0]
289
- name ||= @button
290
- "[(FakeMouseEvent) button=#{name}, x=#{x}, y=#{y}, z=#{z}]"
291
- end
292
- end
293
-
294
257
  class MouseEvents < Events
295
258
  def on(mouse_event, &block)
296
- return on_all(&block) if mouse_event == Curses::ALL_MOUSE_EVENTS
259
+ return on_all(&block) if mouse_event == ICurses::ALL_MOUSE_EVENTS
297
260
  super(mouse_event, &block)
298
261
  end
299
262
 
@@ -334,8 +297,8 @@ module UI
334
297
  end
335
298
  end
336
299
 
337
- module Curses
338
- class Window
300
+ module ICurses
301
+ class IWindow
339
302
  alias :height :maxy
340
303
  alias :width :maxx
341
304
  alias :clear_line :clrtoeol
@@ -345,11 +308,11 @@ module Curses
345
308
  def size; UI::Size.new(height: maxy, width: maxx) end
346
309
 
347
310
  def cursor=(new)
348
- setpos(new.y, new.x) # or fail "Could not set cursor: #{new} #{size}"
311
+ move(new.y, new.x) # or fail "Could not set cursor: #{new} #{size}"
349
312
  end
350
313
 
351
314
  def pos=(new)
352
- move(new.y, new.x)
315
+ mvwin(new.y, new.x)
353
316
  end
354
317
 
355
318
  def size=(new)
@@ -361,18 +324,18 @@ module Curses
361
324
  end
362
325
 
363
326
  def getch1(timeout=-1)
364
- self.timeout=(timeout)
327
+ self.timeout(timeout)
365
328
  getch
366
329
  end
367
330
 
368
- def on_line(n) setpos(n, curx) ;self;end
369
- def on_column(n) setpos(cury, n) ;self;end
370
- def next_line; setpos(cury + 1, 0) ;self;end
371
- def mv_left(n) setpos(cury, curx - 1) ;self;end
372
- def line_start(l=0) setpos(l, 0) ;self;end
373
- def from_left(size) setpos(cury, size) ;self;end
374
- def from_right(size) setpos(cury, (maxx - size)) ;self;end
375
- def center(size) setpos(cury, (maxx / 2) - (size / 2)) ;self;end
331
+ def on_line(n) move(n, curx) ;self;end
332
+ def on_column(n) move(cury, n) ;self;end
333
+ def next_line; move(cury + 1, 0) ;self;end
334
+ def mv_left(n) move(cury, curx - 1) ;self;end
335
+ def line_start(l=0) move(l, 0) ;self;end
336
+ def from_left(size) move(cury, size) ;self;end
337
+ def from_right(size) move(cury, (maxx - size)) ;self;end
338
+ def center(size) move(cury, (maxx / 2) - (size / 2)) ;self;end
376
339
 
377
340
  def center_string(string)
378
341
  center(string.size)
@@ -380,24 +343,32 @@ module Curses
380
343
  self end
381
344
 
382
345
  def insert_top
383
- setpos(0, 0)
346
+ move(0, 0)
384
347
  insertln
385
348
  self end
386
349
 
387
350
  def append_bottom
388
- setpos(0, 0)
351
+ move(0, 0)
389
352
  deleteln
390
- setpos(maxy - 1, 0)
353
+ move(maxy - 1, 0)
391
354
  self end
392
355
  end
393
356
 
394
- class MouseEvent
357
+ class IMouseEvent
395
358
  def pos
396
359
  UI::Point.new(x: x, y: y)
397
360
  end
398
361
 
399
362
  def to_fake
400
- UI::FakeMouseEvent.new(self)
363
+ IMouseEvent.new(self)
364
+ end
365
+
366
+ def to_s
367
+ name = ICurses.constants.
368
+ select { |c| c =~ /^BUTTON_/ }.
369
+ select { |c| ICurses.const_get(c) & @bstate > 0 }[0]
370
+ name ||= @button
371
+ "[(IMouseEvent) button=#{name}, x=#{x}, y=#{y}, z=#{z}]"
401
372
  end
402
373
  end
403
374
  end