Compare commits

..

23 commits

Author SHA1 Message Date
visionmercer
82c859317b fix filename display on windows 2026-06-24 13:37:45 +02:00
visionmercer
bd11dfb7ca nyan 2026-06-24 12:41:56 +02:00
4cf569b01e Update readme.md 2026-06-24 12:19:17 +02:00
visionmercer
52c0e73659 update readme 2026-06-24 12:18:21 +02:00
visionmercer
14d3e11ac1 fixes #9 2026-06-24 12:00:25 +02:00
visionmercer
58844384f2 restructure and additions to IPC 2026-06-24 11:44:10 +02:00
cd290f0d2b Update todo.md 2026-06-23 22:13:08 +02:00
visionmercer
a3c7f66b2f losing faith. 2026-06-23 12:47:05 +02:00
visionmercer
a781ef1453 Getting better 2026-06-23 12:40:40 +02:00
visionmercer
96c694f78d A better fix maybe? 2026-06-23 12:33:49 +02:00
visionmercer
c2d86b6f46 Windows IPC fix, maybe? 2026-06-23 12:22:51 +02:00
visionmercer
301a6423db remove debug print 2026-06-23 12:17:59 +02:00
visionmercer
19701af4ec A few fixes to IPC 2026-06-23 12:10:10 +02:00
visionmercer
19bf4b5288 Add Inter-Process Communication 2026-06-23 10:21:17 +02:00
visionmercer
46cdaf4e63 lint 2026-06-10 10:35:36 +02:00
visionmercer
c1d109e198 reject crlf embrace lf 2026-06-10 10:32:15 +02:00
visionmercer
b52fdf512c reset on exit more. 2026-06-10 08:52:52 +02:00
visionmercer
abc48259bd Reset colors on exit? 2026-06-09 11:23:36 +02:00
visionmercer
0de770ce99 comments not needed here 2026-06-09 11:03:47 +02:00
visionmercer
a894f36ffa baha 2026-06-09 10:58:47 +02:00
visionmercer
8af3bc9649 bah 2026-06-09 10:57:46 +02:00
visionmercer
765372aa6b respect repeat regulation 2026-06-09 10:56:09 +02:00
visionmercer
b6c1a1c1c0 marquee and cleanup 2026-06-09 10:36:00 +02:00
5 changed files with 1047 additions and 448 deletions

742
cimp.bas
View file

@ -1,79 +1,141 @@
declare library"terminkey"
declare library "terminkey" function terminkey%()
function terminkey% () sub echooff()
sub echooff () sub echoon()
sub echoon () function termwidth()
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()
end declare end declare
$console:only $console:only
on error goto quit
const KEY_UP = 1001 const key_up=1001
const KEY_DOWN = 1002 const key_down=1002
const KEY_RIGHT = 1003 const key_right=1003
const KEY_LEFT = 1004 const key_left=1004
dim slash as string
$if WIN then slash="/"
Shell "chcp 65001 > nul" $if win then
shell"chcp 65001 > nul"
slash="\"
$end if $end if
redim file(0) as string redim file(0) as string
if command$ = "" then chdir _startdir$
print "please specify file to play." if command$=""then
system print"please specify file to play."
goto quit
end if 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 echooff
cursoroff cursoroff
chdir _startdir$
dim volume as single dim volume as single
dim repeat as integer dim repeat as integer
dim shuffle as integer dim shuffle as integer
dim nooutput as integer dim nooutput as integer
dim timevis as integer dim timevis as integer
dim nyan as integer dim nyan as integer
dim marqueeoffset as integer
volume = 1 dim i as integer
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 musichandle as long
dim oldhandle as long dim oldhandle as long
dim keyin as integer dim keyin as integer
dim playnext as integer dim playnext as integer
@ -82,150 +144,344 @@ dim songname as string
dim progress as string dim progress as string
dim progressbar as string dim progressbar as string
if shuffle = -1 then shufflearray file() dim tw as integer
i = 0 dim fixedwidth as integer
musichandle = _sndopen(file(i)) dim maxtitlewidth as integer
if musichandle = 0 then dim visibletitle as string
print "Error: could not open file "; file(i) dim currentsongwidth as integer
system dim paddedtitle as string
end if dim paddedlength as integer
_sndvol musichandle, volume dim idx as integer
_sndplay musichandle dim addedwidth as integer
state = "playing " dim charidx as integer
songname = beforelast(".", afterlast("/", file(i))) dim nextchar as string
dim marqueeframe as integer
while keyin <> 27 volume=1
keyin = terminkey repeat=0
select case keyin shuffle=0
case KEY_UP nooutput=0
volume = volume + (0.01 + (volume / 10)) 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
end if
_sndvol musichandle,volume
_sndplay musichandle
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
if volume > 1 then volume = 1 if volume > 1 then volume = 1
_sndvol musichandle, volume
case KEY_DOWN
volume = volume - (0.01 + (volume / 10))
if volume < 0 then volume = 0 if volume < 0 then volume = 0
_sndvol musichandle, volume _sndvol musichandle, volume
case KEY_RIGHT elseif left$(client_cmd, 4) = "ADD:" then
if _sndgetpos(musichandle) + 5 < _sndlen(musichandle) then dim new_file as string
_sndsetpos musichandle, _sndgetpos(musichandle) + 5 new_file = _trim$(mid$(client_cmd, 5))
else if _fileexists(new_file) then
if lcase$(right$(new_file, 4)) = ".m3u" then
parsem3u new_file, file()
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 playnext = 1
end if end if
case KEY_LEFT elseif client_cmd = "GET_PLAYLIST" then
if _sndgetpos(musichandle) - 5 > 0 then dim p_idx as long
_sndsetpos musichandle, _sndgetpos(musichandle) - 5 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)
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 else
playnext = -1 playnext=1
end if
case key_left
if _sndgetpos(musichandle)-5>0 then
_sndsetpos musichandle,_sndgetpos(musichandle)-5
else
playnext=-1
end if end if
case asc("q") case asc("q")
keyin = 27 keyin=27
case asc("z") case asc("z")
if _sndgetpos(musichandle) > 2 then if _sndgetpos(musichandle)>2 then
_sndsetpos musichandle, 0 _sndsetpos musichandle,0
else else
playnext = -1 playnext=-1
end if end if
case asc("x") case asc("x")
if _sndplaying(musichandle) then if _sndplaying(musichandle) then
_sndsetpos musichandle, 0 _sndsetpos musichandle,0
else else
_sndplay musichandle _sndplay musichandle
end if end if
case asc("c"), asc(" ") case asc("c"),asc(" ")
if _sndplaying(musichandle) then if _sndplaying(musichandle) then
_sndpause musichandle _sndpause musichandle
state = "paused " state="paused "
else else
_sndplay musichandle _sndplay musichandle
state = "playing " state="playing "
end if end if
case asc("v") case asc("v")
_sndstop musichandle _sndstop musichandle
state = "stopped " state="stopped "
case asc("b") case asc("b")
playnext = 1 playnext=1
case asc("t") case asc("t")
timevis = -timevis timevis=-timevis
case asc("s") case asc("s")
shufflearray file() shufflearray file()
case else
end select end select
if _sndgetpos(musichandle) = _sndlen(musichandle) then playnext = 1 if _sndgetpos(musichandle)=_sndlen(musichandle) then playnext=1
if playnext <> 0 then if playnext<>0 then
oldhandle = musichandle oldhandle=musichandle
i = i + playnext if repeat=1 and playnext=1 then
if i > ubound(file) then i = 0 playnext=0
if i < lbound(file) then i = ubound(file) else
musichandle = _sndopen(file(i)) i=i+playnext
if musichandle <> 0 then if i>ubound(file) then
_sndvol musichandle, volume 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
_sndplay musichandle _sndplay musichandle
_sndstop oldhandle _sndstop oldhandle
_sndclose oldhandle _sndclose oldhandle
state = "playing " state="playing "
songname = beforelast(".", afterlast("/", file(i))) songname=beforelast(".",afterlast(slash,file(i)))
playnext = 0 playnext=0
else else
musichandle=oldhandle musichandle=oldhandle
end if end if
end if end if
if timevis = 1 then
progress = " -" + timeleft(musichandle) if timevis=1 then
progress=" -"+timeleft(musichandle)
else else
progress = " " + timeelapsed(musichandle) progress=" "+timeelapsed(musichandle)
end if end if
if nooutput=0 then if nooutput=0 then
if nyan = -1 then tw=termwidth
print termcolor(7); state; AnimatedRainbowText(songname); termcolor(7); progress; clearrest 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
else else
print termcolor(7); state; termcolor(3); songname; termcolor(7); progress; clearrest ' Terminal is wide enough, no scrolling needed
visibletitle=songname
marqueeoffset=0
end if end if
progressbar = bar(UWidth(state) + UWidth(songname) + UWidth(progress), (_sndgetpos(musichandle) / _sndlen(musichandle)) * 100, 11, 7)
' 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)
print progressbar; clearrest; cursorback; print progressbar; clearrest; cursorback;
end if end if
_limit 30 _limit 30
if _exit then goto quit if _exit then goto quit
wend
wend
quit: quit:
_sndclose musichandle _sndclose musichandle
ipc_cleanup
print clearrest print clearrest
print clearrest; print clearrest;
print cursorback; print cursorback;
print chr$(27)+"[0m";
cursoron cursoron
echoon echoon
system system
sub shufflearray (stringarray() as string) sub shufflearray (stringarray() as string)
randomize timer randomize timer
for n = ubound(stringarray) to 1 step -1 dim n as long,j as long
j = int(rnd * n) for n=ubound(stringarray) to 1 step -1
swap stringarray(n), stringarray(j) j=int(rnd*n)
swap stringarray(n),stringarray(j)
next next
end sub end sub
sub ParseM3U (filename$, array$()) sub parsem3u (filename$,array$())
for i = len(filename$) to 1 step -1 dim i as long,f as long,count as long
if mid$(filename$, i, 1) = "/" or mid$(filename$, i, 1) = "\" then dim basepath$,l$,resolvedpath$
basePath$ = left$(filename$, i) 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 exit for
end if end if
next next
f = freefile f=freefile
open filename$ for input as #f open filename$ for input as #f
count = 0 count=0
do until eof(f) do until eof(f)
line input #f, l$ line input #f,l$
l$ = _trim$(l$) l$=_trim$(l$)
if len(l$) > 0 and left$(l$, 1) <> "#" then if len(l$)>0 and left$(l$,1)<>"#"then
resolvedPath$ = l$ resolvedpath$=l$
if not _fileexists(resolvedPath$) then if not _fileexists(resolvedpath$) then
resolvedPath$ = basePath$ + l$ resolvedpath$=basepath$+l$
end if end if
if _fileexists(resolvedPath$) then if _fileexists(resolvedpath$) then
array$(ubound(array$)) = resolvedPath$ array$(ubound(array$))=resolvedpath$
redim _preserve array$(ubound(array$) + 1) redim _preserve array$(ubound(array$)+1)
end if end if
end if end if
loop loop
@ -234,195 +490,189 @@ end sub
function timeleft$ (handle&) function timeleft$ (handle&)
dim seconds as integer dim seconds as integer
seconds = _sndlen(handle&) - _sndgetpos(handle&) seconds=_sndlen(handle&)-_sndgetpos(handle&)
if seconds < 0 then seconds = 0 if seconds<0 then seconds=0
timeleft$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2) timeleft$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
end function end function
function timeelapsed$ (handle&) function timeelapsed$ (handle&)
dim seconds as integer dim seconds as integer
seconds = _sndgetpos(handle&) seconds=_sndgetpos(handle&)
if seconds < 0 then seconds = 0 if seconds<0 then seconds=0
timeelapsed$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2) timeelapsed$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
end function end function
function termcolor$ (colorvalue as _unsigned long) function termcolor$ (colorvalue as _unsigned long)
select case colorvalue select case colorvalue
case 0 to 7 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 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 case 16 to 255
termcolor = chr$(27) + "[38;5;" + _trim$(str$(colorvalue)) + "m" termcolor=chr$(27)+"[38;5;"+_trim$(str$(colorvalue))+"m"
case is > 255 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;2;"+_trim$(str$(_red32(colorvalue)))+";"+_trim$(str$(_green32(colorvalue)))+";"+_trim$(str$(_blue32(colorvalue)))+"m"
end select end select
end function end function
function cursorback$ () function cursorback$ ()
cursorback = chr$(27) + "[F" cursorback=chr$(27)+"[F"
end function end function
function clearline$ function clearline$
clearline = chr$(27) + "[2K" clearline=chr$(27)+"[2K"
end function end function
function clearrest$ function clearrest$
clearrest = chr$(27) + "[K" clearrest=chr$(27)+"[K"
end function end function
sub cursoroff () sub cursoroff ()
print chr$(27); "[?25l"; print chr$(27);"[?25l";
end sub end sub
sub cursoron () sub cursoron ()
print chr$(27); "[?25h"; print chr$(27);"[?25h";
end sub 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 done as string
dim notdone as string dim notdone as string
for i = 1 to int((percent / 100) * length) dim i as integer
done = done + "━" for i=1 to int((percent/100)*length)
done=done+"━"
next i next i
for i = 1 to length - int((percent / 100) * length) for i=1 to length-int((percent/100)*length)
notdone = notdone + "━" notdone=notdone+"━"
next i next i
bar$ = termcolor(color1) + done + termcolor(color2) + notdone bar$=termcolor(color1)+done+termcolor(color2)+notdone
end function end function
function afterlast$ (delim as string, strng as string) function afterlast$ (delim as string,strng as string)
afterlast = mid$(strng, _instrrev(strng, delim) + 1) afterlast=mid$(strng,_instrrev(strng,delim)+1)
end function end function
function beforelast$ (delim as string, strng as string) function beforelast$ (delim as string,strng as string)
beforelast = left$(strng, _instrrev(strng, delim) - 1) beforelast=left$(strng,_instrrev(strng,delim)-1)
end function end function
function animatedrainbowtext$ (text$)
function AnimatedRainbowText$ (text$)
static offset as double static offset as double
dim result as string dim result as string
dim L as long, i as long dim l as long,i as long
dim r as integer, g as integer, b as integer dim r as integer,g as integer,b as integer
dim hue as double, f as double dim hue as double,f as double
dim sector as integer, v as integer, p as integer, q as integer, t as integer dim sector as integer,v as integer,p as integer,q as integer,t as integer
dim rgbpart$
L = ulen(text$) l=ulen(text$)
if L = 0 then exit function if l=0 then exit function
offset = offset + 5.0 offset=offset+5.0
if offset >= 360 then offset = offset - 360 if offset>=360 then offset=offset-360
for i = 1 to L for i=1 to l
hue = MOD_Double(offset + ((i - 1) / L) * 360, 360) hue=mod_double(offset+((i-1)/l)*360,360)
sector = int(hue / 60) sector=int(hue/60)
f = (hue / 60) - sector f=(hue/60)-sector
v = 255: p = 0: q = 255 * (1 - f): t = 255 * f v=255:p=0:q=255*(1-f):t=255*f
select case sector select case sector
case 0: r = v: g = t: b = p case 0:r=v:g=t:b=p
case 1: r = q: g = v: b = p case 1:r=q:g=v:b=p
case 2: r = p: g = v: b = t case 2:r=p:g=v:b=t
case 3: r = p: g = q: b = v case 3:r=p:g=q:b=v
case 4: r = t: g = p: b = v case 4:r=t:g=p:b=v
case 5: r = v: g = p: b = q case 5:r=v:g=p:b=q
end select end select
rgbPart$ = _trim$(str$(r)) + ";" + _trim$(str$(g)) + ";" + _trim$(str$(b)) rgbpart$=_trim$(str$(r))+";"+_trim$(str$(g))+";"+_trim$(str$(b))
result = result + chr$(27) + "[38;2;" + rgbPart$ + "m" + umid$(text$, i, 1) result=result+chr$(27)+"[38;2;"+rgbpart$+"m"+umid$(text$,i,1)
next i next i
AnimatedRainbowText$ = result + chr$(27) + "[0m" animatedrainbowtext$=result+chr$(27)+"[0m"
end function end function
function MOD_Double (value as double, m as double) function mod_double (value as double,m as double)
MOD_Double = value - (m * int(value / m)) mod_double=value-(m*int(value/m))
end function end function
function ulen% (txt$) function ulen% (txt$)
dim count%, i%, b% dim count%,i%,b%
count% = 0 count%=0
for i% = 1 to len(txt$) for i%=1 to len(txt$)
b% = asc(txt$, i%) b%=asc(txt$,i%)
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
count% = count% + 1 count%=count%+1
end if end if
next next
ulen% = count% ulen%=count%
end function end function
function umid$ (txt$, startChar%, numChars%) function umid$ (txt$,startchar%,numchars%)
if startChar% < 1 or numChars% <= 0 or txt$ = "" then exit function if startchar%<1 or numchars%<=0 or txt$=""then exit function
dim byteIdx%, charCount%, startByte%, endByte% dim byteidx%,charcount%,startbyte%,endbyte%,b%
byteIdx% = 1 byteidx%=1
charCount% = 0 charcount%=0
' 1. Find the starting byte of the character at startChar% while byteidx%<=len(txt$)
while byteIdx% <= len(txt$) b%=asc(txt$,byteidx%)
b% = asc(txt$, byteIdx%) if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then charcount%=charcount%+1
charCount% = charCount% + 1 if charcount%=startchar% then startbyte%=byteidx%
if charCount% = startChar% then startByte% = byteIdx%
end if end if
if startByte% > 0 then exit while if startbyte%>0 then exit while
byteIdx% = byteIdx% + 1 byteidx%=byteidx%+1
wend wend
if startByte% = 0 then exit function if startbyte%=0 then exit function
' 2. Find the byte where the sequence ends byteidx%=startbyte%
byteIdx% = startByte% dim charsfound%
dim charsFound% charsfound%=0
charsFound% = 0
' We look for the start of the "lastChar + 1" to find the boundary while byteidx%<=len(txt$)
while byteIdx% <= len(txt$) b%=asc(txt$,byteidx%)
b% = asc(txt$, byteIdx%) if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then charsfound%=charsfound%+1
charsFound% = charsFound% + 1
end if end if
' If we found the start of the character AFTER our range, stop if charsfound%>numchars% then exit while
if charsFound% > numChars% then exit while byteidx%=byteidx%+1
byteIdx% = byteIdx% + 1
wend wend
' byteIdx now points to the start of the next char, or LEN+1 umid$=mid$(txt$,startbyte%,byteidx%-startbyte%)
umid$ = mid$(txt$, startByte%, byteIdx% - startByte%)
end function end function
function UWidth% (txt$) function uwidth% (txt$)
dim totalWidth%, i%, char$, cp& dim totalwidth%,i%,char$,cp&
totalWidth% = 0 totalwidth%=0
for i% = 1 to ulen(txt$) for i%=1 to ulen(txt$)
char$ = umid(txt$, i%, 1) char$=umid(txt$,i%,1)
cp& = GetCodePoint&(char$) cp&=getcodepoint&(char$)
if cp& > &H1100 then if cp&>&h1100 then
totalWidth% = totalWidth% + 2 totalwidth%=totalwidth%+2
else else
totalWidth% = totalWidth% + 1 totalwidth%=totalwidth%+1
end if end if
next next
UWidth% = totalWidth% uwidth%=totalwidth%
end function end function
function GetCodePoint& (utf8Char$) function getcodepoint& (utf8char$)
dim lLength as integer dim llength as integer
lLength = len(utf8Char$) llength=len(utf8char$)
dim b1 as _unsigned _byte, b2 as _unsigned _byte dim b1 as _unsigned _byte,b2 as _unsigned _byte
dim b3 as _unsigned _byte, b4 as _unsigned _byte dim b3 as _unsigned _byte,b4 as _unsigned _byte
select case lLength select case llength
case 1 case 1
GetCodePoint& = asc(utf8Char$, 1) getcodepoint&=asc(utf8char$,1)
case 2 case 2
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2) b1=asc(utf8char$,1):b2=asc(utf8char$,2)
GetCodePoint& = (b1 and &H1F) * 64 + (b2 and &H3F) getcodepoint&=(b1 and &h1f)*64+(b2 and &h3f)
case 3 case 3
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 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) getcodepoint&=(b1 and &h0f)*4096+(b2 and &h3f)*64+(b3 and &h3f)
case 4 case 4
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3): b4 = asc(utf8Char$, 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) getcodepoint&=(b1 and &h07)*262144+(b2 and &h3f)*4096+(b3 and &h3f)*64+(b4 and &h3f)
end select end select
end function end function

View file

@ -2,18 +2,21 @@
**c**onsole **i**nterface **m**usic **p**layer **c**onsole **i**nterface **m**usic **p**layer
A lightweight, keyboard-driven music player for the terminal, written in A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the It plays audio files and M3U playlists directly from the command line, featuring
command line, with a minimal two-line display showing playback state. a minimal two-line display showing playback state and an IPC remote control
interface.
## Features ## Features
- Plays individual audio files or M3U playlists - Plays individual audio files or M3U playlists.
- Real-time progress bar and time display (elapsed or remaining) - Real-time progress bar and time display (elapsed or remaining).
- Volume control and seeking via arrow keys - Volume control and seeking via arrow keys.
- Shuffle and repeat modes - Shuffle and repeat modes.
- Cross-platform: works on Linux, macOS, and Windows - **Client/Server IPC Architecture**: Control a running instance of `cimp` from
- Unicode-aware display another terminal window or script.
- Cross-platform: works on Linux, macOS, and Windows.
- Unicode-aware display with support for full-width character width matching.
## Building ## Building
@ -36,9 +39,12 @@ qb64 -x cimp.bas
cimp [options] <file|playlist> [file2 ...] cimp [options] <file|playlist> [file2 ...]
``` ```
Pass one or more audio files or `.m3u` playlists. At least one file is required. Pass one or more audio files or `.m3u` playlists. At least one file is required
unless sending a remote control command.
### Options ### Global & Server Options
These flags control the primary player instance when launched.
| Flag | Long form | Description | | Flag | Long form | Description |
| -------- | -------------- | ---------------------------------------- | | -------- | -------------- | ---------------------------------------- |
@ -47,21 +53,65 @@ Pass one or more audio files or `.m3u` playlists. At least one file is required.
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) | | `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
| `-n` | `--nooutput` | Run silently with no display | | `-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 ### Examples
#### Play a single file normally
```shell ```shell
# Play a single file
cimp song.mp3 cimp song.mp3
```
# Play a playlist shuffled at 80% volume ### Remote control examples (Run from a separate terminal)
cimp -s -v 80 playlist.m3u
# Play multiple files with repeat #### Pause the music
cimp -r track1.ogg track2.ogg track3.ogg
```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
``` ```
## Keybindings ## Keybindings
When interacting directly with the player interface terminal, use these keys:
| Key | Action | | Key | Action |
| ------------- | ----------------------------------------- | | ------------- | ----------------------------------------- |
| `↑` / `↓` | Volume up / down | | `↑` / `↓` | Volume up / down |
@ -85,13 +135,19 @@ both compilers:
## Files ## Files
| File | Description | | File | Description |
| ------------- | ---------------------------------------------------------------------------- | | ------------- | --------------------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source | | `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection | | `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
## Notes ## 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. - 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 - Volume adjustment is non-linear for a more natural response and better control
at lower ranges. at lower ranges.
```
```

View file

@ -3,11 +3,23 @@
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#include <conio.h> #include <conio.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#else #else
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/ioctl.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 #endif
// Special key codes (unified across platforms) // Special key codes (unified across platforms)
@ -21,8 +33,17 @@ void echooff();
void echoon(); void echoon();
int termwidth(); 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 #ifdef _WIN32
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
static OVERLAPPED oOverlap;
static BOOL fPendingIO = FALSE;
int terminkey() { int terminkey() {
if (_kbhit()) { if (_kbhit()) {
int ch = _getch(); int ch = _getch();
@ -65,8 +86,126 @@ int termwidth() {
return -1; 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 #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() { int terminkey() {
struct termios oldt, newt; struct termios oldt, newt;
int ch; int ch;
@ -126,4 +265,102 @@ int termwidth() {
return -1; 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 #endif

44
test_ipc.bas Normal file
View file

@ -0,0 +1,44 @@
$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 Normal file
View file

@ -0,0 +1,12 @@
# 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