3 import std
.concurrency
;
7 import arsd
.simpledisplay
;
24 // ////////////////////////////////////////////////////////////////////////// //
25 public GxRect
getWorkArea () {
26 auto dpy
= XDisplayConnection
.get
;
27 auto root
= RootWindow(dpy
, DefaultScreen(dpy
));
28 auto atomWTF
= GetAtom
!("_NET_WORKAREA", true)(dpy
);
34 auto status
= XGetWindowProperty(dpy
, root
, atomWTF
, 0, 4, /*False*/0, AnyPropertyType
, &aType
, &format
, &itemCount
, &bytesAfter
, cast(void**)&propRes
);
35 if (status
>= Success
) {
36 if (propRes
!is null) {
37 auto rc
= GxRect(propRes
[0], propRes
[1], propRes
[2], propRes
[3]);
42 return GxRect(0, 0, 800, 600);
46 // ////////////////////////////////////////////////////////////////////////// //
47 __gshared SimpleWindow sdhint
;
48 __gshared string hinttext
= "not playing";
49 __gshared Image hintbackbuf
;
50 __gshared
uint* hintvbuf
;
51 __gshared Timer hintHideTimer
;
52 __gshared
int hintX
, hintY
;
55 // ////////////////////////////////////////////////////////////////////////// //
56 void setHint(T
:const(char)[]) (T
str) {
57 static if (is(T
== typeof(null))) {
60 if (hinttext
== str) return;
61 static if (is(T
== string
)) hinttext
= str; else hinttext
= str.idup
;
62 repaintHintWindow(false);
67 // ////////////////////////////////////////////////////////////////////////// //
68 void createHintWindow (int x
, int y
) {
69 if (sdhint
!is null) {
72 sdpyWindowClass
= "AMPER_HINT_WINDOW";
73 sdhint
= new SimpleWindow(120, gxTextHeightUtf
+2, "AmperHint", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.undecorated
);
74 sdhint
.setNetWMWindowType(GetAtom
!"_NET_WM_WINDOW_TYPE_DOCK"(sdhint
.display
)); // sorry for this hack
79 int textWidth
= gxTextWidthUtf(hinttext
)+4;
80 sdhint
.resize(textWidth
, sdhint
.height
);
83 repaintHintWindow(true);
87 void copyVBufToImage () {
88 import core
.stdc
.string
: memcpy
;
89 memcpy(hintbackbuf
.getDataPointer
, hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*4);
93 void repaintHintWindow (bool forced
) {
94 if (sdhint
is null || sdhint
.closed
) return;
95 if (!forced
&& sdhint
.hidden
) return;
98 int textWidth
= gxTextWidthUtf(hinttext
)+4;
99 if (textWidth
< 8) textWidth
= 8;
100 auto wrc
= getWorkArea();
103 if (nx
+textWidth
> wrc
.x1
) nx
= wrc
.x1
-textWidth
+1;
104 if (nx
< wrc
.x0
) nx
= wrc
.x0
;
105 if (ny
+sdhint
.height
> wrc
.y1
) ny
= wrc
.y1
-sdhint
.height
+1;
106 if (ny
< wrc
.y0
) ny
= wrc
.y0
;
109 sdhint
.moveResize(hintX
, hintY
, textWidth
, sdhint
.height
);
112 if (hintbackbuf
is null || hintbackbuf
.width
!= sdhint
.width || hintbackbuf
.height
!= sdhint
.height
) {
113 import core
.stdc
.stdlib
: realloc
;
114 hintbackbuf
= new Image(sdhint
.width
, sdhint
.height
);
115 hintvbuf
= cast(uint*)realloc(hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*hintvbuf
[0].sizeof
);
116 if (hintvbuf
is null) assert(0, "out of memory");
120 auto VBufWidthSave
= VBufWidth
;
121 auto VBufHeightSave
= VBufHeight
;
122 auto vglTexBufSave
= vglTexBuf
;
123 VBufWidth
= hintbackbuf
.width
;
124 VBufHeight
= hintbackbuf
.height
;
125 vglTexBuf
= hintvbuf
;
127 VBufWidth
= VBufWidthSave
;
128 VBufHeight
= VBufHeightSave
;
129 vglTexBuf
= vglTexBufSave
;
132 gxClearScreen(gxRGB
!(255, 255, 0));
133 gxDrawRect(0, 0, VBufWidth
, VBufHeight
, gxRGB
!(0, 0, 0));
134 gxClipRect
.shrinkBy(2, 1);
135 gxDrawTextUtf(gxClipRect
.x0
, gxClipRect
.y0
, hinttext
, gxRGB
!(0, 0, 0));
139 auto painter
= sdhint
.draw();
140 painter
.drawImage(Point(0, 0), hintbackbuf
);
146 // ////////////////////////////////////////////////////////////////////////// //
147 __gshared NotificationAreaIcon trayicon
;
148 __gshared Image trayimage
;
149 __gshared MemoryImage icon
; // 0: normal
152 void hideShowWindows () {
153 if (sdampwin
is null || sdampwin
.closed
) return;
154 if (sdampwin
.hidden
) {
155 vglShowWindow(sdampwin
);
157 if (sdplwin
is null || sdplwin
.closed
) {
158 if (dgGreatePListWindow
!is null) dgGreatePListWindow();
160 vglShowWindow(sdplwin
);
162 switchToWindow(sdampwin
);
166 vglHideWindow(sdplwin
);
167 vglHideWindow(sdampwin
);
172 void prepareTrayIcon () {
173 static immutable ubyte[] nticonpng
= cast(immutable(ubyte)[])import("skins/notifyicon.png");
174 //icon = readPng("skins/notifyicon.png");
175 icon
= imageFromPng(readPng(nticonpng
));
176 trayimage
= Image
.fromMemoryImage(icon
);
177 trayicon
= new NotificationAreaIcon("Amper", trayimage
, (int x
, int y
, MouseButton button
, ModifierState mods
) {
178 //conwritefln!"x=%d; y=%d; button=%u; mods=0x%04x"(x, y, button, mods);
179 if (button
== MouseButton
.middle
) {
185 if (button
== MouseButton
.left
) {
186 concmd("win_toggle");
190 trayicon
.onEnter
= delegate (int x
, int y
, ModifierState mods
) {
191 //conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
193 conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
194 int wx, wy, wdt, hgt;
195 trayicon.getWindowRect(wx, wy, wdt, hgt);
196 conwriteln("window rect: wx=", wx, "; wy=", wy, "; wdt=", wdt, "; hgt=", hgt);
198 if (sdhint
is null || sdhint
.hidden
) {
199 createHintWindow(x
+18, y
+2);
200 if (hintHideTimer
!is null) hintHideTimer
.destroy();
201 hintHideTimer
= new Timer(3000, delegate () {
202 if (hintHideTimer
!is null) {
203 hintHideTimer
.destroy();
204 hintHideTimer
= null;
205 if (sdhint
!is null && !sdhint
.closed
) sdhint
.hide();
210 trayicon
.onLeave
= delegate () {
211 //conwriteln("icon leave");
212 //if (sdhint !is null && !sdhint.closed) sdhint.hide();
217 // ////////////////////////////////////////////////////////////////////////// //
218 __gshared
ubyte vbNewScale
= 1;
221 // ////////////////////////////////////////////////////////////////////////// //
222 class ScrollTitleEvent
{}
223 __gshared ScrollTitleEvent evScrollTitle
;
225 shared static this () {
226 evScrollTitle
= new ScrollTitleEvent();
230 // ////////////////////////////////////////////////////////////////////////// //
231 __gshared
bool mainDrag
= false;
232 __gshared
int mainDrawPrevX
, mainDrawPrevY
;
235 // ////////////////////////////////////////////////////////////////////////// //
236 void closeAllIfMainIsClosed () {
238 (glconCtlWindow
is null || glconCtlWindow
.closed
) ||
239 (sdampwin
is null || sdampwin
.closed
);
241 if (sdhint
!is null && !sdhint
.closed
) sdhint
.close();
242 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
243 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
244 if (glconCtlWindow
!is null && !glconCtlWindow
.closed
) glconCtlWindow
.close();
249 // ////////////////////////////////////////////////////////////////////////// //
250 // create hidden control window
251 void createCtlWindow () {
252 sdpyWindowClass
= "AMPER_PLAYER_CTL";
253 glconCtlWindow
= new SimpleWindow(1, 1, "AmperCtl", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.hidden
);
255 glconCtlWindow
.onClosing
= delegate () {
256 //conwriteln("closing ctl window...");
257 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
258 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
261 glconCtlWindow
.onDestroyed
= delegate () {
262 //conwriteln("ctl window destroyed");
263 closeAllIfMainIsClosed();
266 glconCtlWindow
.addEventListener((QuitEvent evt
) {
267 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
268 scope(exit
) closeAllIfMainIsClosed();
269 if (glconCtlWindow
.closed
) return;
270 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
274 void rebuildRepaint () {
275 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
276 scope(exit
) closeAllIfMainIsClosed();
277 if (glconCtlWindow
.closed
) return;
278 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
279 //conwriteln("rebuilding screen");
281 //if (aplayIsPlaying) ampMain.curtime = aplayCurTime;
283 if (sdampwin
!is null && !sdampwin
.closed
&& !sdampwin
.hidden
) sdampwin
.redrawOpenGlSceneNow();
284 if (sdplwin
!is null && !sdplwin
.closed
&& !sdplwin
.hidden
) sdplwin
.redrawOpenGlSceneNow();
290 glconCtlWindow
.addEventListener((GLConScreenRebuildEvent evt
) { rebuildRepaint(); });
291 glconCtlWindow
.addEventListener((GLConScreenRepaintEvent evt
) { rebuildRepaint(); });
293 glconCtlWindow
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
294 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
295 scope(exit
) closeAllIfMainIsClosed();
296 bool sendAnother
= false;
297 bool prevVisible
= isConsoleVisible
;
300 scope(exit
) consoleUnlock();
302 sendAnother
= !conQueueEmpty();
304 if (glconCtlWindow
.closed
) return;
305 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
306 if (sendAnother
) glconPostDoConCommands();
308 if (prevVisible || isConsoleVisible) glconPostScreenRepaintDelayed();
309 if (vbNewScale != vbufEffScale) glconPostScreenRepaint();
311 if (vbNewScale
!= vglWindowScale(glconCtlWindow
)) {
312 vbNewScale
= vglScaleWindow(glconCtlWindow
, vbNewScale
);
313 vglScaleWindow(sdplwin
, vbNewScale
);
315 glconPostScreenRebuild();
319 glconCtlWindow
.addEventListener((ScrollTitleEvent evt
) {
320 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
321 scope(exit
) closeAllIfMainIsClosed();
322 if (glconCtlWindow
.closed
) return;
323 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
324 //conwriteln("scrolling title");
325 ampMain
.scrollTitle();
326 if (!glconCtlWindow
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postTimeout(evScrollTitle
, 100);
327 glconPostScreenRebuild();
330 glconCtlWindow
.addEventListener((EventFileLoaded evt
) {
332 conwriteln("ERROR loading '", evt
.filename
, "'");
333 setHint("not playing");
334 //glconCtlWindow.close();
338 setHint(evt
.artist
~" \u2014 "~evt
.title
);
339 //conwriteln("playing '", evt.filename, "': ", evt.artist, " -- ", evt.title);
342 glconCtlWindow
.addEventListener((EventFileComplete evt
) {
343 //glconCtlWindow.close();
344 setHint("not playing");
345 concmd("song_next tan");
350 // ////////////////////////////////////////////////////////////////////////// //
351 void createAmpWindow () {
352 sdpyWindowClass
= "AMPER_PLAYER";
354 sdampwin
= new SimpleWindow(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
, "Amper", OpenGlOptions
.yes
, Resizability
.fixedSize
, WindowTypes
.undecorated
);
355 sdampwin
.setMinSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
356 sdampwin
.setMaxSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
357 //sdwin.hideCursor();
359 static if (is(typeof(&sdampwin
.closeQuery
))) {
360 sdampwin
.closeQuery
= delegate () { concmd("quit"); glconPostDoConCommands(); };
363 sdampwin
.windowResized
= delegate (int wdt
, int hgt
) {
364 sdampwin
.setMinSize(wdt
, hgt
);
365 sdampwin
.setMaxSize(wdt
, hgt
);
368 sdampwin
.redrawOpenGlScene
= delegate () {
370 //conwriteln("PAINT: sdampwin");
373 sdampwin
.visibleForTheFirstTime
= delegate () {
374 vglRegisterWindow
!"main"(sdampwin
);
379 sdampwin.handlePulse = delegate () {
383 sdampwin
.handleKeyEvent
= delegate (KeyEvent event
) {
384 if (event
.pressed
&& event
== "Escape") { concmd("quit"); return; }
385 //if (event.pressed && event == "Plus") { concmd("v_scale 2"); return; }
386 //if (event.pressed && event == "Minus") { concmd("v_scale 1"); return; }
387 //conwriteln(event.toStr);
388 ampMain
.onKey(event
);
389 glconPostScreenRebuild();
390 //sdwin.redrawOpenGlSceneNow();
393 sdampwin
.handleMouseEvent
= delegate (MouseEvent event
) {
395 if (event
.type
== MouseEventType
.motion
/*&& event.modifierState&ModifierState.leftButtonDown*/) {
399 int nx
= event
.x
*vbufEffScale
;
400 int ny
= event
.y
*vbufEffScale
;
401 XTranslateCoordinates(sdampwin
.display
, sdampwin
.window
, RootWindow(sdampwin
.display
, DefaultScreen(sdampwin
.display
)), nx
, ny
, &nx
, &ny
, &dummyw
);
402 int dx
= nx
-mainDrawPrevX
;
403 int dy
= ny
-mainDrawPrevY
;
407 XWindowAttributes xwa
;
408 XGetWindowAttributes(sdampwin
.display
, sdampwin
.window
, &xwa
);
409 XTranslateCoordinates(sdampwin
.display
, sdampwin
.window
, RootWindow(sdampwin
.display
, DefaultScreen(sdampwin
.display
)), xwa
.x
, xwa
.y
, &xwa
.x
, &xwa
.y
, &dummyw
);
410 sdampwin
.move(xwa
.x
+dx
, xwa
.y
+dy
);
412 if (sdplwin
!is null && !sdplwin
.closed
) {
413 XGetWindowAttributes(sdplwin
.display
, sdplwin
.window
, &xwa
);
414 XTranslateCoordinates(sdplwin
.display
, sdplwin
.window
, RootWindow(sdplwin
.display
, DefaultScreen(sdplwin
.display
)), xwa
.x
, xwa
.y
, &xwa
.x
, &xwa
.y
, &dummyw
);
415 sdplwin
.move(xwa
.x
+dx
, xwa
.y
+dy
);
420 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) mainDrag
= false;
423 if (!ampMain
.onMouse(event
)) {
425 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
426 if (ampMain.widgetAt(event.x, event.y) is null) {
430 mainDrawPrevX = event.x*vbufEffScale;
431 mainDrawPrevY = event.y*vbufEffScale;
432 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), mainDrawPrevX, mainDrawPrevY, &mainDrawPrevX, &mainDrawPrevY, &dummyw);
437 glconPostScreenRebuild();
438 //sdwin.redrawOpenGlSceneNow();
442 sdampwin.handleCharEvent = delegate (dchar ch) {
446 sdampwin
.onFocusChange
= delegate (bool focused
) { ampMain
.focusChanged(focused
); };
448 if (!sdampwin
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postEvent(evScrollTitle
);
452 // ////////////////////////////////////////////////////////////////////////// //
453 void createPListWindow () {
454 sdpyWindowClass
= "AMPER_PLAYLIST";
456 sdplwin
= new SimpleWindow(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
, "Amper Playlist", OpenGlOptions
.yes
, Resizability
.allowResizing
, WindowTypes
.undecorated
);
457 sdplwin
.setMinSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
458 sdplwin
.setMaxSize(4096, 4096);
459 //sdplwin.setResizeGranularity(5, 4);
460 sdplwin
.setResizeGranularity(25, 29);
462 static if (is(typeof(&sdplwin
.closeQuery
))) {
463 sdplwin
.closeQuery
= delegate () { /*concmd("quit"); glconPostDoConCommands();*/ };
466 sdplwin
.redrawOpenGlScene
= delegate () {
468 //conwriteln("PAINT: sdplwin");
469 //conwriteln(ampPList.curitem);
472 sdplwin
.visibleForTheFirstTime
= delegate () {
473 vglRegisterWindow
!"normal"(sdplwin
);
474 if (sdampwin
!is null && !sdampwin
.closed
) {
475 switchToWindow(sdampwin
);
479 XWindowAttributes xwa;
480 XGetWindowAttributes(sdampwin.display, sdampwin.window, &xwa);
481 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), xwa.x, xwa.y, &xwa.x, &xwa.y, &dummyw);
482 sdplwin.move(xwa.x, xwa.y+sdampwin.height);
488 sdplwin
.handleKeyEvent
= delegate (KeyEvent event
) {
489 ampPList
.onKey(event
);
490 glconPostScreenRebuild();
491 //sdplwin.redrawOpenGlSceneNow();
494 sdplwin
.handleMouseEvent
= delegate (MouseEvent event
) {
495 //conwriteln("PLMOUSE BEFORE: ", ampPList.curitem, " : ", event.type);
496 ampPList
.onMouse(event
);
497 //conwriteln("PLMOUSE AFTER: ", ampPList.curitem, " : ", event.type);
498 glconPostScreenRebuild();
499 //sdplwin.redrawOpenGlSceneNow();
502 sdplwin
.windowResized
= delegate (int wdt
, int hgt
) {
503 ampPList
.imgrc
= GxRect(0, 0, wdt
/vbufEffScale
, hgt
/vbufEffScale
);
504 //vglResizeBuffer(wdt/vbufEffScale, hgt/vbufEffScale, vbufEffScale);
505 //sdplwin.redrawOpenGlSceneNow();
508 sdplwin
.onFocusChange
= delegate (bool focused
) { ampPList
.focusChanged(focused
); };
512 // ////////////////////////////////////////////////////////////////////////// //
514 void fakeScanDir () {
515 ampPList.appendListItem(PListItem("Sonata Arctica \u2014 Replica", "/mnt/muzax/wtf", 142));
516 ampPList.appendListItem(PListItem("Zonata \u2014 Geronimo", "/mnt/muzax/wtf", 242));
518 foreach (immutable int idx; 3..42) {
519 import std.format : format;
520 ampPList.appendListItem(PListItem("song #%d".format(idx), "/mnt/muzax/wtf", 100+idx));
526 // ////////////////////////////////////////////////////////////////////////// //
527 void scanDir (ConString path
, bool append
) {
528 void appendFile (const(char)[] fname
) {
530 auto sio
= AudioStream
.open(VFile(fname
));
532 ampPList
.appendListItem(PListItem(sio
.artist
~" \u2014 "~sio
.title
, fname
.idup
, cast(int)(sio
.timetotal
/1000)));
533 //conwriteln("+++ ", de.name, ": ", sio.artist, " -- ", sio.title, ": ", cast(int)(sio.timetotal/1000));
536 //conwriteln("+++ ", de.name, ": FUUUUUUU");
538 } catch (Exception e
) {}
544 if (!append
) ampPList
.clear();
545 if (path
.exists
&& path
.isFile
) { appendFile(path
); return; }
546 foreach (DirEntry
de; dirEntries(path
.idup
, SpanMode
.shallow
)) {
547 if (!de.isFile
) continue;
550 } catch (Exception e
) {
551 conwriteln("ERROR scanning: ", e
.msg
);
553 //if (modeShuffle) ampPList.state.curitem = ampPList.findShuffleFirst();
554 //conwriteln(ampPList.findShuffleFirst(), " : ", ampPList.state.shuffleidx, " : ", ampPList.state.curplayingitem);
558 // ////////////////////////////////////////////////////////////////////////// //
559 void main (string
[] args
) {
562 glconShowKey
= "M-Grave";
564 conRegVar
!vbNewScale(1, 8, "v_scale", "window scale");
566 conRegVar
!skinfile("skin_file", "load skin from the given file",
567 delegate (ConVarBase self
, string oldval
, string newval
) {
568 if (!amperStarted
) { skinfile
= newval
; return; }
572 glconPostScreenRebuild();
573 } catch (Exception e
) {
574 conwriteln("ERROR loading skin: ", e
.msg
);
580 scope(exit
) aplayShutdown();
582 conProcessQueue(); // load config
583 conProcessArgs
!true(args
);
585 loadSkin
!true(skinfile
);
590 dgGreatePListWindow
= delegate () { createPListWindow(); };
591 if (!plVisible
) sdplwin
.hide();
595 GlobalHotkeyManager
.register("M-H-A", delegate () { hideShowWindows(); });
596 } catch (Exception e
) {
597 conwriteln("ERROR registering hotkey!");
600 conRegFunc
!((ConString path
, bool append
=false) {
602 scanDir(path
, append
);
603 } catch (Exception e
) {
604 conwriteln("scanning error: ", e
.msg
);
606 glconPostScreenRebuild();
607 })("scan_dir", "scan the given directory; 2nd ard is \"append\" bool flag");
611 glconPostScreenRebuild();
612 })("pl_clear", "clear playlist");
614 conRegFunc
!(() { hideShowWindows(); })("win_toggle", "show/hide Amper windows");
616 conRegFunc
!((int plidx
, bool forcestart
=false) {
617 if (!sdampwin
.closed
) {
618 ampPList
.playSongByIndex(plidx
, forcestart
);
619 glconPostScreenRebuild();
621 })("song_play_by_index", "play song from playlist by index");
623 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playPrevSong(); })("song_prev", "play previous song");
624 conRegFunc
!((bool forceplay
=false){ if (!sdampwin
.closed
) ampPList
.playNextSong(forceplay
); })("song_next", "play next song");
625 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playCurrentSong(); })("song_play", "play current song");
626 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.stopSong(); })("song_stop", "stop current song");
627 conRegFunc
!((){ aplayTogglePause(); })("song_pause_toggle", "pause/unpause current song");
628 conRegFunc
!((bool pause
){ aplayPause(pause
); })("song_pause", "pause/unpause current song, with bool arg");
630 conRegFunc
!((uint msecs
){ aplaySeekMS(msecs
*1000); })("song_seek_abs", "absolute seek, in seconds");
631 conRegFunc
!((int msecs
){ aplaySeekMS(aplayCurTimeMS
+msecs
*1000); })("song_seek_rel", "relative seek, in seconds");
633 foreach (string path
; args
[1..$]) concmdf
!"scan_dir \"%s\" tan"(path
);
636 scope(exit
) stopRPCServer();
639 glconCtlWindow
.eventLoop(0);
642 conProcessQueue(int.max
/4);