hyphenation test
[bioacid.git] / bioacid.d
blobd62afefc528cdcddfc6d129f4c32282125077d4c
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;
19 import std.datetime;
21 import arsd.color;
22 import arsd.image;
23 import arsd.simpledisplay;
25 import iv.cmdcon;
26 import iv.cmdcon.gl;
27 import iv.nanovega;
28 import iv.nanovega.blendish;
29 import iv.nanovega.textlayouter;
30 import iv.strex;
31 import iv.tox;
32 import iv.txtser;
33 import iv.utfutil;
34 import iv.vfs.io;
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[]);
102 bndSetFont(uiFont);
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;
196 int drawY = y0;
197 //FIXME: not GHeight!
198 int lidx = lay.findLineAtY(layTopY);
199 if (lidx >= 0 && lidx < lay.lineCount) {
200 vg.save();
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;
206 int startx = x0;
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);
215 vg.fillColor(c);
216 lastStyle = w.style;
217 setColor = false;
218 //conprintfln("new color: 0x%08x; fontid=%d; fontsize=%d", w.style.color, w.style.fontface, w.style.fontsize);
220 // background color
222 auto c = NVGColor(w.style.bgcolor);
223 if (!c.isTransparent) {
224 vg.save();
225 scope(exit) vg.restore();
226 vg.beginPath();
227 vg.fillColor(c);
228 vg.rect(startx+w.x+0.5f, drawY+0.5, w.fullwidth+1, ln.h);
229 vg.fill();
230 vg.beginPath();
233 // element
234 auto oid = w.objectIdx;
235 if (oid >= 0) {
236 //vg.fill();
237 vg.save();
238 scope(exit) vg.restore();
239 lay.objectAtIndex(oid).draw(vg, startx+w.x, drawY+ln.h+ln.desc);
240 //vg.beginPath();
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);
249 drawY += ln.h;
250 hgt -= ln.h;
251 ++lidx;
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 {
275 bool ipv6;
276 bool udp;
277 bool localDiscovery;
278 bool holePunching;
279 ushort startPort;
280 ushort endPort;
281 ushort tcpPort;
282 ubyte proxyType;
283 ushort proxyPort;
284 string proxyAddr;
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];
320 struct ContactInfo {
321 enum Kind {
322 // normal contact
323 Friend,
324 // pending authorization acceptance
325 // statusmsg: request text
326 // lastonlinetime: request time
327 PengingAuthAccept,
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)
332 PengingAuthRequest,
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
335 KillFuckDie,
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
345 CommonOptions opts;
349 // ////////////////////////////////////////////////////////////////////////// //
350 void syncFile(bool fullsync=false) (Imp!"core.stdc.stdio".FILE* fl) nothrow @nogc {
351 if (fl !is null) {
352 import core.stdc.stdio : fileno;
353 int fd = fileno(fl);
354 if (fd >= 0) {
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;
373 try {
374 v.txtser(VFile(fo, own:false), skipstname:true);
375 } catch (Exception e) {
376 fclose(fo);
377 unlink(tmpfname.tempCString);
378 return false;
380 syncFile(fo);
381 fclose(fo);
383 if (rename(tmpfname.tempCString, fname.tempCString) != 0) {
384 unlink(tmpfname.tempCString);
385 return false;
388 return true;
392 // ////////////////////////////////////////////////////////////////////////// //
394 log format (it looks like text, but it isn't text):
395 T[YYYY/MM/DD HH:NN:SS]: text-in-utf8
396 date in local time.
397 text: chars in range of [0..31, 92, 127] are \xHH-encoded
398 T is message type:
399 !: server/app notification
400 <: outgoing
401 >: incoming
402 if char before text is '!' instead of space, this is "/me" message
404 static struct LogFile {
405 public:
406 // this struct must not outlive LogFile
407 static struct Msg {
408 enum Kind { Notification, Incoming, Outgoing }
409 Kind kind;
410 bool isMe; // "/me" message?
411 SysTime time;
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
419 usize pos;
420 @property bool empty () const => (pos >= raw.length);
421 @property dchar front () const {
422 if (pos < raw.length) {
423 char ch = raw.ptr[pos];
424 if (ch <= 127) {
425 if (ch != '\\') return ch;
426 if (raw.length-pos < 2) return ch;
427 ch = raw.ptr[pos+1];
428 if (ch != 'x' && ch != 'X') {
429 switch (ch) {
430 case 'e': return '\e';
431 case 'n': return '\n';
432 case 'r': return '\r';
433 case 't': return '\t';
434 default:
436 return ch;
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);
445 Utf8DecoderFast dc;
446 uint cpos = pos;
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);
451 } else {
452 return Utf8DecoderFast.replacement;
455 void popFront () {
456 if (pos < raw.length) {
457 char ch = raw.ptr[pos];
458 if (ch <= 127) {
459 ++pos;
460 if (ch != '\\') return;
461 if (raw.length-pos < 1) { pos = raw.length; return; }
462 ch = raw.ptr[pos++];
463 if (ch != 'x' && ch != 'X') return;
464 if (raw.length-pos < 2) { pos = raw.length; return; }
465 pos += 2;
466 return;
468 Utf8DecoderFast dc;
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);
480 private:
481 char[] data;
483 public:
484 Msg[] messages;
486 public:
487 @disable this (this); // no copies
489 this (const(char)[] fname) nothrow => load(fname);
491 ~this () nothrow {
492 clear();
495 // no `Msg` should outlive this call!
496 void clear () nothrow {
497 delete messages;
498 delete data;
501 void load (const(char)[] fname) nothrow {
502 import std.internal.cstring;
504 if (messages.length) {
505 messages.length = 0;
506 messages.assumeSafeAppend;
508 if (data.length) {
509 data.length = 0;
510 data.assumeSafeAppend;
513 if (fname.length == 0) return;
515 try {
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) {}
522 parseData();
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;
532 // count '\n'
533 int nlcount = 0;
535 auto dp = data.ptr;
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;
539 ++nlcount;
540 dp = nx+1;
543 messages.reserve(nlcount+1);
545 static int toInt (const(char)[] s) nothrow @trusted @nogc {
546 int res = 0;
547 while (s.length && s.ptr[0].isdigit) {
548 res = res*10+s.ptr[0]-'0';
549 s = s[1..$];
551 return res;
554 // parse messages
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;
566 Msg msg;
567 // message kind
568 switch (dp[0]) {
569 case '!': msg.kind = msg.Kind.Notification; break;
570 case '<': msg.kind = msg.Kind.Outgoing; break;
571 case '>': msg.kind = msg.Kind.Incoming; break;
572 default: continue;
574 // "/me"?
575 switch (dp[23]) {
576 case ' ': msg.isMe = false; break;
577 case '!': msg.isMe = true; break;
578 default: continue;
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;
592 bool timeok = false;
593 try {
594 msg.time = SysTime(DateTime(year, month, day, hour, minute, second));
595 timeok = true;
596 } catch (Exception e) {}
597 if (!timeok) continue;
598 msg.rawText = dp[24..(nx-dp)];
599 messages ~= msg;
603 public:
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);
619 if (wr == 0) {
620 import core.stdc.errno;
621 if (errno != EINTR) return false;
622 continue;
624 s = s[wr..$];
626 return true;
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;
644 // write date
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) {
663 usize end = 0;
664 while (end < text.length) {
665 char ch = text.ptr[end];
666 if (ch < ' ' || ch == 92 || ch == 127) break;
667 ++end;
669 if (end > 0) {
670 if (!xwrite(text[0..end])) return false;
671 text = text[end..$];
672 } else {
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;
678 default:
679 import core.stdc.stdio : snprintf;
680 char[16] buf = void;
681 auto len = snprintf(buf.ptr, buf.length, "\\x%02x", cast(uint)text.ptr[0]);
682 if (!xwrite(buf[0..len])) return false;
683 break;
685 text = text[1..$];
689 return xwrite("\n");
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 {
695 try {
696 return appendLine(fname, kind, text, isMe, Clock.currTime);
697 } catch (Exception e) {}
698 return false;
703 // ////////////////////////////////////////////////////////////////////////// //
704 final class Group {
705 private:
706 this (Account aOwner) { acc = aOwner; }
708 private:
709 bool mDirty; // true: write contact's config
710 string diskFileName;
712 public:
713 Account acc;
714 GroupOptions info;
716 public:
717 void markDirty () pure nothrow @safe @nogc => mDirty = true;
719 void save () {
720 import std.file : mkdirRecurse;
721 import std.path : dirName;
722 // save this contact
723 assert(acc !is null);
724 // create disk name
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(); } }
766 @nogc {
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 {
782 public:
783 enum Status { Offline, Online, Away }
785 private:
786 this (Account aOwner) { acc = aOwner; }
788 private:
789 bool mDirty; // true: write contact's config
791 public:
792 Account acc;
793 string diskFileName;
794 ContactInfo info;
795 Status status = Status.Offline; // not saved, so if it safe to change it
797 public:
798 void markDirty () pure nothrow @safe @nogc => mDirty = true;
800 void save () {
801 import std.file : mkdirRecurse;
802 import std.path : dirName;
803 // save this contact
804 assert(acc !is null);
805 // create disk name
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;
818 public:
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);
824 return grp.visible;
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 () {
846 try {
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 {
888 public:
889 void saveGroups () {
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");
898 private:
899 // returns `null` if there is no such file or file cannot be loaded
900 ubyte[] loadToxCoreData () {
901 if (toxDataDiskName.length == 0) return null;
902 try {
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[]);
907 return res[];
908 } catch (Exception e) {
910 return null;
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;
934 try {
935 VFile(fo, own:false).rawWriteExact(savedata[0..size]);
936 } catch (Exception e) {
937 unlink(ofnametmp.tempCString);
938 return false;
940 syncFile(fo);
941 fclose(fo);
943 if (rename(ofnametmp.tempCString, ofname.tempCString) != 0) {
944 unlink(ofnametmp.tempCString);
945 return false;
948 return true;
951 public:
952 ToxP tox;
953 string toxDataDiskName;
954 string basePath; // with trailing "/"
955 ProtoOptions protoOpts;
956 AccountConfig info;
957 Group[] groups;
958 Contact[PubKey] contacts;
960 public:
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 == "./") {
967 aBaseDir = "./";
968 } else if (aBaseDir == "/") {
969 assert(0, "wtf?!");
970 } else {
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"));
979 // load groups
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);
986 g.info = gi;
987 bool found = false;
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) {
996 GroupOptions gi;
997 gi.gid = 0;
998 gi.name = "default";
999 gi.note = "default group for new contacts";
1000 gi.opened = true;
1001 auto g = new Group(this);
1002 g.info = gi;
1003 groups ~= g;
1006 // create morons group if necessary
1007 if (!hasMoronsGroup) {
1008 GroupOptions gi;
1009 gi.gid = gi.gid.max;
1010 gi.name = "<morons>";
1011 gi.note = "group for completely ignored dumbfucks";
1012 gi.opened = false;
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;
1020 gi.hmcOnOpen = 0;
1021 auto g = new Group(this);
1022 g.info = gi;
1023 groups ~= g;
1024 saveGroups();
1027 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1029 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1031 // load contacts
1032 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1033 if (de.name.baseName == "." || de.name.baseName == "..") continue;
1034 try {
1035 import std.file : exists;
1036 if (!de.isDir) continue;
1037 string cfgfn = de.name~"/config.rc";
1038 if (!cfgfn.exists) continue;
1039 ContactInfo ci;
1040 ci.txtunser(VFile(cfgfn));
1041 auto c = new Contact(this);
1042 c.diskFileName = cfgfn;
1043 c.info = ci;
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
1048 c.save();
1050 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1051 } catch (Exception e) {
1052 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1056 // create toxcore
1057 char* toxProxyHost = null;
1058 scope(exit) {
1059 import core.stdc.stdlib : free;
1060 if (toxProxyHost !is null) free(toxProxyHost);
1062 ubyte[] savedata;
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...");
1095 } else {
1096 // load data
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
1102 TOX_ERR_NEW error;
1103 tox = tox_new(toxOpts, &error);
1104 if (tox is null) {
1105 import std.conv : to;
1106 assert(0, "cannot create ToxCore instance: error is "~error.to!string);
1108 if (createNewToxCoreAccount) saveToxCoreData();
1111 ~this () {
1112 if (tox !is null) tox_kill(tox);
1115 // will not write contact to disk
1116 Contact createEmptyContact () {
1117 auto c = new Contact(this);
1118 c.info.gid = 0;
1119 c.info.nick = "test contact";
1120 c.info.pubkey[] = 0;
1121 return c;
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;
1130 public:
1131 static Account CreateNew (string aBaseDir, string aAccName) {
1132 import std.file : mkdirRecurse;
1133 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1134 aBaseDir = "./";
1135 } else if (aBaseDir == "/") {
1136 assert(0, "wtf?!");
1137 } else {
1138 mkdirRecurse(aBaseDir);
1139 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1141 mkdirRecurse(aBaseDir~"/contacts");
1142 // write protocol options
1144 ProtoOptions popt;
1145 popt.txtser(VFile(aBaseDir~"proto.rc", "w"), skipstname:true);
1147 // account options
1149 AccountConfig acc;
1150 acc.nick = aAccName;
1151 acc.showPopup = true;
1152 acc.blinkActivity = true;
1153 acc.hideEmptyGroups = false;
1154 acc.ftranAllowed = true;
1155 acc.resendRotDays = 4;
1156 acc.hmcOnOpen = 10;
1157 acc.txtser(VFile(aBaseDir~"config.rc", "w"), skipstname:true);
1159 // create default group
1161 GroupOptions[1] grp;
1162 grp[0].gid = 0;
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);
1168 // now load it
1169 return new Account(aBaseDir);
1174 // ////////////////////////////////////////////////////////////////////////// //
1175 class ListItemBase {
1176 private:
1177 this () {}
1179 protected:
1180 bool mVisible = true;
1182 public:
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 {
1193 public:
1194 Account acc;
1196 public:
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) {
1202 int hgt = height;
1204 nvg.newPath();
1205 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1206 nvg.fillColor(NVGColor("#5fff"));
1207 nvg.fill();
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 {
1218 public:
1219 Group group;
1221 public:
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) {
1227 int hgt = height;
1229 nvg.newPath();
1230 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1231 nvg.fillColor(NVGColor("#5888"));
1232 nvg.fill();
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 {
1243 public:
1244 Contact ct;
1246 public:
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) {
1254 int hgt = height;
1257 nvg.newPath();
1258 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1259 nvg.fillColor(NVGColor("#5888"));
1260 nvg.fill();
1263 // draw icon
1264 nvg.newPath();
1265 int iw, ih;
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]));
1270 nvg.fill();
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
1283 final class CList {
1284 private:
1285 int mActiveItem = -1; // active item (may be different from selected with cursor)
1286 int mTopY;
1288 public:
1289 ListItemBase[] items;
1291 public:
1292 this () {}
1294 void opOpAssign(string op:"~") (ListItemBase li) {
1295 if (li is null) return;
1296 items ~= li;
1299 void buildAccount (Account acc) {
1300 if (acc is null) return;
1301 items ~= new ListItemAccount(acc);
1302 Contact[] css;
1303 scope(exit) delete css;
1304 foreach (Group g; acc.groups) {
1305 items ~= new ListItemGroup(g);
1306 css.length = 0;
1307 css.assumeSafeAppend;
1308 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
1309 import std.algorithm : sort;
1310 css.sort!((a, b) {
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()
1316 char c1 = s1[idx];
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);
1326 // true: eaten
1327 bool onMouse (MouseEvent event) {
1328 return false;
1331 // true: eaten
1332 bool onKey (KeyEvent event) {
1333 return false;
1336 // real rect is scissored
1337 void drawAt (int x0, int y0, int wdt, int hgt) {
1338 nvg.save();
1339 scope(exit) nvg.restore();
1340 //nvg.fontFace = "ui";
1341 nvg.fontSize = 20;
1342 nvg.fontBlur = 0;
1343 nvg.textAlign = NVGTextAlign.H.Left;
1344 nvg.textAlign = NVGTextAlign.V.Baseline;
1345 int y = 0;
1346 foreach (immutable iidx, ListItemBase li; items) {
1347 if (iidx != mActiveItem && !li.visible) continue;
1348 li.setupFont();
1349 int lh = li.height;
1350 if (lh < 1) continue;
1351 nvg.save();
1352 scope(exit) nvg.restore();
1353 version(all) {
1354 nvg.intersectScissor(x0, y0+y, wdt, lh);
1355 li.drawAt(x0, y0+y, wdt);
1356 } else {
1357 nvg.translate(x0+0.5f, y0+y+0.5f);
1358 nvg.intersectScissor(0, 0, wdt, lh);
1359 li.drawAt(0, 0, wdt);
1361 y += lh;
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");
1376 Account acc;
1377 try {
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;
1389 c.save();
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
1404 // first time setup
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");
1412 loadFonts(nvg);
1414 loadFmtFonts();
1416 try {
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)); }
1430 char[12] tbuf;
1431 int tbufpos = 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);
1452 lay.putExpander();
1453 lay.put("!");
1454 lay.putExpander();
1455 lay.put("2018/02/12");
1456 lay.putNBSP();
1457 lay.fontStyle.color = NVGColor("#fff").asUint;
1458 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1459 lay.put("11:09:03");
1460 lay.endPara();
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ёна КОЧЕРГА!)");
1465 lay.endPara();
1466 lay.finalize();
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;
1484 lay.relayout(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") {
1492 // add random text
1493 lay.fontStyle.fontsize = 16;
1494 lay.fontStyle.color = NVGColor.k8orange.asUint;
1495 lay.fontStyle.monospace = true;
1496 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1497 lay.put("ketmar");
1498 lay.fontStyle.monospace = false;
1499 lay.putHardSpace(64);
1500 lay.putExpander();
1501 lay.put("!");
1502 lay.putExpander();
1503 lay.put("2018/02/12");
1504 lay.putNBSP();
1505 lay.fontStyle.color = NVGColor("#fff").asUint;
1506 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1507 lay.put("11:09:03");
1508 lay.endPara();
1509 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1510 lay.fontStyle.color = NVGColor.k8orange.asUint;
1511 lay.fontStyle.fontsize = 20;
1512 import std.random;
1513 // words
1514 version(none) {
1515 foreach (immutable widx; 0..uniform!"[]"(2, 28)) {
1516 // one word
1517 if (widx != 0) lay.put(" ");
1518 foreach (; 0..uniform!"[]"(1, 9)) {
1519 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1520 lay.put(ch);
1521 //lay.putSoftHypen();
1524 } else {
1525 // long word, for hyphenation test
1526 foreach (immutable idx; 0..228) {
1527 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1528 lay.put(ch);
1529 if (idx%24 == 23) lay.putSoftHypen();
1532 lay.endPara();
1533 lay.finalize();
1534 // redraw
1535 glconResetNextFrame();
1536 return;
1540 mouseEventDG = delegate (MouseEvent event) {
1541 if (clist !is null && clist.onMouse(event)) return;
1544 charEventDG = delegate (dchar ch) {
1547 focusEventDG = delegate (bool focused) {
1550 // draw main screen
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
1568 int cx = 1;
1569 int cy = 1;
1570 int wdt = optCListWidth;
1571 int hgt = nvg.height-cy*2;
1573 nvg.shapeAntiAlias = true;
1574 nvg.nonZeroFill;
1575 nvg.strokeWidth = 1;
1578 nvg.save();
1579 scope(exit) nvg.restore();
1581 nvg.newPath();
1582 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
1584 int w, h;
1585 nvg.imageSize(nvgSkullsImg, w, h);
1586 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
1588 nvg.strokeColor(NVGColor("#f70"));
1589 nvg.fill();
1590 nvg.stroke();
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);
1598 nvg.save();
1599 scope(exit) nvg.restore();
1601 // draw chat log
1602 cx += wdt+2;
1603 wdt = nvg.width-cx-1;
1604 nvg.newPath();
1605 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
1606 nvg.fillColor(NVGColor.black);
1607 nvg.strokeColor(NVGColor("#f70"));
1608 nvg.fill();
1609 nvg.stroke();
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");