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
;
28 import iv
.nanovega
.blendish
;
29 import iv
.nanovega
.textlayouter
;
37 // ////////////////////////////////////////////////////////////////////////// //
38 alias LayTextClass
= LayTextD
;
41 // ////////////////////////////////////////////////////////////////////////// //
42 __gshared string accBaseDir
= ".";
44 __gshared NVGContext nvg
= null;
45 __gshared
int nvgSkullsImg
= 0;
47 __gshared LayFontStash laf
;
48 __gshared LayTextClass lay
;
50 __gshared
int optCListWidth
= -1;
51 __gshared
int lastWindowWidth
= -1;
54 // ////////////////////////////////////////////////////////////////////////// //
55 enum TTFontStyle
: ubyte { Normal
= 0, Italic
= 1, Bold
= 2 }
57 __gshared
int uiFont
, galmapFont
;
58 __gshared
int[4] textFontId
;
59 __gshared
int[4] monoFontId
;
60 __gshared
int[4] uiFontId
;
62 __gshared string
[4] textFontNames
= [
63 "PT Sans:noaa", //"Arial:noaa", //"~/ttf/ms/arial.ttf:noaa", // normal
64 "PT Sans:italic:noaa", //"~/ttf/ms/ariali.ttf:noaa", // italic
65 "PT Sans:bold:noaa", //"~/ttf/ms/arialbd.ttf:noaa", // bold
66 "PT Sans:italic:bold:noaa", //"~/ttf/ms/arialbi.ttf:noaa", // italic+bold
68 __gshared string
[4] monoFontNames
= [
69 //"/usr/share/fonts/ms/andalemo.ttf:noaa", // normal
70 "PT Mono:noaa", //"Courier New:noaa", //"~/ttf/ms/cour.ttf:noaa", // normal
71 "PT Mono:italic:noaa", //"~/ttf/ms/couri.ttf:noaa", // italic
72 "PT Mono:bold:noaa", //"~/ttf/ms/courbd.ttf:noaa", // bold
73 "PT Mono:italic:bold:noaa", //"~/ttf/ms/courbi.ttf:noaa", // italic+bold
75 __gshared string
[4] uiFontNames
= [
76 "PT Sans:noaa", //"Arial:noaa", //"~/ttf/ms/verdana.ttf:noaa", // normal
77 "PT Sans:italic:noaa", //"~/ttf/ms/ariali.ttf:noaa", // italic
78 "PT Sans:bold:noaa", //"~/ttf/ms/arialbd.ttf:noaa", // bold
79 "PT Sans:italic:bold:noaa", //"~/ttf/ms/arialbi.ttf:noaa", // italic+bold
83 void loadFonts (NVGContext vg
) {
84 void loadFont (ref int fid
, string name
, string path
) {
85 fid
= vg
.createFont(name
, path
);
86 if (fid
< 0) assert(0, "can't load font '"~name
~"' from '"~path
~"'");
87 //conwriteln("created font [", name, "] (", path, "): ", fid);
90 void loadFontSet (string namebase
, int[] ids
, string
[] pathes
) {
91 static immutable string
[4] pfx
= ["", "i", "b", "ib"];
92 assert(ids
.length
>= 4);
93 assert(pathes
.length
>= 4);
94 foreach (immutable idx
; 0..4) loadFont(ids
[idx
], namebase
~pfx
[idx
], pathes
[idx
]);
97 loadFontSet("text", textFontId
[], textFontNames
[]);
98 loadFontSet("mono", monoFontId
[], monoFontNames
[]);
100 loadFontSet("ui", uiFontId
[], uiFontNames
[]);
106 // ////////////////////////////////////////////////////////////////////////// //
107 private void loadFmtFonts () {
108 laf
= new LayFontStash(nvg
);
110 void loadFontSet (string namebase
, string
[] pathes
) {
111 static immutable string
[4] pfx
= ["", "i", "b", "ib"];
112 assert(pathes
.length
>= 4);
113 foreach (immutable idx
; 0..4) laf
.addFont(namebase
~pfx
[idx
], pathes
[idx
]);
116 /*if (laf.ownsFontContext)*/ {
117 loadFontSet("text", textFontNames
[]);
118 loadFontSet("mono", monoFontNames
[]);
123 // ////////////////////////////////////////////////////////////////////////// //
124 static immutable uint[256] ctiOffline
= [
125 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x26262626U
,0xb0272427U
,0xec262526U
,0xec262526U
,0xb2262626U
,0x27262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
126 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x1c2b202bU
,0xee272527U
,0xff272527U
,0xff000030U
,0xff000033U
,0xff272527U
,0xee272527U
,0x1c2b202bU
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
127 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x95262626U
,0xff272527U
,0xff00007dU
,0xff0000fdU
,0xff0000fdU
,0xff000084U
,0xff272527U
,0x95262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
128 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xcc262526U
,0xff000029U
,0xff0000fdU
,0xff0000fdU
,0xff0000fdU
,0xff0000fdU
,0xff000028U
,0xcc262526U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
129 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xe0272527U
,0xfc272527U
,0xff0000beU
,0xff0000fdU
,0xff0000fdU
,0xff0000f8U
,0xf2262526U
,0xe0272527U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
130 0x00000000U
,0x00000000U
,0x00000000U
,0x3f282428U
,0xe8272527U
,0xff272527U
,0xff00002eU
,0xff000051U
,0xff0000a9U
,0xff00007fU
,0xff272527U
,0xe8272527U
,0x3e262626U
,0x00000000U
,0x00000000U
,0x00000000U
,
131 0x00000000U
,0x3c262626U
,0xf2272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000041U
,0xfd272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xf3272527U
,0x3a292329U
,0x00000000U
,
132 0x00000000U
,0x9d262526U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000034U
,0xff000034U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x9c262626U
,0x00000000U
,
133 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000045U
,0xff0000fdU
,0xff0000fdU
,0xff000045U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
134 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000063U
,0xff0000fdU
,0xff0000fdU
,0xff000063U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
135 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff00002cU
,0xff0000c8U
,0xff0000c3U
,0xff00002cU
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
136 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000067U
,0xff0000fdU
,0xff0000fdU
,0xff00005fU
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
137 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000029U
,0xff0000fdU
,0xff0000fdU
,0xff0000fdU
,0xff0000fbU
,0xff000025U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
138 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000031U
,0xff0000fdU
,0xff0000fdU
,0xff0000fdU
,0xff0000f8U
,0xff000026U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
139 0x00000000U
,0x8c272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff000031U
,0xff000036U
,0xff000036U
,0xff000030U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x8a272527U
,0x00000000U
,
140 0x00000000U
,0x25262626U
,0xc0262626U
,0xfe272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xc2262526U
,0x21262626U
,0x00000000U
,
143 static immutable uint[256] ctiOnline
= [
144 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x26262626U
,0xb0272427U
,0xec262526U
,0xec262526U
,0xb2262626U
,0x27262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
145 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x1c2b202bU
,0xee272527U
,0xff272527U
,0xff003000U
,0xff003300U
,0xff272527U
,0xee272527U
,0x1c2b202bU
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
146 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x95262626U
,0xff272527U
,0xff007d00U
,0xff00fd00U
,0xff00fd00U
,0xff008400U
,0xff272527U
,0x95262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
147 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xcc262526U
,0xff002900U
,0xff00fd00U
,0xff00fd00U
,0xff00fd00U
,0xff00fd00U
,0xff002800U
,0xcc262526U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
148 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xe0272527U
,0xfc272527U
,0xff00be00U
,0xff00fd00U
,0xff00fd00U
,0xff00f800U
,0xf2262526U
,0xe0272527U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
149 0x00000000U
,0x00000000U
,0x00000000U
,0x3f282428U
,0xe8272527U
,0xff272527U
,0xff002e00U
,0xff005100U
,0xff00a900U
,0xff007f00U
,0xff272527U
,0xe8272527U
,0x3e262626U
,0x00000000U
,0x00000000U
,0x00000000U
,
150 0x00000000U
,0x3c262626U
,0xf2272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff004100U
,0xfd272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xf3272527U
,0x3a292329U
,0x00000000U
,
151 0x00000000U
,0x9d262526U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff003400U
,0xff003400U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x9c262626U
,0x00000000U
,
152 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff004500U
,0xff00fd00U
,0xff00fd00U
,0xff004500U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
153 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff006300U
,0xff00fd00U
,0xff00fd00U
,0xff006300U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
154 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff002c00U
,0xff00c800U
,0xff00c300U
,0xff002c00U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
155 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff006700U
,0xff00fd00U
,0xff00fd00U
,0xff005f00U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
156 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff002900U
,0xff00fd00U
,0xff00fd00U
,0xff00fd00U
,0xff00fb00U
,0xff002500U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
157 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff003100U
,0xff00fd00U
,0xff00fd00U
,0xff00fd00U
,0xff00f800U
,0xff002600U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
158 0x00000000U
,0x8c272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff003100U
,0xff003600U
,0xff003600U
,0xff003000U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x8a272527U
,0x00000000U
,
159 0x00000000U
,0x25262626U
,0xc0262626U
,0xfe272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xc2262526U
,0x21262626U
,0x00000000U
,
162 static immutable uint[256] ctiAway
= [
163 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x26262626U
,0xb0272427U
,0xec262526U
,0xec262526U
,0xb2262626U
,0x27262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
164 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x1c2b202bU
,0xee272527U
,0xff272527U
,0xff230d13U
,0xff260d14U
,0xff272527U
,0xee272527U
,0x1c2b202bU
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
165 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0x95262626U
,0xff272527U
,0xff5c2131U
,0xffb94362U
,0xffb94362U
,0xff602333U
,0xff272527U
,0x95262626U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
166 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xcc262526U
,0xff1e0b10U
,0xffb94362U
,0xffb94362U
,0xffb94362U
,0xffb94362U
,0xff1d0b0fU
,0xcc262526U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
167 0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,0xe0272527U
,0xfc272527U
,0xff8b324aU
,0xffb94362U
,0xffb94362U
,0xffb54260U
,0xf2262526U
,0xe0272527U
,0x00000000U
,0x00000000U
,0x00000000U
,0x00000000U
,
168 0x00000000U
,0x00000000U
,0x00000000U
,0x3f282428U
,0xe8272527U
,0xff272527U
,0xff220c12U
,0xff3b161fU
,0xff7b2d41U
,0xff5c2231U
,0xff272527U
,0xe8272527U
,0x3e262626U
,0x00000000U
,0x00000000U
,0x00000000U
,
169 0x00000000U
,0x3c262626U
,0xf2272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff2f1119U
,0xfd272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xf3272527U
,0x3a292329U
,0x00000000U
,
170 0x00000000U
,0x9d262526U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff260e14U
,0xff260e14U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x9c262626U
,0x00000000U
,
171 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff32121aU
,0xffb94362U
,0xffb94362U
,0xff32121aU
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
172 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff491a26U
,0xffb94362U
,0xffb94362U
,0xff491a26U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
173 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff200c11U
,0xff92354dU
,0xff8e344bU
,0xff200c11U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
174 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff4b1b28U
,0xffb94362U
,0xffb94362U
,0xff461925U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
175 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff1e0b10U
,0xffb94362U
,0xffb94362U
,0xffb94362U
,0xffb74361U
,0xff1b0a0eU
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
176 0x00000000U
,0xb0272427U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff240d13U
,0xffb94362U
,0xffb94362U
,0xffb94362U
,0xffb54260U
,0xff1c0a0fU
,0xff272527U
,0xff272527U
,0xff272527U
,0xb0272427U
,0x00000000U
,
177 0x00000000U
,0x8c272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff240d13U
,0xff280e15U
,0xff280e15U
,0xff230d13U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0x8a272527U
,0x00000000U
,
178 0x00000000U
,0x25262626U
,0xc0262626U
,0xfe272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xff272527U
,0xc2262526U
,0x21262626U
,0x00000000U
,
182 __gshared
int[3] statusImgId
;
184 //enum Status { Offline, Online, Away }
185 void buildStatusImages () {
186 statusImgId
[0] = nvg
.createImageRGBA(16, 16, ctiOffline
[], NVGImageFlags
.NoFiltering
);
187 statusImgId
[1] = nvg
.createImageRGBA(16, 16, ctiOnline
[], NVGImageFlags
.NoFiltering
);
188 statusImgId
[2] = nvg
.createImageRGBA(16, 16, ctiAway
[], NVGImageFlags
.NoFiltering
);
192 // ////////////////////////////////////////////////////////////////////////// //
193 //vg.fillColor(NVGColor.orange);
194 void drawLayouter(LT
) (NVGContext vg
, LT lay
, int layTopY
, int x0
, int y0
, int hgt
) if (is(LT
: LayTextImpl
!CT
, CT
)) {
195 if (vg
is null || lay
is null || hgt
< 1 || lay
.lineCount
== 0 || lay
.width
< 1) return;
197 //FIXME: not GHeight!
198 int lidx
= lay
.findLineAtY(layTopY
);
199 if (lidx
>= 0 && lidx
< lay
.lineCount
) {
201 scope(exit
) vg
.restore();
202 vg
.intersectScissor(x0
, y0
, lay
.width
, hgt
);
203 drawY
-= layTopY
-lay
.line(lidx
).y
;
204 vg
.textAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
205 LayFontStyle lastStyle
;
207 bool setColor
= true;
208 while (lidx
< lay
.lineCount
&& hgt
> 0) {
209 auto ln
= lay
.line(lidx
);
210 foreach (ref LayWord w
; lay
.lineWords(lidx
)) {
211 if (lastStyle
!= w
.style || setColor
) {
212 if (w
.style
.fontface
!= lastStyle
.fontface
) vg
.fontFace(lay
.fontFace(w
.style
.fontface
));
213 vg
.fontSize(w
.style
.fontsize
);
214 auto c
= NVGColor(w
.style
.color
);
218 //conprintfln("new color: 0x%08x; fontid=%d; fontsize=%d", w.style.color, w.style.fontface, w.style.fontsize);
222 auto c
= NVGColor(w
.style
.bgcolor
);
223 if (!c
.isTransparent
) {
225 scope(exit
) vg
.restore();
228 vg
.rect(startx
+w
.x
+0.5f, drawY
+0.5, w
.fullwidth
+1, ln
.h
);
234 auto oid
= w
.objectIdx
;
238 scope(exit
) vg
.restore();
239 lay
.objectAtIndex(oid
).draw(vg
, startx
+w
.x
, drawY
+ln
.h
+ln
.desc
);
241 } else if (!w
.expander
) {
242 vg
.text(startx
+w
.x
, drawY
+ln
.h
+ln
.desc
, lay
.wordText(w
));
244 //TODO: draw lines over whitespace
245 if (lastStyle
.underline
) vg
.rect(startx
+w
.x
+0.5f, drawY
+ln
.h
+ln
.desc
+1+0.5f, w
.w
, 1);
246 if (lastStyle
.strike
) vg
.rect(startx
+w
.x
+0.5f, drawY
+ln
.h
+ln
.desc
-w
.asc
/3+0.5f, w
.w
, 2);
247 if (lastStyle
.overline
) vg
.rect(startx
+w
.x
+0.5f, drawY
+ln
.h
+ln
.desc
-w
.asc
-1+0.5f, w
.w
, 1);
257 // ////////////////////////////////////////////////////////////////////////// //
258 enum TriOption
{ Default
= -1, No
= 0, Yes
= 1 }
261 // ////////////////////////////////////////////////////////////////////////// //
262 struct CommonOptions
{
263 TriOption showOffline
= TriOption
.Default
; // show this contact even if it is offline
264 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this contact
265 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity for this contact
266 TriOption skipUnread
= TriOption
.Default
; // skip this contacts in `next_unread` command
267 TriOption ftranAllowed
= TriOption
.Default
; // file transfers allowed for this contact
268 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
269 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
270 //@SRZNonDefaultOnly TriOption confAutoJoin; // automatically join the conference when we're going online
274 struct ProtoOptions
{
288 struct AccountConfig
{
289 string nick
; // my nick
290 string statusmsg
; // my status message
291 bool showOffline
; // show offline persons?
292 bool showPopup
; // show popups for messages?
293 bool blinkActivity
; // blink tray icon if we have some activity (unread msg, transfer request, etc.)?
294 bool skipUnread
; // skip contacts in `next_unread` command?
295 bool hideEmptyGroups
; // hide empty groups? (can be overriden by `hideNoVisible` group option)
296 bool ftranAllowed
; // file transfers allowed for this group (-1: use default value)
297 int resendRotDays
; // how many days message should be in "resend queue" if contact is offline
298 int hmcOnOpen
; // how many history messages we should show when we opening a chat with a contact
302 struct GroupOptions
{
303 uint gid
; // group id; there is always group with gid 0, it is "common" default group
304 string name
; // group name
305 string note
; // group notes/description
306 bool opened
; // is this group opened?
307 TriOption showOffline
= TriOption
.Default
; // show offline persons in this group
308 TriOption showPopup
= TriOption
.Default
; // show popups for messages from this group
309 TriOption blinkActivity
= TriOption
.Default
; // blink tray icon if we have some activity (unread msg, transfer request, etc.) for this group
310 TriOption skipUnread
= TriOption
.Default
; // skip contacts from this group in `next_unread` command
311 TriOption hideIfNoVisibleMembers
= TriOption
.Default
; // hide this group if there are no visible items in it
312 TriOption ftranAllowed
; // file transfers allowed for this group
313 int resendRotDays
= -1; // how many days message should be in "resend queue" if contact is offline (-1: use default value)
314 int hmcOnOpen
= -1; // how many history messages we should show when we opening a chat with a contact (-1: use default value)
318 alias PubKey
= ubyte[32];
324 // pending authorization acceptance
325 // statusmsg: request text
326 // lastonlinetime: request time
328 // auth requested, awaiting acceptance
329 // statusmsg: request text
330 // lastonlinetime: request time
331 // this will change to `Friend` when contact gets online (it means that auth is accepted)
333 // this contact was deleted and put in "total ignore" mode
334 // i.e. any activity from this contact (especially auth requests) will be silently dropped on the floor
337 uint gid
; // group id (see groups.rc)
338 string nick
; // empty: unauthorized
339 @SRZNonDefaultOnly string visnick
; // empty: use `nick`
340 @SRZNonDefaultOnly string statusmsg
;
341 @SRZNonDefaultOnly uint lastonlinetime
; // local unixtime; changed when contact status changed between offline and online (any kind of online)
342 Kind kind
= Kind
.Friend
;
343 string note
; // contact notes/description
344 PubKey pubkey
; // used as unique contact id, same as directory name
349 // ////////////////////////////////////////////////////////////////////////// //
350 void syncFile(bool fullsync
=false) (Imp
!"core.stdc.stdio".FILE
* fl
) nothrow @nogc {
352 import core
.stdc
.stdio
: fileno
;
355 import core
.sys
.posix
.unistd
: fsync
, fdatasync
;
356 static if (fullsync
) fsync(fd
); else fdatasync(fd
);
362 // ////////////////////////////////////////////////////////////////////////// //
363 // write to temporary file, then atomically replace
364 bool serialize(T
) (in auto ref T v
, string fname
) {
365 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
;
366 import core
.sys
.posix
.unistd
: unlink
;
367 import std
.internal
.cstring
;
369 string tmpfname
= fname
~".$$$";
370 FILE
* fo
= fopen(tmpfname
.tempCString
, "w");
371 if (fo
is null) return false;
374 v
.txtser(VFile(fo
, own
:false), skipstname
:true);
375 } catch (Exception e
) {
377 unlink(tmpfname
.tempCString
);
383 if (rename(tmpfname
.tempCString
, fname
.tempCString
) != 0) {
384 unlink(tmpfname
.tempCString
);
392 // ////////////////////////////////////////////////////////////////////////// //
394 log format (it looks like text, but it isn't text):
395 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
397 text: chars in range of [0..31, 92, 127] are \xHH-encoded
399 !: server/app notification
402 if char before text is '!' instead of space, this is "/me" message
404 static struct LogFile
{
406 // this struct must not outlive LogFile
408 enum Kind
{ Notification
, Incoming
, Outgoing
}
410 bool isMe
; // "/me" message?
412 char[] rawText
; // undecoded
414 // decode raw text to dchars; returns forward range
415 auto byDChar () const nothrow @safe @nogc {
416 static struct Range
{
417 pure nothrow @trusted @nogc:
418 const(char)[] raw
; // anchored
420 @property bool empty () const => (pos
>= raw
.length
);
421 @property dchar front () const {
422 if (pos
< raw
.length
) {
423 char ch
= raw
.ptr
[pos
];
425 if (ch
!= '\\') return ch
;
426 if (raw
.length
-pos
< 2) return ch
;
428 if (ch
!= 'x' && ch
!= 'X') {
430 case 'e': return '\e';
431 case 'n': return '\n';
432 case 'r': return '\r';
433 case 't': return '\t';
438 if (raw
.length
-pos
< 4) return '?';
439 int n0
= raw
.ptr
[pos
+2].digitInBase(16);
440 if (n0
< 0) return '?';
441 int n1
= raw
.ptr
[pos
+3].digitInBase(16);
442 if (n1
< 0) return '?';
443 return cast(char)(n0
*16+n1
);
447 while (!dc
.decode(cast(ubyte)raw
.ptr
[cpos
++])) {
448 if (cpos
>= raw
.length
) break; // no more chars
450 return (dc
.complete ? dc
.codepoint
: dc
.replacement
);
452 return Utf8DecoderFast
.replacement
;
456 if (pos
< raw
.length
) {
457 char ch
= raw
.ptr
[pos
];
460 if (ch
!= '\\') return;
461 if (raw
.length
-pos
< 1) { pos
= raw
.length
; return; }
463 if (ch
!= 'x' && ch
!= 'X') return;
464 if (raw
.length
-pos
< 2) { pos
= raw
.length
; return; }
469 while (!dc
.decode(cast(ubyte)raw
.ptr
[pos
++])) {
470 if (pos
>= raw
.length
) break; // no more chars
474 Range
save () const => Range(raw
, pos
);
476 return Range(rawText
, 0);
487 @disable this (this); // no copies
489 this (const(char)[] fname
) nothrow => load(fname
);
495 // no `Msg` should outlive this call!
496 void clear () nothrow {
501 void load (const(char)[] fname
) nothrow {
502 import std
.internal
.cstring
;
504 if (messages
.length
) {
506 messages
.assumeSafeAppend
;
510 data
.assumeSafeAppend
;
513 if (fname
.length
== 0) return;
516 auto fi
= VFile(fname
);
517 if (fi
.size
> int.max
/8) assert(0, "log file too big");
518 data
.length
= cast(int)fi
.size
;
519 fi
.rawReadExact(data
);
520 } catch (Exception e
) {}
525 // `data` should be set
526 private void parseData () nothrow {
527 import core
.stdc
.string
: memchr
;
529 assert(messages
.length
== 0); // sorry
530 if (data
.length
== 0) return;
536 while (dp
< data
.ptr
+data
.length
) {
537 auto nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
538 if (nx
is null) break;
543 messages
.reserve(nlcount
+1);
545 static int toInt (const(char)[] s
) nothrow @trusted @nogc {
547 while (s
.length
&& s
.ptr
[0].isdigit
) {
548 res
= res
*10+s
.ptr
[0]-'0';
555 auto dp
= data
.ptr
, nx
= data
.ptr
+data
.length
;
556 for (; dp
< data
.ptr
+data
.length
; dp
= nx
+1) {
557 nx
= cast(char*)memchr(dp
, '\n', data
.length
-(dp
-data
.ptr
));
558 if (nx
is null) nx
= data
.ptr
+data
.length
;
559 // 25 is the minimal message length
560 if (nx
-dp
< 25) continue;
561 if (dp
[1] != '[' || dp
[6] != '/' || dp
[9] != '/' || dp
[12] != ' ' || dp
[15] != ':' || dp
[18] != ':' || dp
[21] != ']' || dp
[22] != ':') continue;
562 foreach (immutable cidx
, immutable char ch
; dp
[0..21]) {
563 if (cidx
< 2 || cidx
== 6 || cidx
== 9 || cidx
== 12 || cidx
== 15 || cidx
== 18) continue;
564 if (!ch
.isdigit
) continue;
569 case '!': msg
.kind
= msg
.Kind
.Notification
; break;
570 case '<': msg
.kind
= msg
.Kind
.Outgoing
; break;
571 case '>': msg
.kind
= msg
.Kind
.Incoming
; break;
576 case ' ': msg
.isMe
= false; break;
577 case '!': msg
.isMe
= true; break;
580 int year
= toInt(dp
[2..6]);
581 if (year
< 2018 || year
> 2036) continue; //FIXME in 2036! ;-)
582 int month
= toInt(dp
[7..9]);
583 if (month
< 1 || month
> 12) continue;
584 int day
= toInt(dp
[10..12]);
585 if (day
< 1 || day
> 31) continue;
586 int hour
= toInt(dp
[13..15]);
587 if (hour
< 0 || hour
> 23) continue;
588 int minute
= toInt(dp
[16..18]);
589 if (minute
< 0 || minute
> 59) continue;
590 int second
= toInt(dp
[19..21]);
591 if (second
< 0 || second
> 59) continue;
594 msg
.time
= SysTime(DateTime(year
, month
, day
, hour
, minute
, second
));
596 } catch (Exception e
) {}
597 if (!timeok
) continue;
598 msg
.rawText
= dp
[24..(nx
-dp
)];
604 // append line to log file; text is in utf8
605 // WARNING! does no validity checks!
606 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
, SysTime time
) nothrow {
607 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, fwrite
;
608 import std
.internal
.cstring
;
610 if (fname
.length
== 0) return false;
612 FILE
* fo
= fopen(fname
.tempCString
, "a");
613 if (fo
is null) return false;
614 scope(exit
) { syncFile(fo
); fclose(fo
); }
616 bool xwrite (const(char)[] s
...) nothrow @nogc {
617 while (s
.length
> 0) {
618 auto wr
= fwrite(s
.ptr
, 1, s
.length
, fo
);
620 import core
.stdc
.errno
;
621 if (errno
!= EINTR
) return false;
629 bool wrnum (int n
, int wdt
) nothrow @nogc {
630 import core
.stdc
.stdio
: snprintf
;
631 if (n
< 0) return false; // alas
632 char[128] buf
= void;
633 auto len
= (wdt
> 0 ?
snprintf(buf
.ptr
, buf
.length
, "%0*d", wdt
, n
) : snprintf(buf
.ptr
, buf
.length
, "%d", n
));
634 if (len
< 1) return false;
635 return xwrite(buf
[0..len
]);
638 final switch (kind
) {
639 case LogFile
.Msg
.Kind
.Notification
: if (!xwrite("!")) return false; break;
640 case LogFile
.Msg
.Kind
.Incoming
: if (!xwrite(">")) return false; break;
641 case LogFile
.Msg
.Kind
.Outgoing
: if (!xwrite("<")) return false; break;
645 auto dt = cast(DateTime
)time
;
646 if (!xwrite("[")) return false;
647 if (!wrnum(dt.year
, 4)) return false;
648 if (!xwrite("/")) return false;
649 if (!wrnum(cast(int)dt.month
, 2)) return false;
650 if (!xwrite("/")) return false;
651 if (!wrnum(dt.day
, 2)) return false;
652 if (!xwrite(" ")) return false;
653 if (!wrnum(dt.hour
, 2)) return false;
654 if (!xwrite(":")) return false;
655 if (!wrnum(dt.minute
, 2)) return false;
656 if (!xwrite(":")) return false;
657 if (!wrnum(dt.second
, 2)) return false;
658 if (!xwrite("]:")) return false;
659 if (!xwrite(isMe ?
"!" : " ")) return false;
661 // write encoded string
662 while (text
.length
) {
664 while (end
< text
.length
) {
665 char ch
= text
.ptr
[end
];
666 if (ch
< ' ' || ch
== 92 || ch
== 127) break;
670 if (!xwrite(text
[0..end
])) return false;
673 switch (text
.ptr
[0]) {
674 case '\e': if (!xwrite(`\e`)) return false; break;
675 case '\n': if (!xwrite(`\n`)) return false; break;
676 case '\r': if (!xwrite(`\r`)) return false; break;
677 case '\t': if (!xwrite(`\t`)) return false; break;
679 import core
.stdc
.stdio
: snprintf
;
681 auto len
= snprintf(buf
.ptr
, buf
.length
, "\\x%02x", cast(uint)text
.ptr
[0]);
682 if (!xwrite(buf
[0..len
])) return false;
692 // append line to log file; text is in utf8
693 // WARNING! does no validity checks!
694 static bool appendLine (const(char)[] fname
, LogFile
.Msg
.Kind kind
, const(char)[] text
, bool isMe
=false) nothrow {
696 return appendLine(fname
, kind
, text
, isMe
, Clock
.currTime
);
697 } catch (Exception e
) {}
703 // ////////////////////////////////////////////////////////////////////////// //
706 this (Account aOwner
) { acc
= aOwner
; }
709 bool mDirty
; // true: write contact's config
717 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
720 import std
.file
: mkdirRecurse
;
721 import std
.path
: dirName
;
723 assert(acc
!is null);
725 if (diskFileName
.length
== 0) {
726 diskFileName
= acc
.basePath
~"/contacts/groups.rc";
728 mkdirRecurse(diskFileName
.dirName
);
729 if (serialize(info
, diskFileName
)) mDirty
= false;
732 @property bool visible () const nothrow @trusted @nogc {
733 if (!hideIfNoVisibleMembers
) return true; // always visible
734 // check if we have any visible members
735 foreach (const(Contact
) c
; acc
.contacts
.byValue
) {
736 if (c
.gid
!= info
.gid
) continue;
737 if (c
.visibleNoGroupCheck
) return true;
739 return false; // nobody's here
742 private bool getTriOpt(string
fld, string fld2
=null) () const nothrow @trusted @nogc {
743 enum lo
= "info."~fld;
744 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
745 static if (fld2
.length
) return mixin("acc.info."~fld2
); else return mixin("acc.info."~fld);
748 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
749 enum lo
= "info."~fld;
750 if (mixin(lo
) >= 0) return mixin(lo
);
751 return mixin("acc.info."~fld);
754 @property nothrow @safe {
755 uint gid () const pure @nogc => info
.gid
;
757 bool opened () const @nogc => info
.opened
;
758 void opened (bool v
) @nogc { pragma(inline
, true); if (info
.opened
!= v
) { info
.opened
= v
; markDirty(); } }
760 string
name () const @nogc => info
.name
;
761 void name (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unnamed>"; if (info
.name
!= v
) { info
.name
= v
; markDirty(); } }
763 string
note () const @nogc => info
.note
;
764 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
767 bool showOffline () const => getTriOpt
!"showOffline";
768 bool showPopup () const => getTriOpt
!"showPopup";
769 bool blinkActivity () const => getTriOpt
!"blinkActivity";
770 bool skipUnread () const => getTriOpt
!"skipUnread";
771 bool hideIfNoVisibleMembers () const => getTriOpt
!("hideIfNoVisibleMembers", "hideEmptyGroups");
772 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
773 int resendRotDays () const => getIntOpt
!"resendRotDays";
774 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
780 // ////////////////////////////////////////////////////////////////////////// //
781 final class Contact
{
783 enum Status
{ Offline
, Online
, Away
}
786 this (Account aOwner
) { acc
= aOwner
; }
789 bool mDirty
; // true: write contact's config
795 Status status
= Status
.Offline
; // not saved, so if it safe to change it
798 void markDirty () pure nothrow @safe @nogc => mDirty
= true;
801 import std
.file
: mkdirRecurse
;
802 import std
.path
: dirName
;
804 assert(acc
!is null);
806 if (diskFileName
.length
== 0) {
807 diskFileName
= acc
.basePath
~"/contacts/"~tox_hex(info
.pubkey
[])~"/config.rc";
808 acc
.contacts
[info
.pubkey
] = this;
810 mkdirRecurse(diskFileName
.dirName
);
811 mkdirRecurse(diskFileName
.dirName
~"/avatars");
812 mkdirRecurse(diskFileName
.dirName
~"/files");
813 mkdirRecurse(diskFileName
.dirName
~"/fileparts");
814 mkdirRecurse(diskFileName
.dirName
~"/logs");
815 if (serialize(info
, diskFileName
)) mDirty
= false;
819 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (showOffline || status
!= Status
.Offline
);
821 @property bool visible () const nothrow @trusted @nogc {
822 if (!showOffline
&& status
== Status
.Offline
) return false;
823 auto grp
= acc
.groupById(gid
);
827 @property nothrow @safe {
828 ContactInfo
.Kind
kind () const @nogc => info
.kind
;
829 void kind (ContactInfo
.Kind v
) @nogc { pragma(inline
, true); if (info
.kind
!= v
) { info
.kind
= v
; markDirty(); } }
831 uint gid () const @nogc => info
.gid
;
832 void gid (uint v
) @nogc { pragma(inline
, true); if (info
.gid
!= v
) { info
.gid
= v
; markDirty(); } }
834 string
nick () const @nogc => info
.nick
;
835 void nick (string v
) @nogc { pragma(inline
, true); if (v
.length
== 0) v
= "<unknown>"; if (info
.nick
!= v
) { info
.nick
= v
; markDirty(); } }
837 string
visnick () const @nogc => info
.visnick
;
838 void visnick (string v
) @nogc { pragma(inline
, true); if (info
.visnick
!= v
) { info
.visnick
= v
; markDirty(); } }
840 string
displayNick () const @nogc => (info
.visnick
.length ? info
.visnick
: info
.nick
);
842 string
statusmsg () const @nogc => info
.statusmsg
;
843 void statusmsg (string v
) @nogc { pragma(inline
, true); if (info
.statusmsg
!= v
) { info
.statusmsg
= v
; markDirty(); } }
845 void setLastOnlineNow () {
847 auto ut
= Clock
.currTime
.toUTC().toUnixTime();
848 if (info
.lastonlinetime
!= cast(uint)ut
) { info
.lastonlinetime
= cast(uint)ut
; markDirty(); }
849 } catch (Exception e
) {}
852 string
note () const @nogc => info
.note
;
853 void note (string v
) @nogc { pragma(inline
, true); if (info
.note
!= v
) { info
.note
= v
; markDirty(); } }
856 private bool getTriOpt(string
fld) () const nothrow @trusted @nogc {
857 enum lo
= "info.opts."~fld;
858 if (mixin(lo
) != TriOption
.Default
) return (mixin(lo
) == TriOption
.Yes
);
859 auto grp
= acc
.groupById(info
.gid
);
860 enum go
= "grp.info."~fld;
861 if (mixin(go
) != TriOption
.Default
) return (mixin(go
) == TriOption
.Yes
);
862 return mixin("acc.info."~fld);
865 private int getIntOpt(string
fld) () const nothrow @trusted @nogc {
866 enum lo
= "info.opts."~fld;
867 if (mixin(lo
) >= 0) return mixin(lo
);
868 auto grp
= acc
.groupById(info
.gid
);
869 enum go
= "grp.info."~fld;
870 if (mixin(go
) >= 0) return mixin(go
);
871 return mixin("acc.info."~fld);
874 @property nothrow @safe @nogc {
875 bool showOffline () const => getTriOpt
!"showOffline";
876 bool showPopup () const => getTriOpt
!"showPopup";
877 bool blinkActivity () const => getTriOpt
!"blinkActivity";
878 bool skipUnread () const => getTriOpt
!"skipUnread";
879 bool ftranAllowed () const => getTriOpt
!"ftranAllowed";
880 int resendRotDays () const => getIntOpt
!"resendRotDays";
881 int hmcOnOpen () const => getIntOpt
!"hmcOnOpen";
886 // ////////////////////////////////////////////////////////////////////////// //
887 final class Account
{
890 import std
.algorithm
: sort
;
891 GroupOptions
[] glist
;
892 scope(exit
) delete glist
;
893 foreach (Group g
; groups
) glist
~= g
.info
;
894 glist
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
895 glist
.serialize(basePath
~"contacts/groups.rc");
899 // returns `null` if there is no such file or file cannot be loaded
900 ubyte[] loadToxCoreData () {
901 if (toxDataDiskName
.length
== 0) return null;
903 auto fl
= VFile(toxDataDiskName
);
904 if (fl
.size
> int.max
/8) throw new Exception("toxcore data corrupted");
905 auto res
= new ubyte[](cast(int)fl
.size
);
906 fl
.rawReadExact(res
[]);
908 } catch (Exception e
) {
913 bool saveToxCoreData () {
914 import core
.stdc
.stdio
: FILE
, fopen
, fclose
, rename
;
915 import core
.stdc
.stdlib
: malloc
, free
;
916 import core
.sys
.posix
.unistd
: unlink
;
917 import std
.internal
.cstring
;
919 if (tox
is null || toxDataDiskName
.length
== 0) return false;
921 auto size
= tox_get_savedata_size(tox
);
922 if (size
> int.max
/8) return false; //throw new Exception("save data too big");
924 char* savedata
= cast(char*)malloc(size
);
925 if (savedata
is null) return false;
926 scope(exit
) if (savedata
!is null) free(savedata
);
928 tox_get_savedata(tox
, savedata
);
930 auto ofname
= toxDataDiskName
;
931 auto ofnametmp
= toxDataDiskName
~".$$$";
932 FILE
* fo
= fopen(ofnametmp
.tempCString
, "w");
933 if (fo
is null) return false;
935 VFile(fo
, own
:false).rawWriteExact(savedata
[0..size
]);
936 } catch (Exception e
) {
937 unlink(ofnametmp
.tempCString
);
943 if (rename(ofnametmp
.tempCString
, ofname
.tempCString
) != 0) {
944 unlink(ofnametmp
.tempCString
);
953 string toxDataDiskName
;
954 string basePath
; // with trailing "/"
955 ProtoOptions protoOpts
;
958 Contact
[PubKey
] contacts
;
961 this (string aBaseDir
) {
962 import std
.algorithm
: sort
;
963 import std
.file
: DirEntry
, SpanMode
, dirEntries
;
964 import std
.path
: absolutePath
, baseName
;
966 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
968 } else if (aBaseDir
== "/") {
971 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
974 basePath
= aBaseDir
.absolutePath
;
975 toxDataDiskName
= basePath
~"toxdata.tox";
976 protoOpts
.txtunser(VFile(basePath
~"proto.rc"));
977 info
.txtunser(VFile(basePath
~"config.rc"));
980 GroupOptions
[] glist
;
981 glist
.txtunser(VFile(basePath
~"contacts/groups.rc"));
982 bool hasDefaultGroup
= false;
983 bool hasMoronsGroup
= false;
984 foreach (ref GroupOptions gi
; glist
[]) {
985 auto g
= new Group(this);
988 foreach (ref gg
; groups
[]) if (gg
.gid
== g
.gid
) { delete gg
; gg
= g
; found
= true; }
989 if (!found
) groups
~= g
;
990 if (g
.gid
== 0) hasDefaultGroup
= true;
991 if (g
.gid
== g
.gid
.max
) hasMoronsGroup
= true;
994 // create default group if necessary
995 if (!hasDefaultGroup
) {
999 gi
.note
= "default group for new contacts";
1001 auto g
= new Group(this);
1006 // create morons group if necessary
1007 if (!hasMoronsGroup
) {
1009 gi
.gid
= gi
.gid
.max
;
1010 gi
.name
= "<morons>";
1011 gi
.note
= "group for completely ignored dumbfucks";
1013 gi
.showOffline
= TriOption
.No
;
1014 gi
.showPopup
= TriOption
.No
;
1015 gi
.blinkActivity
= TriOption
.No
;
1016 gi
.skipUnread
= TriOption
.Yes
;
1017 gi
.hideIfNoVisibleMembers
= TriOption
.Yes
;
1018 gi
.ftranAllowed
= TriOption
.No
;
1019 gi
.resendRotDays
= 0;
1021 auto g
= new Group(this);
1027 groups
.sort
!((in ref a
, in ref b
) => a
.gid
< b
.gid
);
1029 if (!hasDefaultGroup ||
!hasMoronsGroup
) saveGroups();
1032 foreach (DirEntry
de; dirEntries(basePath
~"contacts", SpanMode
.shallow
)) {
1033 if (de.name
.baseName
== "." ||
de.name
.baseName
== "..") continue;
1035 import std
.file
: exists
;
1036 if (!de.isDir
) continue;
1037 string cfgfn
= de.name
~"/config.rc";
1038 if (!cfgfn
.exists
) continue;
1040 ci
.txtunser(VFile(cfgfn
));
1041 auto c
= new Contact(this);
1042 c
.diskFileName
= cfgfn
;
1044 contacts
[c
.info
.pubkey
] = c
;
1045 // fix contact group
1046 if (groupById
!false(c
.gid
) is null) {
1047 c
.info
.gid
= 0; // move to default group
1050 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1051 } catch (Exception e
) {
1052 conwriteln("ERROR loading contact from '", de.name
, "/config.rc'");
1057 char* toxProxyHost
= null;
1059 import core
.stdc
.stdlib
: free
;
1060 if (toxProxyHost
!is null) free(toxProxyHost
);
1063 scope(exit
) delete savedata
;
1065 auto toxOpts
= tox_options_new();
1066 assert(toxOpts
!is null);
1067 scope(exit
) if (toxOpts
!is null) tox_options_free(toxOpts
);
1068 tox_options_default(toxOpts
);
1070 bool createNewToxCoreAccount
= false;
1071 toxOpts
.tox_options_set_ipv6_enabled(protoOpts
.ipv6
);
1072 toxOpts
.tox_options_set_udp_enabled(protoOpts
.udp
);
1073 toxOpts
.tox_options_set_local_discovery_enabled(protoOpts
.localDiscovery
);
1074 toxOpts
.tox_options_set_hole_punching_enabled(protoOpts
.holePunching
);
1075 toxOpts
.tox_options_set_start_port(protoOpts
.startPort
);
1076 toxOpts
.tox_options_set_end_port(protoOpts
.endPort
);
1077 toxOpts
.tox_options_set_tcp_port(protoOpts
.tcpPort
);
1078 toxOpts
.tox_options_set_proxy_type(protoOpts
.proxyType
);
1079 if (protoOpts
.proxyType
!= TOX_PROXY_TYPE_NONE
) {
1080 import core
.stdc
.stdlib
: malloc
;
1081 toxOpts
.tox_options_set_proxy_port(protoOpts
.proxyPort
);
1082 // create proxy address string
1083 toxProxyHost
= cast(char*)malloc(protoOpts
.proxyAddr
.length
+1);
1084 if (toxProxyHost
is null) assert(0, "out of memory");
1085 toxProxyHost
[0..protoOpts
.proxyAddr
.length
] = protoOpts
.proxyAddr
[];
1086 toxProxyHost
[protoOpts
.proxyAddr
.length
] = 0;
1087 toxOpts
.tox_options_set_proxy_host(toxProxyHost
);
1089 savedata
= loadToxCoreData();
1090 if (savedata
is null) {
1091 // create new tox instance
1092 createNewToxCoreAccount
= true;
1093 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_NONE
);
1094 conwriteln("creating new ToxCore account...");
1097 toxOpts
.tox_options_set_savedata_type(TOX_SAVEDATA_TYPE_TOX_SAVE
);
1098 toxOpts
.tox_options_set_savedata_length(savedata
.length
);
1099 toxOpts
.tox_options_set_savedata_data(savedata
.ptr
, savedata
.length
);
1101 // create tox instance
1103 tox
= tox_new(toxOpts
, &error
);
1105 import std
.conv
: to
;
1106 assert(0, "cannot create ToxCore instance: error is "~error
.to
!string
);
1108 if (createNewToxCoreAccount
) saveToxCoreData();
1112 if (tox
!is null) tox_kill(tox
);
1115 // will not write contact to disk
1116 Contact
createEmptyContact () {
1117 auto c
= new Contact(this);
1119 c
.info
.nick
= "test contact";
1120 c
.info
.pubkey
[] = 0;
1124 // returns `null` if there is no such group, and `dofail` is `true`
1125 inout(Group
) groupById(bool dofail
=true) (uint agid
) inout nothrow @nogc {
1126 foreach (const Group g
; groups
) if (g
.gid
== agid
) return cast(typeof(return))g
;
1127 static if (dofail
) assert(0, "group not found"); else return null;
1131 static Account
CreateNew (string aBaseDir
, string aAccName
) {
1132 import std
.file
: mkdirRecurse
;
1133 if (aBaseDir
.length
== 0 || aBaseDir
== "." || aBaseDir
== "./") {
1135 } else if (aBaseDir
== "/") {
1138 mkdirRecurse(aBaseDir
);
1139 if (aBaseDir
[$-1] != '/') aBaseDir
~= '/';
1141 mkdirRecurse(aBaseDir
~"/contacts");
1142 // write protocol options
1145 popt
.txtser(VFile(aBaseDir
~"proto.rc", "w"), skipstname
:true);
1150 acc
.nick
= aAccName
;
1151 acc
.showPopup
= true;
1152 acc
.blinkActivity
= true;
1153 acc
.hideEmptyGroups
= false;
1154 acc
.ftranAllowed
= true;
1155 acc
.resendRotDays
= 4;
1157 acc
.txtser(VFile(aBaseDir
~"config.rc", "w"), skipstname
:true);
1159 // create default group
1161 GroupOptions
[1] grp
;
1163 grp
[0].name
= "default";
1164 grp
[0].opened
= true;
1165 //grp[0].hideIfNoVisible = TriOption.Yes;
1166 grp
[].txtser(VFile(aBaseDir
~"contacts/groups.rc", "w"), skipstname
:true);
1169 return new Account(aBaseDir
);
1174 // ////////////////////////////////////////////////////////////////////////// //
1175 class ListItemBase
{
1180 bool mVisible
= true;
1183 void setupFont () => nvg
.fontFace
= "ui"; // setup font face for this item
1184 @property int height () => cast(int)nvg
.textFontHeight
;
1185 @property bool visible () => mVisible
;
1186 bool onMouse (MouseEvent event
) => false; // true: eaten
1187 bool onKey (KeyEvent event
) => false; // true: eaten
1188 void drawAt (int x0
, int y0
, int wdt
) {} // real rect is scissored
1192 class ListItemAccount
: ListItemBase
{
1197 this (Account aAcc
) { assert(aAcc
!is null); acc
= aAcc
; }
1199 override void setupFont () => nvg
.fontFace
= "uib"; // bold
1201 override void drawAt (int x0
, int y0
, int wdt
) {
1205 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1206 nvg
.fillColor(NVGColor("#5fff"));
1209 nvg
.fillColor(NVGColor("#fff"));
1210 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1211 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1212 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, acc
.info
.nick
);
1217 class ListItemGroup
: ListItemBase
{
1222 this (Group aGroup
) { assert(aGroup
!is null); group
= aGroup
; }
1224 override @property bool visible () => group
.visible
;
1226 override void drawAt (int x0
, int y0
, int wdt
) {
1230 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
1231 nvg
.fillColor(NVGColor("#5888"));
1234 nvg
.fillColor(NVGColor("#ff0"));
1235 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1236 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
1237 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, group
.name
);
1242 class ListItemContact
: ListItemBase
{
1247 this (Contact aCt
) { assert(aCt
!is null); ct
= aCt
; }
1249 override @property bool visible () => ct
.visible
;
1251 override @property int height () { import std
.algorithm
: max
; return max(cast(int)nvg
.textFontHeight
, 16); } //FIXME: 16 is image height
1253 override void drawAt (int x0
, int y0
, int wdt
) {
1258 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1259 nvg.fillColor(NVGColor("#5888"));
1266 nvg
.imageSize(statusImgId
[ct
.status
], iw
, ih
);
1267 //conwriteln("image: (", iw, "x", ih, ")");
1268 nvg
.rect(x0
, y0
+(hgt
-ih
)/2, iw
, ih
);
1269 nvg
.fillPaint(nvg
.imagePattern(x0
, y0
+(hgt
-ih
)/2, iw
, ih
, 0, statusImgId
[ct
.status
]));
1272 nvg
.fillColor(NVGColor("#f70"));
1273 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1274 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1275 //conwriteln(nvg.textFontDescender);
1276 nvg
.text(x0
+4+iw
+4, y0
+hgt
+cast(int)nvg
.textFontDescender
, ct
.displayNick
);
1281 // ////////////////////////////////////////////////////////////////////////// //
1282 // visible contact list
1285 int mActiveItem
= -1; // active item (may be different from selected with cursor)
1289 ListItemBase
[] items
;
1294 void opOpAssign(string op
:"~") (ListItemBase li
) {
1295 if (li
is null) return;
1299 void buildAccount (Account acc
) {
1300 if (acc
is null) return;
1301 items
~= new ListItemAccount(acc
);
1303 scope(exit
) delete css
;
1304 foreach (Group g
; acc
.groups
) {
1305 items
~= new ListItemGroup(g
);
1307 css
.assumeSafeAppend
;
1308 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
1309 import std
.algorithm
: sort
;
1311 string s0
= a
.displayNick
;
1312 string s1
= b
.displayNick
;
1313 auto xlen
= (s0
.length
< s1
.length ? s0
.length
: s1
.length
);
1314 foreach (immutable idx
, char c0
; s0
[0..xlen
]) {
1315 if (c0
>= 'A' && c0
<= 'Z') c0
+= 32; // poor man's tolower()
1317 if (c1
>= 'A' && c1
<= 'Z') c1
+= 32; // poor man's tolower()
1318 if (auto d
= c0
-c1
) return (d
< 0);
1320 return (s0
.length
< s1
.length
);
1322 foreach (Contact c
; css
) items
~= new ListItemContact(c
);
1327 bool onMouse (MouseEvent event
) {
1332 bool onKey (KeyEvent event
) {
1336 // real rect is scissored
1337 void drawAt (int x0
, int y0
, int wdt
, int hgt
) {
1339 scope(exit
) nvg
.restore();
1340 //nvg.fontFace = "ui";
1343 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
1344 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
1346 foreach (immutable iidx
, ListItemBase li
; items
) {
1347 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
1350 if (lh
< 1) continue;
1352 scope(exit
) nvg
.restore();
1354 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
1355 li
.drawAt(x0
, y0
+y
, wdt
);
1357 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
1358 nvg
.intersectScissor(0, 0, wdt
, lh
);
1359 li
.drawAt(0, 0, wdt
);
1362 if (y
>= hgt
) break;
1368 // ////////////////////////////////////////////////////////////////////////// //
1369 __gshared CList clist
;
1372 // ////////////////////////////////////////////////////////////////////////// //
1373 void main (string
[] args
) {
1374 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
1378 acc
= new Account("_fakeacc");
1379 } catch (Exception e
) {
1380 conwriteln("creating account...");
1381 acc
= Account
.CreateNew("_fakeacc", "ketmar");
1383 // create fake contact
1384 if (acc
.contacts
.length
== 0) {
1385 conwriteln("creating fake contact...");
1386 auto c
= acc
.createEmptyContact();
1387 c
.info
.nick
= "test contact";
1388 c
.info
.pubkey
[] = 0x55;
1392 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
1394 glconShowKey
= "M-Grave";
1395 glconSetAndSealFPS(0); // draw-on-demand
1397 conProcessQueue(256*1024); // load config
1398 conProcessArgs
!true(args
);
1399 conProcessQueue(256*1024);
1401 //setOpenGLContextVersion(3, 2); // up to GLSL 150
1402 setOpenGLContextVersion(2, 0); // it's enough
1405 oglSetupDG
= delegate () {
1406 if (glconCtlWindow
.width
> 1 && optCListWidth
< 0) optCListWidth
= glconCtlWindow
.width
/5;
1408 glconCtlWindow
.vsync
= false;
1410 nvg
= createGL2NVG(NVG_ANTIALIAS|NVG_STENCIL_STROKES
);
1411 if (nvg
is null) assert(0, "cannot initialize NanoVG");
1417 static immutable skullsPng
= /*cast(immutable(ubyte)[])*/import("data/skulls.png");
1418 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
1419 auto xi
= loadImageFromMemory(skullsPng
[]);
1420 scope(exit
) delete xi
;
1421 nvgSkullsImg
= nvg
.createImageFromMemoryImage(xi
, NVGImageFlags
.NoFiltering|NVGImageFlags
.RepeatX|NVGImageFlags
.RepeatY
);
1422 if (nvgSkullsImg
== 0) assert(0, "cannot load background image");
1423 } catch (Exception e
) {
1424 assert(0, "cannot load background image");
1426 buildStatusImages();
1428 layFixFontDG
= delegate (ref LayFontStyle st
) nothrow @trusted @nogc {
1429 //{ import core.stdc.stdio; printf("monospace=%d; italic=%d; bold=%d\n", (st.monospace ? 1 : 0), (st.italic ? 1 : 0), (st.bold ? 1 : 0)); }
1432 void put (const(char)[] s
...) nothrow @trusted @nogc {
1433 if (s
.length
== 0) return;
1434 if (tbuf
.length
-tbufpos
< s
.length
) assert(0, "wtf?!");
1435 tbuf
.ptr
[tbufpos
..tbufpos
+s
.length
] = s
[];
1436 tbufpos
+= cast(int)s
.length
;
1438 put(st
.monospace ?
"mono" : "text");
1439 if (st
.italic
) put("i");
1440 if (st
.bold
) put("b");
1441 st
.fontface
= laf
.fontFaceId(tbuf
[0..tbufpos
]);
1444 lay
= new LayTextClass(laf
, glconCtlWindow
.width
/2);
1445 lay
.fontStyle
.fontsize
= 16;
1446 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1447 lay
.fontStyle
.monospace
= true;
1448 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
1449 lay
.put("ketmar (бля)");
1450 lay
.fontStyle
.monospace
= false;
1451 lay
.putHardSpace(64);
1455 lay
.put("2018/02/12");
1457 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
1458 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
1459 lay
.put("11:09:03");
1461 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
1462 lay
.fontStyle
.color
= NVGColor
.darkorange
.asUint
;
1463 lay
.fontStyle
.fontsize
= 20;
1464 lay
.put("this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1467 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
1468 lastWindowWidth
= glconCtlWindow
.width
;
1470 clist
= new CList();
1471 clist
.buildAccount(acc
);
1473 glconCtlWindow
.setMinSize(640, 480);
1476 resizeEventDG
= delegate (int wdt
, int hgt
) {
1477 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
1478 if (wdt
> 1 && optCListWidth
> 0 && lastWindowWidth
> 0 && lastWindowWidth
!= wdt
) {
1479 immutable double frc
= lastWindowWidth
/optCListWidth
;
1480 optCListWidth
= cast(int)(wdt
/frc
);
1481 if (optCListWidth
< 64) optCListWidth
= 64;
1482 lastWindowWidth
= wdt
;
1487 keyEventDG
= delegate (KeyEvent event
) {
1488 //if (event.pressed && event == "*-Q") { concmd("quit"); return; }
1489 if (event
== "U-*-Q") { concmd("quit"); return; }
1490 if (clist
!is null && clist
.onKey(event
)) return;
1491 if (event
== "D-Space") {
1493 lay
.fontStyle
.fontsize
= 16;
1494 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
1495 lay
.fontStyle
.monospace
= true;
1496 lay
.fontStyle
.bgcolor
= NVGColor("#222").asUint
;
1498 lay
.fontStyle
.monospace
= false;
1499 lay
.putHardSpace(64);
1503 lay
.put("2018/02/12");
1505 lay
.fontStyle
.color
= NVGColor("#fff").asUint
;
1506 lay
.fontStyle
.bgcolor
= NVGColor("#006").asUint
;
1507 lay
.put("11:09:03");
1509 lay
.fontStyle
.bgcolor
= NVGColor
.transparent
.asUint
;
1510 lay
.fontStyle
.color
= NVGColor
.k8orange
.asUint
;
1511 lay
.fontStyle
.fontsize
= 20;
1515 foreach (immutable widx
; 0..uniform
!"[]"(2, 28)) {
1517 if (widx
!= 0) lay
.put(" ");
1518 foreach (; 0..uniform
!"[]"(1, 9)) {
1519 char ch
= cast(char)('a'+uniform
!"[]"(0, 25));
1521 //lay.putSoftHypen();
1525 // long word, for hyphenation test
1526 foreach (immutable idx
; 0..228) {
1527 char ch
= cast(char)('a'+uniform
!"[]"(0, 25));
1529 if (idx
%24 == 23) lay
.putSoftHypen();
1535 glconResetNextFrame();
1540 mouseEventDG
= delegate (MouseEvent event
) {
1541 if (clist
!is null && clist
.onMouse(event
)) return;
1544 charEventDG
= delegate (dchar ch
) {
1547 focusEventDG
= delegate (bool focused
) {
1551 redrawFrameDG
= delegate () {
1552 if (nvg
is null) return;
1554 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
1555 glViewport(0, 0, glconCtlWindow
.width
, glconCtlWindow
.height
);
1556 glMatrixMode(GL_MODELVIEW
);
1557 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
1559 glClearColor(0, 0, 0, 0);
1560 glClear(glNVGClearFlags
/*|GL_COLOR_BUFFER_BIT*/);
1563 nvg
.beginFrame(glconCtlWindow
.width
, glconCtlWindow
.height
);
1564 scope(exit
) nvg
.endFrame();
1566 if (clist
!is null && optCListWidth
> 0) {
1567 // draw contact list
1570 int wdt
= optCListWidth
;
1571 int hgt
= nvg
.height
-cy
*2;
1573 nvg
.shapeAntiAlias
= true;
1575 nvg
.strokeWidth
= 1;
1579 scope(exit
) nvg
.restore();
1582 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
1585 nvg
.imageSize(nvgSkullsImg
, w
, h
);
1586 nvg
.fillPaint(nvg
.imagePattern(0, 0, w
, h
, 0, nvgSkullsImg
));
1588 nvg
.strokeColor(NVGColor("#f70"));
1592 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
1594 clist
.drawAt(cx
+3, cy
+3, wdt
-3*2, hgt
-3*2);
1599 scope(exit
) nvg
.restore();
1603 wdt
= nvg
.width
-cx
-1;
1605 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
1606 nvg
.fillColor(NVGColor
.black
);
1607 nvg
.strokeColor(NVGColor("#f70"));
1611 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
1612 lay
.relayout(wdt
-3*2); // this is harmess if width wasn't changed
1613 nvg
.drawLayouter(lay
, 0, cx
+3, cy
+3, hgt
);
1619 glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");