486 lines
10 KiB
Ruby
Executable File
486 lines
10 KiB
Ruby
Executable File
require 'curses'
|
|
|
|
class NCursesUI
|
|
attr_accessor :logger
|
|
|
|
def initialize cloud, options = {}
|
|
defaults = {
|
|
:colors => {
|
|
:default => [:cyan, :blue],
|
|
:playlist => [:cyan, :blue],
|
|
:playlist_active => [:white, :blue],
|
|
:progress => [:cyan, :blue],
|
|
:progress_bar => [:blue, :cyan],
|
|
:title => [:cyan, :black],
|
|
:artist => [:cyan, :black]
|
|
}
|
|
}.freeze
|
|
|
|
@options = defaults.merge(options || {})
|
|
@cloud = cloud
|
|
@state = :running
|
|
@frac = 0
|
|
@title = "None"
|
|
@op = " "
|
|
@time = 0
|
|
@timeleft = 0
|
|
@playlist = []
|
|
end
|
|
|
|
def run
|
|
begin
|
|
stdscr = Curses.init_screen
|
|
Curses.start_color
|
|
Colors.init @options[:colors]
|
|
stdscr.keypad true
|
|
Curses.nonl
|
|
Curses.cbreak
|
|
Curses.noecho
|
|
Curses.curs_set 0
|
|
Curses.timeout = 5
|
|
@p = NProgress.new stdscr, 0, 0, :progress, :progress_bar
|
|
@l = NPlaylist.new stdscr, 4, 0, :playlist, :playlist_active, 0, 0, @playlist
|
|
@i = NInfobox.new stdscr, 4, 0, :playlist, 0, 8
|
|
@d = NDownloadBox.new stdscr, Curses.lines-1, 0, :default, 0, 1
|
|
@l.active = 0
|
|
last_ch = nil
|
|
while(@state != :close)
|
|
ch = Curses.getch
|
|
last_ch = ch if ch
|
|
Curses.setpos 3, 0
|
|
Curses.clrtoeol
|
|
# Nutils.print stdscr, 3, 0, "Test %s" % [last_ch], :red
|
|
case ch
|
|
when Curses::KEY_RESIZE
|
|
@p.resize
|
|
@l.resize
|
|
@i.resize
|
|
@d.resize
|
|
Curses.refresh
|
|
when 110, 78, 'n', 'N', Curses::KEY_DOWN
|
|
@cloud.nextTrack
|
|
when 112, 80, 'p', 'P', Curses::KEY_UP
|
|
@cloud.prevTrack
|
|
when 113, 81, 'q', 'Q', 27, Curses::KEY_EXIT
|
|
@cloud.quit
|
|
when 61, 43, '=', '+'
|
|
@cloud.volumeUp
|
|
when 45, 95, '-', '_'
|
|
@cloud.volumeDown
|
|
when 109, 77, 'm', 'M'
|
|
@cloud.toggleMute
|
|
when 68, 100, 'd', 'D'
|
|
@cloud.download
|
|
when 118, 86, 'v', 'V'
|
|
@i.visible = !@i.visible
|
|
@l.dirty = true
|
|
when 32, ' '
|
|
@cloud.pause
|
|
end
|
|
|
|
if @error
|
|
Nutils.print stdscr, 3, 0, "Error: #{@error}", :red
|
|
Curses.refresh
|
|
end
|
|
tr = " %s " % [Nutils.timestr(@timetotal)]
|
|
t = " %-#{Curses.cols-tr.size-1}s%s" % [Nutils.timestr(@time), tr]
|
|
@p.value = @frac
|
|
@p.text = t
|
|
@p.refresh
|
|
Nutils.print stdscr, 1, 0, "#{@op} #{@title}", :title
|
|
Nutils.print stdscr, 2, 0, " by #{@username}", :artist
|
|
@l.refresh
|
|
@i.refresh
|
|
@d.refresh
|
|
stdscr.refresh
|
|
end
|
|
rescue => ex
|
|
ensure
|
|
@l.close if @l
|
|
@p.close if @p
|
|
stdscr.close
|
|
Curses.echo
|
|
Curses.nocbreak
|
|
Curses.nl
|
|
Curses.close_screen
|
|
puts ex.inspect if ex
|
|
puts ex.backtrace if ex
|
|
# Colors.debug
|
|
end
|
|
end
|
|
|
|
def cloud_update(arg)
|
|
case arg[:state]
|
|
when :load
|
|
@playlist |= arg[:tracks]
|
|
@l.list = @playlist if @l
|
|
when :shuffle
|
|
@playlist = arg[:tracks]
|
|
@l.list = @playlist if @l
|
|
when :next, :previous
|
|
pos = arg[:position]
|
|
@l.active = pos if @l
|
|
when :download
|
|
if arg[:error]
|
|
@error = arg[:error]
|
|
end
|
|
if arg[:count]
|
|
count = arg[:count]
|
|
if(count > 0)
|
|
@l.height = -1
|
|
@d.visible = true
|
|
@d.count = count
|
|
@d.title = arg[:name] if arg[:name]
|
|
else
|
|
@l.height = 0
|
|
@d.visible = false
|
|
@d.title = ""
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def player_update(arg)
|
|
|
|
case arg[:state]
|
|
when :load
|
|
track = arg[:track]
|
|
if track.nil?
|
|
@error = "Nothing found!"
|
|
else
|
|
@error = nil
|
|
@title = track["title"]
|
|
@username = track["user"]["username"]
|
|
@timetotal = track["duration"]
|
|
@error = track[:error] if track[:error]
|
|
end
|
|
when :info
|
|
frame = arg[:frame].to_f
|
|
frames = frame + arg[:frameleft]
|
|
@frac = frame/frames
|
|
@time = arg[:time].to_i
|
|
when :pause
|
|
@op = "\u2161"
|
|
when :resume, :play
|
|
@op = "\u25B6"
|
|
when :stop
|
|
@op = "\u25FC"
|
|
when :error
|
|
@error = arg[:error]
|
|
end
|
|
end
|
|
|
|
def close
|
|
@state = :close
|
|
end
|
|
end
|
|
|
|
class Nutils
|
|
def self.print(scr, row, col, text, color, width = (Curses.cols))
|
|
width = [Curses.cols, col+width].min - col
|
|
t = "%-#{width}s" % [scroll(text, width)]
|
|
scr.attron(Colors.map(color)) if color
|
|
scr.setpos row, col
|
|
scr.addstr t
|
|
scr.attroff(Colors.map(color)) if color
|
|
end
|
|
|
|
def self.scroll(text, width, offset=0)
|
|
return unless text
|
|
ellipsis = "*"
|
|
t = text
|
|
if t.size+offset > width
|
|
t = t[offset..(width-ellipsis.size-1)] << ellipsis
|
|
end
|
|
t
|
|
end
|
|
|
|
def self.timestr(sec)
|
|
sec = sec.to_i
|
|
"%02d:%02d" % [sec/60, sec%60]
|
|
end
|
|
|
|
end
|
|
|
|
class Colors
|
|
$map = {}
|
|
$counter = 0
|
|
def self.init colormap = {}
|
|
colormap.each do |key, colors|
|
|
self.add(key, colors[0], colors[1])
|
|
end
|
|
end
|
|
|
|
def self.map(key)
|
|
$map[key] || $map[:default]
|
|
end
|
|
|
|
def self.add(key, fg, bg)
|
|
Curses.init_pair $counter, ncg(fg), ncg(bg)
|
|
$map[key] = Curses.color_pair $counter
|
|
$counter += 1
|
|
end
|
|
|
|
def self.debug
|
|
puts "colors supported: #{Curses.colors}"
|
|
puts "map: #{$map}"
|
|
end
|
|
# get ncurses color constant
|
|
def self.ncg(color)
|
|
color = :black unless color
|
|
Curses.const_get "COLOR_#{color.upcase}"
|
|
end
|
|
end
|
|
|
|
class NProgress
|
|
attr_reader :value
|
|
attr_accessor :text
|
|
def initialize scr, row, col, color, bar_color, width=0, value = 0, text = ""
|
|
@width = width
|
|
@color = color
|
|
@bar_color = bar_color
|
|
@row = row
|
|
@col = col
|
|
@winfg = Curses::Window.new 1, 1, @row, @col
|
|
@winbg = Curses::Window.new 1, self.width, @row, @col
|
|
@value = value
|
|
@text = text
|
|
refresh
|
|
end
|
|
|
|
def width
|
|
[@col + @width, Curses.cols].min - @col
|
|
end
|
|
|
|
def value=(val)
|
|
@value = val
|
|
@winfg.resize(1, fgw) if fgw > 0
|
|
end
|
|
|
|
def refresh
|
|
offset = fgw
|
|
Nutils.print @winbg, 0, offset, @text[offset..-1], @color
|
|
Nutils.print @winfg, 0, 0, @text, @bar_color if fgw > 0
|
|
@winbg.refresh
|
|
@winfg.refresh if fgw > 0
|
|
end
|
|
|
|
def resize
|
|
end
|
|
|
|
def close
|
|
@winbg.close
|
|
@winfg.close
|
|
end
|
|
|
|
private
|
|
def fgw
|
|
w = width() == 0 ? Curses.cols - @col : width()
|
|
(w * @value).floor
|
|
end
|
|
end
|
|
|
|
class NPlaylist
|
|
attr_writer :list
|
|
attr_accessor :dirty
|
|
def initialize scr, row, col, color, active_color, w, h, l
|
|
@list = l
|
|
@row = row
|
|
@col = col
|
|
@width = w
|
|
@height = h
|
|
@color = color
|
|
@active_color = active_color
|
|
@apos = -1
|
|
@win = Curses::Window.new height, width, @row, @col
|
|
@dirty = true
|
|
refresh
|
|
end
|
|
|
|
def width
|
|
w = [@col + @width, Curses.cols].min - @col
|
|
if w <= 0
|
|
Curses.cols - @col + w
|
|
else
|
|
w
|
|
end
|
|
end
|
|
|
|
def height
|
|
h = [@row + @height, Curses.lines].min - @row
|
|
if h <= 0
|
|
Curses.lines - @row + h
|
|
else
|
|
h
|
|
end
|
|
end
|
|
|
|
def width=(val)
|
|
@width = val
|
|
resize
|
|
refresh
|
|
end
|
|
|
|
def height=(val)
|
|
@height = val
|
|
resize
|
|
refresh
|
|
end
|
|
|
|
def active=(pos)
|
|
@apos = pos
|
|
@dirty = true
|
|
end
|
|
|
|
def resize
|
|
@win.resize height, width
|
|
@dirty = true
|
|
end
|
|
|
|
def refresh
|
|
return unless @dirty
|
|
if !@list.is_a?(Array) || @list.empty?
|
|
Nutils.print @win, 1, 2, "Empty playlist", @color, width - 3
|
|
else
|
|
r = 1
|
|
size = height - 2
|
|
offset = ([[size/2.0, @apos].max, [@list.size, size].max-(size/2.0)].min - size/2.0).ceil
|
|
|
|
@list[offset..@list.size].each do |t|
|
|
tl = t["title"]
|
|
if @apos == r - 1 + offset
|
|
tl = ">#{tl}"
|
|
color = @active_color
|
|
else
|
|
tl = " #{tl}"
|
|
color = @color
|
|
end
|
|
tr = "[%6s]" % Nutils.timestr(t["duration"])
|
|
tr = "[D]#{tr}" if t["downloadable"]
|
|
wr = tr.size
|
|
wl = width - 3- wr
|
|
Nutils.print @win, r, 1, tl, color, wl+1
|
|
Nutils.print @win, r, 2+wl, tr, color, wr
|
|
r += 1
|
|
if(r >= height - 1)
|
|
# print arrow down
|
|
break
|
|
end
|
|
end
|
|
end
|
|
@win.attron(Colors.map(@color)) if @color
|
|
@win.box 0, 0
|
|
@win.attroff(Colors.map(@color)) if @color
|
|
@win.refresh
|
|
@dirty = false
|
|
end
|
|
|
|
def close
|
|
@win.close
|
|
end
|
|
end
|
|
|
|
class NInfobox
|
|
attr_accessor :visible
|
|
def initialize scr, row, col, color, w, h
|
|
@scr = scr
|
|
@row = row
|
|
@col = col
|
|
@color = color
|
|
@width = w
|
|
@height = h
|
|
@win = Curses::Window.new height, width, @row, @col
|
|
@visible = false
|
|
refresh
|
|
end
|
|
|
|
def width
|
|
w = [@col + @width, Curses.cols].min - @col
|
|
if w == 0
|
|
Curses.cols - @col
|
|
else
|
|
w
|
|
end
|
|
end
|
|
|
|
def height
|
|
h = [@row + @height, Curses.lines].min - @row
|
|
if h == 0
|
|
Curses.lines - @row
|
|
else
|
|
h
|
|
end
|
|
end
|
|
|
|
def resize
|
|
@win.resize height, width
|
|
end
|
|
|
|
def refresh
|
|
return unless @visible
|
|
Nutils.print @win, 1, 2, "Cloudruby v1", :default
|
|
Nutils.print @win, 2, 4, "Curses version: #{(Curses.const_defined?"VERSION")?Curses::VERSION : "N/A"}", :default
|
|
Nutils.print @win, 3, 4, "Ruby version: #{RUBY_VERSION}", :default
|
|
Nutils.print @win, 4, 4, "Author: kulpae <my.shando@gmail.com>", :artist
|
|
Nutils.print @win, 5, 4, "Website: uraniumlane.net", :title
|
|
Nutils.print @win, 6, 4, "License: MIT", :default
|
|
@win.attron(Colors.map(@color)) if @color
|
|
@win.box 0, 0
|
|
@win.attroff(Colors.map(@color)) if @color
|
|
@win.refresh
|
|
end
|
|
|
|
def close
|
|
@win.close
|
|
end
|
|
end
|
|
|
|
class NDownloadBox
|
|
attr_accessor :visible, :count, :title
|
|
def initialize scr, row, col, color, w, h
|
|
@scr = scr
|
|
@row = row
|
|
@col = col
|
|
@color = color
|
|
@width = w
|
|
@height = h
|
|
@win = Curses::Window.new height, width, @row, @col
|
|
@visible = false
|
|
@count = 0
|
|
@title = ""
|
|
refresh
|
|
end
|
|
|
|
def width
|
|
w = [@col + @width, Curses.cols].min - @col
|
|
if w == 0
|
|
Curses.cols - @col
|
|
else
|
|
w
|
|
end
|
|
end
|
|
|
|
def height
|
|
h = [@row + @height, Curses.lines].min - @row
|
|
if h == 0
|
|
Curses.lines - @row
|
|
else
|
|
h
|
|
end
|
|
end
|
|
|
|
def resize
|
|
@win.resize height, width
|
|
end
|
|
|
|
def refresh
|
|
return unless @visible
|
|
Nutils.print @win, 0, 0, "Downloading #{@count} track#{@count > 1?"s":""} | #{title}", :default
|
|
@win.refresh
|
|
end
|
|
|
|
def close
|
|
@win.close
|
|
end
|
|
end
|