Files
bin/ncurses_ui.rb
T
2026-06-11 15:14:00 +02:00

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