playlist skin fix; dynamic skin reloading
[amper.git] / amper.d
blob8d398880093bc7b2cc5a0985f557a0ab3c1380c0
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 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) {
180 //trayicon.close();
181 //trayicon = null;
182 concmd("quit");
183 return;
185 if (button == MouseButton.left) {
186 concmd("win_toggle");
187 return;
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 () {
237 bool doQuit =
238 (glconCtlWindow is null || glconCtlWindow.closed) ||
239 (sdampwin is null || sdampwin.closed);
240 if (doQuit) {
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; }
271 concmd("quit");
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();
286 //glFlush();
287 flushGui();
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;
299 consoleLock();
300 scope(exit) consoleUnlock();
301 conProcessQueue();
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);
314 } else {
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) {
331 if (!evt.success) {
332 conwriteln("ERROR loading '", evt.filename, "'");
333 setHint("not playing");
334 //glconCtlWindow.close();
335 concmd("song_next");
336 return;
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 () {
369 ampMain.onPaint();
370 //conwriteln("PAINT: sdampwin");
373 sdampwin.visibleForTheFirstTime = delegate () {
374 vglRegisterWindow!"main"(sdampwin);
375 prepareTrayIcon();
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) {
394 if (mainDrag) {
395 if (event.type == MouseEventType.motion /*&& event.modifierState&ModifierState.leftButtonDown*/) {
396 flushGui();
397 Window dummyw;
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;
404 mainDrawPrevX = nx;
405 mainDrawPrevY = ny;
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);
418 flushGui();
420 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) mainDrag = false;
421 return;
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) {
427 flushGui();
428 Window dummyw;
429 mainDrag = true;
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 () {
467 ampPList.onPaint();
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);
476 flushGui();
478 Window dummyw;
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);
483 flushGui();
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) {
529 try {
530 auto sio = AudioStream.open(VFile(fname));
531 if (sio.valid) {
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));
534 sio.close();
535 } else {
536 //conwriteln("+++ ", de.name, ": FUUUUUUU");
538 } catch (Exception e) {}
541 // scan directory
542 import std.file;
543 try {
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;
548 appendFile(de.name);
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) {
560 //vbNewScale = 2;
561 //vbufEffScale = 2;
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; }
569 try {
570 loadSkin(newval);
571 skinfile = newval;
572 glconPostScreenRebuild();
573 } catch (Exception e) {
574 conwriteln("ERROR loading skin: ", e.msg);
579 aplayStart();
580 scope(exit) aplayShutdown();
582 conProcessQueue(); // load config
583 conProcessArgs!true(args);
585 loadSkin!true(skinfile);
587 createCtlWindow();
588 createAmpWindow();
589 createPListWindow();
590 dgGreatePListWindow = delegate () { createPListWindow(); };
591 if (!plVisible) sdplwin.hide();
592 flushGui();
594 try {
595 GlobalHotkeyManager.register("M-H-A", delegate () { hideShowWindows(); });
596 } catch (Exception e) {
597 conwriteln("ERROR registering hotkey!");
600 conRegFunc!((ConString path, bool append=false) {
601 try {
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");
609 conRegFunc!(() {
610 ampPList.clear();
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);
635 startRPCServer();
636 scope(exit) stopRPCServer();
638 amperStarted = true;
639 glconCtlWindow.eventLoop(0);
641 flushGui();
642 conProcessQueue(int.max/4);