1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 import std
.concurrency
;
23 import arsd
.simpledisplay
;
40 // ////////////////////////////////////////////////////////////////////////// //
41 public GxRect
getWorkArea () {
43 getWorkAreaRect(rc
.x0
, rc
.y0
, rc
.width
, rc
.height
);
48 // ////////////////////////////////////////////////////////////////////////// //
49 class GlobalHotkeyEx
: GlobalHotkey
{
51 override void doHandle () { concmd(cmd
); }
53 this (ConString kname
, ConString acmd
) {
59 __gshared GlobalHotkeyEx
[] ghbindings
; // key is key, value is command
62 void removeAllBindings () {
64 foreach (GlobalHotkeyEx b
; ghbindings
) {
65 try { GlobalHotkeyManager
.unregister(b
.key
.toStrBuf(knbuf
[])); } catch (Exception e
) {}
67 ghbindings
.length
= 0;
68 ghbindings
.assumeSafeAppend
;
72 void addBinding (ConString key
, ConString cmd
) {
75 if (cmd
.length
== 0) {
76 GlobalHotkeyManager
.unregister(key
);
77 foreach (immutable idx
, GlobalHotkeyEx bind
; ghbindings
) {
78 if (bind
.key
== key
) {
79 foreach (immutable cc
; idx
+1..ghbindings
.length
) ghbindings
[cc
-1] = ghbindings
[cc
];
80 ghbindings
[$-1] = null;
81 ghbindings
.length
-= 1;
82 ghbindings
.assumeSafeAppend
;
90 auto bind
= new GlobalHotkeyEx(key
, cmd
);
91 GlobalHotkeyManager
.unregister(bind
.key
.toStrBuf(knbuf
[]));
92 GlobalHotkeyManager
.register(bind
);
93 foreach (immutable idx
, GlobalHotkeyEx b
; ghbindings
) {
95 ghbindings
[idx
] = bind
;
99 auto optr
= ghbindings
.ptr
;
101 if (optr
!is ghbindings
.ptr
) {
102 import core
.memory
: GC
;
103 if (ghbindings
.ptr
is GC
.addrOf(ghbindings
.ptr
)) {
104 GC
.setAttr(ghbindings
.ptr
, GC
.BlkAttr
.NO_INTERIOR
);
107 } catch (Exception e
) {
108 conwriteln("ERROR registering hotkey: '", key
, "'");
113 // ////////////////////////////////////////////////////////////////////////// //
114 __gshared SimpleWindow sdhint
;
115 __gshared string hinttext
= "not playing";
116 __gshared Image hintbackbuf
;
117 __gshared
uint* hintvbuf
;
118 __gshared Timer hintHideTimer
;
119 __gshared
int hintX
, hintY
;
122 // ////////////////////////////////////////////////////////////////////////// //
123 void setHint(T
:const(char)[]) (T
str) {
124 static if (is(T
== typeof(null))) {
127 if (hinttext
== str) return;
128 static if (is(T
== string
)) hinttext
= str; else hinttext
= str.idup
;
129 if (sdhint
is null || sdhint
.closed
) return;
130 if (sdhint
.hidden
) return;
131 createHintWindow (hintX
, hintY
);
132 //repaintHintWindow(false);
137 // ////////////////////////////////////////////////////////////////////////// //
138 GxPoint
hintWindowTextOffset () { return GxPoint(3, 2); }
140 GxSize
hintWindowSize () {
141 int textWidth
= gxTextWidthUtf(hinttext
)+6;
142 if (textWidth
< 8) textWidth
= 8;
143 return GxSize(textWidth
, gxTextHeightUtf
+4);
147 void createHintWindow (int x
, int y
) {
148 if (sdhint
!is null) {
153 sdpyWindowClass
= "AMPER_HINT_WINDOW";
154 auto wsz
= hintWindowSize();
156 auto wrc
= getWorkArea();
159 if (nx
+wsz
.width
> wrc
.x1
) nx
= wrc
.x1
-wsz
.width
+1;
160 if (nx
< wrc
.x0
) nx
= wrc
.x0
;
161 if (ny
+wsz
.height
> wrc
.y1
) ny
= wrc
.y1
-wsz
.height
+1;
162 if (ny
< wrc
.y0
) ny
= wrc
.y0
;
166 sdhint
= new SimpleWindow(wsz
.width
, wsz
.height
, "AmperHint", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.undecorated
);
167 sdhint
.setNetWMWindowType(GetAtom
!"_NET_WM_WINDOW_TYPE_DOCK"(sdhint
.display
)); // sorry for this hack
168 sdhint
.moveResize(hintX
, hintY
, wsz
.width
, wsz
.height
);
169 repaintHintWindow(true);
173 void repaintHintWindow (bool forced
) {
174 if (sdhint
is null || sdhint
.closed
) return;
175 if (!forced
&& sdhint
.hidden
) return;
177 auto wsz
= hintWindowSize();
179 if (hintbackbuf
is null || hintbackbuf
.width
!= wsz
.width || hintbackbuf
.height
!= wsz
.height
) {
180 import core
.stdc
.stdlib
: realloc
;
181 hintbackbuf
= new Image(wsz
.width
, wsz
.height
);
182 hintvbuf
= cast(uint*)realloc(hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*hintvbuf
[0].sizeof
);
183 if (hintvbuf
is null) assert(0, "out of memory");
186 if (hintbackbuf
!is null) {
187 auto VBufWidthSave
= VBufWidth
;
188 auto VBufHeightSave
= VBufHeight
;
189 auto vglTexBufSave
= vglTexBuf
;
190 VBufWidth
= hintbackbuf
.width
;
191 VBufHeight
= hintbackbuf
.height
;
192 vglTexBuf
= hintvbuf
;
194 VBufWidth
= VBufWidthSave
;
195 VBufHeight
= VBufHeightSave
;
196 vglTexBuf
= vglTexBufSave
;
199 gxClearScreen(gxRGB
!(255, 255, 0));
200 gxDrawRect(0, 0, VBufWidth
, VBufHeight
, gxRGB
!(0, 0, 0));
201 auto tofs
= hintWindowTextOffset
;
202 gxClipRect
.shrinkBy(tofs
.x
, tofs
.y
);
203 gxDrawTextUtf(gxClipRect
.x0
, gxClipRect
.y0
, hinttext
, gxRGB
!(0, 0, 0));
206 import core
.stdc
.string
: memcpy
;
207 memcpy(hintbackbuf
.getDataPointer
, hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*4);
210 auto painter
= sdhint
.draw();
211 painter
.drawImage(Point(0, 0), hintbackbuf
);
218 // ////////////////////////////////////////////////////////////////////////// //
219 __gshared NotificationAreaIcon trayicon
;
220 __gshared Image trayimage
;
221 __gshared MemoryImage icon
; // 0: normal
224 void hideShowWindows () {
225 if (sdampwin
is null || sdampwin
.closed
) return;
226 if (sdampwin
.hidden
) {
229 if (sdplwin
is null || sdplwin
.closed
) {
230 if (dgCreatePListWindow
!is null) dgCreatePListWindow();
236 if (sdeqwin
is null || sdeqwin
.closed
) {
237 if (dgCreateEqWindow
!is null) dgCreateEqWindow();
242 switchToWindow(sdampwin
);
245 if (sdeqwin
!is null && !sdeqwin
.closed
) sdeqwin
.hide();
246 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.hide();
252 void prepareTrayIcon () {
253 static immutable ubyte[] nticonpng
= cast(immutable(ubyte)[])import("skins/notifyicon.png");
254 //icon = readPng("skins/notifyicon.png");
255 icon
= imageFromPng(readPng(nticonpng
));
256 trayimage
= Image
.fromMemoryImage(icon
);
257 trayicon
= new NotificationAreaIcon("Amper", trayimage
, (int x
, int y
, MouseButton button
, ModifierState mods
) {
258 //conwritefln!"x=%d; y=%d; button=%u; mods=0x%04x"(x, y, button, mods);
259 if (button
== MouseButton
.middle
) {
265 if (button
== MouseButton
.left
) {
266 concmd("win_toggle");
270 trayicon
.onEnter
= delegate (int x
, int y
, ModifierState mods
) {
271 //conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
273 conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
274 int wx, wy, wdt, hgt;
275 trayicon.getWindowRect(wx, wy, wdt, hgt);
276 conwriteln("window rect: wx=", wx, "; wy=", wy, "; wdt=", wdt, "; hgt=", hgt);
278 if (sdhint
is null || sdhint
.hidden
) {
279 createHintWindow(x
+18, y
+2);
280 if (hintHideTimer
!is null) hintHideTimer
.destroy();
281 hintHideTimer
= new Timer(3000, delegate () {
282 if (hintHideTimer
!is null) {
283 hintHideTimer
.destroy();
284 hintHideTimer
= null;
285 if (sdhint
!is null && !sdhint
.closed
) sdhint
.hide();
290 trayicon
.onLeave
= delegate () {
291 //conwriteln("icon leave");
292 //if (sdhint !is null && !sdhint.closed) sdhint.hide();
297 // ////////////////////////////////////////////////////////////////////////// //
298 __gshared
ubyte vbNewScale
= 1;
301 // ////////////////////////////////////////////////////////////////////////// //
302 class ScrollTitleEvent
{}
303 __gshared ScrollTitleEvent evScrollTitle
;
305 shared static this () {
306 evScrollTitle
= new ScrollTitleEvent();
310 // ////////////////////////////////////////////////////////////////////////// //
311 __gshared
bool mainDrag
= false;
312 __gshared
int mainDrawPrevX
, mainDrawPrevY
;
315 // ////////////////////////////////////////////////////////////////////////// //
316 void closeAllIfMainIsClosed () {
318 (glconCtlWindow
is null || glconCtlWindow
.closed
) ||
319 (sdampwin
is null || sdampwin
.closed
);
321 if (sdhint
!is null && !sdhint
.closed
) sdhint
.close();
322 if (sdeqwin
!is null && !sdeqwin
.closed
) sdeqwin
.close();
323 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
324 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
325 if (glconCtlWindow
!is null && !glconCtlWindow
.closed
) glconCtlWindow
.close();
330 // ////////////////////////////////////////////////////////////////////////// //
331 // create hidden control window
332 void createCtlWindow () {
333 sdpyWindowClass
= "AMPER_PLAYER_CTL";
334 glconCtlWindow
= new SimpleWindow(1, 1, "AmperCtl", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.hidden
);
336 glconCtlWindow
.onClosing
= delegate () {
337 //conwriteln("closing ctl window...");
338 if (sdeqwin
!is null && !sdeqwin
.closed
) sdeqwin
.close();
339 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
340 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
343 glconCtlWindow
.onDestroyed
= delegate () {
344 //conwriteln("ctl window destroyed");
345 closeAllIfMainIsClosed();
348 glconCtlWindow
.addEventListener((QuitEvent evt
) {
349 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
350 scope(exit
) closeAllIfMainIsClosed();
351 if (glconCtlWindow
.closed
) return;
352 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
356 void rebuildRepaint () {
357 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
358 scope(exit
) closeAllIfMainIsClosed();
359 if (glconCtlWindow
.closed
) return;
360 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
361 //conwriteln("rebuilding screen");
363 //if (aplayIsPlaying) ampMain.curtime = aplayCurTime;
365 if (sdampwin
!is null && !sdampwin
.closed
&& !sdampwin
.hidden
) sdampwin
.redraw();
366 if (sdplwin
!is null && !sdplwin
.closed
&& !sdplwin
.hidden
) sdplwin
.redraw();
367 if (sdeqwin
!is null && !sdeqwin
.closed
&& !sdeqwin
.hidden
) sdeqwin
.redraw();
373 glconCtlWindow
.addEventListener((GLConScreenRebuildEvent evt
) { rebuildRepaint(); });
374 glconCtlWindow
.addEventListener((GLConScreenRepaintEvent evt
) { rebuildRepaint(); });
376 glconCtlWindow
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
377 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
378 scope(exit
) closeAllIfMainIsClosed();
379 bool sendAnother
= false;
380 bool prevVisible
= isConsoleVisible
;
383 scope(exit
) consoleUnlock();
385 sendAnother
= !conQueueEmpty();
387 if (glconCtlWindow
.closed
) return;
388 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
389 if (sendAnother
) glconPostDoConCommands();
391 if (prevVisible || isConsoleVisible) glconPostScreenRepaintDelayed();
392 if (vbNewScale != vbufEffScale) glconPostScreenRepaint();
395 if (vbNewScale != vglWindowScale(glconCtlWindow)) {
396 vbNewScale = vglScaleWindow(glconCtlWindow, vbNewScale);
397 vglScaleWindow(sdplwin, vbNewScale);
399 glconPostScreenRebuild();
402 glconPostScreenRebuild();
405 glconCtlWindow
.addEventListener((ScrollTitleEvent evt
) {
406 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
407 scope(exit
) closeAllIfMainIsClosed();
408 if (glconCtlWindow
.closed
) return;
409 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
410 //conwriteln("scrolling title");
411 ampMain
.scrollTitle();
412 if (!glconCtlWindow
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postTimeout(evScrollTitle
, 100);
413 glconPostScreenRebuild();
416 glconCtlWindow
.addEventListener((EventFileLoaded evt
) {
418 conwriteln("ERROR loading '", evt
.filename
, "'");
419 ampMain
.newSong("not playing");
420 setHint("not playing");
423 string cursong
= evt
.artist
~" \u2014 "~evt
.title
;
425 ampMain
.newSong(cursong
);
426 //conwriteln("playing '", evt.filename, "': ", evt.artist, " -- ", evt.title);
430 glconCtlWindow
.addEventListener((EventFileScanned evt
) {
431 //conwriteln("scanned: '", evt.filename, "': ", evt.success);
432 if (ampPList
is null) return;
434 ampPList
.scanResult(evt
.filename
, evt
.album
, evt
.artist
, evt
.title
, evt
.durationms
);
436 ampPList
.scanResultFailed(evt
.filename
);
440 glconCtlWindow
.addEventListener((EventFileComplete evt
) {
441 //glconCtlWindow.close();
442 setHint("not playing");
443 concmd("song_next tan");
448 // ////////////////////////////////////////////////////////////////////////// //
449 void createAmpWindow () {
450 ampMain
= new AmpMainWindow();
451 sdampwin
= new EgfxWindow(ampMain
, "AMPER_PLAYER", "Amper");
452 if (!sdampwin
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postEvent(evScrollTitle
);
456 // ////////////////////////////////////////////////////////////////////////// //
457 void createPListWindow () {
458 ampPList
= new AmpPListWindow();
459 sdplwin
= new EgfxWindow(ampPList
, "AMPER_PLAYLIST", "Amper Playlist", 25, 29);
463 void createEqWindow () {
464 ampEq
= new AmpEqWindow();
465 sdeqwin
= new EgfxWindow(ampEq
, "AMPER_EQIALIZER", "Amper Eqializer");
469 // ////////////////////////////////////////////////////////////////////////// //
471 void fakeScanDir () {
472 ampPList.appendListItem(PListItem("Sonata Arctica \u2014 Replica", "/mnt/muzax/wtf", 142));
473 ampPList.appendListItem(PListItem("Zonata \u2014 Geronimo", "/mnt/muzax/wtf", 242));
475 foreach (immutable int idx; 3..42) {
476 import std.format : format;
477 ampPList.appendListItem(PListItem("song #%d".format(idx), "/mnt/muzax/wtf", 100+idx));
483 // ////////////////////////////////////////////////////////////////////////// //
484 void scanDir (ConString path
, bool append
) {
485 void appendFile (const(char)[] fname
) {
486 ampPList
.appendListItem(fname
.idup
);
492 if (!append
) ampPList
.clear();
493 if (path
.exists
&& path
.isFile
) { appendFile(path
); return; }
494 foreach (DirEntry
de; dirEntries(path
.idup
, SpanMode
.shallow
)) {
495 if (!de.isFile
) continue;
498 } catch (Exception e
) {
499 conwriteln("ERROR scanning: ", e
.msg
);
501 //if (modeShuffle) ampPList.state.curitem = ampPList.findShuffleFirst();
502 //conwriteln(ampPList.findShuffleFirst(), " : ", ampPList.state.shuffleidx, " : ", ampPList.state.curplayingitem);
506 // ////////////////////////////////////////////////////////////////////////// //
507 void main (string
[] args
) {
510 glconShowKey
= "M-Grave";
512 //conRegVar!vbNewScale(1, 8, "v_scale", "window scale");
514 conRegFunc
!((ConString key
, ConString cmd
) { addBinding(key
, cmd
); })("gh_bind", "global hotkey bind: key command");
515 conRegFunc
!((ConString key
) { addBinding(key
, null); })("gh_unbind", "global hotkey unbind: key");
516 conRegFunc
!((ConString key
) { removeAllBindings(); })("gh_unbind_all", "unbind all global hotkeys unbind: key");
518 conRegVar
!skinfile("skin_file", "load skin from the given file",
519 delegate (ConVarBase self
, string oldval
, string newval
) {
523 glconPostScreenRebuild();
524 } catch (Exception e
) {
525 conwriteln("ERROR loading skin: ", e
.msg
);
531 scope(exit
) aplayShutdown();
533 loadSkin
!true("!BUILTIN!");
537 if (plVisible
) createPListWindow();
538 if (eqVisible
) createEqWindow();
539 dgCreatePListWindow
= delegate () { createPListWindow(); };
540 dgCreateEqWindow
= delegate () { createEqWindow(); };
542 concmd("gh_bind M-H-A win_toggle");
543 concmd("gh_bind M-H-Z song_prev");
544 concmd("gh_bind M-H-X song_play");
545 concmd("gh_bind M-H-C song_pause_toggle");
546 concmd("gh_bind M-H-V song_stop");
547 concmd("gh_bind M-H-B song_next");
548 concmd("gh_bind M-H-Left \"song_seek_rel -10\"");
549 concmd("gh_bind M-H-Right \"song_seek_rel +10\"");
550 concmd("gh_bind M-H-Up \"soft_volume_rel +2\"");
551 concmd("gh_bind M-H-Down \"soft_volume_rel -2\"");
552 concmd("gh_bind M-H-Delete \"soft_volume 31\"");
555 conRegFunc
!((ConString path
, bool append
=false) {
557 scanDir(path
, append
);
558 } catch (Exception e
) {
559 conwriteln("scanning error: ", e
.msg
);
561 glconPostScreenRebuild();
562 })("scan_dir", "scan the given directory; 2nd ard is \"append\" bool flag");
566 glconPostScreenRebuild();
567 })("pl_clear", "clear playlist");
569 conRegFunc
!(() { hideShowWindows(); })("win_toggle", "show/hide Amper windows");
571 conRegFunc
!((int plidx
, bool forcestart
=false) {
572 if (!sdampwin
.closed
) {
573 ampPList
.playSongByIndex(plidx
, forcestart
);
574 glconPostScreenRebuild();
576 })("song_play_by_index", "play song from playlist by index");
578 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playPrevSong(); })("song_prev", "play previous song");
579 conRegFunc
!((bool forceplay
=false){ if (!sdampwin
.closed
) ampPList
.playNextSong(forceplay
); })("song_next", "play next song");
580 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playCurrentSong(); })("song_play", "play current song");
581 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.stopSong(); })("song_stop", "stop current song");
582 conRegFunc
!((){ aplayTogglePause(); })("song_pause_toggle", "pause/unpause current song");
583 conRegFunc
!((bool pause
){ aplayPause(pause
); })("song_pause", "pause/unpause current song, with bool arg");
585 conRegFunc
!((uint msecs
){ aplaySeekMS(msecs
*1000); })("song_seek_abs", "absolute seek, in seconds");
586 conRegFunc
!((int msecs
){ aplaySeekMS(aplayCurTimeMS
+msecs
*1000); })("song_seek_rel", "relative seek, in seconds");
589 concmd("exec /etc/amper.rc tan"); // global config
590 conProcessQueue(); // load config
592 import core
.stdc
.stdlib
: getenv
;
593 auto home
= getenv("HOME");
594 if (home
!is null && home
[0]) {
595 import std
.string
: fromStringz
;
596 string s
= home
.fromStringz
.idup
;
597 if (s
[$-1] != '/') s
~= '/';
599 //conwriteln("home config: [", s, "]");
600 concmdf
!"exec \"%s\" tan"(s
); // user config
601 conProcessQueue(); // load config
605 concmd("exec amper.rc tan");
608 conProcessArgs
!true(args
);
609 conProcessQueue(int.max
/4);
612 scope(exit
) stopRPCServer();
615 static class EventFixupPListPosition
{}
616 glconCtlWindow
.addEventListener((EventFixupPListPosition evt
) {
617 int xmain
, ymain
, wdtmain
, hgtmain
;
618 int xpl
, ypl
, wdtpl
, hgtpl
;
619 int xwork
, ywork
, wdtwork
, hgtwork
;
620 getWorkAreaRect(xwork
, ywork
, wdtwork
, hgtwork
);
621 sdampwin
.getWindowRect(xmain
, ymain
, wdtmain
, hgtmain
);
622 sdplwin
.getWindowRect(xpl
, ypl
, wdtpl
, hgtpl
);
623 conwriteln("work: (", xwork
, ",", ywork
, ")-(", wdtwork
, "x", hgtwork
, ")");
624 conwriteln("main: (", xmain
, ",", ymain
, ")-(", wdtmain
, "x", hgtmain
, ")");
625 conwriteln("list: (", xpl
, ",", ypl
, ")-(", wdtpl
, "x", hgtpl
, ")");
626 if (GxRect(xmain
, ymain
, wdtmain
, hgtmain
).overlaps(GxRect(xpl
, ypl
, wdtpl
, hgtpl
))) {
628 int ny
= ymain
+hgtmain
;
629 if (nx
+wdtpl
> xwork
+wdtwork
) nx
= xwork
+wdtwork
-wdtpl
;
630 if (ny
+hgtpl
> ywork
+hgtwork
) ny
= ywork
+hgtwork
-hgtpl
;
631 if (nx
< xwork
) nx
= xwork
;
632 if (ny
< ywork
) ny
= ywork
;
633 conwriteln("newpos: (", nx
, ",", ny
, ")");
634 sdplwin
.move(nx
, ny
);
635 //sdplwin.move(50, 50);
639 sdplwin
.visibleForTheFirstTime
= delegate () {
641 switchToWindow(sdampwin
);
642 //glconCtlWindow.postTimeout(new EventFixupPListPosition(), 50);
648 foreach (string path
; args
[1..$]) concmdf
!"scan_dir \"%s\" tan"(path
);
650 glconCtlWindow
.eventLoop(0);
653 conProcessQueue(int.max
/4);