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
;
198 __gshared
int layWinHeight
= 0;
199 __gshared
int layOffset
= 0; // from bottom
201 __gshared
int optCListWidth
= -1;
202 __gshared
int lastWindowWidth
= -1;
204 __gshared NVGImage
[5] statusImgId
;
207 shared static ~this () {
208 //{ import core.stdc.stdio; printf("******************************\n"); }
209 nvgSkullsImg
.clear();
210 //{ import core.stdc.stdio; printf("---\n"); }
211 foreach (ref img
; statusImgId
[]) img
.clear();
217 void buildStatusImages () {
219 statusImgId
[ContactStatus
.Offline
] = nvg
.createImageRGBA(16, 16, ctiOffline
[], NVGImageFlags
.NoFiltering
);
220 statusImgId
[ContactStatus
.Online
] = nvg
.createImageRGBA(16, 16, ctiOnline
[], NVGImageFlags
.NoFiltering
);
221 statusImgId
[ContactStatus
.Away
] = nvg
.createImageRGBA(16, 16, ctiAway
[], NVGImageFlags
.NoFiltering
);
222 statusImgId
[ContactStatus
.Connecting
] = nvg
.createImageRGBA(16, 16, ctiOffline
[], NVGImageFlags
.NoFiltering
);
224 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
225 statusImgId
[ContactStatus
.Offline
] = nvg
.createImageRGBA(16, 16, baph16Gray
[], NVGImageFlags
.NoFiltering
);
226 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
227 statusImgId
[ContactStatus
.Online
] = nvg
.createImageRGBA(16, 16, baph16Online
[], NVGImageFlags
.NoFiltering
);
228 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
229 statusImgId
[ContactStatus
.Away
] = nvg
.createImageRGBA(16, 16, baph16Away
[], NVGImageFlags
.NoFiltering
);
230 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
231 statusImgId
[ContactStatus
.Busy
] = nvg
.createImageRGBA(16, 16, baph16Busy
[], NVGImageFlags
.NoFiltering
);
232 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
233 statusImgId
[ContactStatus
.Connecting
] = nvg
.createImageRGBA(16, 16, baph16Orange
[], NVGImageFlags
.NoFiltering
);
234 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
235 kittyOut
= nvg
.createImageRGBA(16, 16, kittyOutgoing
[], NVGImageFlags
.NoFiltering
);
236 kittyMsg
= nvg
.createImageRGBA(16, 16, kittyMessage
[], NVGImageFlags
.NoFiltering
);
241 // ////////////////////////////////////////////////////////////////////////// //
242 void loadFonts (NVGContext vg
) {
243 vg
.fonsContext
.fonsAddStashFonts(fstash
);
244 bndSetFont(vg
.findFont("ui"));
248 // ////////////////////////////////////////////////////////////////////////// //
249 class MessageStart
: LayObject
{
250 long msgid
; // >0: outgoing, unacked yet
252 this (long aid
=-1) nothrow @safe @nogc { msgid
= aid
; }
254 override int width () => 0;
255 override int spacewidth () => 0;
256 override int height () => 0;
257 override int ascent () => 0;
258 override int descent () => 0;
259 override bool canbreak () => true;
260 override bool spaced () => false;
262 override void draw (NVGContext ctx
, float x
, float y
) {}
266 class MessageOutMark
: LayObject
{
267 long msgid
; // >0: outgoing, unacked yet
269 this (long aid
=-1) nothrow @safe @nogc { msgid
= aid
; }
271 override int width () => kittyOut
.width
;
272 override int spacewidth () => 4;
273 override int height () => kittyOut
.height
;
274 override int ascent () => height
;
275 override int descent () => 0;
276 override bool canbreak () => true;
277 override bool spaced () => false;
279 override void draw (NVGContext ctx
, float x
, float y
) {
282 scope(exit
) nvg
.restore
;
284 nvg
.rect(x
+0.5, y
+0.5, width
, height
);
285 nvg
.fillPaint(nvg
.imagePattern(0, 0, width
, height
, 0, kittyOut
));
292 // ////////////////////////////////////////////////////////////////////////// //
293 static struct LayUrl
{
295 uint wordidx
; // first word
299 __gshared LayUrl
[uint] layUrlList
; // for each word
303 lay
.wipeAll(true); // clear log, but delete objects
309 void addDividerLine (bool doflushgui
=false) {
310 if (glconCtlWindow
is null || glconCtlWindow
.closed
) return;
313 bool inFrame
= nvg
.inFrame
;
315 glconCtlWindow
.setAsCurrentOpenGlContext(); // make this window active
316 nvg
.beginFrame(glconCtlWindow
.width
, glconCtlWindow
.height
);
321 if (doflushgui
) flushGui();
322 glconCtlWindow
.releaseCurrentOpenGlContext();
326 lay
.fontStyle
.fontsize
= 2;
327 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
328 lay
.fontStyle
.bgcolor
= NVGColor("#aa0").asUint
;
329 lay
.fontStyle
.monospace
= true;
336 glconPostScreenRepaint();
340 // `ct` can be `null` for "my message" or "system message"
341 void addTextToLog (Account acc
, Contact ct
, LogFile
.Msg
.Kind kind
, bool action
, const(char)[] msg
, SysTime time
, long msgid
=-1, bool doflushgui
=false) {
342 if (glconCtlWindow
is null || glconCtlWindow
.closed
) return;
345 bool inFrame
= nvg
.inFrame
;
347 glconCtlWindow
.setAsCurrentOpenGlContext(); // make this window active
348 nvg
.beginFrame(glconCtlWindow
.width
, glconCtlWindow
.height
);
353 if (doflushgui
) flushGui();
354 glconCtlWindow
.releaseCurrentOpenGlContext();
358 // add "message start" mark
359 lay
.putObject(new MessageStart(msgid
));
361 lay
.fontStyle
.fontsize
= 16;
362 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
363 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
364 lay
.fontStyle
.monospace
= true;
368 final switch (kind
) {
369 case LogFile
.Msg
.Kind
.Outgoing
: textColor
= NVGColor
.k8orange
; lay
.fontStyle
.color
= NVGColor("#c40").asUint
; lay
.put(acc
.info
.nick
); break;
370 case LogFile
.Msg
.Kind
.Incoming
: textColor
= NVGColor("#ccc"); lay
.fontStyle
.color
= NVGColor("#666").asUint
; lay
.put(ct
.info
.nick
); break;
371 case LogFile
.Msg
.Kind
.Notification
: textColor
= NVGColor("#0c0"); lay
.fontStyle
.color
= textColor
.asUint
; lay
.put("*system*"); break;
374 lay
.fontStyle
.monospace
= false;
375 //lay.putHardSpace(64);
377 // add "message outgoing" mark
378 if (kind
== LogFile
.Msg
.Kind
.Outgoing
&& msgid
> 0) {
380 //conwriteln("msgoutmark");
381 lay
.putObject(new MessageOutMark(msgid
));
387 import std
.format
: format
;
388 auto dt = cast(DateTime
)time
;
389 string tstr
= "%04u/%02u/%02u".format(dt.year
, dt.month
, dt.day
);
394 lay
.fontStyle
.color
= NVGColor("#aaa").asUint
;
395 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
398 import std
.format
: format
;
399 auto dt = cast(DateTime
)time
;
400 string tstr
= "%02u:%02u:%02u".format(dt.hour
, dt.minute
, dt.second
);
406 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
407 lay
.fontStyle
.fontsize
= 20;
410 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
414 void xput (const(char)[] str) {
416 auto nl
= str.indexOf('\n');
417 if (nl
< 0) { lay
.put(str); break; }
418 if (nl
> 0) lay
.put(str[0..nl
]);
424 msg
= msg
.xstripright
;
426 lay
.fontStyle
.color
= textColor
.asUint
;
428 auto nfo
= urlDetect(msg
);
434 xput(msg
[0..nfo
.pos
]);
435 string url
= msg
[nfo
.pos
..nfo
.end
].idup
;
436 msg
= msg
[nfo
.end
..$];
437 auto stword
= lay
.nextWordIndex
;
439 auto c
= lay
.fontStyle
.color
;
440 scope(exit
) { lay
.popStyles
; lay
.fontStyle
.color
= c
; }
441 lay
.fontStyle
.href
= true;
442 lay
.fontStyle
.underline
= true;
443 lay
.fontStyle
.color
= NVGColor("#06f").asUint
;
445 while (stword
< lay
.nextWordIndex
) {
446 layUrlList
[stword
] = LayUrl(url
, stword
);
455 glconPostScreenRepaint();
459 void addTextToLog (Account acc
, Contact ct
, in ref LogFile
.Msg msg
, long msgid
=-1, bool doflushgui
=false) {
460 import iv
.utfutil
: utf8Encode
;
463 scope(exit
) delete text
;
465 foreach (dchar dc
; msg
.byDChar
) {
467 auto len
= utf8Encode(buf
[], dc
);
470 addTextToLog(acc
, ct
, msg
.kind
, msg
.isMe
, text
, msg
.time
, msgid
, doflushgui
);
474 void ackLogMessage (long msgid
) {
475 if (msgid
<= 0) return;
476 foreach (immutable uint widx
; 0..lay
.wordCount
) {
477 auto w
= lay
.wordByIndex(widx
);
478 int oidx
= w
.objectIdx
;
480 if (auto maw
= cast(MessageOutMark
)lay
.objectAtIndex(oidx
)) {
481 if (maw
.msgid
== msgid
) {
482 maw
.msgid
= -1; // reset mark
483 glconPostScreenRepaint(); // redraw
491 // ////////////////////////////////////////////////////////////////////////// //
494 this (Account aOwner
) { acc
= aOwner
; }
497 bool mDirty
; // true: write contact's config
505 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
508 import std
.file
: mkdirRecurse
;
509 import std
.path
: dirName
;
511 assert(acc
!is null);
513 if (diskFileName
.length
== 0) {
514 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
516 mkdirRecurse(diskFileName
.dirName
);
517 if (serialize(info
, diskFileName
)) mDirty
= false;
520 @property bool visible () const nothrow @trusted @nogc {
521 if (!hideIfNoVisibleMembers
) return true; // always visible
522 // check if we have any visible members
523 foreach (const(Contact
) c
; acc
.contacts
.byValue
) {
524 if (c
.gid
!= info
.gid
) continue;
525 if (c
.visibleNoGroupCheck
) return true;
527 return false; // nobody's here
530 private bool getTriOpt(string
fld, string fld2
=null) () const nothrow @trusted @nogc {
531 enum lo
= "info."~fld;
532 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
533 static if (fld2
.length
) return mixin("acc.info."~fld2
); else return mixin("acc.info."~fld);
536 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
537 enum lo
= "info."~fld;
538 if (mixin(lo
) >= 0) return mixin(lo
);
539 return mixin("acc.info."~fld);
542 @property nothrow @safe {
543 uint gid () const pure @nogc => info
.gid
;
545 bool opened () const @nogc => info
.opened
;
546 void opened (bool v
) @nogc { pragma(inline
, true); if (info
.opened
!= v
) { info
.opened
= v
; markDirty(); } }
548 string
name () const @nogc => info
.name
;
549 void name (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unnamed>"; if (info
.name
!= v
) { info
.name
= v
; markDirty(); } }
551 string
note () const @nogc => info
.note
;
552 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
555 bool showOffline () const => getTriOpt
!"showOffline";
556 bool showPopup () const => getTriOpt
!"showPopup";
557 bool blinkActivity () const => getTriOpt
!"blinkActivity";
558 bool skipUnread () const => getTriOpt
!"skipUnread";
559 bool hideIfNoVisibleMembers () const => getTriOpt
!("hideIfNoVisibleMembers", "hideEmptyGroups");
560 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
561 int resendRotDays () const => getIntOpt
!"resendRotDays";
562 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
568 // ////////////////////////////////////////////////////////////////////////// //
569 final class Contact
{
571 // `Connecting` for non-account means "awaiting authorization"
574 bool isMe
; // "/me" message?
577 long msgid
; // ==0: unknown yet
580 MonoTime nextSendTime
;
584 this (Account aOwner
) { acc
= aOwner
; edit
= new MiniEdit(); }
587 bool mDirty
; // true: write contact's config
593 ContactStatus status
= ContactStatus
.Offline
; // not saved, so if it safe to change it
599 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
601 void loadUnreadCount () nothrow {
602 assert(diskFileName
.length
);
603 assert(acc
!is null);
605 import std
.path
: dirName
;
606 auto fi
= VFile(diskFileName
.dirName
~"/logs/unread.dat");
607 unreadCount
= fi
.readNum
!int;
608 } catch (Exception e
) {
613 void saveUnreadCount () nothrow {
614 assert(diskFileName
.length
);
615 assert(acc
!is null);
617 import std
.path
: dirName
;
618 auto fo
= VFile(diskFileName
.dirName
~"/logs/unread.dat", "w");
619 fo
.writeNum(unreadCount
);
620 } catch (Exception e
) {
624 void loadResendQueue () {
625 import std
.path
: dirName
;
626 string fname
= diskFileName
.dirName
~"/logs/resend.log";
629 auto ctt
= MonoTime
.currTime
;
630 foreach (const ref lmsg
; lf
.messages
) {
632 xmsg
.isMe
= lmsg
.isMe
;
633 xmsg
.time
= lmsg
.time
;
634 xmsg
.text
= lmsg
.text
;
636 xmsg
.nextSendTime
= ctt
;
641 void saveResendQueue () {
642 import std
.file
: mkdirRecurse
, remove
;
643 import std
.path
: dirName
;
644 assert(diskFileName
.length
);
645 assert(acc
!is null);
646 mkdirRecurse(diskFileName
.dirName
~"/logs");
647 string fname
= diskFileName
.dirName
~"/logs/resend.log";
648 try { remove(fname
); } catch (Exception e
) {}
649 if (resendQueue
.length
) {
650 foreach (const ref msg
; resendQueue
) {
651 LogFile
.appendLine(fname
, LogFile
.Msg
.Kind
.Outgoing
, msg
.text
, msg
.isMe
, msg
.time
);
657 import std
.file
: mkdirRecurse
;
658 import std
.path
: dirName
;
660 assert(acc
!is null);
662 if (diskFileName
.length
== 0) {
663 diskFileName
= acc
.basePath
~"/contacts/"~tox_hex(info
.pubkey
[])~"/config.rc";
664 acc
.contacts
[info
.pubkey
] = this;
666 mkdirRecurse(diskFileName
.dirName
);
667 mkdirRecurse(diskFileName
.dirName
~"/avatars");
668 mkdirRecurse(diskFileName
.dirName
~"/files");
669 mkdirRecurse(diskFileName
.dirName
~"/fileparts");
670 mkdirRecurse(diskFileName
.dirName
~"/logs");
672 if (serialize(info
, diskFileName
)) mDirty
= false;
676 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (showOffline || status
!= ContactStatus
.Offline || unreadCount
> 0);
678 @property bool visible () const nothrow @trusted @nogc {
679 if (unreadCount
> 0) return true;
680 if (!showOffline
&& status
== ContactStatus
.Offline
) return false;
681 auto grp
= acc
.groupById(gid
);
685 @property nothrow @safe {
686 ContactInfo
.Kind
kind () const @nogc => info
.kind
;
687 void kind (ContactInfo
.Kind v
) @nogc { pragma(inline
, true); if (info
.kind
!= v
) { info
.kind
= v
; markDirty(); } }
689 uint gid () const @nogc => info
.gid
;
690 void gid (uint v
) @nogc { pragma(inline
, true); if (info
.gid
!= v
) { info
.gid
= v
; markDirty(); } }
692 string
nick () const @nogc => info
.nick
;
693 void nick (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unknown>"; if (info
.nick
!= v
) { info
.nick
= v
; markDirty(); } }
695 string
visnick () const @nogc => info
.visnick
;
696 void visnick (string v
) @nogc { pragma(inline
, true); if (info
.visnick
!= v
) { info
.visnick
= v
; markDirty(); } }
698 string
displayNick () const @nogc => (info
.visnick
.length ? info
.visnick
: info
.nick
);
700 string
statusmsg () const @nogc => info
.statusmsg
;
701 void statusmsg (string v
) @nogc { pragma(inline
, true); if (info
.statusmsg
!= v
) { info
.statusmsg
= v
; markDirty(); } }
703 void setLastOnlineNow () {
705 auto ut
= Clock
.currTime
.toUTC().toUnixTime();
706 if (info
.lastonlinetime
!= cast(uint)ut
) { info
.lastonlinetime
= cast(uint)ut
; markDirty(); }
707 } catch (Exception e
) {}
710 string
note () const @nogc => info
.note
;
711 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
714 private bool getTriOpt(string
fld) () const nothrow @trusted @nogc {
715 enum lo
= "info.opts."~fld;
716 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
717 auto grp
= acc
.groupById(info
.gid
);
718 enum go
= "grp.info."~fld;
719 if (mixin(go
) != TriOption
.Default
) return (mixin(go
) == TriOption
.Yes
);
720 return mixin("acc.info."~fld);
723 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
724 enum lo
= "info.opts."~fld;
725 if (mixin(lo
) >= 0) return mixin(lo
);
726 auto grp
= acc
.groupById(info
.gid
);
727 enum go
= "grp.info."~fld;
728 if (mixin(go
) >= 0) return mixin(go
);
729 return mixin("acc.info."~fld);
732 @property nothrow @safe @nogc {
733 bool showOffline () const => getTriOpt
!"showOffline";
734 bool showPopup () const => getTriOpt
!"showPopup";
735 bool blinkActivity () const => getTriOpt
!"blinkActivity";
736 bool skipUnread () const => getTriOpt
!"skipUnread";
737 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
738 int resendRotDays () const => getIntOpt
!"resendRotDays";
739 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
742 void loadLogInto (ref LogFile lf
) {
743 import std
.file
: exists
;
744 import std
.path
: dirName
;
745 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
746 if (lname
.exists
) lf
.load(lname
); else lf
.clear();
749 void appendToLog (LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) {
750 import std
.path
: dirName
;
751 string lname
= diskFileName
.dirName
~"/logs/hugelog.log";
752 LogFile
.appendLine(lname
, kind
, text
, isMe
, time
);
755 void ackReceived (long msgid
) {
756 if (msgid
<= 0) return; // wtf?!
757 bool changed
= false;
759 while (idx
< resendQueue
.length
) {
760 if (resendQueue
[idx
].msgid
== msgid
) {
761 foreach (immutable c
; idx
+1..resendQueue
.length
) resendQueue
[c
-1] = resendQueue
[c
];
762 resendQueue
[$-1] = XMsg
.init
;
763 resendQueue
.length
-= 1;
764 resendQueue
.assumeSafeAppend
;
770 if (changed
) saveResendQueue();
773 void processResendQueue () {
774 if (status
== ContactStatus
.Offline || status
== ContactStatus
.Connecting
) return;
776 auto ctt
= MonoTime
.currTime
+30.seconds
;
777 foreach (ref XMsg msg
; resendQueue
) {
778 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, msg
.text
, msg
.isMe
);
779 if (msgid
< 0) break;
781 msg
.nextSendTime
= ctt
;
782 if (msg
.resendCount
++ != 0) doSave
= true;
784 if (doSave
) saveResendQueue();
787 void send (const(char)[] text
) {
788 void sendOne (const(char)[] text
, bool action
) {
789 if (text
.length
== 0) return; // just in case
791 SysTime now
= Clock
.currTime
;
792 long msgid
= toxCoreSendMessage(acc
.toxpk
, info
.pubkey
, text
, action
);
793 if (msgid
< 0) { conwriteln("ERROR sending message to '", info
.nick
, "'"); return; }
795 // add this to resend queue
800 xmsg
.nextSendTime
= MonoTime
.currTime
+30.seconds
;
801 xmsg
.resendCount
= 0;
805 import std
.format
: format
;
806 auto dt = cast(DateTime
)now
;
807 xmsg
.text
= "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year
, dt.month
, dt.day
, dt.hour
, dt.minute
, dt.second
, text
);
812 if (activeContact
is this) addTextToLog(acc
, this, LogFile
.Msg
.Kind
.Outgoing
, action
, text
, now
, msgid
);
813 appendToLog(LogFile
.Msg
.Kind
.Outgoing
, text
, action
, now
);
816 bool action
= text
.startsWith("/me ");
817 if (action
) text
= text
[4..$].xstripleft
;
819 while (text
.length
) {
820 auto ep
= text
.indexOf('\n');
821 if (ep
< 0) ep
= text
.length
; else ++ep
; // include '\n'
822 // remove line if it contains only spaces
823 bool hasNonSpace
= false;
824 foreach (immutable char ch
; text
[0..ep
]) if (ch
> ' ') { hasNonSpace
= true; break; }
825 if (hasNonSpace
) break;
828 while (text
.length
&& text
[$-1] <= ' ') text
= text
[0..$-1];
829 if (text
.length
== 0) return; // nothing to do
832 //TODO: split at word boundaries
833 enum ReservedSpace
= 23+3+3;
835 // k8: toxcore developers are idiots, so we have to do dynalloc here
836 auto tmpbuf
= new char[](tox_max_message_length()+64);
837 scope(exit
) delete tmpbuf
;
840 while (text
.length
) {
841 int epos
= tox_max_message_length()-ReservedSpace
;
842 if (epos
< text
.length
) {
845 if (text
[epos
-1] < 128) break;
846 if ((text
[epos
-1]&0xc0) == 0xc0) break;
850 epos
= cast(int)text
.length
;
853 if (first
&& epos
>= text
.length
) {
854 sendOne(text
[0..epos
], action
);
857 if (!first
) { tmpbuf
[0..3] = "..."; ofs
= 3; }
858 tmpbuf
[ofs
..ofs
+epos
] = text
[0..epos
];
859 tmpbuf
[ofs
+epos
..ofs
+epos
+3] = "...";
860 sendOne(tmpbuf
[0..ofs
+epos
+3], action
);
863 text
= text
[epos
..$];
871 // ////////////////////////////////////////////////////////////////////////// //
872 final class Account
{
875 import std
.algorithm
: sort
;
876 GroupOptions
[] glist
;
877 scope(exit
) delete glist
;
878 foreach (Group g
; groups
) glist
~= g
.info
;
879 glist
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
880 glist
.serialize(basePath
~"contacts/groups.rc");
884 ContactStatus mStatus
= ContactStatus
.Offline
;
887 PubKey toxpk
= toxCoreEmptyKey
;
888 string toxDataDiskName
;
889 string basePath
; // with trailing "/"
890 ProtoOptions protoOpts
;
893 Contact
[PubKey
] contacts
;
896 bool mIAmConnecting
= false;
897 bool mIAmOnline
= false;
898 bool forceOnline
= true; // set to `false` to stop autoreconnecting
899 //bool restoreOnline = false; // will be set to `true` if we need to reconnect
900 //bool doRefreshNicks = false;
903 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting
;
905 @property ContactStatus
status () const nothrow @safe @nogc {
906 if (!toxpk
.isValidKey
) return ContactStatus
.Offline
;
907 if (mIAmConnecting
) return ContactStatus
.Connecting
;
908 if (!mIAmOnline
) return ContactStatus
.Offline
;
912 @property bool isOnline () const nothrow @safe @nogc {
913 if (!toxpk
.isValidKey
) return false;
914 if (mIAmConnecting
) return false;
915 if (!mIAmOnline
) return false;
919 @property void status (ContactStatus v
) {
920 if (!toxpk
.isValidKey
) return;
921 conwriteln("changing status to ", v
, " (old: ", mStatus
, ")");
922 if (v
== ContactStatus
.Connecting
) v
= ContactStatus
.Online
;
923 if (v
== ContactStatus
.Offline
) {
928 if (mStatus
== ContactStatus
.Offline
) {
929 if (v
!= ContactStatus
.Offline
) mIAmConnecting
= true;
931 toxCoreSetStatus(toxpk
, v
);
933 glconPostScreenRepaint();
938 void processResendQueue () {
939 if (!isOnline
) return;
940 foreach (Contact ct
; contacts
.byValue
) ct
.processResendQueue();
943 void saveResendQueue () {
944 foreach (Contact ct
; contacts
.byValue
) ct
.saveResendQueue();
949 toxpk
= toxCoreOpenAccount(toxDataDiskName
);
950 if (!toxpk
.isValidKey
) {
951 conwriteln("creating new Tox account...");
952 string nick
= info
.nick
;
953 if (nick
.length
> 0) {
955 if (nick
.length
> tox_max_name_length()) nick
= nick
[0..tox_max_name_length()];
959 toxpk
= toxCoreCreateAccount(toxDataDiskName
, nick
);
960 if (!toxpk
.isValidKey
) throw new Exception("cannot create Tox account");
964 // load contacts from ToxCore data and add 'em to contact database
965 void toxLoadKnownContacts () {
966 if (!toxpk
.isValidKey
) return;
967 toxCoreForEachFriend(toxpk
, delegate (in ref PubKey self
, in ref PubKey frpub
, scope const(char)[] nick
) {
968 if (nick
.length
== 0) nick
= "<unknown>";
969 auto c
= (frpub
in contacts ? contacts
[frpub
] : null);
971 conwriteln("NEW friend with pk [", tox_hex(frpub
), "]; name is: ", nick
);
972 c
= new Contact(this);
974 c
.info
.nick
= nick
.idup
;
975 c
.info
.pubkey
[] = frpub
[];
976 c
.info
.opts
.showOffline
= TriOption
.Yes
;
977 contacts
[c
.info
.pubkey
] = c
;
980 if (clist
!is null) clist
.buildAccount(this);
981 } else if (c
.info
.nick
!= nick
) {
982 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; new name is: ", nick
);
983 if (nick
!= "<unknown>" && (c
.info
.nick
== "<unknown>" || c
.info
.nick
.length
== 0)) {
984 c
.info
.nick
= nick
.idup
;
988 conwriteln("OLD friend with pk [", tox_hex(frpub
), "]; old name is: ", nick
);
990 return false; // don't stop
995 // connection established
996 void toxConnectionDropped () {
998 conprintfln("TOX[%s] CONNECTION DROPPED", timp
.srvalias
);
999 mIAmConnecting
= false;
1003 mIAmConnecting
= true;
1005 foreach (Contact ct
; contacts
.byValue
) ct
.status
= ContactStatus
.Offline
;
1006 glconPostScreenRepaint();
1009 // connection established
1010 void toxConnectionEstablished () {
1012 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp
.srvalias
);
1013 mIAmConnecting
= false;
1015 toxCoreSetStatusMessage(toxpk
, "Come taste the gasoline! [BioAcid]");
1016 toxLoadKnownContacts();
1017 glconPostScreenRepaint();
1020 void toxFriendOffline (in ref PubKey fpk
) {
1021 if (auto ct
= fpk
in contacts
) {
1022 if (ct
.status
!= ContactStatus
.Offline
) {
1023 conwriteln("friend <", ct
.info
.nick
, "> gone offline");
1024 ct
.status
= ContactStatus
.Offline
;
1025 glconPostScreenRepaint();
1030 void toxSelfStatus (ContactStatus cst
) {
1031 if (mStatus
!= cst
) {
1033 glconPostScreenRepaint();
1037 void toxFriendStatus (in ref PubKey fpk
, ContactStatus cst
) {
1038 if (auto ct
= fpk
in contacts
) {
1039 if (ct
.status
!= cst
) {
1040 conwriteln("status for friend <", ct
.info
.nick
, "> changed to ", cst
);
1042 //if (ct.status != ContactStatus.Offline && ct.status != ContactStatus.Connecting) ct.processResendQueue();
1043 glconPostScreenRepaint();
1048 void toxFriendStatusMessage (in ref PubKey fpk
, string msg
) {
1049 if (auto ct
= fpk
in contacts
) {
1050 if (ct
.info
.statusmsg
!= msg
) {
1051 conwriteln("status message for friend <", ct
.info
.nick
, "> changed to <", msg
, ">");
1052 ct
.info
.statusmsg
= msg
;
1054 glconPostScreenRepaint();
1059 void toxFriendReqest (in ref PubKey fpk
, const(char)[] msg
) {
1061 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp
.srvalias
, tox_hex(fpk
[]), msg
);
1064 void toxFriendMessage (in ref PubKey fpk
, bool action
, string msg
, SysTime time
) {
1065 if (auto ct
= fpk
in contacts
) {
1066 LogFile
.Msg
.Kind kind
= LogFile
.Msg
.Kind
.Incoming
;
1067 ct
.appendToLog(kind
, msg
, action
, time
);
1068 if (*ct
is activeContact
) {
1069 // if inactive or invisible, add divider line and increase unread count
1070 if (!mainWindowVisible ||
!mainWindowActive
) {
1071 if (ct
.unreadCount
== 0) addDividerLine();
1072 ct
.unreadCount
+= 1;
1073 ct
.saveUnreadCount();
1075 addTextToLog(this, *ct
, kind
, action
, msg
, time
);
1077 ct
.unreadCount
+= 1;
1078 ct
.saveUnreadCount();
1083 // ack for sent message
1084 void toxMessageAck (in ref PubKey fpk
, long msgid
) {
1086 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp
.srvalias
, tox_hex(fpk
[]), msgid
);
1087 if (auto ct
= fpk
in contacts
) {
1088 if (*ct
is activeContact
) ackLogMessage(msgid
);
1089 ct
.ackReceived(msgid
);
1094 @property string
srvalias () const pure nothrow @safe @nogc => info
.nick
;
1097 this (string aBaseDir
) {
1098 import std
.algorithm
: sort
;
1099 import std
.file
: DirEntry
, SpanMode
, dirEntries
;
1100 import std
.path
: absolutePath
, baseName
;
1102 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1104 } else if (aBaseDir
== "/") {
1107 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1110 basePath
= aBaseDir
.absolutePath
;
1111 toxDataDiskName
= basePath
~"toxdata.tox";
1112 protoOpts
.txtunser(VFile(basePath
~"proto.rc"));
1113 info
.txtunser(VFile(basePath
~"config.rc"));
1116 GroupOptions
[] glist
;
1117 glist
.txtunser(VFile(basePath
~"contacts/groups.rc"));
1118 bool hasDefaultGroup
= false;
1119 bool hasMoronsGroup
= false;
1120 foreach (ref GroupOptions gi
; glist
[]) {
1121 auto g
= new Group(this);
1124 foreach (ref gg
; groups
[]) if (gg
.gid
== g
.gid
) { delete gg
; gg
= g
; found
= true; }
1125 if (!found
) groups
~= g
;
1126 if (g
.gid
== 0) hasDefaultGroup
= true;
1127 if (g
.gid
== g
.gid
.max
) hasMoronsGroup
= true;
1130 // create default group if necessary
1131 if (!hasDefaultGroup
) {
1134 gi
.name
= "default";
1135 gi
.note
= "default group for new contacts";
1137 auto g
= new Group(this);
1142 // create morons group if necessary
1143 if (!hasMoronsGroup
) {
1145 gi
.gid
= gi
.gid
.max
;
1146 gi
.name
= "<morons>";
1147 gi
.note
= "group for completely ignored dumbfucks";
1149 gi
.showOffline
= TriOption
.No
;
1150 gi
.showPopup
= TriOption
.No
;
1151 gi
.blinkActivity
= TriOption
.No
;
1152 gi
.skipUnread
= TriOption
.Yes
;
1153 gi
.hideIfNoVisibleMembers
= TriOption
.Yes
;
1154 gi
.ftranAllowed
= TriOption
.No
;
1155 gi
.resendRotDays
= 0;
1157 auto g
= new Group(this);
1163 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
1165 if (!hasDefaultGroup ||
!hasMoronsGroup
) saveGroups();
1168 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
1169 if (de.name
.baseName
== "." ||
de.name
.baseName
== "..") continue;
1171 import std
.file
: exists
;
1172 if (!de.isDir
) continue;
1173 string cfgfn
= de.name
~"/config.rc";
1174 if (!cfgfn
.exists
) continue;
1176 ci
.txtunser(VFile(cfgfn
));
1177 auto c
= new Contact(this);
1178 c
.diskFileName
= cfgfn
;
1180 contacts
[c
.info
.pubkey
] = c
;
1181 c
.loadResendQueue();
1182 c
.loadUnreadCount();
1183 // fix contact group
1184 if (groupById
!false(c
.gid
) is null) {
1185 c
.info
.gid
= 0; // move to default group
1188 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1189 } catch (Exception e
) {
1190 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
1195 assert(toxpk
.isValidKey
, "something is VERY wrong here");
1196 conwriteln("created ToxCore for [", tox_hex(toxpk
[]), "]");
1200 if (toxpk
.isValidKey
) {
1201 toxCoreCloseAccount(toxpk
);
1202 toxpk
[] = toxCoreEmptyKey
[];
1206 // will not write contact to disk
1207 Contact
createEmptyContact () {
1208 auto c
= new Contact(this);
1210 c
.info
.nick
= "test contact";
1211 c
.info
.pubkey
[] = 0;
1215 // returns `null` if there is no such group, and `dofail` is `true`
1216 inout(Group
) groupById(bool dofail
=true) (uint agid
) inout nothrow @nogc {
1217 foreach (const Group g
; groups
) if (g
.gid
== agid
) return cast(typeof(return))g
;
1218 static if (dofail
) assert(0, "group not found"); else return null;
1221 int opApply () (scope int delegate (ref Contact ct
) dg
) {
1222 foreach (Contact ct
; contacts
.byValue
) if (auto res
= dg(ct
)) return res
;
1227 static Account
CreateNew (string aBaseDir
, string aAccName
) {
1228 import std
.file
: mkdirRecurse
;
1229 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1231 } else if (aBaseDir
== "/") {
1234 mkdirRecurse(aBaseDir
);
1235 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1237 mkdirRecurse(aBaseDir
~"/contacts");
1238 // write protocol options
1241 popt
.txtser(VFile(aBaseDir
~"proto.rc", "w"), skipstname
:true);
1246 acc
.nick
= aAccName
;
1247 acc
.showPopup
= true;
1248 acc
.blinkActivity
= true;
1249 acc
.hideEmptyGroups
= false;
1250 acc
.ftranAllowed
= true;
1251 acc
.resendRotDays
= 4;
1253 acc
.txtser(VFile(aBaseDir
~"config.rc", "w"), skipstname
:true);
1255 // create default group
1257 GroupOptions
[1] grp
;
1259 grp
[0].name
= "default";
1260 grp
[0].opened
= true;
1261 //grp[0].hideIfNoVisible = TriOption.Yes;
1262 grp
[].txtser(VFile(aBaseDir
~"contacts/groups.rc", "w"), skipstname
:true);
1265 return new Account(aBaseDir
);
1270 // ////////////////////////////////////////////////////////////////////////// //
1271 class ListItemBase
{
1276 bool mVisible
= true;
1279 void setupFont () => nvg
.fontFace
= "ui"; // setup font face for this item
1280 @property int height () => cast(int)nvg
.textFontHeight
;
1281 @property bool visible () => mVisible
;
1282 bool onMouse (MouseEvent event
) => false; // true: eaten
1283 bool onKey (KeyEvent event
) => false; // true: eaten
1284 void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {} // real rect is scissored
1286 @property Account
ownerAcc () => null;
1290 class ListItemAccount
: ListItemBase
{
1295 this (Account aAcc
) { assert(aAcc
!is null); acc
= aAcc
; }
1297 override @property Account
ownerAcc () => acc
;
1299 override void setupFont () => nvg
.fontFace
= "uib"; // bold
1301 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1305 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1306 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
1309 final switch (acc
.status
) {
1310 case ContactStatus
.Connecting
: nvg
.fillColor(NVGColor
.k8orange
); break;
1311 case ContactStatus
.Offline
: nvg
.fillColor(NVGColor("#f00")); break;
1312 case ContactStatus
.Online
: nvg
.fillColor(NVGColor("#fff")); break;
1313 case ContactStatus
.Away
: nvg
.fillColor(NVGColor("#7557C7")); break;
1314 case ContactStatus
.Busy
: nvg
.fillColor(NVGColor("#0057C7")); break;
1316 if (acc
.isConnecting
) nvg
.fillColor(NVGColor
.k8orange
);
1317 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1318 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1319 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, acc
.info
.nick
);
1324 class ListItemGroup
: ListItemBase
{
1329 this (Group aGroup
) { assert(aGroup
!is null); group
= aGroup
; }
1331 override @property Account
ownerAcc () => group
.acc
;
1333 override @property bool visible () => group
.visible
;
1335 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1339 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1340 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
1343 nvg
.fillColor(selected ? NVGColor
.white
: NVGColor
.yellow
);
1344 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1345 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1346 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, group
.name
);
1351 class ListItemContact
: ListItemBase
{
1356 this (Contact aCt
) { assert(aCt
!is null); ct
= aCt
; }
1358 override @property Account
ownerAcc () => ct
.acc
;
1360 override @property bool visible () => ct
.visible
;
1362 override @property int height () { import std
.algorithm
: max
; return max(cast(int)nvg
.textFontHeight
, 16); } //FIXME: 16 is image height
1364 override void drawAt (int x0
, int y0
, int wdt
, bool selected
=false) {
1369 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1370 nvg
.fillColor(NVGColor("#9600"));
1378 if (ct
.unreadCount
== 0) icon
= statusImgId
[ct
.status
]; else icon
= kittyMsg
;
1379 nvg
.imageSize(icon
, iw
, ih
);
1380 //conwriteln("image: (", iw, "x", ih, ")");
1381 nvg
.rect(x0
, y0
+(hgt
-ih
)/2, iw
, ih
);
1382 nvg
.fillPaint(nvg
.imagePattern(x0
, y0
+(hgt
-ih
)/2, iw
, ih
, 0, icon
));
1385 nvg
.fillColor(NVGColor("#f70"));
1386 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1387 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1388 //conwriteln(nvg.textFontDescender);
1389 nvg
.text(x0
+4+iw
+4, y0
+hgt
+cast(int)nvg
.textFontDescender
, ct
.displayNick
);
1394 // ////////////////////////////////////////////////////////////////////////// //
1395 // visible contact list
1397 private import core
.time
;
1400 int mActiveItem
= -1; // active item (may be different from selected with cursor)
1402 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
1403 //MonoTime mLastClick = MonoTime.zero;
1404 //int mLastClickItem = -1;
1407 ListItemBase
[] items
;
1412 // the first one is "main"; can return `null`
1413 Account
mainAccount () {
1414 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) return lc
.acc
;
1418 // the first one is "main"; can return `null`
1419 Account
accountByPK (in ref PubKey pk
) {
1420 foreach (ListItemBase li
; items
) {
1421 if (auto lc
= cast(ListItemAccount
)li
) {
1422 if (lc
.acc
.toxpk
[] == pk
[]) return lc
.acc
;
1428 void forEachAccount (scope void delegate (Account acc
) dg
) {
1429 if (dg
is null) return;
1430 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) dg(lc
.acc
);
1433 void opOpAssign(string op
:"~") (ListItemBase li
) {
1434 if (li
is null) return;
1439 //nvg.fontFace = "ui";
1442 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1443 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1446 void removeAccount (Account acc
) {
1447 if (acc
is null) return;
1448 usize pos
= 0, dest
= 0;
1449 while (pos
< items
.length
) {
1450 if (items
[pos
].ownerAcc
!is acc
) {
1451 if (pos
!= dest
) items
[dest
++] = items
[pos
];
1455 items
.length
= dest
;
1458 void buildAccount (Account acc
) {
1459 if (acc
is null) return;
1461 items
~= new ListItemAccount(acc
);
1463 scope(exit
) delete css
;
1464 foreach (Group g
; acc
.groups
) {
1465 items
~= new ListItemGroup(g
);
1467 css
.assumeSafeAppend
;
1468 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
1469 import std
.algorithm
: sort
;
1471 string s0
= a
.displayNick
;
1472 string s1
= b
.displayNick
;
1473 auto xlen
= (s0
.length
< s1
.length ? s0
.length
: s1
.length
);
1474 foreach (immutable idx
, char c0
; s0
[0..xlen
]) {
1475 if (c0
>= 'A' && c0
<= 'Z') c0
+= 32; // poor man's tolower()
1477 if (c1
>= 'A' && c1
<= 'Z') c1
+= 32; // poor man's tolower()
1478 if (auto d
= c0
-c1
) return (d
< 0);
1480 return (s0
.length
< s1
.length
);
1482 foreach (Contact c
; css
) items
~= new ListItemContact(c
);
1487 // should be called after clist was drawn at least once
1488 int itemAtY (int aty
) {
1489 if (aty
< 0 || mLastHeight
< 1 || aty
>= mLastHeight
) return -1;
1491 scope(exit
) nvg
.restore();
1493 foreach (immutable iidx
, ListItemBase li
; items
) {
1494 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
1497 if (lh
< 1) continue;
1498 if (aty
< lh
) return cast(int)iidx
;
1504 // if called with `null` ct, deactivate
1505 void delegate (Contact ct
) onActivateContactCB
;
1508 bool onMouse (MouseEvent event
) {
1509 if (mLastWidth
< 1 || mLastHeight
< 1) return false;
1510 int mx
= event
.x
-mLastX
;
1511 int my
= event
.y
-mLastY
;
1512 if (mx
< 0 || my
< 0 || mx
>= mLastWidth || my
>= mLastHeight
) return false;
1513 if (event
== "LMB-Down") {
1514 int it
= itemAtY(my
);
1515 if (it
>= 0 && it
!= mActiveItem
) {
1516 if (auto ci
= cast(ListItemContact
)items
[it
]) {
1518 if (onActivateContactCB
!is null) onActivateContactCB(ci
.ct
);
1519 glconPostScreenRepaint();
1527 bool onKey (KeyEvent event
) {
1531 // real rect is scissored
1532 void drawAt (int x0
, int y0
, int wdt
, int hgt
) {
1538 scope(exit
) nvg
.restore();
1541 foreach (immutable iidx
, ListItemBase li
; items
) {
1542 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
1545 if (lh
< 1) continue;
1547 scope(exit
) nvg
.restore();
1549 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
1550 li
.drawAt(x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
1552 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
1553 nvg
.intersectScissor(0, 0, wdt
, lh
);
1554 li
.drawAt(0, 0, wdt
);
1557 if (y
>= hgt
) break;
1563 // ////////////////////////////////////////////////////////////////////////// //
1564 __gshared CList clist
;
1565 __gshared Contact activeContact
;
1568 void loadAccount (string nick
) {
1572 acc
= new Account(nick
);
1573 } catch (Exception e
) {
1574 conwriteln("creating account...");
1575 assert(0, "not yet");
1576 acc
= Account
.CreateNew("_fakeacc", "ketmar");
1578 // create fake contact
1579 if (acc
.contacts
.length
== 0) {
1580 conwriteln("creating fake contact...");
1581 auto c
= acc
.createEmptyContact();
1582 c
.info
.nick
= "test contact";
1583 c
.info
.pubkey
[] = 0x55;
1587 clist
.buildAccount(acc
);
1591 // ////////////////////////////////////////////////////////////////////////// //
1593 void doActivateContact (Contact ct
) {
1594 if (activeContact
is ct
) return;
1599 //conwriteln("clear log");
1600 if (clist
!is null) clist
.mActiveItem
= -1;
1601 glconPostScreenRepaint();
1603 //conwriteln("activated contact <", ct.info.nick, ">: [", tox_hex(ct.info.pubkey), "]");
1605 ct
.loadLogInto(log
);
1606 auto mcount
= cast(int)log
.messages
.length
;
1607 int left
= ct
.hmcOnOpen
;
1608 if (left
< ct
.unreadCount
) left
= ct
.unreadCount
;
1609 if (left
> mcount
) left
= mcount
;
1610 if (mcount
> left
) mcount
= left
;
1612 foreach (const ref msg
; log
.messages
[$-mcount
..$]) {
1613 if (left
== ct
.unreadCount
) addDividerLine();
1614 addTextToLog(ct
.acc
, ct
, msg
);
1618 if (ct
.unreadCount
!= 0) { ct
.unreadCount
= 0; ct
.saveUnreadCount(); }
1625 //FIXME: scan all accounts
1626 void fixTrayIcon () {
1627 if (clist
is null) return;
1628 auto acc
= clist
.mainAccount
;
1629 if (acc
is null) return;
1631 foreach (Contact ct
; acc
) unc
+= ct
.unreadCount
;
1633 import std
.format
: format
;
1635 setHint("unread: %d".format(unc
));
1637 setTrayStatus(acc
.status
);
1638 final switch (acc
.status
) {
1639 case ContactStatus
.Connecting
: setHint("connecting..."); break;
1640 case ContactStatus
.Offline
: setHint("offline"); break;
1641 case ContactStatus
.Online
: setHint("online"); break;
1642 case ContactStatus
.Away
: setHint("away"); break;
1643 case ContactStatus
.Busy
: setHint("busy"); break;
1649 void fixUnreadIndicators () {
1650 if (!mainWindowVisible ||
!mainWindowActive
) return; // nothing to do
1651 if (activeContact
is null || activeContact
.unreadCount
== 0) return; // nothing to do
1652 activeContact
.unreadCount
= 0;
1653 activeContact
.unreadCount
= 0;
1654 activeContact
.saveUnreadCount();
1659 // ////////////////////////////////////////////////////////////////////////// //
1660 //__gshared string accountNameToLoad = "_fakeacc";
1661 __gshared string accountNameToLoad
= "";
1662 __gshared string globalHotkey
= "M-H-F";
1665 void main (string
[] args
) {
1666 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
1668 conRegVar
!accountNameToLoad("starting_account", "account to load");
1670 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
1672 //glconShowKey = "M-Grave";
1673 glconSetAndSealFPS(0); // draw-on-demand
1675 conProcessQueue(256*1024); // load config
1676 conProcessArgs
!true(args
);
1677 conProcessQueue(256*1024);
1679 if (accountNameToLoad
.length
== 0) assert(0, "no account to load");
1681 //setOpenGLContextVersion(3, 2); // up to GLSL 150
1682 setOpenGLContextVersion(2, 0); // it's enough
1687 NVGPathSet svp
= null;
1689 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
1690 sdpyWindowClass
= "BIOACID";
1691 auto sdmain
= new SimpleWindow(800, 600, "BioAcid", OpenGlOptions
.yes
, Resizability
.allowResizing
);
1692 glconCtlWindow
= sdmain
;
1694 sdmain
.visibilityChanged
= delegate (bool vis
) { mainWindowVisible
= vis
; fixUnreadIndicators(); };
1695 sdmain
.onFocusChange
= delegate (bool focused
) { mainWindowActive
= focused
; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
1698 if (globalHotkey
.length
> 0) {
1699 GlobalHotkeyManager
.register(globalHotkey
, delegate () { concmd("win_toggle"); glconPostDoConCommands
!true(); });
1701 } catch (Exception e
) {
1702 conwriteln("ERROR registering hotkey!");
1706 if (sdmain
!is null) sdmain
.close();
1707 })("quit", "quit BioAcid");
1710 if (sdmain
!is null && !sdmain
.closed
) {
1711 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
1712 if (!mainWindowVisible
) {
1713 // this strange code brings window to the current desktop it if was on a different one
1716 } else if (sdmain
.visible
) {
1723 })("win_toggle", "show/hide main window");
1725 sdmain
.addEventListener((GLConScreenRepaintEvent evt
) {
1726 if (sdmain
.closed
) return;
1727 if (isQuitRequested
) { sdmain
.close(); return; }
1728 sdmain
.redrawOpenGlSceneNow();
1731 sdmain
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
1732 glconProcessEventMessage();
1735 sdmain
.addEventListener((PopupCheckerEvent evt
) {
1736 popupCheckExpirations();
1740 // ////////////////////////////////////////////////////////////////////// //
1742 sdmain
.addEventListener((ToxEventBase evt
) {
1743 auto acc
= clist
.accountByPK(evt
.self
);
1745 if (acc
is null) return;
1747 bool fixTray
= false;
1748 scope(exit
) { if (fixTray
) fixTrayIcon(); glconPostScreenRepaint(); }
1751 if (auto e
= cast(ToxEventConnection
)evt
) {
1752 if (e
.who
[] == acc
.toxpk
[]) {
1753 if (e
.connected
) acc
.toxConnectionEstablished(); else acc
.toxConnectionDropped();
1756 if (!e
.connected
) acc
.toxFriendOffline(e
.who
);
1761 if (auto e
= cast(ToxEventStatus
)evt
) {
1762 if (e
.who
[] == acc
.toxpk
[]) {
1763 acc
.toxSelfStatus(e
.status
);
1766 acc
.toxFriendStatus(e
.who
, e
.status
);
1771 if (auto e
= cast(ToxEventStatusMsg
)evt
) {
1772 if (e
.who
[] != acc
.toxpk
[]) {
1773 acc
.toxFriendStatusMessage(e
.who
, e
.message
);
1777 // incoming text message?
1778 if (auto e
= cast(ToxEventMessage
)evt
) {
1779 if (e
.who
[] != acc
.toxpk
[]) {
1780 acc
.toxFriendMessage(e
.who
, e
.action
, e
.message
, e
.time
);
1785 // ack outgoing text message?
1786 if (auto e
= cast(ToxEventMessageAck
)evt
) {
1787 if (e
.who
[] != acc
.toxpk
[]) {
1788 acc
.toxMessageAck(e
.who
, e
.msgid
);
1793 //glconProcessEventMessage();
1797 // ////////////////////////////////////////////////////////////////////// //
1798 sdmain
.onClosing
= delegate () {
1799 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
1802 sdmain
.setAsCurrentOpenGlContext();
1803 scope(exit
) { flushGui(); sdmain
.releaseCurrentOpenGlContext(); }
1807 assert(nvg
is null);
1808 if (sdhint
!is null) sdhint
.close();
1809 if (trayicon
!is null) trayicon
.close();
1812 sdmain
.closeQuery
= delegate () {
1814 glconPostDoConCommands
!true();
1818 sdmain
.visibleForTheFirstTime
= delegate () {
1819 if (sdmain
.width
> 1 && optCListWidth
< 0) optCListWidth
= sdmain
.width
/5;
1820 sdmain
.setAsCurrentOpenGlContext(); // make this window active
1821 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
1822 sdmain
.vsync
= false;
1824 glconInit(sdmain
.width
, sdmain
.height
);
1826 nvg
= nvgCreateContext(NVGContextFlag
.Antialias
, NVGContextFlag
.StencilStrokes
, NVGContextFlag
.FontNoAA
);
1827 if (nvg
is null) assert(0, "cannot initialize NanoVG");
1831 static immutable skullsPng
= /*cast(immutable(ubyte)[])*/import("data/skulls.png");
1832 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
1833 auto xi
= loadImageFromMemory(skullsPng
[]);
1834 scope(exit
) delete xi
;
1835 //{ import core.stdc.stdio; printf("creating background image...\n"); }
1836 nvgSkullsImg
= nvg
.createImageFromMemoryImage(xi
, NVGImageFlags
.NoFiltering
, NVGImageFlags
.RepeatX
, NVGImageFlags
.RepeatY
);
1837 //{ import core.stdc.stdio; printf("background image created\n"); }
1838 if (!nvgSkullsImg
.valid
) assert(0, "cannot load background image");
1839 } catch (Exception e
) {
1840 assert(0, "cannot load background image");
1842 buildStatusImages();
1846 lay
= new LayTextClass(laf
, sdmain
.width
/2);
1847 lay
.fontStyle
.fontsize
= 16;
1848 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1849 lay
.fontStyle
.monospace
= true;
1850 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
1851 lay
.put("ketmar (бля)");
1852 lay
.fontStyle
.monospace
= false;
1853 lay
.putHardSpace(64);
1857 lay
.put("2018/02/12");
1859 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
1860 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
1861 lay
.put("11:09:03");
1863 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
1864 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1865 lay
.fontStyle
.fontsize
= 20;
1866 lay
.put("this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1869 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
1870 lastWindowWidth
= sdmain
.width
;
1872 clist
= new CList();
1873 loadAccount(accountNameToLoad
);
1874 //clist.buildAccount(acc);
1875 clist
.onActivateContactCB
= delegate (Contact ct
) { doActivateContact(ct
); };
1877 sdmain
.setMinSize(640, 480);
1880 //sdmain.redrawOpenGlSceneNow();
1883 sdmain
.windowResized
= delegate (int wdt
, int hgt
) {
1884 if (sdmain
.closed
) return;
1885 glconResize(wdt
, hgt
);
1886 glconPostScreenRepaint
/*Delayed*/();
1887 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
1888 if (wdt
> 1 && optCListWidth
> 0 && lastWindowWidth
> 0 && lastWindowWidth
!= wdt
) {
1889 immutable double frc
= lastWindowWidth
/optCListWidth
;
1890 optCListWidth
= cast(int)(wdt
/frc
);
1891 if (optCListWidth
< 64) optCListWidth
= 64;
1892 lastWindowWidth
= wdt
;
1898 int mouseX
= -666, mouseY
= -666;
1901 sdmain
.handleKeyEvent
= delegate (KeyEvent event
) {
1902 if (sdmain
.closed
) return;
1903 scope(exit
) glconPostDoConCommands
!true();
1904 if (glconKeyEvent(event
)) return;
1906 auto acc
= clist
.mainAccount
;
1908 if (event
== "D-Escape") {
1909 if (sdmain
!is null && !sdmain
.closed
&& sdmain
.visible
) {
1916 if (event
== "D-C-Q") { concmd("quit"); return; }
1917 if (event
== "D-C-1") { acc
.status
= ContactStatus
.Online
; return; }
1918 if (event
== "D-C-2") { acc
.status
= ContactStatus
.Away
; return; }
1919 if (event
== "D-C-0") { acc
.status
= ContactStatus
.Offline
; return; }
1921 if (event
== "D-C-W") { doActivateContact(null); return; }
1923 if (clist
!is null && clist
.onKey(event
)) return;
1925 if (event
== "D-C-S-Enter") {
1926 static PopupWindow
.Kind kind
;
1927 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1928 showPopup(kind
, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1929 if (kind
== PopupWindow
.Kind
.max
) kind
= PopupWindow
.Kind
.min
; else ++kind
;
1933 if (activeContact
!is null) {
1934 if (event
== "D-Enter") {
1935 auto text
= activeContact
.edit
.text
;
1936 activeContact
.edit
.clear();
1937 activeContact
.send(text
);
1938 glconPostScreenRepaint();
1941 if (activeContact
.edit
.onKey(event
)) return;
1945 if (event == "D-Space") {
1947 lay.fontStyle.fontsize = 16;
1948 lay.fontStyle.color = NVGColor.k8orange.asUint;
1949 lay.fontStyle.monospace = true;
1950 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1952 lay.fontStyle.monospace = false;
1953 lay.putHardSpace(64);
1957 lay.put("2018/02/12");
1959 lay.fontStyle.color = NVGColor("#fff").asUint;
1960 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1961 lay.put("11:09:03");
1963 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1964 lay.fontStyle.color = NVGColor.k8orange.asUint;
1965 lay.fontStyle.fontsize = 20;
1969 foreach (immutable widx; 0..uniform!"[]"(2, 28)) {
1971 if (widx != 0) lay.put(" ");
1972 foreach (; 0..uniform!"[]"(1, 9)) {
1973 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1975 //lay.putSoftHypen();
1979 // long word, for hyphenation test
1980 foreach (immutable idx; 0..228) {
1981 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1983 if (idx%24 == 23) lay.putSoftHypen();
1991 glconPostScreenRepaint();
1997 int msLastPressX
= -666, msLastPressY
= -666;
1998 bool msDoLogButton
= false;
2000 sdmain
.handleMouseEvent
= delegate (MouseEvent event
) {
2001 if (sdmain
.closed
) return;
2002 scope(exit
) glconPostDoConCommands
!true();
2003 if (isConsoleVisible
) return;
2008 //FIXME: process it here, not in renderer
2009 if (event
== "LMB-Down") {
2010 msLastPressX
= mouseX
;
2011 msLastPressY
= mouseY
;
2012 msDoLogButton
= true;
2013 glconPostScreenRepaint();
2016 if (clist
!is null) {
2017 sdmain
.setAsCurrentOpenGlContext(); // make this window active
2018 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
2020 bool inFrame
= nvg
.inFrame
;
2021 if (!inFrame
) nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
2022 scope(exit
) if (!inFrame
) nvg
.endFrame();
2024 if (clist
.onMouse(event
)) { /*glconPostScreenRepaint();*/ return; }
2028 if (layWinHeight
> 0) {
2029 enum ScrollHeight
= 32;
2030 if (event
== "WheelUp") {
2031 layOffset
+= ScrollHeight
;
2032 if (layOffset
> lay
.textHeight
-layWinHeight
) layOffset
= lay
.textHeight
-layWinHeight
;
2033 } else if (event
== "WheelDown") {
2034 layOffset
-= ScrollHeight
;
2036 if (layOffset
< 0) layOffset
= 0;
2039 // don't spam with repaint events
2040 if (event
.type
!= MouseEventType
.motion
) glconPostScreenRepaint();
2043 sdmain
.handleCharEvent
= delegate (dchar ch
) {
2044 if (sdmain
.closed
) return;
2045 scope(exit
) glconPostDoConCommands
!true();
2046 if (glconCharEvent(ch
)) return;
2048 if (activeContact
!is null) {
2049 if (activeContact
.edit
.onChar(ch
)) return;
2054 sdmain
.redrawOpenGlScene
= delegate () {
2055 glconPostDoConCommands
!true();
2056 if (sdmain
.closed
) return;
2057 sdmain
.setAsCurrentOpenGlContext(); // make this window active
2058 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
2061 scope(exit
) glconDraw();
2063 if (nvg
is null) return;
2065 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
2066 glViewport(0, 0, sdmain
.width
, sdmain
.height
);
2067 glMatrixMode(GL_MODELVIEW
);
2068 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
2070 glClearColor(0, 0, 0, 0);
2071 glClear(glNVGClearFlags
/*|GL_COLOR_BUFFER_BIT*/);
2074 nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
2075 scope(exit
) nvg
.endFrame();
2077 if (clist
!is null && optCListWidth
> 0) {
2078 // draw contact list
2081 int wdt
= optCListWidth
;
2082 int hgt
= nvg
.height
-cy
*2;
2084 nvg
.shapeAntiAlias
= true;
2086 nvg
.strokeWidth
= 1;
2090 scope(exit
) nvg
.restore();
2093 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
2096 nvg
.imageSize(nvgSkullsImg
, w
, h
);
2097 nvg
.fillPaint(nvg
.imagePattern(0, 0, w
, h
, 0, nvgSkullsImg
));
2099 nvg
.strokeColor(NVGColor("#f70"));
2103 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
2105 clist
.drawAt(cx
+3, cy
+3, wdt
-3*2, hgt
-3*2);
2110 scope(exit
) nvg
.restore();
2112 //nvg.transform(NVGMatrix.init);
2114 auto xf = nvg.currTransform;
2115 nvg.currTransform = xf;
2120 wdt
= nvg
.width
-cx
-1;
2123 // calculate editor dimensions and draw editor
2124 if (activeContact
!is null) {
2126 scope(exit
) nvg
.restore();
2128 auto edinfo
= activeContact
.edit
.calcHeight(nvg
, wdt
-3*2);
2129 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
2131 int edy
= cy
+hgt
-cast(int)edinfo
.height
-3*2;
2134 nvg
.roundedRect(cx
+0.5f, edy
+0.5f, wdt
, edinfo
.height
+3*2, 6);
2135 nvg
.fillColor(NVGColor
.black
);
2136 nvg
.strokeColor(NVGColor("#f70"));
2140 nvg
.intersectScissor(cx
+2.5f, edy
+2.5f, wdt
-3*2+2, edinfo
.height
+2);
2141 activeContact
.edit
.draw(nvg
, cx
+3, edy
+3, wdt
-3*2, cast(int)edinfo
.height
);
2143 hgt
-= cast(int)edinfo
.height
+3*2;
2147 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
2148 nvg
.fillColor(NVGColor
.black
);
2149 nvg
.strokeColor(NVGColor("#f70"));
2153 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
2155 immutable float scalex
= (wdt
-3*2-10*2)/BaphometDims
;
2156 immutable float scaley
= (baphHgt
-3*2-10*2)/BaphometDims
;
2157 immutable float scale
= (scalex
< scaley ? scalex
: scaley
)/1.5f;
2158 immutable float sz
= BaphometDims
*scale
;
2159 nvg
.strokeColor(NVGColor("#400"));
2160 nvg
.fillColor(NVGColor("#400"));
2161 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
);
2164 immutable float sbx
= cx
+wdt
-BND_SCROLLBAR_WIDTH
-1.5f;
2165 immutable float sby
= cy
+3.5f;
2166 wdt
-= BND_SCROLLBAR_WIDTH
+1;
2170 lay
.relayout(wdt
); // this is harmess if width wasn't changed
2171 int ty
= lay
.textHeight
-hgt
+1-layOffset
;
2173 layWinHeight
= cast(int)hgt
;
2175 if (lay
.textHeight
> hgt
) {
2176 float h
= lay
.textHeight
-hgt
;
2177 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, ty
/h
, hgt
/h
);
2179 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, 1, 1);
2182 nvg
.intersectScissor(cx
+2.5f, cy
+2.5f, wdt
+2, hgt
+2);
2183 nvg
.drawLayouter(lay
, ty
, cx
+3, cy
+3, hgt
);
2185 if (msDoLogButton
) {
2186 msDoLogButton
= false;
2187 auto widx
= lay
.wordAtXY(msLastPressX
-(cx
+3), ty
+(msLastPressY
-(cy
+3)));
2189 if (auto hr
= cast(uint)widx
in layUrlList
) {
2190 conwriteln("URL CLICK: <", hr
.url
);
2201 sdmain
.eventLoop(15000,
2202 // pulser: process resend queues here
2204 if (sdmain
.closed || clist
is null) return;
2205 clist
.forEachAccount(delegate (Account acc
) { acc
.processResendQueue(); });
2208 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
2209 clist
.forEachAccount(delegate (Account acc
) { acc
.saveResendQueue(); acc
.saveResendQueue(); });
2210 toxCoreShutdownAll();
2213 conProcessQueue(int.max
/4);