vanilization
[amper.git] / amper.d
blobb788809a58f8c11630ca99df9f77295bf124a508
1 module amper;
3 import std.concurrency;
5 import arsd.color;
6 import arsd.image;
7 import arsd.simpledisplay;
9 import iv.cmdcon;
10 import iv.cmdcongl;
11 import iv.sdpyutil;
12 import iv.strex;
13 import iv.vfs;
15 import aplayer;
17 import egfx;
19 import amperrpc;
20 import amperskin;
21 import amperopts;
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);
29 Atom aType;
30 int format;
31 uint itemCount;
32 uint bytesAfter;
33 int* propRes;
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]);
38 XFree(propRes);
39 return rc;
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))) {
58 setHint("");
59 } else {
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) {
70 sdhint.show();
71 } else {
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
76 hintX = x;
77 hintY = y;
79 int textWidth = gxTextWidthUtf(hinttext)+4;
80 sdhint.resize(textWidth, sdhint.height);
82 sdhint.move(x, y);
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();
101 int nx = hintX;
102 int ny = hintY;
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;
107 hintX = nx;
108 hintY = ny;
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");
118 int textWidth;
120 auto VBufWidthSave = VBufWidth;
121 auto VBufHeightSave = VBufHeight;
122 auto vglTexBufSave = vglTexBuf;
123 VBufWidth = hintbackbuf.width;
124 VBufHeight = hintbackbuf.height;
125 vglTexBuf = hintvbuf;
126 scope(exit) {
127 VBufWidth = VBufWidthSave;
128 VBufHeight = VBufHeightSave;
129 vglTexBuf = vglTexBufSave;
131 gxClipReset();
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));
136 copyVBufToImage();
139 auto painter = sdhint.draw();
140 painter.drawImage(Point(0, 0), hintbackbuf);
142 flushGui();
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);
156 if (plVisible) {
157 if (sdplwin is null || sdplwin.closed) {
158 if (dgGreatePListWindow !is null) dgGreatePListWindow();
159 } else {
160 vglShowWindow(sdplwin);
162 switchToWindow(sdampwin);
163 flushGui();
165 } else {
166 vglHideWindow(sdplwin);
167 vglHideWindow(sdampwin);
172 void prepareTrayIcon () {
173 icon = readPng("skins/notifyicon.png");
174 trayimage = Image.fromMemoryImage(icon);
175 trayicon = new NotificationAreaIcon("Amper", trayimage, (int x, int y, MouseButton button, ModifierState mods) {
176 //conwritefln!"x=%d; y=%d; button=%u; mods=0x%04x"(x, y, button, mods);
177 if (button == MouseButton.middle) {
178 //trayicon.close();
179 //trayicon = null;
180 concmd("quit");
181 return;
183 if (button == MouseButton.left) {
184 concmd("win_toggle");
185 return;
188 trayicon.onEnter = delegate (int x, int y, ModifierState mods) {
189 //conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
191 conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
192 int wx, wy, wdt, hgt;
193 trayicon.getWindowRect(wx, wy, wdt, hgt);
194 conwriteln("window rect: wx=", wx, "; wy=", wy, "; wdt=", wdt, "; hgt=", hgt);
196 if (sdhint is null || sdhint.hidden) {
197 createHintWindow(x, y+2);
198 if (hintHideTimer !is null) hintHideTimer.destroy();
199 hintHideTimer = new Timer(3000, delegate () {
200 if (hintHideTimer !is null) {
201 hintHideTimer.destroy();
202 hintHideTimer = null;
203 if (sdhint !is null && !sdhint.closed) sdhint.hide();
208 trayicon.onLeave = delegate () {
209 //conwriteln("icon leave");
210 //if (sdhint !is null && !sdhint.closed) sdhint.hide();
215 // ////////////////////////////////////////////////////////////////////////// //
216 __gshared ubyte vbNewScale = 1;
219 // ////////////////////////////////////////////////////////////////////////// //
220 class ScrollTitleEvent {}
221 __gshared ScrollTitleEvent evScrollTitle;
223 shared static this () {
224 evScrollTitle = new ScrollTitleEvent();
228 // ////////////////////////////////////////////////////////////////////////// //
229 __gshared bool mainDrag = false;
230 __gshared int mainDrawPrevX, mainDrawPrevY;
233 // ////////////////////////////////////////////////////////////////////////// //
234 void closeAllIfMainIsClosed () {
235 bool doQuit =
236 (glconCtlWindow is null || glconCtlWindow.closed) ||
237 (sdampwin is null || sdampwin.closed);
238 if (doQuit) {
239 if (sdhint !is null && !sdhint.closed) sdhint.close();
240 if (sdplwin !is null && !sdplwin.closed) sdplwin.close();
241 if (sdampwin !is null && !sdampwin.closed) sdampwin.close();
242 if (glconCtlWindow !is null && !glconCtlWindow.closed) glconCtlWindow.close();
247 // ////////////////////////////////////////////////////////////////////////// //
248 // create hidden control window
249 void createCtlWindow () {
250 sdpyWindowClass = "AMPER_PLAYER_CTL";
251 glconCtlWindow = new SimpleWindow(1, 1, "AmperCtl", OpenGlOptions.no, Resizability.fixedSize, WindowTypes.hidden);
253 glconCtlWindow.onClosing = delegate () {
254 //conwriteln("closing ctl window...");
255 if (sdplwin !is null && !sdplwin.closed) sdplwin.close();
256 if (sdampwin !is null && !sdampwin.closed) sdampwin.close();
259 glconCtlWindow.onDestroyed = delegate () {
260 //conwriteln("ctl window destroyed");
261 closeAllIfMainIsClosed();
264 glconCtlWindow.addEventListener((QuitEvent evt) {
265 scope(exit) if (!conQueueEmpty()) glconPostDoConCommands();
266 scope(exit) closeAllIfMainIsClosed();
267 if (glconCtlWindow.closed) return;
268 if (isQuitRequested) { glconCtlWindow.close(); return; }
269 concmd("quit");
272 void rebuildRepaint () {
273 scope(exit) if (!conQueueEmpty()) glconPostDoConCommands();
274 scope(exit) closeAllIfMainIsClosed();
275 if (glconCtlWindow.closed) return;
276 if (isQuitRequested) { glconCtlWindow.close(); return; }
277 //conwriteln("rebuilding screen");
279 //if (aplayIsPlaying) ampMain.curtime = aplayCurTime;
281 if (sdampwin !is null && !sdampwin.closed && !sdampwin.hidden) sdampwin.redrawOpenGlSceneNow();
282 if (sdplwin !is null && !sdplwin.closed && !sdplwin.hidden) sdplwin.redrawOpenGlSceneNow();
284 //glFlush();
285 flushGui();
288 glconCtlWindow.addEventListener((GLConScreenRebuildEvent evt) { rebuildRepaint(); });
289 glconCtlWindow.addEventListener((GLConScreenRepaintEvent evt) { rebuildRepaint(); });
291 glconCtlWindow.addEventListener((GLConDoConsoleCommandsEvent evt) {
292 scope(exit) if (!conQueueEmpty()) glconPostDoConCommands();
293 scope(exit) closeAllIfMainIsClosed();
294 bool sendAnother = false;
295 bool prevVisible = isConsoleVisible;
297 consoleLock();
298 scope(exit) consoleUnlock();
299 conProcessQueue();
300 sendAnother = !conQueueEmpty();
302 if (glconCtlWindow.closed) return;
303 if (isQuitRequested) { glconCtlWindow.close(); return; }
304 if (sendAnother) glconPostDoConCommands();
306 if (prevVisible || isConsoleVisible) glconPostScreenRepaintDelayed();
307 if (vbNewScale != vbufEffScale) glconPostScreenRepaint();
309 if (vbNewScale != vglWindowScale(glconCtlWindow)) {
310 vbNewScale = vglScaleWindow(glconCtlWindow, vbNewScale);
311 vglScaleWindow(sdplwin, vbNewScale);
312 } else {
313 glconPostScreenRebuild();
317 glconCtlWindow.addEventListener((ScrollTitleEvent evt) {
318 scope(exit) if (!conQueueEmpty()) glconPostDoConCommands();
319 scope(exit) closeAllIfMainIsClosed();
320 if (glconCtlWindow.closed) return;
321 if (isQuitRequested) { glconCtlWindow.close(); return; }
322 //conwriteln("scrolling title");
323 ampMain.scrollTitle();
324 if (!glconCtlWindow.eventQueued!ScrollTitleEvent) glconCtlWindow.postTimeout(evScrollTitle, 100);
325 glconPostScreenRebuild();
328 glconCtlWindow.addEventListener((EventFileLoaded evt) {
329 if (!evt.success) {
330 conwriteln("ERROR loading '", evt.filename, "'");
331 setHint("not playing");
332 //glconCtlWindow.close();
333 concmd("song_next");
334 return;
336 setHint(evt.artist~" \u2014 "~evt.title);
337 //conwriteln("playing '", evt.filename, "': ", evt.artist, " -- ", evt.title);
340 glconCtlWindow.addEventListener((EventFileComplete evt) {
341 //glconCtlWindow.close();
342 setHint("not playing");
343 concmd("song_next tan");
348 // ////////////////////////////////////////////////////////////////////////// //
349 void createAmpWindow () {
350 sdpyWindowClass = "AMPER_PLAYER";
352 sdampwin = new SimpleWindow(skinImgMain.width*vbufEffScale, skinImgMain.height*vbufEffScale, "Amper", OpenGlOptions.yes, Resizability.fixedSize, WindowTypes.undecorated);
353 sdampwin.setMinSize(skinImgMain.width*vbufEffScale, skinImgMain.height*vbufEffScale);
354 sdampwin.setMaxSize(skinImgMain.width*vbufEffScale, skinImgMain.height*vbufEffScale);
355 //sdwin.hideCursor();
357 static if (is(typeof(&sdampwin.closeQuery))) {
358 sdampwin.closeQuery = delegate () { concmd("quit"); glconPostDoConCommands(); };
361 sdampwin.windowResized = delegate (int wdt, int hgt) {
362 sdampwin.setMinSize(wdt, hgt);
363 sdampwin.setMaxSize(wdt, hgt);
366 sdampwin.redrawOpenGlScene = delegate () {
367 ampMain.onPaint();
368 //conwriteln("PAINT: sdampwin");
371 sdampwin.visibleForTheFirstTime = delegate () {
372 vglRegisterWindow!"main"(sdampwin);
373 prepareTrayIcon();
377 sdampwin.handlePulse = delegate () {
381 sdampwin.handleKeyEvent = delegate (KeyEvent event) {
382 if (event.pressed && event == "Escape") { concmd("quit"); return; }
383 //if (event.pressed && event == "Plus") { concmd("v_scale 2"); return; }
384 //if (event.pressed && event == "Minus") { concmd("v_scale 1"); return; }
385 //conwriteln(event.toStr);
386 ampMain.onKey(event);
387 glconPostScreenRebuild();
388 //sdwin.redrawOpenGlSceneNow();
391 sdampwin.handleMouseEvent = delegate (MouseEvent event) {
392 if (mainDrag) {
393 if (event.type == MouseEventType.motion /*&& event.modifierState&ModifierState.leftButtonDown*/) {
394 flushGui();
395 Window dummyw;
397 int nx = event.x*vbufEffScale;
398 int ny = event.y*vbufEffScale;
399 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), nx, ny, &nx, &ny, &dummyw);
400 int dx = nx-mainDrawPrevX;
401 int dy = ny-mainDrawPrevY;
402 mainDrawPrevX = nx;
403 mainDrawPrevY = ny;
405 XWindowAttributes xwa;
406 XGetWindowAttributes(sdampwin.display, sdampwin.window, &xwa);
407 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), xwa.x, xwa.y, &xwa.x, &xwa.y, &dummyw);
408 sdampwin.move(xwa.x+dx, xwa.y+dy);
410 if (sdplwin !is null && !sdplwin.closed) {
411 XGetWindowAttributes(sdplwin.display, sdplwin.window, &xwa);
412 XTranslateCoordinates(sdplwin.display, sdplwin.window, RootWindow(sdplwin.display, DefaultScreen(sdplwin.display)), xwa.x, xwa.y, &xwa.x, &xwa.y, &dummyw);
413 sdplwin.move(xwa.x+dx, xwa.y+dy);
416 flushGui();
418 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) mainDrag = false;
419 return;
421 if (!ampMain.onMouse(event)) {
423 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
424 if (ampMain.widgetAt(event.x, event.y) is null) {
425 flushGui();
426 Window dummyw;
427 mainDrag = true;
428 mainDrawPrevX = event.x*vbufEffScale;
429 mainDrawPrevY = event.y*vbufEffScale;
430 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), mainDrawPrevX, mainDrawPrevY, &mainDrawPrevX, &mainDrawPrevY, &dummyw);
435 glconPostScreenRebuild();
436 //sdwin.redrawOpenGlSceneNow();
440 sdampwin.handleCharEvent = delegate (dchar ch) {
444 sdampwin.onFocusChange = delegate (bool focused) { ampMain.focusChanged(focused); };
446 if (!sdampwin.eventQueued!ScrollTitleEvent) glconCtlWindow.postEvent(evScrollTitle);
450 // ////////////////////////////////////////////////////////////////////////// //
451 void createPListWindow () {
452 sdpyWindowClass = "AMPER_PLAYLIST";
454 sdplwin = new SimpleWindow(skinImgMain.width*vbufEffScale, skinImgMain.height*vbufEffScale, "Amper Playlist", OpenGlOptions.yes, Resizability.allowResizing, WindowTypes.undecorated);
455 sdplwin.setMinSize(skinImgMain.width*vbufEffScale, skinImgMain.height*vbufEffScale);
456 sdplwin.setMaxSize(4096, 4096);
457 //sdplwin.setResizeGranularity(5, 4);
458 sdplwin.setResizeGranularity(25, 29);
460 static if (is(typeof(&sdplwin.closeQuery))) {
461 sdplwin.closeQuery = delegate () { /*concmd("quit"); glconPostDoConCommands();*/ };
464 sdplwin.redrawOpenGlScene = delegate () {
465 ampPList.onPaint();
466 //conwriteln("PAINT: sdplwin");
467 //conwriteln(ampPList.curitem);
470 sdplwin.visibleForTheFirstTime = delegate () {
471 vglRegisterWindow!"normal"(sdplwin);
472 if (sdampwin !is null && !sdampwin.closed) {
473 switchToWindow(sdampwin);
474 flushGui();
476 Window dummyw;
477 XWindowAttributes xwa;
478 XGetWindowAttributes(sdampwin.display, sdampwin.window, &xwa);
479 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), xwa.x, xwa.y, &xwa.x, &xwa.y, &dummyw);
480 sdplwin.move(xwa.x, xwa.y+sdampwin.height);
481 flushGui();
486 sdplwin.handleKeyEvent = delegate (KeyEvent event) {
487 ampPList.onKey(event);
488 glconPostScreenRebuild();
489 //sdplwin.redrawOpenGlSceneNow();
492 sdplwin.handleMouseEvent = delegate (MouseEvent event) {
493 //conwriteln("PLMOUSE BEFORE: ", ampPList.curitem, " : ", event.type);
494 ampPList.onMouse(event);
495 //conwriteln("PLMOUSE AFTER: ", ampPList.curitem, " : ", event.type);
496 glconPostScreenRebuild();
497 //sdplwin.redrawOpenGlSceneNow();
500 sdplwin.windowResized = delegate (int wdt, int hgt) {
501 ampPList.imgrc = GxRect(0, 0, wdt/vbufEffScale, hgt/vbufEffScale);
502 //vglResizeBuffer(wdt/vbufEffScale, hgt/vbufEffScale, vbufEffScale);
503 //sdplwin.redrawOpenGlSceneNow();
506 sdplwin.onFocusChange = delegate (bool focused) { ampPList.focusChanged(focused); };
510 // ////////////////////////////////////////////////////////////////////////// //
512 void fakeScanDir () {
513 ampPList.appendListItem(PListItem("Sonata Arctica \u2014 Replica", "/mnt/muzax/wtf", 142));
514 ampPList.appendListItem(PListItem("Zonata \u2014 Geronimo", "/mnt/muzax/wtf", 242));
516 foreach (immutable int idx; 3..42) {
517 import std.format : format;
518 ampPList.appendListItem(PListItem("song #%d".format(idx), "/mnt/muzax/wtf", 100+idx));
524 // ////////////////////////////////////////////////////////////////////////// //
525 void scanDir (ConString path, bool append) {
526 void appendFile (const(char)[] fname) {
527 try {
528 auto sio = AudioStream.open(VFile(fname));
529 if (sio.valid) {
530 ampPList.appendListItem(PListItem(sio.artist~" \u2014 "~sio.title, fname.idup, cast(int)(sio.timetotal/1000)));
531 //conwriteln("+++ ", de.name, ": ", sio.artist, " -- ", sio.title, ": ", cast(int)(sio.timetotal/1000));
532 sio.close();
533 } else {
534 //conwriteln("+++ ", de.name, ": FUUUUUUU");
536 } catch (Exception e) {}
539 // scan directory
540 import std.file;
541 try {
542 if (!append) ampPList.clear();
543 if (path.exists && path.isFile) { appendFile(path); return; }
544 foreach (DirEntry de; dirEntries(path.idup, SpanMode.shallow)) {
545 if (!de.isFile) continue;
546 appendFile(de.name);
548 } catch (Exception e) {
549 conwriteln("ERROR scanning: ", e.msg);
551 //if (modeShuffle) ampPList.state.curitem = ampPList.findShuffleFirst();
552 //conwriteln(ampPList.findShuffleFirst(), " : ", ampPList.state.shuffleidx, " : ", ampPList.state.curplayingitem);
556 // ////////////////////////////////////////////////////////////////////////// //
557 void main (string[] args) {
558 //vbNewScale = 2;
559 //vbufEffScale = 2;
560 glconShowKey = "M-Grave";
562 conRegVar!vbNewScale(1, 8, "v_scale", "window scale");
564 aplayStart();
565 scope(exit) aplayShutdown();
567 conProcessQueue(); // load config
568 conProcessArgs!true(args);
570 loadSkin();
572 createCtlWindow();
573 createAmpWindow();
574 createPListWindow();
575 dgGreatePListWindow = delegate () { createPListWindow(); };
576 if (!plVisible) sdplwin.hide();
577 flushGui();
579 try {
580 GlobalHotkeyManager.register("M-H-A", delegate () { hideShowWindows(); });
581 } catch (Exception e) {
582 conwriteln("ERROR registering hotkey!");
585 conRegFunc!((ConString path, bool append=false) {
586 try {
587 scanDir(path, append);
588 } catch (Exception e) {
589 conwriteln("scanning error: ", e.msg);
591 glconPostScreenRebuild();
592 })("scan_dir", "scan the given directory; 2nd ard is \"append\" bool flag");
594 conRegFunc!(() {
595 ampPList.clear();
596 glconPostScreenRebuild();
597 })("pl_clear", "clear playlist");
599 conRegFunc!(() { hideShowWindows(); })("win_toggle", "show/hide Amper windows");
601 conRegFunc!((int plidx, bool forcestart=false) {
602 if (!sdampwin.closed) {
603 ampPList.playSongByIndex(plidx, forcestart);
604 glconPostScreenRebuild();
606 })("song_play_by_index", "play song from playlist by index");
608 conRegFunc!((){ if (!sdampwin.closed) ampPList.playPrevSong(); })("song_prev", "play previous song");
609 conRegFunc!((bool forceplay=false){ if (!sdampwin.closed) ampPList.playNextSong(forceplay); })("song_next", "play next song");
610 conRegFunc!((){ if (!sdampwin.closed) ampPList.playCurrentSong(); })("song_play", "play current song");
611 conRegFunc!((){ if (!sdampwin.closed) ampPList.stopSong(); })("song_stop", "stop current song");
612 conRegFunc!((){ aplayTogglePause(); })("song_pause_toggle", "pause/unpause current song");
613 conRegFunc!((bool pause){ aplayPause(pause); })("song_pause", "pause/unpause current song, with bool arg");
615 conRegFunc!((uint msecs){ aplaySeekMS(msecs*1000); })("song_seek_abs", "absolute seek, in seconds");
616 conRegFunc!((int msecs){ aplaySeekMS(aplayCurTimeMS+msecs*1000); })("song_seek_rel", "relative seek, in seconds");
618 foreach (string path; args[1..$]) concmdf!"scan_dir \"%s\" tan"(path);
620 startRPCServer();
621 scope(exit) stopRPCServer();
623 glconCtlWindow.eventLoop(0);
625 flushGui();
626 conProcessQueue(int.max/4);