Compare commits

..

No commits in common. "master" and "0.1" have entirely different histories.

5 changed files with 448 additions and 1047 deletions

736
cimp.bas
View file

@ -1,141 +1,79 @@
declare library"terminkey"
function terminkey%()
sub echooff()
sub echoon()
function termwidth()
function ipc_init&()
function ipc_check_message%(byval buf as _offset, byval max_len as long)
sub ipc_send_message(buf as string)
sub ipc_cleanup()
declare library "terminkey"
function terminkey% ()
sub echooff ()
sub echoon ()
function termwidth ()
end declare
$console:only
on error goto quit
const key_up=1001
const key_down=1002
const key_right=1003
const key_left=1004
dim slash as string
slash="/"
$if win then
shell"chcp 65001 > nul"
slash="\"
const KEY_UP = 1001
const KEY_DOWN = 1002
const KEY_RIGHT = 1003
const KEY_LEFT = 1004
$if WIN then
Shell "chcp 65001 > nul"
$end if
redim file(0) as string
chdir _startdir$
if command$=""then
print"please specify file to play."
goto quit
if command$ = "" then
print "please specify file to play."
system
end if
dim ipc_status as long
ipc_status = ipc_init
if ipc_status = 0 then
' --- CLIENT MODE ---
dim cmd_msg as string
Select Case command$(1)
Case "--next"
cmd_msg = "NEXT"
Case "--prev"
cmd_msg = "PREV"
Case "-v", "--volume"
cmd_msg = "VOL:" + command$(2)
case "--pause"
cmd_msg = "PAUSE"
case "--stop"
cmd_msg = "STOP"
case "--play"
cmd_msg = "PLAY"
case "--quit"
cmd_msg = "QUIT"
case "--shuffle"
cmd_msg = "SHUFFLE"
case "--repeat"
cmd_msg = "REPEAT"
case "--repeat-1"
cmd_msg = "REPEAT1"
case "--nyan"
cmd_msg = "NYAN"
Case "--add"
For i = 2 To _commandcount
cmd_msg = "ADD:" + _cwd$ + slash + command$(i)
ipc_send_message cmd_msg
Next i
System
Case "--playlist"
cmd_msg = "GET_PLAYLIST"
Case Else
' Default behavior: Resolve target file/playlist and replace active queue
cmd_msg = "PLAY:" + _cwd$ + slash + command$(1)
ipc_send_message cmd_msg
If _commandcount > 1 Then
For i = 2 To _commandcount
cmd_msg = "ADD:" + _cwd$ + slash + command$(i)
ipc_send_message cmd_msg
Next i
System
End If
End Select
' Send instruction to the main player instance
ipc_send_message cmd_msg
' --- Two-way Client feedback for --playlist ---
if cmd_msg = "GET_PLAYLIST" then
ipc_cleanup
_delay 0.1
dim client_listen as long
client_listen = ipc_init
if client_listen = 1 then
dim reply_buf as string
reply_buf = space$(4096) + chr$(0)
dim start_wait as double
start_wait = timer
do while timer - start_wait < 2
dim reply_len as long
reply_len = ipc_check_message(_offset(reply_buf), 4096)
if reply_len > 0 then
print left$(reply_buf, reply_len)
exit do
end if
_limit 30
loop
ipc_cleanup
end if
end if
system ' Exit client instance completely
elseif ipc_status = -1 then
print "Error initializing Inter-Process Communication."
goto quit
end if
' ---SERVER MODE (Main Player Execution)---
echooff
cursoroff
chdir _startdir$
dim volume as single
dim repeat as integer
dim shuffle as integer
dim nooutput as integer
dim timevis as integer
dim nyan as integer
dim marqueeoffset as integer
dim i as integer
volume = 1
repeat = 0
shuffle = 0
nooutput = 0
timevis = 1
nyan = 0
for i = 1 to _commandcount
select case command$(i)
case "-v", "--volume"
i = i + 1
volume = val(command$(i)) / 100
case "-s", "--shuffle"
shuffle = -1
case "-r", "--repeat"
if command$(i + 1) = "1" then
repeat = 1
else
repeat = -1
end if
case "-n", "--nooutput"
nooutput = -1
case "-N", "--nyan"
nyan = -1
case else
if _fileexists(command$(i)) then
if lcase$(right$(command$(i), 4)) = ".m3u" then
ParseM3U command$(i), file()
else
file(ubound(file)) = command$(i)
redim _preserve file(ubound(file) + 1)
end if
end if
end select
next
redim _preserve file(ubound(file) - 1)
dim musichandle as long
dim oldhandle as long
dim keyin as integer
dim playnext as integer
@ -144,344 +82,150 @@ dim songname as string
dim progress as string
dim progressbar as string
dim tw as integer
dim fixedwidth as integer
dim maxtitlewidth as integer
dim visibletitle as string
dim currentsongwidth as integer
dim paddedtitle as string
dim paddedlength as integer
dim idx as integer
dim addedwidth as integer
dim charidx as integer
dim nextchar as string
dim marqueeframe as integer
volume=1
repeat=0
shuffle=0
nooutput=0
timevis=1
nyan=0
marqueeoffset=0
for i=1 to _commandcount
select case command$(i)
case "-v","--volume"
i=i+1
volume=val(command$(i))/100
case "-s","--shuffle"
shuffle=-1
case "-r","--repeat"
if command$(i+1)="1"then
repeat=1
else
repeat=-1
end if
case "-n","--nooutput"
nooutput=-1
case "-N","--nyan"
nyan=-1
case else
if _fileexists(command$(i)) then
if lcase$(right$(command$(i),4))=".m3u"then
parsem3u command$(i),file()
else
file(ubound(file))=command$(i)
redim _preserve file(ubound(file)+1)
end if
end if
end select
next
redim _preserve file(ubound(file)-1)
if shuffle=-1 then shufflearray file()
i=0
musichandle=_sndopen(file(i))
if musichandle=0 then
print"Error: could not open file "; file(i)
goto quit
if shuffle = -1 then shufflearray file()
i = 0
musichandle = _sndopen(file(i))
if musichandle = 0 then
print "Error: could not open file "; file(i)
system
end if
_sndvol musichandle,volume
_sndvol musichandle, volume
_sndplay musichandle
state="playing "
songname=beforelast(".",afterlast("/",file(i)))
state = "playing "
songname = beforelast(".", afterlast("/", file(i)))
while keyin<>27
keyin=terminkey
' ---Poll Client Messages---
dim incoming_buf as string
incoming_buf = space$(512) + chr$(0)
dim msg_len as long
msg_len = ipc_check_message(_offset(incoming_buf), 512)
if msg_len > 0 then
dim client_cmd as string
client_cmd = left$(incoming_buf, msg_len)
if client_cmd = "NEXT" then
playnext = 1
elseif client_cmd = "PLAY" then
keyin=asc("x")
elseif client_cmd = "PAUSE" then
keyin=asc("c")
elseif client_cmd = "STOP" then
keyin=asc("v")
elseif client_cmd = "QUIT" then
keyin=27
elseif client_cmd = "REPEAT" then
repeat=-1
elseif client_cmd = "REPEAT1" then
repeat=1
elseif client_cmd = "SHUFFLE" then
keyin=asc("s")
elseif client_cmd = "NYAN" then
nyan = not nyan
elseif left$(client_cmd, 4) = "VOL:" then
volume = val(mid$(client_cmd, 5)) / 100
while keyin <> 27
keyin = terminkey
select case keyin
case KEY_UP
volume = volume + (0.01 + (volume / 10))
if volume > 1 then volume = 1
_sndvol musichandle, volume
case KEY_DOWN
volume = volume - (0.01 + (volume / 10))
if volume < 0 then volume = 0
_sndvol musichandle, volume
elseif left$(client_cmd, 4) = "ADD:" then
dim new_file as string
new_file = _trim$(mid$(client_cmd, 5))
if _fileexists(new_file) then
if lcase$(right$(new_file, 4)) = ".m3u" then
parsem3u new_file, file()
case KEY_RIGHT
if _sndgetpos(musichandle) + 5 < _sndlen(musichandle) then
_sndsetpos musichandle, _sndgetpos(musichandle) + 5
else
redim _preserve file(ubound(file) + 1) as string
file(ubound(file)) = new_file
end if
end if
elseif left$(client_cmd, 5) = "PLAY:" then
dim replace_file as string
replace_file = _trim$(mid$(client_cmd, 6))
if _fileexists(replace_file) then
redim file(0) as string
if lcase$(right$(replace_file, 4)) = ".m3u" then
parsem3u replace_file, file()
else
file(0) = replace_file
end if
i = -1
playnext = 1
end if
elseif client_cmd = "GET_PLAYLIST" then
dim p_idx as long
dim playlist_payload as string
playlist_payload = "=== Current Playlist ===" + chr$(10)
for p_idx = lbound(file) to ubound(file)
if p_idx = i then
playlist_payload = playlist_payload + "-> " + file(p_idx) + chr$(10)
case KEY_LEFT
if _sndgetpos(musichandle) - 5 > 0 then
_sndsetpos musichandle, _sndgetpos(musichandle) - 5
else
playlist_payload = playlist_payload + " " + file(p_idx) + chr$(10)
end if
next p_idx
ipc_cleanup
_delay 0.1
ipc_send_message playlist_payload
_delay 0.1
dim reinit as long
reinit = ipc_init
end if
end if
' --- End Poll Client Messages ---
select case keyin
case key_up
volume=volume+(0.01+(volume/10))
if volume>1 then volume=1
_sndvol musichandle,volume
case key_down
volume=volume-(0.01+(volume/10))
if volume<0 then volume=0
_sndvol musichandle,volume
case key_right
if _sndgetpos(musichandle)+5<_sndlen(musichandle) then
_sndsetpos musichandle,_sndgetpos(musichandle)+5
else
playnext=1
end if
case key_left
if _sndgetpos(musichandle)-5>0 then
_sndsetpos musichandle,_sndgetpos(musichandle)-5
else
playnext=-1
playnext = -1
end if
case asc("q")
keyin=27
keyin = 27
case asc("z")
if _sndgetpos(musichandle)>2 then
_sndsetpos musichandle,0
if _sndgetpos(musichandle) > 2 then
_sndsetpos musichandle, 0
else
playnext=-1
playnext = -1
end if
case asc("x")
if _sndplaying(musichandle) then
_sndsetpos musichandle,0
_sndsetpos musichandle, 0
else
_sndplay musichandle
end if
case asc("c"),asc(" ")
case asc("c"), asc(" ")
if _sndplaying(musichandle) then
_sndpause musichandle
state="paused "
state = "paused "
else
_sndplay musichandle
state="playing "
state = "playing "
end if
case asc("v")
_sndstop musichandle
state="stopped "
state = "stopped "
case asc("b")
playnext=1
playnext = 1
case asc("t")
timevis=-timevis
timevis = -timevis
case asc("s")
shufflearray file()
case else
end select
if _sndgetpos(musichandle)=_sndlen(musichandle) then playnext=1
if playnext<>0 then
oldhandle=musichandle
if repeat=1 and playnext=1 then
playnext=0
else
i=i+playnext
if i>ubound(file) then
if repeat=-1 then
i=0
else
goto quit
end if
elseif i<lbound(file) then
if repeat=-1 then
i=ubound(file)
else
i=lbound(file)
end if
end if
end if
musichandle=_sndopen(file(i))
if musichandle<>0 then
_sndvol musichandle,volume
if _sndgetpos(musichandle) = _sndlen(musichandle) then playnext = 1
if playnext <> 0 then
oldhandle = musichandle
i = i + playnext
if i > ubound(file) then i = 0
if i < lbound(file) then i = ubound(file)
musichandle = _sndopen(file(i))
if musichandle <> 0 then
_sndvol musichandle, volume
_sndplay musichandle
_sndstop oldhandle
_sndclose oldhandle
state="playing "
songname=beforelast(".",afterlast(slash,file(i)))
playnext=0
state = "playing "
songname = beforelast(".", afterlast("/", file(i)))
playnext = 0
else
musichandle=oldhandle
end if
end if
if timevis=1 then
progress=" -"+timeleft(musichandle)
if timevis = 1 then
progress = " -" + timeleft(musichandle)
else
progress=" "+timeelapsed(musichandle)
progress = " " + timeelapsed(musichandle)
end if
if nooutput=0 then
tw=termwidth
fixedwidth=uwidth(state)+uwidth(progress)
maxtitlewidth=tw-fixedwidth-2
currentsongwidth=uwidth(songname)
if currentsongwidth>maxtitlewidth and maxtitlewidth>4 then
paddedtitle=songname+" "
paddedlength=ulen(paddedtitle)
visibletitle=""
addedwidth=0
idx=0
while addedwidth<maxtitlewidth
charidx=((marqueeoffset+idx) mod paddedlength)+1
nextchar=umid(paddedtitle,charidx,1)
if addedwidth+uwidth(nextchar)>maxtitlewidth then exit while
visibletitle=visibletitle+nextchar
addedwidth=addedwidth+uwidth(nextchar)
idx=idx+1
wend
if addedwidth<maxtitlewidth then
visibletitle=visibletitle+space$(maxtitlewidth-addedwidth)
end if
marqueeframe=marqueeframe+1
if marqueeframe mod 4=0 then
marqueeoffset=marqueeoffset+1
if marqueeoffset>=paddedlength then marqueeoffset=0
end if
if nyan = -1 then
print termcolor(7); state; AnimatedRainbowText(songname); termcolor(7); progress; clearrest
else
' Terminal is wide enough, no scrolling needed
visibletitle=songname
marqueeoffset=0
print termcolor(7); state; termcolor(3); songname; termcolor(7); progress; clearrest
end if
' Reset marquee offset if song changes
if playnext<>0 then marqueeoffset=0
' Print the text line
if nyan=-1 then
print termcolor(7); state; animatedrainbowtext(visibletitle); termcolor(7); progress; clearrest
else
print termcolor(7); state; termcolor(3); visibletitle; termcolor(7); progress; clearrest
end if
' Generate and print progress bar matching the layout width
progressbar=bar(uwidth(state)+uwidth(visibletitle)+uwidth(progress),(_sndgetpos(musichandle)/_sndlen(musichandle))*100,11,7)
progressbar = bar(UWidth(state) + UWidth(songname) + UWidth(progress), (_sndgetpos(musichandle) / _sndlen(musichandle)) * 100, 11, 7)
print progressbar; clearrest; cursorback;
end if
_limit 30
if _exit then goto quit
wend
wend
quit:
_sndclose musichandle
ipc_cleanup
print clearrest
print clearrest;
print cursorback;
print chr$(27)+"[0m";
cursoron
echoon
system
sub shufflearray (stringarray() as string)
randomize timer
dim n as long,j as long
for n=ubound(stringarray) to 1 step -1
j=int(rnd*n)
swap stringarray(n),stringarray(j)
for n = ubound(stringarray) to 1 step -1
j = int(rnd * n)
swap stringarray(n), stringarray(j)
next
end sub
sub parsem3u (filename$,array$())
dim i as long,f as long,count as long
dim basepath$,l$,resolvedpath$
for i=len(filename$) to 1 step -1
if mid$(filename$,i,1)="/"or mid$(filename$,i,1)="\"then
basepath$=left$(filename$,i)
sub ParseM3U (filename$, array$())
for i = len(filename$) to 1 step -1
if mid$(filename$, i, 1) = "/" or mid$(filename$, i, 1) = "\" then
basePath$ = left$(filename$, i)
exit for
end if
next
f=freefile
f = freefile
open filename$ for input as #f
count=0
count = 0
do until eof(f)
line input #f,l$
l$=_trim$(l$)
if len(l$)>0 and left$(l$,1)<>"#"then
resolvedpath$=l$
if not _fileexists(resolvedpath$) then
resolvedpath$=basepath$+l$
line input #f, l$
l$ = _trim$(l$)
if len(l$) > 0 and left$(l$, 1) <> "#" then
resolvedPath$ = l$
if not _fileexists(resolvedPath$) then
resolvedPath$ = basePath$ + l$
end if
if _fileexists(resolvedpath$) then
array$(ubound(array$))=resolvedpath$
redim _preserve array$(ubound(array$)+1)
if _fileexists(resolvedPath$) then
array$(ubound(array$)) = resolvedPath$
redim _preserve array$(ubound(array$) + 1)
end if
end if
loop
@ -490,189 +234,195 @@ end sub
function timeleft$ (handle&)
dim seconds as integer
seconds=_sndlen(handle&)-_sndgetpos(handle&)
if seconds<0 then seconds=0
timeleft$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
seconds = _sndlen(handle&) - _sndgetpos(handle&)
if seconds < 0 then seconds = 0
timeleft$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2)
end function
function timeelapsed$ (handle&)
dim seconds as integer
seconds=_sndgetpos(handle&)
if seconds<0 then seconds=0
timeelapsed$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
seconds = _sndgetpos(handle&)
if seconds < 0 then seconds = 0
timeelapsed$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2)
end function
function termcolor$ (colorvalue as _unsigned long)
select case colorvalue
case 0 to 7
termcolor=chr$(27)+"[0;3"+_trim$(str$(colorvalue))+"m"
termcolor = chr$(27) + "[0;3" + _trim$(str$(colorvalue)) + "m"
case 8 to 15
termcolor=chr$(27)+"[1;3"+_trim$(str$(colorvalue-8))+"m"
termcolor = chr$(27) + "[1;3" + _trim$(str$(colorvalue - 8)) + "m"
case 16 to 255
termcolor=chr$(27)+"[38;5;"+_trim$(str$(colorvalue))+"m"
case is>255
termcolor=chr$(27)+"[38;2;"+_trim$(str$(_red32(colorvalue)))+";"+_trim$(str$(_green32(colorvalue)))+";"+_trim$(str$(_blue32(colorvalue)))+"m"
termcolor = chr$(27) + "[38;5;" + _trim$(str$(colorvalue)) + "m"
case is > 255
termcolor = chr$(27) + "[38;2;" + _trim$(str$(_red32(colorvalue))) + ";" + _trim$(str$(_green32(colorvalue))) + ";" + _trim$(str$(_blue32(colorvalue))) + "m"
end select
end function
function cursorback$ ()
cursorback=chr$(27)+"[F"
cursorback = chr$(27) + "[F"
end function
function clearline$
clearline=chr$(27)+"[2K"
clearline = chr$(27) + "[2K"
end function
function clearrest$
clearrest=chr$(27)+"[K"
clearrest = chr$(27) + "[K"
end function
sub cursoroff ()
print chr$(27);"[?25l";
print chr$(27); "[?25l";
end sub
sub cursoron ()
print chr$(27);"[?25h";
print chr$(27); "[?25h";
end sub
function bar$ (length as integer,percent as integer,color1 as long,color2 as long)
function bar$ (length as integer, percent as integer, color1 as long, color2 as long)
dim done as string
dim notdone as string
dim i as integer
for i=1 to int((percent/100)*length)
done=done+"━"
for i = 1 to int((percent / 100) * length)
done = done + "━"
next i
for i=1 to length-int((percent/100)*length)
notdone=notdone+"━"
for i = 1 to length - int((percent / 100) * length)
notdone = notdone + "━"
next i
bar$=termcolor(color1)+done+termcolor(color2)+notdone
bar$ = termcolor(color1) + done + termcolor(color2) + notdone
end function
function afterlast$ (delim as string,strng as string)
afterlast=mid$(strng,_instrrev(strng,delim)+1)
function afterlast$ (delim as string, strng as string)
afterlast = mid$(strng, _instrrev(strng, delim) + 1)
end function
function beforelast$ (delim as string,strng as string)
beforelast=left$(strng,_instrrev(strng,delim)-1)
function beforelast$ (delim as string, strng as string)
beforelast = left$(strng, _instrrev(strng, delim) - 1)
end function
function animatedrainbowtext$ (text$)
function AnimatedRainbowText$ (text$)
static offset as double
dim result as string
dim l as long,i as long
dim r as integer,g as integer,b as integer
dim hue as double,f as double
dim sector as integer,v as integer,p as integer,q as integer,t as integer
dim rgbpart$
dim L as long, i as long
dim r as integer, g as integer, b as integer
dim hue as double, f as double
dim sector as integer, v as integer, p as integer, q as integer, t as integer
l=ulen(text$)
if l=0 then exit function
offset=offset+5.0
if offset>=360 then offset=offset-360
L = ulen(text$)
if L = 0 then exit function
offset = offset + 5.0
if offset >= 360 then offset = offset - 360
for i=1 to l
hue=mod_double(offset+((i-1)/l)*360,360)
sector=int(hue/60)
f=(hue/60)-sector
v=255:p=0:q=255*(1-f):t=255*f
for i = 1 to L
hue = MOD_Double(offset + ((i - 1) / L) * 360, 360)
sector = int(hue / 60)
f = (hue / 60) - sector
v = 255: p = 0: q = 255 * (1 - f): t = 255 * f
select case sector
case 0:r=v:g=t:b=p
case 1:r=q:g=v:b=p
case 2:r=p:g=v:b=t
case 3:r=p:g=q:b=v
case 4:r=t:g=p:b=v
case 5:r=v:g=p:b=q
case 0: r = v: g = t: b = p
case 1: r = q: g = v: b = p
case 2: r = p: g = v: b = t
case 3: r = p: g = q: b = v
case 4: r = t: g = p: b = v
case 5: r = v: g = p: b = q
end select
rgbpart$=_trim$(str$(r))+";"+_trim$(str$(g))+";"+_trim$(str$(b))
result=result+chr$(27)+"[38;2;"+rgbpart$+"m"+umid$(text$,i,1)
rgbPart$ = _trim$(str$(r)) + ";" + _trim$(str$(g)) + ";" + _trim$(str$(b))
result = result + chr$(27) + "[38;2;" + rgbPart$ + "m" + umid$(text$, i, 1)
next i
animatedrainbowtext$=result+chr$(27)+"[0m"
AnimatedRainbowText$ = result + chr$(27) + "[0m"
end function
function mod_double (value as double,m as double)
mod_double=value-(m*int(value/m))
function MOD_Double (value as double, m as double)
MOD_Double = value - (m * int(value / m))
end function
function ulen% (txt$)
dim count%,i%,b%
count%=0
for i%=1 to len(txt$)
b%=asc(txt$,i%)
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
count%=count%+1
dim count%, i%, b%
count% = 0
for i% = 1 to len(txt$)
b% = asc(txt$, i%)
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
count% = count% + 1
end if
next
ulen%=count%
ulen% = count%
end function
function umid$ (txt$,startchar%,numchars%)
if startchar%<1 or numchars%<=0 or txt$=""then exit function
function umid$ (txt$, startChar%, numChars%)
if startChar% < 1 or numChars% <= 0 or txt$ = "" then exit function
dim byteidx%,charcount%,startbyte%,endbyte%,b%
byteidx%=1
charcount%=0
dim byteIdx%, charCount%, startByte%, endByte%
byteIdx% = 1
charCount% = 0
while byteidx%<=len(txt$)
b%=asc(txt$,byteidx%)
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
charcount%=charcount%+1
if charcount%=startchar% then startbyte%=byteidx%
' 1. Find the starting byte of the character at startChar%
while byteIdx% <= len(txt$)
b% = asc(txt$, byteIdx%)
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
charCount% = charCount% + 1
if charCount% = startChar% then startByte% = byteIdx%
end if
if startbyte%>0 then exit while
byteidx%=byteidx%+1
if startByte% > 0 then exit while
byteIdx% = byteIdx% + 1
wend
if startbyte%=0 then exit function
if startByte% = 0 then exit function
byteidx%=startbyte%
dim charsfound%
charsfound%=0
' 2. Find the byte where the sequence ends
byteIdx% = startByte%
dim charsFound%
charsFound% = 0
while byteidx%<=len(txt$)
b%=asc(txt$,byteidx%)
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
charsfound%=charsfound%+1
' We look for the start of the "lastChar + 1" to find the boundary
while byteIdx% <= len(txt$)
b% = asc(txt$, byteIdx%)
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
charsFound% = charsFound% + 1
end if
if charsfound%>numchars% then exit while
byteidx%=byteidx%+1
' If we found the start of the character AFTER our range, stop
if charsFound% > numChars% then exit while
byteIdx% = byteIdx% + 1
wend
umid$=mid$(txt$,startbyte%,byteidx%-startbyte%)
' byteIdx now points to the start of the next char, or LEN+1
umid$ = mid$(txt$, startByte%, byteIdx% - startByte%)
end function
function uwidth% (txt$)
dim totalwidth%,i%,char$,cp&
totalwidth%=0
for i%=1 to ulen(txt$)
char$=umid(txt$,i%,1)
cp&=getcodepoint&(char$)
function UWidth% (txt$)
dim totalWidth%, i%, char$, cp&
totalWidth% = 0
for i% = 1 to ulen(txt$)
char$ = umid(txt$, i%, 1)
cp& = GetCodePoint&(char$)
if cp&>&h1100 then
totalwidth%=totalwidth%+2
if cp& > &H1100 then
totalWidth% = totalWidth% + 2
else
totalwidth%=totalwidth%+1
totalWidth% = totalWidth% + 1
end if
next
uwidth%=totalwidth%
UWidth% = totalWidth%
end function
function getcodepoint& (utf8char$)
dim llength as integer
llength=len(utf8char$)
dim b1 as _unsigned _byte,b2 as _unsigned _byte
dim b3 as _unsigned _byte,b4 as _unsigned _byte
function GetCodePoint& (utf8Char$)
dim lLength as integer
lLength = len(utf8Char$)
dim b1 as _unsigned _byte, b2 as _unsigned _byte
dim b3 as _unsigned _byte, b4 as _unsigned _byte
select case llength
select case lLength
case 1
getcodepoint&=asc(utf8char$,1)
GetCodePoint& = asc(utf8Char$, 1)
case 2
b1=asc(utf8char$,1):b2=asc(utf8char$,2)
getcodepoint&=(b1 and &h1f)*64+(b2 and &h3f)
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2)
GetCodePoint& = (b1 and &H1F) * 64 + (b2 and &H3F)
case 3
b1=asc(utf8char$,1):b2=asc(utf8char$,2):b3=asc(utf8char$,3)
getcodepoint&=(b1 and &h0f)*4096+(b2 and &h3f)*64+(b3 and &h3f)
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3)
GetCodePoint& = (b1 and &H0F) * 4096 + (b2 and &H3F) * 64 + (b3 and &H3F)
case 4
b1=asc(utf8char$,1):b2=asc(utf8char$,2):b3=asc(utf8char$,3):b4=asc(utf8char$,4)
getcodepoint&=(b1 and &h07)*262144+(b2 and &h3f)*4096+(b3 and &h3f)*64+(b4 and &h3f)
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3): b4 = asc(utf8Char$, 4)
GetCodePoint& = (b1 and &H07) * 262144 + (b2 and &H3F) * 4096 + (b3 and &H3F) * 64 + (b4 and &H3F)
end select
end function

View file

@ -2,21 +2,18 @@
**c**onsole **i**nterface **m**usic **p**layer
A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
It plays audio files and M3U playlists directly from the command line, featuring
a minimal two-line display showing playback state and an IPC remote control
interface.
A lightweight, keyboard-driven music player for the terminal, written in
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the
command line, with a minimal two-line display showing playback state.
## Features
- Plays individual audio files or M3U playlists.
- Real-time progress bar and time display (elapsed or remaining).
- Volume control and seeking via arrow keys.
- Shuffle and repeat modes.
- **Client/Server IPC Architecture**: Control a running instance of `cimp` from
another terminal window or script.
- Cross-platform: works on Linux, macOS, and Windows.
- Unicode-aware display with support for full-width character width matching.
- Plays individual audio files or M3U playlists
- Real-time progress bar and time display (elapsed or remaining)
- Volume control and seeking via arrow keys
- Shuffle and repeat modes
- Cross-platform: works on Linux, macOS, and Windows
- Unicode-aware display
## Building
@ -39,12 +36,9 @@ qb64 -x cimp.bas
cimp [options] <file|playlist> [file2 ...]
```
Pass one or more audio files or `.m3u` playlists. At least one file is required
unless sending a remote control command.
Pass one or more audio files or `.m3u` playlists. At least one file is required.
### Global & Server Options
These flags control the primary player instance when launched.
### Options
| Flag | Long form | Description |
| -------- | -------------- | ---------------------------------------- |
@ -53,65 +47,21 @@ These flags control the primary player instance when launched.
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
| `-n` | `--nooutput` | Run silently with no display |
### Remote Control (Client Mode) Commands
If `cimp` is already running in a terminal, running it again with any of the
following arguments will send an instruction to the active player instead of
starting a new one:
| Command | Description |
| ------------------------- | ---------------------------------------------------------- |
| `--play` | Resume playback |
| `--pause` | Pause playback |
| `--stop` | Stop playback |
| `--next` | Skip to the next track |
| `--prev` | Skip to the previous track |
| `-v <n>` / `--volume <n>` | Remotely adjust volume (0100) |
| `--shuffle` | Reshuffle the current playlist queue |
| `--repeat` | Set player to repeat all |
| `--repeat-1` | Set player to repeat current track |
| `--add <file>` | Append a new file or playlist to the active queue |
| `--playlist` | Fetch and print the current playback queue from the server |
| `--quit` | Remotely close the running player instance |
### Examples
#### Play a single file normally
```shell
# Play a single file
cimp song.mp3
```
### Remote control examples (Run from a separate terminal)
# Play a playlist shuffled at 80% volume
cimp -s -v 80 playlist.m3u
#### Pause the music
```shell
cimp --pause
```
#### Lower the volume to 20%
```shell
cimp --volume 20
```
#### Add another track to the background player's active queue
```shell
cimp --add extra_track.flac
```
#### View the active playlist queue
```shell
cimp --playlist
# Play multiple files with repeat
cimp -r track1.ogg track2.ogg track3.ogg
```
## Keybindings
When interacting directly with the player interface terminal, use these keys:
| Key | Action |
| ------------- | ----------------------------------------- |
| `↑` / `↓` | Volume up / down |
@ -136,18 +86,12 @@ both compilers:
## Files
| File | Description |
| ------------- | --------------------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
| `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
| ------------- | ---------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source |
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection |
## Notes
- **IPC Backend**: On Windows, IPC relies on Named Pipes (`\\.\pipe\cimp_ipc`).
On Linux/macOS, it binds to a local Unix Domain Socket at
`/tmp/cimp_ipc_<uid>.sock`.
- M3U paths can be absolute or relative to the playlist file's directory.
- Volume adjustment is non-linear for a more natural response and better control
at lower ranges.
```
```

View file

@ -3,23 +3,11 @@
#ifdef _WIN32
#include <windows.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#else
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <stdint.h>
#endif
// Special key codes (unified across platforms)
@ -33,17 +21,8 @@ void echooff();
void echoon();
int termwidth();
int ipc_init();
int ipc_check_message(uintptr_t buffer_ptr, int max_len);
void ipc_send_message(const char* message);
void ipc_cleanup();
#ifdef _WIN32
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
static OVERLAPPED oOverlap;
static BOOL fPendingIO = FALSE;
int terminkey() {
if (_kbhit()) {
int ch = _getch();
@ -86,126 +65,8 @@ int termwidth() {
return -1;
}
int ipc_init() {
// Create a regular blocking pipe, but enable FILE_FLAG_OVERLAPPED for safe async I/O
hServerPipe = CreateNamedPipe(
"\\\\.\\pipe\\cimp_ipc",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
1,
4096,
4096,
0,
NULL
);
if (hServerPipe == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
if (err == ERROR_ACCESS_DENIED || err == ERROR_PIPE_BUSY) {
return 0; // Already running (Client Mode)
}
return -1; // Other error
}
// Initialize the overlapped structure and create an event
memset(&oOverlap, 0, sizeof(OVERLAPPED));
oOverlap.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
fPendingIO = FALSE;
return 1; // Server mode successfully started
}
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
if (hServerPipe == INVALID_HANDLE_VALUE) return 0;
char* buffer = (char*)buffer_ptr;
DWORD bytesRead = 0;
if (!fPendingIO) {
// Start an asynchronous listen operation
if (!ConnectNamedPipe(hServerPipe, &oOverlap)) {
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
fPendingIO = TRUE;
} else if (err == ERROR_PIPE_CONNECTED) {
// Client already connected before we listened!
SetEvent(oOverlap.hEvent);
fPendingIO = TRUE;
}
}
}
if (fPendingIO) {
// Check if the connection event has triggered without waiting (0ms timeout)
if (WaitForSingleObject(oOverlap.hEvent, 0) == WAIT_OBJECT_0) {
DWORD cbRet = 0;
// Connection is ready, let's see if there is data
if (GetOverlappedResult(hServerPipe, &oOverlap, &cbRet, FALSE)) {
DWORD bytesAvail = 0;
if (PeekNamedPipe(hServerPipe, NULL, 0, NULL, &bytesAvail, NULL) && bytesAvail > 0) {
// Data has arrived, read it synchronously
BOOL success = ReadFile(hServerPipe, buffer, max_len - 1, &bytesRead, NULL);
if (success && bytesRead > 0) {
buffer[bytesRead] = '\0';
// Clean up and reset for the next client connection
DisconnectNamedPipe(hServerPipe);
ResetEvent(oOverlap.hEvent);
fPendingIO = FALSE;
return (int)bytesRead;
}
}
} else {
// Client disconnected or pipe broke before sending
DisconnectNamedPipe(hServerPipe);
ResetEvent(oOverlap.hEvent);
fPendingIO = FALSE;
}
}
}
return 0;
}
void ipc_send_message(const char* message) {
HANDLE hPipe = CreateFile(
"\\\\.\\pipe\\cimp_ipc",
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hPipe != INVALID_HANDLE_VALUE) {
DWORD bytesWritten = 0;
WriteFile(hPipe, message, strlen(message), &bytesWritten, NULL);
CloseHandle(hPipe);
}
}
void ipc_cleanup() {
if (hServerPipe != INVALID_HANDLE_VALUE) {
DisconnectNamedPipe(hServerPipe);
CloseHandle(hServerPipe);
hServerPipe = INVALID_HANDLE_VALUE;
}
if (oOverlap.hEvent != NULL) {
CloseHandle(oOverlap.hEvent);
oOverlap.hEvent = NULL;
}
}
#else
static int server_fd = -1;
static char socket_path[256] = "";
static void get_socket_path() {
if (socket_path[0] == '\0') {
uid_t uid = getuid();
snprintf(socket_path, sizeof(socket_path), "/tmp/cimp_ipc_%u.sock", (unsigned int)uid);
}
}
int terminkey() {
struct termios oldt, newt;
int ch;
@ -265,102 +126,4 @@ int termwidth() {
return -1;
}
int ipc_init() {
get_socket_path();
int test_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (test_fd >= 0) {
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (connect(test_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
close(test_fd);
return 0; // Already running
}
close(test_fd);
}
unlink(socket_path);
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd < 0) {
return -1;
}
int flags = fcntl(server_fd, F_GETFL, 0);
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
close(server_fd);
server_fd = -1;
return -1;
}
if (listen(server_fd, 5) < 0) {
close(server_fd);
server_fd = -1;
unlink(socket_path);
return -1;
}
return 1;
}
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
if (server_fd < 0) return 0;
char* buffer = (char*)buffer_ptr;
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
return 0;
}
int total_read = 0;
while (total_read < max_len - 1) {
int r = read(client_fd, buffer + total_read, max_len - 1 - total_read);
if (r <= 0) break;
total_read += r;
}
buffer[total_read] = '\0';
close(client_fd);
return total_read;
}
void ipc_send_message(const char* message) {
get_socket_path();
int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (client_fd < 0) return;
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
int total_written = 0;
int len = strlen(message);
while (total_written < len) {
int w = write(client_fd, message + total_written, len - total_written);
if (w <= 0) break;
total_written += w;
}
}
close(client_fd);
}
void ipc_cleanup() {
if (server_fd >= 0) {
close(server_fd);
server_fd = -1;
}
get_socket_path();
unlink(socket_path);
}
#endif

View file

@ -1,44 +0,0 @@
$console:only
declare library "terminkey"
function ipc_init&()
function ipc_check_message%(byval buf as _offset, byval max_len as long)
sub ipc_send_message(buf as string)
sub ipc_cleanup()
sub get_absolute_path(rel_path as string, byval abs_path as _offset, byval max_len as long)
end declare
dim r as long
r = ipc_init
print "ipc_init returned:"; r
if r = 1 then
print "We are the server! Waiting for a message..."
dim msg as string
msg = space$(100) + chr$(0)
dim start_time as double
start_time = timer
do while timer - start_time < 3
dim n as long
n = ipc_check_message(_offset(msg), 100)
if n > 0 then
print "Received: "; left$(msg, n)
exit do
end if
_limit 10
loop
ipc_cleanup
elseif r = 0 then
print "We are the client! Sending a message..."
ipc_send_message "Hello from client!"
else
print "Error initializing IPC"
end if
dim rel as string
rel = "readme.md"
dim abs_path as string
abs_path = space$(512) + chr$(0)
get_absolute_path rel, _offset(abs_path), 512
print "Absolute path of '"; rel; "' is: '"; _trim$(abs_path); "'"
system

12
todo.md
View file

@ -1,12 +0,0 @@
# todo
## ipc
add ipc functionality so it's possible while the program runs to:
- ~play next and previous track with `cimp --next` and `cimp --prev`~
- ~change volume with `cimp --volume xx`~
- ~add file to playlist with `cimp --add filename.ext`~
- if started without `--add` it should clear the playlist and load and play the given file or playlist.
- if given the flag `--playlist` it should output the song array as a playlist.
- start stop pause and quit would be good, repeat as well.
the c functions are ready in terminkey.h and an example of how it works is found in test_ipc.bas
## recurcive add all music in folder