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/>.
17 module bioacid
is aliced
;
23 import arsd
.simpledisplay
;
30 import iv
.nanovega
.blendish
;
31 import iv
.nanovega
.textlayouter
;
40 version(sfnt_test
) import iv
.nanovega
.simplefont
;
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared
bool mainWindowActive
= false;
53 __gshared
bool mainWindowVisible
= false;
56 shared static this () {
57 toxCoreSendEvent
= delegate (Object msg
) {
58 if (msg
is null) return; // just in case
60 if (glconCtlWindow
is null || glconCtlWindow
.closed
) return;
61 glconCtlWindow
.postEvent(msg
);
62 } catch (Exception e
) {}
67 // ////////////////////////////////////////////////////////////////////////// //
68 string
getBrowserCommand (bool forceOpera
=false) {
69 __gshared string browser
;
70 if (forceOpera
) return "opera";
71 if (browser
.length
== 0) {
72 import core
.stdc
.stdlib
: getenv
;
73 const(char)* evar
= getenv("BROWSER");
74 if (evar
!is null && evar
[0]) {
75 import std
.string
: fromStringz
;
76 browser
= evar
.fromStringz
.idup
;
85 void openUrl (ConString url
, bool forceOpera
=false) {
87 import std
.stdio
: File
;
90 auto frd
= File("/dev/null");
91 auto fwr
= File("/dev/null", "w");
92 spawnProcess([getBrowserCommand(forceOpera
), url
.idup
], frd
, fwr
, fwr
, null, Config
.detached
);
93 } catch (Exception e
) {
94 conwriteln("ERROR executing URL viewer (", e
.msg
, ")");
100 // ////////////////////////////////////////////////////////////////////////// //
102 int pos
= -1, len
= 0;
104 @property bool valid () const pure nothrow @safe @nogc => (pos
>= 0 && len
> 0);
105 @property int end () const pure nothrow @safe @nogc => (pos
>= 0 && len
> 0 ? pos
+len
: 0);
107 static UrlInfo
Invalid () pure nothrow @safe @nogc => UrlInfo
.init
;
111 UrlInfo
urlDetect (const(char)[] text
) nothrow @trusted @nogc {
114 auto dlpos
= text
.indexOf("://");
115 if (dlpos
< 3) return res
;
117 //{ import core.stdc.stdio; printf("det: <%.*s>\n", cast(uint)text.length, text.ptr); }
119 bool isProto (const(char)[] prt
) nothrow @trusted @nogc {
120 if (dlpos
< prt
.length
) return false;
121 if (!strEquCI(prt
, text
[dlpos
-prt
.length
..dlpos
])) return false;
122 // check word boundary
123 if (dlpos
== prt
.length
) return true;
124 return !isalpha(text
[dlpos
-prt
.length
-1]);
127 if (isProto("ftp")) res
.pos
= cast(int)(dlpos
-3);
128 else if (isProto("http")) res
.pos
= cast(int)(dlpos
-4);
129 else if (isProto("https")) res
.pos
= cast(int)(dlpos
-5);
132 dlpos
+= 3; // skip "://"
135 for (; dlpos
< text
.length
; ++dlpos
) {
136 char ch
= text
[dlpos
];
137 if (ch
== '/') break;
138 if (!(isalnum(ch
) || ch
== '.' || ch
== '-' || ch
== ':' || ch
== '@')) break;
144 bool wasSharp
= false;
146 for (; dlpos
< text
.length
; ++dlpos
) {
147 char ch
= text
[dlpos
];
156 if (ch
== '(' || ch
== '[' || ch
== '{') {
158 case '(': ch
= ')'; break;
159 case '[': ch
= ']'; break;
160 case '{': ch
= '}'; break;
162 if (brcSP
< brcStack
.length
) brcStack
[brcSP
++] = ch
;
166 if (ch
== ')' || ch
== ']' || ch
== '}') {
167 if (brcSP
== 0 || brcStack
[brcSP
-1] != ch
) break;
173 if (dlpos
== text
.length ||
(!isalnum(text
[dlpos
+1]) && text
[dlpos
+1] != '_')) break;
177 if (ch
<= ' ' || ch
>= 127) break;
180 res
.len
= cast(int)(dlpos
-res
.pos
);
186 // ////////////////////////////////////////////////////////////////////////// //
187 alias LayTextClass
= LayTextD
;
190 // ////////////////////////////////////////////////////////////////////////// //
191 __gshared string accBaseDir
= ".";
193 __gshared NVGContext nvg
= null;
194 __gshared NVGImage nvgSkullsImg
;
195 __gshared NVGImage kittyOut
, kittyMsg
;
197 __gshared LayTextClass lay
;
199 __gshared
int optCListWidth
= -1;
200 __gshared
int lastWindowWidth
= -1;
202 __gshared NVGImage
[5] statusImgId
;
205 shared static ~this () {
206 //{ import core.stdc.stdio; printf("******************************\n"); }
207 nvgSkullsImg
.clear();
208 //{ import core.stdc.stdio; printf("---\n"); }
209 foreach (ref img
; statusImgId
[]) img
.clear();
215 void buildStatusImages () {
217 statusImgId
[ContactStatus
.Offline
] = nvg
.createImageRGBA(16, 16, ctiOffline
[], NVGImageFlags
.NoFiltering
);
218 statusImgId
[ContactStatus
.Online
] = nvg
.createImageRGBA(16, 16, ctiOnline
[], NVGImageFlags
.NoFiltering
);
219 statusImgId
[ContactStatus
.Away
] = nvg
.createImageRGBA(16, 16, ctiAway
[], NVGImageFlags
.NoFiltering
);
220 statusImgId
[ContactStatus
.Connecting
] = nvg
.createImageRGBA(16, 16, ctiOffline
[], NVGImageFlags
.NoFiltering
);
222 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
223 statusImgId
[ContactStatus
.Offline
] = nvg
.createImageRGBA(16, 16, baph16Gray
[], NVGImageFlags
.NoFiltering
);
224 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
225 statusImgId
[ContactStatus
.Online
] = nvg
.createImageRGBA(16, 16, baph16Online
[], NVGImageFlags
.NoFiltering
);
226 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
227 statusImgId
[ContactStatus
.Away
] = nvg
.createImageRGBA(16, 16, baph16Away
[], NVGImageFlags
.NoFiltering
);
228 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
229 statusImgId
[ContactStatus
.Busy
] = nvg
.createImageRGBA(16, 16, baph16Busy
[], NVGImageFlags
.NoFiltering
);
230 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
231 statusImgId
[ContactStatus
.Connecting
] = nvg
.createImageRGBA(16, 16, baph16Orange
[], NVGImageFlags
.NoFiltering
);
232 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
233 kittyOut
= nvg
.createImageRGBA(16, 16, kittyOutgoing
[], NVGImageFlags
.NoFiltering
);
234 kittyMsg
= nvg
.createImageRGBA(16, 16, kittyMessage
[], NVGImageFlags
.NoFiltering
);
239 // ////////////////////////////////////////////////////////////////////////// //
240 void loadFonts (NVGContext vg
) {
241 vg
.fonsContext
.fonsAddStashFonts(fstash
);
242 bndSetFont(vg
.findFont("ui"));
246 // ////////////////////////////////////////////////////////////////////////// //
247 class MessageStart
: LayObject
{
248 long msgid
; // >0: outgoing, unacked yet
250 this (long aid
=-1) nothrow @safe @nogc { msgid
= aid
; }
252 override int width () => 0;
253 override int spacewidth () => 0;
254 override int height () => 0;
255 override int ascent () => 0;
256 override int descent () => 0;
257 override bool canbreak () => true;
258 override bool spaced () => false;
260 override void draw (NVGContext ctx
, float x
, float y
) {}
264 class MessageOutMark
: LayObject
{
265 long msgid
; // >0: outgoing, unacked yet
267 this (long aid
=-1) nothrow @safe @nogc { msgid
= aid
; }
269 override int width () => kittyOut
.width
;
270 override int spacewidth () => 4;
271 override int height () => kittyOut
.height
;
272 override int ascent () => height
;
273 override int descent () => 0;
274 override bool canbreak () => true;
275 override bool spaced () => false;
277 override void draw (NVGContext ctx
, float x
, float y
) {
280 scope(exit
) nvg
.restore
;
282 nvg
.rect(x
+0.5, y
+0.5, width
, height
);
283 nvg
.fillPaint(nvg
.imagePattern(0, 0, width
, height
, 0, kittyOut
));
290 // ////////////////////////////////////////////////////////////////////////// //
291 static struct LayUrl
{
293 uint wordidx
; // first word
297 __gshared LayUrl
[uint] layUrlList
; // for each word
301 lay
.wipeAll(true); // clear log, but delete objects
306 void addDividerLine (bool doflushgui
=false) {
307 if (glconCtlWindow
is null || glconCtlWindow
.closed
) return;
310 bool inFrame
= nvg
.inFrame
;
312 glconCtlWindow
.setAsCurrentOpenGlContext(); // make this window active
313 nvg
.beginFrame(glconCtlWindow
.width
, glconCtlWindow
.height
);
318 if (doflushgui
) flushGui();
319 glconCtlWindow
.releaseCurrentOpenGlContext();
323 lay
.fontStyle
.fontsize
= 2;
324 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
325 lay
.fontStyle
.bgcolor
= NVGColor("#aa0").asUint
;
326 lay
.fontStyle
.monospace
= true;
333 glconPostScreenRepaint();
337 // `ct` can be `null` for "my message" or "system message"
338 void addTextToLog (Account acc
, Contact ct
, LogFile
.Msg
.Kind kind
, bool action
, const(char)[] msg
, SysTime time
, long msgid
=-1, bool doflushgui
=false) {
339 if (glconCtlWindow
is null || glconCtlWindow
.closed
) return;
342 bool inFrame
= nvg
.inFrame
;
344 glconCtlWindow
.setAsCurrentOpenGlContext(); // make this window active
345 nvg
.beginFrame(glconCtlWindow
.width
, glconCtlWindow
.height
);
350 if (doflushgui
) flushGui();
351 glconCtlWindow
.releaseCurrentOpenGlContext();
355 // add "message start" mark
356 lay
.putObject(new MessageStart(msgid
));
358 lay
.fontStyle
.fontsize
= 16;
359 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
360 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
361 lay
.fontStyle
.monospace
= true;
365 final switch (kind
) {
366 case LogFile
.Msg
.Kind
.Outgoing
: textColor
= NVGColor
.k8orange
; lay
.fontStyle
.color
= NVGColor("#c40").asUint
; lay
.put(acc
.info
.nick
); break;
367 case LogFile
.Msg
.Kind
.Incoming
: textColor
= NVGColor("#ccc"); lay
.fontStyle
.color
= NVGColor("#666").asUint
; lay
.put(ct
.info
.nick
); break;
368 case LogFile
.Msg
.Kind
.Notification
: textColor
= NVGColor("#0c0"); lay
.fontStyle
.color
= textColor
.asUint
; lay
.put("*system*"); break;
371 lay
.fontStyle
.monospace
= false;
372 //lay.putHardSpace(64);
374 // add "message outgoing" mark
375 if (kind
== LogFile
.Msg
.Kind
.Outgoing
&& msgid
> 0) {
377 //conwriteln("msgoutmark");
378 lay
.putObject(new MessageOutMark(msgid
));
384 import std
.format
: format
;
385 auto dt = cast(DateTime
)time
;
386 string tstr
= "%04u/%02u/%02u".format(dt.year
, dt.month
, dt.day
);
391 lay
.fontStyle
.color
= NVGColor("#aaa").asUint
;
392 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
395 import std
.format
: format
;
396 auto dt = cast(DateTime
)time
;
397 string tstr
= "%02u:%02u:%02u".format(dt.hour
, dt.minute
, dt.second
);
403 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
404 lay
.fontStyle
.fontsize
= 20;
407 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
411 void xput (const(char)[] str) {
413 auto nl
= str.indexOf('\n');
414 if (nl
< 0) { lay
.put(str); break; }
415 if (nl
> 0) lay
.put(str[0..nl
]);
421 msg
= msg
.xstripright
;
423 lay
.fontStyle
.color
= textColor
.asUint
;
425 auto nfo
= urlDetect(msg
);
431 xput(msg
[0..nfo
.pos
]);
432 string url
= msg
[nfo
.pos
..nfo
.end
].idup
;
433 msg
= msg
[nfo
.end
..$];
434 auto stword
= lay
.nextWordIndex
;
436 auto c
= lay
.fontStyle
.color
;
437 scope(exit
) { lay
.popStyles
; lay
.fontStyle
.color
= c
; }
438 lay
.fontStyle
.href
= true;
439 lay
.fontStyle
.underline
= true;
440 lay
.fontStyle
.color
= NVGColor("#06f").asUint
;
442 while (stword
< lay
.nextWordIndex
) {
443 layUrlList
[stword
] = LayUrl(url
, stword
);
452 glconPostScreenRepaint();
456 void addTextToLog (Account acc
, Contact ct
, in ref LogFile
.Msg msg
, long msgid
=-1, bool doflushgui
=false) {
457 import iv
.utfutil
: utf8Encode
;
460 scope(exit
) delete text
;
462 foreach (dchar dc
; msg
.byDChar
) {
464 auto len
= utf8Encode(buf
[], dc
);
467 addTextToLog(acc
, ct
, msg
.kind
, msg
.isMe
, text
, msg
.time
, msgid
, doflushgui
);
471 void ackLogMessage (long msgid
) {
472 if (msgid
<= 0) return;
473 foreach (immutable uint widx
; 0..lay
.wordCount
) {
474 auto w
= lay
.wordByIndex(widx
);
475 int oidx
= w
.objectIdx
;
477 if (auto maw
= cast(MessageOutMark
)lay
.objectAtIndex(oidx
)) {
478 if (maw
.msgid
== msgid
) {
479 maw
.msgid
= -1; // reset mark
480 glconPostScreenRepaint(); // redraw
488 // ////////////////////////////////////////////////////////////////////////// //
491 this (Account aOwner
) { acc
= aOwner
; }
494 bool mDirty
; // true: write contact's config
502 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
505 import std
.file
: mkdirRecurse
;
506 import std
.path
: dirName
;
508 assert(acc
!is null);
510 if (diskFileName
.length
== 0) {
511 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
513 mkdirRecurse(diskFileName
.dirName
);
514 if (serialize(info
, diskFileName
)) mDirty
= false;
517 @property bool visible () const nothrow @trusted @nogc {
518 if (!hideIfNoVisibleMembers
) return true; // always visible
519 // check if we have any visible members
520 foreach (const(Contact
) c
; acc
.contacts
.byValue
) {
521 if (c
.gid
!= info
.gid
) continue;
522 if (c
.visibleNoGroupCheck
) return true;
524 return false; // nobody's here
527 private bool getTriOpt(string
fld, string fld2
=null) () const nothrow @trusted @nogc {
528 enum lo
= "info."~fld;
529 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
530 static if (fld2
.length
) return mixin("acc.info."~fld2
); else return mixin("acc.info."~fld);
533 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
534 enum lo
= "info."~fld;
535 if (mixin(lo
) >= 0) return mixin(lo
);
536 return mixin("acc.info."~fld);
539 @property nothrow @safe {
540 uint gid () const pure @nogc => info
.gid
;
542 bool opened () const @nogc => info
.opened
;
543 void opened (bool v
) @nogc { pragma(inline
, true); if (info
.opened
!= v
) { info
.opened
= v
; markDirty(); } }
545 string
name () const @nogc => info
.name
;
546 void name (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unnamed>"; if (info
.name
!= v
) { info
.name
= v
; markDirty(); } }
548 string
note () const @nogc => info
.note
;
549 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
552 bool showOffline () const => getTriOpt
!"showOffline";
553 bool showPopup () const => getTriOpt
!"showPopup";
554 bool blinkActivity () const => getTriOpt
!"blinkActivity";
555 bool skipUnread () const => getTriOpt
!"skipUnread";
556 bool hideIfNoVisibleMembers () const => getTriOpt
!("hideIfNoVisibleMembers", "hideEmptyGroups");
557 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
558 int resendRotDays () const => getIntOpt
!"resendRotDays";
559 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
565 // ////////////////////////////////////////////////////////////////////////// //
566 final class Contact
{
568 // `Connecting` for non-account means "awaiting authorization"
571 bool isMe
; // "/me" message?
574 long msgid
; // ==0: unknown yet
577 MonoTime nextSendTime
;
581 this (Account aOwner
) { acc
= aOwner
; edit
= new MiniEdit(); }
584 bool mDirty
; // true: write contact's config
590 ContactStatus status
= ContactStatus
.Offline
; // not saved, so if it safe to change it
596 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
598 void loadUnreadCount () nothrow {
599 assert(diskFileName
.length
);
600 assert(acc
!is null);
602 import std
.path
: dirName
;
603 auto fi
= VFile(diskFileName
.dirName
~"/logs/unread.dat");
604 unreadCount
= fi
.readNum
!int;
605 } catch (Exception e
) {
610 void saveUnreadCount () nothrow {
611 assert(diskFileName
.length
);
612 assert(acc
!is null);
614 import std
.path
: dirName
;
615 auto fo
= VFile(diskFileName
.dirName
~"/logs/unread.dat", "w");
616 fo
.writeNum(unreadCount
);
617 } catch (Exception e
) {
621 void loadResendQueue () {
622 import std
.path
: dirName
;
623 string fname
= diskFileName
.dirName
~"/logs/resend.log";
626 auto ctt
= MonoTime
.currTime
;
627 foreach (const ref lmsg
; lf
.messages
) {
629 xmsg
.isMe
= lmsg
.isMe
;
630 xmsg
.time
= lmsg
.time
;
631 xmsg
.text
= lmsg
.text
;
633 xmsg
.nextSendTime
= ctt
;
638 void saveResendQueue () {
639 import std
.file
: mkdirRecurse
, remove
;
640 import std
.path
: dirName
;
641 assert(diskFileName
.length
);
642 assert(acc
!is null);
643 mkdirRecurse(diskFileName
.dirName
~"/logs");
644 string fname
= diskFileName
.dirName
~"/logs/resend.log";
645 try { remove(fname
); } catch (Exception e
) {}
646 if (resendQueue
.length
) {
647 foreach (const ref msg
; resendQueue
) {
648 LogFile
.appendLine(fname
, LogFile
.Msg
.Kind
.Outgoing
, msg
.text
, msg
.isMe
, msg
.time
);
654 import std
.file
: mkdirRecurse
;
655 import std
.path
: dirName
;
657 assert(acc
!is null);
659 if (diskFileName
.length
== 0) {
660 diskFileName
= acc
.basePath
~"/contacts/"~tox_hex(info
.pubkey
[])~"/config.rc";
661 acc
.contacts
[info
.pubkey
] = this;
663 mkdirRecurse(diskFileName
.dirName
);
664 mkdirRecurse(diskFileName
.dirName
~"/avatars");
665 mkdirRecurse(diskFileName
.dirName
~"/files");
666 mkdirRecurse(diskFileName
.dirName
~"/fileparts");
667 mkdirRecurse(diskFileName
.dirName
~"/logs");
669 if (serialize(info
, diskFileName
)) mDirty
= false;
673 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (showOffline || status
!= ContactStatus
.Offline
);
675 @property bool visible () const nothrow @trusted @nogc {
676 if (!showOffline
&& status
== ContactStatus
.Offline
) return false;
677 auto grp
= acc
.groupById(gid
);
681 @property nothrow @safe {
682 ContactInfo
.Kind
kind () const @nogc => info
.kind
;
683 void kind (ContactInfo
.Kind v
) @nogc { pragma(inline
, true); if (info
.kind
!= v
) { info
.kind
= v
; markDirty(); } }
685 uint gid () const @nogc => info
.gid
;
686 void gid (uint v
) @nogc { pragma(inline
, true); if (info
.gid
!= v
) { info
.gid
= v
; markDirty(); } }
688 string
nick () const @nogc => info
.nick
;
689 void nick (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unknown>"; if (info
.nick
!= v
) { info
.nick
= v
; markDirty(); } }
691 string
visnick () const @nogc => info
.visnick
;
692 void visnick (string v
) @nogc { pragma(inline
, true); if (info
.visnick
!= v
) { info
.visnick
= v
; markDirty(); } }
694 string
displayNick () const @nogc => (info
.visnick
.length ? info
.visnick
: info
.nick
);
696 string
statusmsg () const @nogc => info
.statusmsg
;
697 void statusmsg (string v
) @nogc { pragma(inline
, true); if (info
.statusmsg
!= v
) { info
.statusmsg
= v
; markDirty(); } }
699 void setLastOnlineNow () {
701 auto ut
= Clock
.currTime
.toUTC().toUnixTime();
702 if (info
.lastonlinetime
!= cast(uint)ut
) { info
.lastonlinetime
= cast(uint)ut
; markDirty(); }
703 } catch (Exception e
) {}
706 string
note () const @nogc => info
.note
;
707 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
710 private bool getTriOpt(string
fld) () const nothrow @trusted @nogc {
711 enum lo
= "info.opts."~fld;
712 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
713 auto grp
= acc
.groupById(info
.gid
);
714 enum go
= "grp.info."~fld;
715 if (mixin(go
) != TriOption
.Default
) return (mixin(go
) == TriOption
.Yes
);
716 return mixin("acc.info."~fld);
719 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
720 enum lo
= "info.opts."~fld;
721 if (mixin(lo
) >= 0) return mixin(lo
);
722 auto grp
= acc
.groupById(info
.gid
);
723 enum go
= "grp.info."~fld;
724 if (mixin(go
) >= 0) return mixin(go
);
725 return mixin("acc.info."~fld);
728 @property nothrow @safe @nogc {
729 bool showOffline () const => getTriOpt
!"showOffline";
730 bool showPopup () const => getTriOpt
!"showPopup";
731 bool blinkActivity () const => getTriOpt
!"blinkActivity";
732 bool skipUnread () const => getTriOpt
!"skipUnread";
733 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
734 int resendRotDays () const => getIntOpt
!"resendRotDays";
735 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
738 void loadLogInto (ref LogFile lf
) {
739 import std
.file
: exists
;
740 import std
.path
: dirName
;
741 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
742 if (lname
.exists
) lf
.load(lname
); else lf
.clear();
745 void appendToLog (LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) {
746 import std
.path
: dirName
;
747 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
748 LogFile
.appendLine(lname
, kind
, text
, isMe
, time
);
751 void ackReceived (long msgid
) {
752 if (msgid
<= 0) return; // wtf?!
753 bool changed
= false;
755 while (idx
< resendQueue
.length
) {
756 if (resendQueue
[idx
].msgid
== msgid
) {
757 foreach (immutable c
; idx
+1..resendQueue
.length
) resendQueue
[c
-1] = resendQueue
[c
];
758 resendQueue
[$-1] = XMsg
.init
;
759 resendQueue
.length
-= 1;
760 resendQueue
.assumeSafeAppend
;
766 if (changed
) saveResendQueue();
769 void processResendQueue () {
770 if (status
== ContactStatus
.Offline || status
== ContactStatus
.Connecting
) return;
772 auto ctt
= MonoTime
.currTime
+30.seconds
;
773 foreach (ref XMsg msg
; resendQueue
) {
774 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, msg
.text
, msg
.isMe
);
775 if (msgid
< 0) break;
777 msg
.nextSendTime
= ctt
;
778 if (msg
.resendCount
++ != 0) doSave
= true;
780 if (doSave
) saveResendQueue();
783 void send (const(char)[] text
) {
784 void sendOne (const(char)[] text
, bool action
) {
785 if (text
.length
== 0) return; // just in case
787 SysTime now
= Clock
.currTime
;
788 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, text
, action
);
789 if (msgid
< 0) { conwriteln("ERROR sending message to '", info
.nick
, "'"); return; }
791 // add this to resend queue
796 xmsg
.nextSendTime
= MonoTime
.currTime
+30.seconds
;
797 xmsg
.resendCount
= 0;
801 import std
.format
: format
;
802 auto dt = cast(DateTime
)now
;
803 xmsg
.text
= "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year
, dt.month
, dt.day
, dt.hour
, dt.minute
, dt.second
, text
);
808 if (activeContact
is this) addTextToLog(acc
, this, LogFile
.Msg
.Kind
.Outgoing
, action
, text
, now
, msgid
);
809 appendToLog(LogFile
.Msg
.Kind
.Outgoing
, text
, action
, now
);
812 bool action
= text
.startsWith("/me ");
813 if (action
) text
= text
[4..$].xstripleft
;
815 while (text
.length
) {
816 auto ep
= text
.indexOf('\n');
817 if (ep
< 0) ep
= text
.length
; else ++ep
; // include '\n'
818 // remove line if it contains only spaces
819 bool hasNonSpace
= false;
820 foreach (immutable char ch
; text
[0..ep
]) if (ch
> ' ') { hasNonSpace
= true; break; }
821 if (hasNonSpace
) break;
824 while (text
.length
&& text
[$-1] <= ' ') text
= text
[0..$-1];
825 if (text
.length
== 0) return; // nothing to do
828 //TODO: split at word boundaries
829 enum ReservedSpace
= 23+3+3;
831 char[TOX_MAX_MESSAGE_LENGTH
+8] tmpbuf
= void;
834 while (text
.length
) {
835 int epos
= TOX_MAX_MESSAGE_LENGTH
-ReservedSpace
;
836 if (epos
< text
.length
) {
839 if (text
[epos
-1] < 128) break;
840 if ((text
[epos
-1]&0xc0) == 0xc0) break;
844 epos
= cast(int)text
.length
;
847 if (first
&& epos
>= text
.length
) {
848 sendOne(text
[0..epos
], action
);
851 if (!first
) { tmpbuf
[0..3] = "..."; ofs
= 3; }
852 tmpbuf
[ofs
..ofs
+epos
] = text
[0..epos
];
853 tmpbuf
[ofs
+epos
..ofs
+epos
+3] = "...";
854 sendOne(tmpbuf
[0..ofs
+epos
+3], action
);
857 text
= text
[epos
..$];
865 // ////////////////////////////////////////////////////////////////////////// //
866 final class Account
{
869 import std
.algorithm
: sort
;
870 GroupOptions
[] glist
;
871 scope(exit
) delete glist
;
872 foreach (Group g
; groups
) glist
~= g
.info
;
873 glist
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
874 glist
.serialize(basePath
~"contacts/groups.rc");
878 ContactStatus mStatus
= ContactStatus
.Offline
;
881 PubKey toxpk
= toxCoreEmptyKey
;
882 string toxDataDiskName
;
883 string basePath
; // with trailing "/"
884 ProtoOptions protoOpts
;
887 Contact
[PubKey
] contacts
;
890 bool mIAmConnecting
= false;
891 bool mIAmOnline
= false;
892 bool forceOnline
= true; // set to `false` to stop autoreconnecting
893 //bool restoreOnline = false; // will be set to `true` if we need to reconnect
894 //bool doRefreshNicks = false;
897 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting
;
899 @property ContactStatus
status () const nothrow @safe @nogc {
900 if (!toxpk
.isValidKey
) return ContactStatus
.Offline
;
901 if (mIAmConnecting
) return ContactStatus
.Connecting
;
902 if (!mIAmOnline
) return ContactStatus
.Offline
;
906 @property bool isOnline () const nothrow @safe @nogc {
907 if (!toxpk
.isValidKey
) return false;
908 if (mIAmConnecting
) return false;
909 if (!mIAmOnline
) return false;
913 @property void status (ContactStatus v
) {
914 if (!toxpk
.isValidKey
) return;
915 conwriteln("changing status to ", v
, " (old: ", mStatus
, ")");
916 if (v
== ContactStatus
.Connecting
) v
= ContactStatus
.Online
;
917 if (v
== ContactStatus
.Offline
) {
922 if (mStatus
== ContactStatus
.Offline
) {
923 if (v
!= ContactStatus
.Offline
) mIAmConnecting
= true;
925 toxCoreSetStatus(toxpk
, v
);
927 glconPostScreenRepaint();
932 void processResendQueue () {
933 if (!isOnline
) return;
934 foreach (Contact ct
; contacts
.byValue
) ct
.processResendQueue();
937 void saveResendQueue () {
938 foreach (Contact ct
; contacts
.byValue
) ct
.saveResendQueue();
943 toxpk
= toxCoreOpenAccount(toxDataDiskName
);
944 if (!toxpk
.isValidKey
) {
945 conwriteln("creating new Tox account...");
946 string nick
= info
.nick
;
947 if (nick
.length
> 0) {
949 if (nick
.length
> TOX_MAX_NAME_LENGTH
) nick
= nick
[0..TOX_MAX_NAME_LENGTH
];
953 toxpk
= toxCoreCreateAccount(toxDataDiskName
, nick
);
954 if (!toxpk
.isValidKey
) throw new Exception("cannot create Tox account");
958 // load contacts from ToxCore data and add 'em to contact database
959 void toxLoadKnownContacts () {
960 if (!toxpk
.isValidKey
) return;
961 toxCoreForEachFriend(toxpk
, delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] nick
) {
962 if (nick
.length
== 0) nick
= "<unknown>";
963 auto c
= (frpub
in contacts ? contacts
[frpub
] : null);
965 conwriteln("NEW friend with pk [", tox_hex(frpub
), "]; name is: ", nick
);
966 c
= new Contact(this);
968 c
.info
.nick
= nick
.idup
;
969 c
.info
.pubkey
[] = frpub
[];
970 c
.info
.opts
.showOffline
= TriOption
.Yes
;
971 contacts
[c
.info
.pubkey
] = c
;
974 if (clist
!is null) clist
.buildAccount(this);
975 } else if (c
.info
.nick
!= nick
) {
976 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; new name is: ", nick
);
977 if (nick
!= "<unknown>" && (c
.info
.nick
== "<unknown>" || c
.info
.nick
.length
== 0)) {
978 c
.info
.nick
= nick
.idup
;
982 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; old name is: ", nick
);
984 return false; // don't stop
989 // connection established
990 void toxConnectionDropped () {
992 conprintfln("TOX[%s] CONNECTION DROPPED", timp
.srvalias
);
993 mIAmConnecting
= false;
997 mIAmConnecting
= true;
999 foreach (Contact ct
; contacts
.byValue
) ct
.status
= ContactStatus
.Offline
;
1000 glconPostScreenRepaint();
1003 // connection established
1004 void toxConnectionEstablished () {
1006 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp
.srvalias
);
1007 mIAmConnecting
= false;
1009 toxCoreSetStatusMessage(toxpk
, "Come taste the gasoline! [BioAcid]");
1010 toxLoadKnownContacts();
1011 glconPostScreenRepaint();
1014 void toxFriendOffline (in ref PubKey fpk
) {
1015 if (auto ct
= fpk
in contacts
) {
1016 if (ct
.status
!= ContactStatus
.Offline
) {
1017 conwriteln("friend <", ct
.info
.nick
, "> gone offline");
1018 ct
.status
= ContactStatus
.Offline
;
1019 glconPostScreenRepaint();
1024 void toxSelfStatus (ContactStatus cst
) {
1025 if (mStatus
!= cst
) {
1027 glconPostScreenRepaint();
1031 void toxFriendStatus (in ref PubKey fpk
, ContactStatus cst
) {
1032 if (auto ct
= fpk
in contacts
) {
1033 if (ct
.status
!= cst
) {
1034 conwriteln("status for friend <", ct
.info
.nick
, "> changed to ", cst
);
1036 //if (ct.status != ContactStatus.Offline && ct.status != ContactStatus.Connecting) ct.processResendQueue();
1037 glconPostScreenRepaint();
1042 void toxFriendStatusMessage (in ref PubKey fpk
, string msg
) {
1043 if (auto ct
= fpk
in contacts
) {
1044 if (ct
.info
.statusmsg
!= msg
) {
1045 conwriteln("status message for friend <", ct
.info
.nick
, "> changed to <", msg
, ">");
1046 ct
.info
.statusmsg
= msg
;
1048 glconPostScreenRepaint();
1053 void toxFriendReqest (in ref PubKey fpk
, const(char)[] msg
) {
1055 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp
.srvalias
, tox_hex(fpk
[]), msg
);
1058 void toxFriendMessage (in ref PubKey fpk
, bool action
, string msg
, SysTime time
) {
1059 if (auto ct
= fpk
in contacts
) {
1060 LogFile
.Msg
.Kind kind
= LogFile
.Msg
.Kind
.Incoming
;
1061 ct
.appendToLog(kind
, msg
, action
, time
);
1062 if (*ct
is activeContact
) {
1063 // if inactive or invisible, add divider line and increase unread count
1064 if (!mainWindowVisible ||
!mainWindowActive
) {
1065 if (ct
.unreadCount
== 0) addDividerLine();
1066 ct
.unreadCount
+= 1;
1067 ct
.saveUnreadCount();
1069 addTextToLog(this, *ct
, kind
, action
, msg
, time
);
1071 ct
.unreadCount
+= 1;
1072 ct
.saveUnreadCount();
1077 // ack for sent message
1078 void toxMessageAck (in ref PubKey fpk
, long msgid
) {
1080 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp
.srvalias
, tox_hex(fpk
[]), msgid
);
1081 if (auto ct
= fpk
in contacts
) {
1082 if (*ct
is activeContact
) ackLogMessage(msgid
);
1083 ct
.ackReceived(msgid
);
1088 @property string
srvalias () const pure nothrow @safe @nogc => info
.nick
;
1091 this (string aBaseDir
) {
1092 import std
.algorithm
: sort
;
1093 import std
.file
: DirEntry
, SpanMode
, dirEntries
;
1094 import std
.path
: absolutePath
, baseName
;
1096 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1098 } else if (aBaseDir
== "/") {
1101 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1104 basePath
= aBaseDir
.absolutePath
;
1105 toxDataDiskName
= basePath
~"toxdata.tox";
1106 protoOpts
.txtunser(VFile(basePath
~"proto.rc"));
1107 info
.txtunser(VFile(basePath
~"config.rc"));
1110 GroupOptions
[] glist
;
1111 glist
.txtunser(VFile(basePath
~"contacts/groups.rc"));
1112 bool hasDefaultGroup
= false;
1113 bool hasMoronsGroup
= false;
1114 foreach (ref GroupOptions gi
; glist
[]) {
1115 auto g
= new Group(this);
1118 foreach (ref gg
; groups
[]) if (gg
.gid
== g
.gid
) { delete gg
; gg
= g
; found
= true; }
1119 if (!found
) groups
~= g
;
1120 if (g
.gid
== 0) hasDefaultGroup
= true;
1121 if (g
.gid
== g
.gid
.max
) hasMoronsGroup
= true;
1124 // create default group if necessary
1125 if (!hasDefaultGroup
) {
1128 gi
.name
= "default";
1129 gi
.note
= "default group for new contacts";
1131 auto g
= new Group(this);
1136 // create morons group if necessary
1137 if (!hasMoronsGroup
) {
1139 gi
.gid
= gi
.gid
.max
;
1140 gi
.name
= "<morons>";
1141 gi
.note
= "group for completely ignored dumbfucks";
1143 gi
.showOffline
= TriOption
.No
;
1144 gi
.showPopup
= TriOption
.No
;
1145 gi
.blinkActivity
= TriOption
.No
;
1146 gi
.skipUnread
= TriOption
.Yes
;
1147 gi
.hideIfNoVisibleMembers
= TriOption
.Yes
;
1148 gi
.ftranAllowed
= TriOption
.No
;
1149 gi
.resendRotDays
= 0;
1151 auto g
= new Group(this);
1157 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
1159 if (!hasDefaultGroup ||
!hasMoronsGroup
) saveGroups();
1162 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
1163 if (de.name
.baseName
== "." ||
de.name
.baseName
== "..") continue;
1165 import std
.file
: exists
;
1166 if (!de.isDir
) continue;
1167 string cfgfn
= de.name
~"/config.rc";
1168 if (!cfgfn
.exists
) continue;
1170 ci
.txtunser(VFile(cfgfn
));
1171 auto c
= new Contact(this);
1172 c
.diskFileName
= cfgfn
;
1174 contacts
[c
.info
.pubkey
] = c
;
1175 c
.loadResendQueue();
1176 c
.loadUnreadCount();
1177 // fix contact group
1178 if (groupById
!false(c
.gid
) is null) {
1179 c
.info
.gid
= 0; // move to default group
1182 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1183 } catch (Exception e
) {
1184 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
1189 assert(toxpk
.isValidKey
, "something is VERY wrong here");
1190 conwriteln("created ToxCore for [", tox_hex(toxpk
[]), "]");
1194 if (toxpk
.isValidKey
) {
1195 toxCoreCloseAccount(toxpk
);
1196 toxpk
[] = toxCoreEmptyKey
[];
1200 // will not write contact to disk
1201 Contact
createEmptyContact () {
1202 auto c
= new Contact(this);
1204 c
.info
.nick
= "test contact";
1205 c
.info
.pubkey
[] = 0;
1209 // returns `null` if there is no such group, and `dofail` is `true`
1210 inout(Group
) groupById(bool dofail
=true) (uint agid
) inout nothrow @nogc {
1211 foreach (const Group g
; groups
) if (g
.gid
== agid
) return cast(typeof(return))g
;
1212 static if (dofail
) assert(0, "group not found"); else return null;
1215 int opApply () (scope int delegate (ref Contact ct
) dg
) {
1216 foreach (Contact ct
; contacts
.byValue
) if (auto res
= dg(ct
)) return res
;
1221 static Account
CreateNew (string aBaseDir
, string aAccName
) {
1222 import std
.file
: mkdirRecurse
;
1223 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1225 } else if (aBaseDir
== "/") {
1228 mkdirRecurse(aBaseDir
);
1229 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1231 mkdirRecurse(aBaseDir
~"/contacts");
1232 // write protocol options
1235 popt
.txtser(VFile(aBaseDir
~"proto.rc", "w"), skipstname
:true);
1240 acc
.nick
= aAccName
;
1241 acc
.showPopup
= true;
1242 acc
.blinkActivity
= true;
1243 acc
.hideEmptyGroups
= false;
1244 acc
.ftranAllowed
= true;
1245 acc
.resendRotDays
= 4;
1247 acc
.txtser(VFile(aBaseDir
~"config.rc", "w"), skipstname
:true);
1249 // create default group
1251 GroupOptions
[1] grp
;
1253 grp
[0].name
= "default";
1254 grp
[0].opened
= true;
1255 //grp[0].hideIfNoVisible = TriOption.Yes;
1256 grp
[].txtser(VFile(aBaseDir
~"contacts/groups.rc", "w"), skipstname
:true);
1259 return new Account(aBaseDir
);
1264 // ////////////////////////////////////////////////////////////////////////// //
1265 class ListItemBase
{
1270 bool mVisible
= true;
1273 void setupFont () => nvg
.fontFace
= "ui"; // setup font face for this item
1274 @property int height () => cast(int)nvg
.textFontHeight
;
1275 @property bool visible () => mVisible
;
1276 bool onMouse (MouseEvent event
) => false; // true: eaten
1277 bool onKey (KeyEvent event
) => false; // true: eaten
1278 void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {} // real rect is scissored
1280 @property Account
ownerAcc () => null;
1284 class ListItemAccount
: ListItemBase
{
1289 this (Account aAcc
) { assert(aAcc
!is null); acc
= aAcc
; }
1291 override @property Account
ownerAcc () => acc
;
1293 override void setupFont () => nvg
.fontFace
= "uib"; // bold
1295 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1299 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1300 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
1303 final switch (acc
.status
) {
1304 case ContactStatus
.Connecting
: nvg
.fillColor(NVGColor
.k8orange
); break;
1305 case ContactStatus
.Offline
: nvg
.fillColor(NVGColor("#f00")); break;
1306 case ContactStatus
.Online
: nvg
.fillColor(NVGColor("#fff")); break;
1307 case ContactStatus
.Away
: nvg
.fillColor(NVGColor("#7557C7")); break;
1308 case ContactStatus
.Busy
: nvg
.fillColor(NVGColor("#0057C7")); break;
1310 if (acc
.isConnecting
) nvg
.fillColor(NVGColor
.k8orange
);
1311 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1312 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1313 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, acc
.info
.nick
);
1318 class ListItemGroup
: ListItemBase
{
1323 this (Group aGroup
) { assert(aGroup
!is null); group
= aGroup
; }
1325 override @property Account
ownerAcc () => group
.acc
;
1327 override @property bool visible () => group
.visible
;
1329 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1333 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1334 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
1337 nvg
.fillColor(selected ? NVGColor
.white
: NVGColor
.yellow
);
1338 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1339 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1340 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, group
.name
);
1345 class ListItemContact
: ListItemBase
{
1350 this (Contact aCt
) { assert(aCt
!is null); ct
= aCt
; }
1352 override @property Account
ownerAcc () => ct
.acc
;
1354 override @property bool visible () => ct
.visible
;
1356 override @property int height () { import std
.algorithm
: max
; return max(cast(int)nvg
.textFontHeight
, 16); } //FIXME: 16 is image height
1358 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1363 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1364 nvg
.fillColor(NVGColor("#9600"));
1372 if (ct
.unreadCount
== 0) icon
= statusImgId
[ct
.status
]; else icon
= kittyMsg
;
1373 nvg
.imageSize(icon
, iw
, ih
);
1374 //conwriteln("image: (", iw, "x", ih, ")");
1375 nvg
.rect(x0
, y0
+(hgt
-ih
)/2, iw
, ih
);
1376 nvg
.fillPaint(nvg
.imagePattern(x0
, y0
+(hgt
-ih
)/2, iw
, ih
, 0, icon
));
1379 nvg
.fillColor(NVGColor("#f70"));
1380 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1381 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1382 //conwriteln(nvg.textFontDescender);
1383 nvg
.text(x0
+4+iw
+4, y0
+hgt
+cast(int)nvg
.textFontDescender
, ct
.displayNick
);
1388 // ////////////////////////////////////////////////////////////////////////// //
1389 // visible contact list
1391 private import core
.time
;
1394 int mActiveItem
= -1; // active item (may be different from selected with cursor)
1396 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
1397 //MonoTime mLastClick = MonoTime.zero;
1398 //int mLastClickItem = -1;
1401 ListItemBase
[] items
;
1406 // the first one is "main"; can return `null`
1407 Account
mainAccount () {
1408 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) return lc
.acc
;
1412 // the first one is "main"; can return `null`
1413 Account
accountByPK (in ref PubKey pk
) {
1414 foreach (ListItemBase li
; items
) {
1415 if (auto lc
= cast(ListItemAccount
)li
) {
1416 if (lc
.acc
.toxpk
[] == pk
[]) return lc
.acc
;
1422 void forEachAccount (scope void delegate (Account acc
) dg
) {
1423 if (dg
is null) return;
1424 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) dg(lc
.acc
);
1427 void opOpAssign(string op
:"~") (ListItemBase li
) {
1428 if (li
is null) return;
1433 //nvg.fontFace = "ui";
1436 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1437 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1440 void removeAccount (Account acc
) {
1441 if (acc
is null) return;
1442 usize pos
= 0, dest
= 0;
1443 while (pos
< items
.length
) {
1444 if (items
[pos
].ownerAcc
!is acc
) {
1445 if (pos
!= dest
) items
[dest
++] = items
[pos
];
1449 items
.length
= dest
;
1452 void buildAccount (Account acc
) {
1453 if (acc
is null) return;
1455 items
~= new ListItemAccount(acc
);
1457 scope(exit
) delete css
;
1458 foreach (Group g
; acc
.groups
) {
1459 items
~= new ListItemGroup(g
);
1461 css
.assumeSafeAppend
;
1462 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
1463 import std
.algorithm
: sort
;
1465 string s0
= a
.displayNick
;
1466 string s1
= b
.displayNick
;
1467 auto xlen
= (s0
.length
< s1
.length ? s0
.length
: s1
.length
);
1468 foreach (immutable idx
, char c0
; s0
[0..xlen
]) {
1469 if (c0
>= 'A' && c0
<= 'Z') c0
+= 32; // poor man's tolower()
1471 if (c1
>= 'A' && c1
<= 'Z') c1
+= 32; // poor man's tolower()
1472 if (auto d
= c0
-c1
) return (d
< 0);
1474 return (s0
.length
< s1
.length
);
1476 foreach (Contact c
; css
) items
~= new ListItemContact(c
);
1481 // should be called after clist was drawn at least once
1482 int itemAtY (int aty
) {
1483 if (aty
< 0 || mLastHeight
< 1 || aty
>= mLastHeight
) return -1;
1485 scope(exit
) nvg
.restore();
1487 foreach (immutable iidx
, ListItemBase li
; items
) {
1488 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
1491 if (lh
< 1) continue;
1492 if (aty
< lh
) return cast(int)iidx
;
1498 // if called with `null` ct, deactivate
1499 void delegate (Contact ct
) onActivateContactCB
;
1502 bool onMouse (MouseEvent event
) {
1503 if (mLastWidth
< 1 || mLastHeight
< 1) return false;
1504 int mx
= event
.x
-mLastX
;
1505 int my
= event
.y
-mLastY
;
1506 if (mx
< 0 || my
< 0 || mx
>= mLastWidth || my
>= mLastHeight
) return false;
1507 if (event
== "LMB-Down") {
1508 int it
= itemAtY(my
);
1509 if (it
>= 0 && it
!= mActiveItem
) {
1510 if (auto ci
= cast(ListItemContact
)items
[it
]) {
1512 if (onActivateContactCB
!is null) onActivateContactCB(ci
.ct
);
1513 glconPostScreenRepaint();
1521 bool onKey (KeyEvent event
) {
1525 // real rect is scissored
1526 void drawAt (int x0
, int y0
, int wdt
, int hgt
) {
1532 scope(exit
) nvg
.restore();
1535 foreach (immutable iidx
, ListItemBase li
; items
) {
1536 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
1539 if (lh
< 1) continue;
1541 scope(exit
) nvg
.restore();
1543 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
1544 li
.drawAt(x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
1546 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
1547 nvg
.intersectScissor(0, 0, wdt
, lh
);
1548 li
.drawAt(0, 0, wdt
);
1551 if (y
>= hgt
) break;
1557 // ////////////////////////////////////////////////////////////////////////// //
1558 __gshared CList clist
;
1559 __gshared Contact activeContact
;
1562 void loadAccount (string nick
) {
1566 acc
= new Account(nick
);
1567 } catch (Exception e
) {
1568 conwriteln("creating account...");
1569 assert(0, "not yet");
1570 acc
= Account
.CreateNew("_fakeacc", "ketmar");
1572 // create fake contact
1573 if (acc
.contacts
.length
== 0) {
1574 conwriteln("creating fake contact...");
1575 auto c
= acc
.createEmptyContact();
1576 c
.info
.nick
= "test contact";
1577 c
.info
.pubkey
[] = 0x55;
1581 clist
.buildAccount(acc
);
1585 // ////////////////////////////////////////////////////////////////////////// //
1587 void doActivateContact (Contact ct
) {
1588 if (activeContact
is ct
) return;
1593 //conwriteln("clear log");
1594 if (clist
!is null) clist
.mActiveItem
= -1;
1595 glconPostScreenRepaint();
1597 //conwriteln("activated contact <", ct.info.nick, ">: [", tox_hex(ct.info.pubkey), "]");
1599 ct
.loadLogInto(log
);
1600 auto mcount
= cast(int)log
.messages
.length
;
1601 int left
= ct
.hmcOnOpen
;
1602 if (left
< ct
.unreadCount
) left
= ct
.unreadCount
;
1603 if (left
> mcount
) left
= mcount
;
1604 if (mcount
> left
) mcount
= left
;
1606 foreach (const ref msg
; log
.messages
[$-mcount
..$]) {
1607 if (left
== ct
.unreadCount
) addDividerLine();
1608 addTextToLog(ct
.acc
, ct
, msg
);
1612 if (ct
.unreadCount
!= 0) { ct
.unreadCount
= 0; ct
.saveUnreadCount(); }
1619 //FIXME: scan all accounts
1620 void fixTrayIcon () {
1621 if (clist
is null) return;
1622 auto acc
= clist
.mainAccount
;
1623 if (acc
is null) return;
1625 foreach (Contact ct
; acc
) unc
+= ct
.unreadCount
;
1627 import std
.format
: format
;
1629 setHint("unread: %d".format(unc
));
1631 setTrayStatus(acc
.status
);
1632 final switch (acc
.status
) {
1633 case ContactStatus
.Connecting
: setHint("connecting..."); break;
1634 case ContactStatus
.Offline
: setHint("offline"); break;
1635 case ContactStatus
.Online
: setHint("online"); break;
1636 case ContactStatus
.Away
: setHint("away"); break;
1637 case ContactStatus
.Busy
: setHint("busy"); break;
1643 void fixUnreadIndicators () {
1644 if (!mainWindowVisible ||
!mainWindowActive
) return; // nothing to do
1645 if (activeContact
is null || activeContact
.unreadCount
== 0) return; // nothing to do
1646 activeContact
.unreadCount
= 0;
1647 activeContact
.unreadCount
= 0;
1648 activeContact
.saveUnreadCount();
1653 // ////////////////////////////////////////////////////////////////////////// //
1654 //__gshared string accountNameToLoad = "_fakeacc";
1655 __gshared string accountNameToLoad
= "";
1656 __gshared string globalHotkey
= "M-H-F";
1659 void main (string
[] args
) {
1660 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
1662 conRegVar
!accountNameToLoad("starting_account", "account to load");
1664 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
1666 glconShowKey
= "M-Grave";
1667 glconSetAndSealFPS(0); // draw-on-demand
1669 conProcessQueue(256*1024); // load config
1670 conProcessArgs
!true(args
);
1671 conProcessQueue(256*1024);
1673 if (accountNameToLoad
.length
== 0) assert(0, "no account to load");
1675 //setOpenGLContextVersion(3, 2); // up to GLSL 150
1676 setOpenGLContextVersion(2, 0); // it's enough
1681 NVGPathSet svp
= null;
1683 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
1684 sdpyWindowClass
= "BIOACID";
1685 auto sdmain
= new SimpleWindow(800, 600, "BioAcid", OpenGlOptions
.yes
, Resizability
.allowResizing
);
1686 glconCtlWindow
= sdmain
;
1688 sdmain
.visibilityChanged
= delegate (bool vis
) { mainWindowVisible
= vis
; fixUnreadIndicators(); };
1689 sdmain
.onFocusChange
= delegate (bool focused
) { mainWindowActive
= focused
; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
1692 if (globalHotkey
.length
> 0) {
1693 GlobalHotkeyManager
.register(globalHotkey
, delegate () { concmd("win_toggle"); glconPostDoConCommands
!true(); });
1695 } catch (Exception e
) {
1696 conwriteln("ERROR registering hotkey!");
1700 if (sdmain
!is null) sdmain
.close();
1701 })("quit", "quit BioAcid");
1704 if (sdmain
!is null && !sdmain
.closed
) {
1705 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
1706 if (!mainWindowVisible
) {
1707 // this strange code brings window to the current desktop it if was on a different one
1710 } else if (sdmain
.visible
) {
1717 })("win_toggle", "show/hide main window");
1719 sdmain
.addEventListener((GLConScreenRepaintEvent evt
) {
1720 if (sdmain
.closed
) return;
1721 if (isQuitRequested
) { sdmain
.close(); return; }
1722 sdmain
.redrawOpenGlSceneNow();
1725 sdmain
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
1726 glconProcessEventMessage();
1729 sdmain
.addEventListener((PopupCheckerEvent evt
) {
1730 popupCheckExpirations();
1734 // ////////////////////////////////////////////////////////////////////// //
1736 sdmain
.addEventListener((ToxEventBase evt
) {
1737 auto acc
= clist
.accountByPK(evt
.self
);
1739 if (acc
is null) return;
1741 bool fixTray
= false;
1742 scope(exit
) { if (fixTray
) fixTrayIcon(); glconPostScreenRepaint(); }
1745 if (auto e
= cast(ToxEventConnection
)evt
) {
1746 if (e
.who
[] == acc
.toxpk
[]) {
1747 if (e
.connected
) acc
.toxConnectionEstablished(); else acc
.toxConnectionDropped();
1750 if (!e
.connected
) acc
.toxFriendOffline(e
.who
);
1755 if (auto e
= cast(ToxEventStatus
)evt
) {
1756 if (e
.who
[] == acc
.toxpk
[]) {
1757 acc
.toxSelfStatus(e
.status
);
1760 acc
.toxFriendStatus(e
.who
, e
.status
);
1765 if (auto e
= cast(ToxEventStatusMsg
)evt
) {
1766 if (e
.who
[] != acc
.toxpk
[]) {
1767 acc
.toxFriendStatusMessage(e
.who
, e
.message
);
1771 // incoming text message?
1772 if (auto e
= cast(ToxEventMessage
)evt
) {
1773 if (e
.who
[] != acc
.toxpk
[]) {
1774 acc
.toxFriendMessage(e
.who
, e
.action
, e
.message
, e
.time
);
1779 // ack outgoing text message?
1780 if (auto e
= cast(ToxEventMessageAck
)evt
) {
1781 if (e
.who
[] != acc
.toxpk
[]) {
1782 acc
.toxMessageAck(e
.who
, e
.msgid
);
1787 //glconProcessEventMessage();
1791 // ////////////////////////////////////////////////////////////////////// //
1792 sdmain
.onClosing
= delegate () {
1793 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
1796 sdmain
.setAsCurrentOpenGlContext();
1797 scope(exit
) { flushGui(); sdmain
.releaseCurrentOpenGlContext(); }
1801 assert(nvg
is null);
1802 if (sdhint
!is null) sdhint
.close();
1803 if (trayicon
!is null) trayicon
.close();
1806 sdmain
.closeQuery
= delegate () {
1808 glconPostDoConCommands
!true();
1812 sdmain
.visibleForTheFirstTime
= delegate () {
1813 if (sdmain
.width
> 1 && optCListWidth
< 0) optCListWidth
= sdmain
.width
/5;
1814 sdmain
.setAsCurrentOpenGlContext(); // make this window active
1815 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
1816 sdmain
.vsync
= false;
1818 glconInit(sdmain
.width
, sdmain
.height
);
1820 nvg
= nvgCreateContext(NVGContextFlag
.Antialias
, NVGContextFlag
.StencilStrokes
, NVGContextFlag
.FontNoAA
);
1821 if (nvg
is null) assert(0, "cannot initialize NanoVG");
1825 static immutable skullsPng
= /*cast(immutable(ubyte)[])*/import("data/skulls.png");
1826 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
1827 auto xi
= loadImageFromMemory(skullsPng
[]);
1828 scope(exit
) delete xi
;
1829 //{ import core.stdc.stdio; printf("creating background image...\n"); }
1830 nvgSkullsImg
= nvg
.createImageFromMemoryImage(xi
, NVGImageFlags
.NoFiltering
, NVGImageFlags
.RepeatX
, NVGImageFlags
.RepeatY
);
1831 //{ import core.stdc.stdio; printf("background image created\n"); }
1832 if (!nvgSkullsImg
.valid
) assert(0, "cannot load background image");
1833 } catch (Exception e
) {
1834 assert(0, "cannot load background image");
1836 buildStatusImages();
1840 lay
= new LayTextClass(laf
, sdmain
.width
/2);
1841 lay
.fontStyle
.fontsize
= 16;
1842 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1843 lay
.fontStyle
.monospace
= true;
1844 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
1845 lay
.put("ketmar (бля)");
1846 lay
.fontStyle
.monospace
= false;
1847 lay
.putHardSpace(64);
1851 lay
.put("2018/02/12");
1853 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
1854 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
1855 lay
.put("11:09:03");
1857 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
1858 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1859 lay
.fontStyle
.fontsize
= 20;
1860 lay
.put("this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1863 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
1864 lastWindowWidth
= sdmain
.width
;
1866 clist
= new CList();
1867 loadAccount(accountNameToLoad
);
1868 //clist.buildAccount(acc);
1869 clist
.onActivateContactCB
= delegate (Contact ct
) { doActivateContact(ct
); };
1871 sdmain
.setMinSize(640, 480);
1874 //sdmain.redrawOpenGlSceneNow();
1877 sdmain
.windowResized
= delegate (int wdt
, int hgt
) {
1878 if (sdmain
.closed
) return;
1879 glconResize(wdt
, hgt
);
1880 glconPostScreenRepaint
/*Delayed*/();
1881 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
1882 if (wdt
> 1 && optCListWidth
> 0 && lastWindowWidth
> 0 && lastWindowWidth
!= wdt
) {
1883 immutable double frc
= lastWindowWidth
/optCListWidth
;
1884 optCListWidth
= cast(int)(wdt
/frc
);
1885 if (optCListWidth
< 64) optCListWidth
= 64;
1886 lastWindowWidth
= wdt
;
1892 int mouseX
= -666, mouseY
= -666;
1895 sdmain
.handleKeyEvent
= delegate (KeyEvent event
) {
1896 if (sdmain
.closed
) return;
1897 scope(exit
) glconPostDoConCommands
!true();
1898 if (glconKeyEvent(event
)) return;
1900 auto acc
= clist
.mainAccount
;
1902 if (event
== "D-Escape") {
1903 if (sdmain
!is null && !sdmain
.closed
&& sdmain
.visible
) {
1910 if (event
== "D-C-Q") { concmd("quit"); return; }
1911 if (event
== "D-C-1") { acc
.status
= ContactStatus
.Online
; return; }
1912 if (event
== "D-C-2") { acc
.status
= ContactStatus
.Away
; return; }
1913 if (event
== "D-C-0") { acc
.status
= ContactStatus
.Offline
; return; }
1915 if (event
== "D-C-W") { doActivateContact(null); return; }
1917 if (clist
!is null && clist
.onKey(event
)) return;
1919 if (event
== "D-C-S-Enter") {
1920 static PopupWindow
.Kind kind
;
1921 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1922 showPopup(kind
, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1923 if (kind
== PopupWindow
.Kind
.max
) kind
= PopupWindow
.Kind
.min
; else ++kind
;
1927 if (activeContact
!is null) {
1928 if (event
== "D-Enter") {
1929 auto text
= activeContact
.edit
.text
;
1930 activeContact
.edit
.clear();
1931 activeContact
.send(text
);
1932 glconPostScreenRepaint();
1935 if (activeContact
.edit
.onKey(event
)) return;
1939 if (event == "D-Space") {
1941 lay.fontStyle.fontsize = 16;
1942 lay.fontStyle.color = NVGColor.k8orange.asUint;
1943 lay.fontStyle.monospace = true;
1944 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1946 lay.fontStyle.monospace = false;
1947 lay.putHardSpace(64);
1951 lay.put("2018/02/12");
1953 lay.fontStyle.color = NVGColor("#fff").asUint;
1954 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1955 lay.put("11:09:03");
1957 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1958 lay.fontStyle.color = NVGColor.k8orange.asUint;
1959 lay.fontStyle.fontsize = 20;
1963 foreach (immutable widx; 0..uniform!"[]"(2, 28)) {
1965 if (widx != 0) lay.put(" ");
1966 foreach (; 0..uniform!"[]"(1, 9)) {
1967 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1969 //lay.putSoftHypen();
1973 // long word, for hyphenation test
1974 foreach (immutable idx; 0..228) {
1975 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1977 if (idx%24 == 23) lay.putSoftHypen();
1985 glconPostScreenRepaint();
1991 int msLastPressX
= -666, msLastPressY
= -666;
1992 bool msDoLogButton
= false;
1994 sdmain
.handleMouseEvent
= delegate (MouseEvent event
) {
1995 if (sdmain
.closed
) return;
1996 scope(exit
) glconPostDoConCommands
!true();
1997 if (isConsoleVisible
) return;
2002 //FIXME: process it here, not in renderer
2003 if (event
== "LMB-Down") {
2004 msLastPressX
= mouseX
;
2005 msLastPressY
= mouseY
;
2006 msDoLogButton
= true;
2007 glconPostScreenRepaint();
2010 if (clist
!is null) {
2011 sdmain
.setAsCurrentOpenGlContext(); // make this window active
2012 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
2014 bool inFrame
= nvg
.inFrame
;
2015 if (!inFrame
) nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
2016 scope(exit
) if (!inFrame
) nvg
.endFrame();
2018 if (clist
.onMouse(event
)) { /*glconPostScreenRepaint();*/ return; }
2020 /*if (svp is null)*/ { glconPostScreenRepaint(); return; } // mouse motion
2023 sdmain
.handleCharEvent
= delegate (dchar ch
) {
2024 if (sdmain
.closed
) return;
2025 scope(exit
) glconPostDoConCommands
!true();
2026 if (glconCharEvent(ch
)) return;
2028 if (activeContact
!is null) {
2029 if (activeContact
.edit
.onChar(ch
)) return;
2034 sdmain
.redrawOpenGlScene
= delegate () {
2035 glconPostDoConCommands
!true();
2036 if (sdmain
.closed
) return;
2038 scope(exit
) glconDraw();
2040 if (nvg
is null) return;
2041 sdmain
.setAsCurrentOpenGlContext(); // make this window active
2042 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
2044 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
2045 glViewport(0, 0, sdmain
.width
, sdmain
.height
);
2046 glMatrixMode(GL_MODELVIEW
);
2047 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
2049 glClearColor(0, 0, 0, 0);
2050 glClear(glNVGClearFlags
/*|GL_COLOR_BUFFER_BIT*/);
2053 nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
2054 scope(exit
) nvg
.endFrame();
2056 if (clist
!is null && optCListWidth
> 0) {
2057 // draw contact list
2060 int wdt
= optCListWidth
;
2061 int hgt
= nvg
.height
-cy
*2;
2063 nvg
.shapeAntiAlias
= true;
2065 nvg
.strokeWidth
= 1;
2069 scope(exit
) nvg
.restore();
2072 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
2075 nvg
.imageSize(nvgSkullsImg
, w
, h
);
2076 nvg
.fillPaint(nvg
.imagePattern(0, 0, w
, h
, 0, nvgSkullsImg
));
2078 nvg
.strokeColor(NVGColor("#f70"));
2082 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
2084 clist
.drawAt(cx
+3, cy
+3, wdt
-3*2, hgt
-3*2);
2089 scope(exit
) nvg
.restore();
2091 //nvg.transform(NVGMatrix.init);
2093 auto xf = nvg.currTransform;
2094 nvg.currTransform = xf;
2099 wdt
= nvg
.width
-cx
-1;
2102 // calculate editor dimensions and draw editor
2103 if (activeContact
!is null) {
2105 scope(exit
) nvg
.restore();
2107 auto edinfo
= activeContact
.edit
.calcHeight(nvg
, wdt
-3*2);
2108 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
2110 int edy
= cy
+hgt
-cast(int)edinfo
.height
-3*2;
2113 nvg
.roundedRect(cx
+0.5f, edy
+0.5f, wdt
, edinfo
.height
+3*2, 6);
2114 nvg
.fillColor(NVGColor
.black
);
2115 nvg
.strokeColor(NVGColor("#f70"));
2119 nvg
.intersectScissor(cx
+3.5f, edy
+3.5f, wdt
-3*2, edinfo
.height
);
2120 activeContact
.edit
.draw(nvg
, cx
+3, edy
+3, wdt
-3*2, cast(int)edinfo
.height
);
2122 hgt
-= cast(int)edinfo
.height
+3*2;
2126 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
2127 nvg
.fillColor(NVGColor
.black
);
2128 nvg
.strokeColor(NVGColor("#f70"));
2132 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
2134 immutable float scalex
= (wdt
-3*2-10*2)/BaphometDims
;
2135 immutable float scaley
= (baphHgt
-3*2-10*2)/BaphometDims
;
2136 immutable float scale
= (scalex
< scaley ? scalex
: scaley
)/1.5f;
2137 immutable float sz
= BaphometDims
*scale
;
2138 nvg
.strokeColor(NVGColor("#400"));
2139 nvg
.fillColor(NVGColor("#400"));
2140 nvg
.renderBaphomet(cx
+10.5f+(wdt
-3*2-10*2)/2-sz
/2, cy
+10.5f+(baphHgt
-3*2-10*2)/2-sz
/2, scale
, scale
);
2143 immutable float sbx
= cx
+wdt
-BND_SCROLLBAR_WIDTH
-1.5f;
2144 immutable float sby
= cy
+3.5f;
2145 wdt
-= BND_SCROLLBAR_WIDTH
+1;
2149 lay
.relayout(wdt
); // this is harmess if width wasn't changed
2150 int ty
= lay
.textHeight
-hgt
+1;
2153 nvg
.bndScrollSlider(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, 0.0f, (lay
.textHeight
> 0 ? ty
+hgt
/cast(float)lay
.textHeight
: 1.0f));
2155 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
, hgt
);
2156 nvg
.drawLayouter(lay
, ty
, cx
+3, cy
+3, hgt
);
2158 if (msDoLogButton
) {
2159 msDoLogButton
= false;
2160 auto widx
= lay
.wordAtXY(msLastPressX
-(cx
+3), ty
+(msLastPressY
-(cy
+3)));
2162 if (auto hr
= cast(uint)widx
in layUrlList
) {
2163 conwriteln("URL CLICK: <", hr
.url
);
2174 sdmain
.eventLoop(15000,
2175 // pulser: process resend queues here
2177 if (sdmain
.closed || clist
is null) return;
2178 clist
.forEachAccount(delegate (Account acc
) { acc
.processResendQueue(); });
2181 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
2182 clist
.forEachAccount(delegate (Account acc
) { acc
.saveResendQueue(); acc
.saveResendQueue(); });
2183 toxCoreShutdownAll();
2186 conProcessQueue(int.max
/4);