better quoting
[bioacid.git] / tkmain.d
blob0f76cdf1e97560685646413e3e2be2d5dc252489
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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module tkmain is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.gxx;
27 import iv.meta;
28 import iv.nanovega;
29 import iv.nanovega.blendish;
30 import iv.nanovega.textlayouter;
31 import iv.strex;
32 import iv.tox;
33 import iv.sdpyutil;
34 import iv.unarray;
35 import iv.utfutil;
36 import iv.vfs.io;
38 version(sfnt_test) import iv.nanovega.simplefont;
40 import accdb;
41 import accobj;
42 import fonts;
43 import icondata;
44 import notifyicon;
45 import toxproto;
47 import tkclist;
48 import tklog;
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared bool mainWindowActive = false;
53 __gshared bool mainWindowVisible = false;
54 __gshared SimpleWindow sdmain;
55 __gshared CList clist;
58 void setupToxCoreSender () {
59 toxCoreSendEvent = delegate (Object msg) {
60 if (msg is null) return; // just in case
61 try {
62 if (glconCtlWindow is null || glconCtlWindow.closed) return;
63 glconCtlWindow.postEvent(msg);
64 } catch (Exception e) {}
69 // ////////////////////////////////////////////////////////////////////////// //
70 string getBrowserCommand (bool forceOpera=false) {
71 __gshared string browser;
72 if (forceOpera) return "opera";
73 if (browser.length == 0) {
74 import core.stdc.stdlib : getenv;
75 const(char)* evar = getenv("BROWSER");
76 if (evar !is null && evar[0]) {
77 import std.string : fromStringz;
78 browser = evar.fromStringz.idup;
79 } else {
80 browser = "opera";
83 return browser;
87 void openUrl (ConString url, bool forceOpera=false) {
88 if (url.length) {
89 import std.stdio : File;
90 import std.process;
91 try {
92 auto frd = File("/dev/null");
93 auto fwr = File("/dev/null", "w");
94 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
95 } catch (Exception e) {
96 conwriteln("ERROR executing URL viewer (", e.msg, ")");
102 // ////////////////////////////////////////////////////////////////////////// //
103 struct UrlInfo {
104 int pos = -1, len = 0;
106 @property bool valid () const pure nothrow @safe @nogc => (pos >= 0 && len > 0);
107 @property int end () const pure nothrow @safe @nogc => (pos >= 0 && len > 0 ? pos+len : 0);
109 static UrlInfo Invalid () pure nothrow @safe @nogc => UrlInfo.init;
113 UrlInfo urlDetect (const(char)[] text) nothrow @trusted @nogc {
114 import iv.strex;
115 UrlInfo res;
116 auto dlpos = text.indexOf("://");
117 if (dlpos < 3) return res;
119 //{ import core.stdc.stdio; printf("det: <%.*s>\n", cast(uint)text.length, text.ptr); }
121 bool isProto (const(char)[] prt) nothrow @trusted @nogc {
122 if (dlpos < prt.length) return false;
123 if (!strEquCI(prt, text[dlpos-prt.length..dlpos])) return false;
124 // check word boundary
125 if (dlpos == prt.length) return true;
126 return !isalpha(text[dlpos-prt.length-1]);
129 if (isProto("ftp")) res.pos = cast(int)(dlpos-3);
130 else if (isProto("http")) res.pos = cast(int)(dlpos-4);
131 else if (isProto("https")) res.pos = cast(int)(dlpos-5);
132 else return res;
134 dlpos += 3; // skip "://"
136 // skip host name
137 for (; dlpos < text.length; ++dlpos) {
138 char ch = text[dlpos];
139 if (ch == '/') break;
140 if (!(isalnum(ch) || ch == '.' || ch == '-' || ch == ':' || ch == '@')) break;
143 // skip path
144 char[64] brcStack;
145 int brcSP = 0;
146 bool wasSharp = false;
148 for (; dlpos < text.length; ++dlpos) {
149 char ch = text[dlpos];
150 if (ch <= ' ' || ch == '<' || ch == '>' || ch == '"' || ch == '\'' || ch >= 127) break;
151 // hash
152 if (ch == '#') {
153 if (wasSharp) break;
154 wasSharp = true;
155 brcSP = 0;
156 continue;
158 // next path part
159 if (ch == '/') {
160 brcSP = 0;
161 continue;
163 // opening bracket
164 if (ch == '(' || ch == '[' || ch == '{') {
165 if (dlpos+1 >= text.length || (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_')) break;
166 final switch (ch) {
167 case '(': ch = ')'; break;
168 case '[': ch = ']'; break;
169 case '{': ch = '}'; break;
171 if (brcSP < brcStack.length) brcStack[brcSP++] = ch;
172 continue;
174 // closing bracket
175 if (ch == ')' || ch == ']' || ch == '}') {
176 if (brcSP == 0 || brcStack[brcSP-1] != ch) break;
177 --brcSP;
178 continue;
180 if (brcSP == 0) {
181 if (ch == '.') {
182 if (brcSP == 0) {
183 if (dlpos+1 >= text.length || (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_')) break;
185 continue;
187 if (!isalnum(ch)) {
188 // other special chars
189 if (dlpos+1 >= text.length) break; // no more chars, ignore
190 if (ch == '-' && dlpos < text.length && (isalnum(text[dlpos+1]) || text[dlpos+1] == '%')) continue;
191 if (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_') {
192 if (ch == '.' || ch == '!' || ch == ';' || ch == ',' || text[dlpos+1] != ch) break; // ignore
194 continue;
199 res.len = cast(int)(dlpos-res.pos);
201 return res;
205 // ////////////////////////////////////////////////////////////////////////// //
206 __gshared NVGContext nvg = null;
207 __gshared NVGImage nvgSkullsImg;
208 __gshared NVGImage kittyOut, kittyMsg, kittyFish;
209 __gshared NVGImage[5] statusImgId;
211 __gshared int lastWindowWidth = -1;
214 shared static ~this () {
215 //{ import core.stdc.stdio; printf("******************************\n"); }
216 nvgSkullsImg.clear();
217 //{ import core.stdc.stdio; printf("---\n"); }
218 foreach (ref img; statusImgId[]) img.clear();
219 kittyOut.clear();
220 kittyMsg.clear();
221 kittyFish.clear();
225 void buildStatusImages () {
226 version(none) {
227 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
228 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, ctiOnline[], NVGImageFlags.NoFiltering);
229 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, ctiAway[], NVGImageFlags.NoFiltering);
230 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
231 } else {
232 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
233 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, baph16Gray[], NVGImageFlags.NoFiltering);
234 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
235 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, baph16Online[], NVGImageFlags.NoFiltering);
236 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
237 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, baph16Away[], NVGImageFlags.NoFiltering);
238 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
239 statusImgId[ContactStatus.Busy] = nvg.createImageRGBA(16, 16, baph16Busy[], NVGImageFlags.NoFiltering);
240 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
241 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, baph16Orange[], NVGImageFlags.NoFiltering);
242 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
243 kittyOut = nvg.createImageRGBA(16, 16, kittyOutgoing[], NVGImageFlags.NoFiltering);
244 kittyMsg = nvg.createImageRGBA(16, 16, kittyMessage[], NVGImageFlags.NoFiltering);
245 kittyFish = nvg.createImageRGBA(16, 16, kittyFish16[], NVGImageFlags.NoFiltering);
250 // ////////////////////////////////////////////////////////////////////////// //
251 void loadFonts () {
252 nvg.fonsContext.addFontsFrom(fstash);
253 bndSetFont(nvg.findFont("ui"));
257 // ////////////////////////////////////////////////////////////////////////// //
258 void loadAccount (string nick, bool allowCreate) {
259 if (nick.length == 0) assert(0, "wtf?!");
261 Account acc;
262 bool newAcc = false;
264 try {
265 acc = new Account(nick);
266 } catch (Exception e) {
267 conwriteln("error opening account... (error: ", e.msg, ")");
268 if (!allowCreate) throw e;
269 acc = Account.CreateNew(nick, nick);
270 newAcc = true;
273 // create fake contact
274 if (newAcc && acc.contacts.length == 0 && nick == "_fakeacc") {
275 conwriteln("creating fake contact...");
276 auto c = acc.createEmptyContact();
277 c.info.nick = "test contact";
278 c.info.pubkey[] = 0x55;
279 c.save();
282 clist.buildAccount(acc);
286 // ////////////////////////////////////////////////////////////////////////// //
287 // null: deactivate
288 void doActivateContact (Contact ct) {
289 if (sdmain is null || sdmain.closed) return;
290 if (activeContact is ct) return;
292 activeContact = ct;
293 wipeLog();
294 if (ct is null) {
295 //conwriteln("clear log");
296 if (clist !is null) clist.resetActiveItem();
297 sdmain.title = "BioAcid";
298 glconPostScreenRepaint();
299 } else if (ct.acceptPending) {
300 addTextToLog(ct.acc, ct, LogFile.Msg.Kind.Notification, false, (ct.statusmsg.length ? ct.statusmsg : "I brought you a tasty fish!"), systimeNow);
301 } else {
302 import std.format : format;
303 sdmain.title = "%s [%s] -- BioAcid".format(ct.info.nick, tox_hex(ct.info.pubkey));
304 LogFile log;
305 ct.loadLogInto(log);
306 auto mcount = cast(int)log.messages.length;
307 int left = ct.hmcOnOpen;
308 if (left < ct.unreadCount) left = ct.unreadCount;
309 if (left > mcount) left = mcount;
310 if (mcount > left) mcount = left;
311 if (mcount > 0) {
312 foreach (const ref msg; log.messages[$-mcount..$]) {
313 if (left == ct.unreadCount) addDividerLine();
314 addTextToLog(ct.acc, ct, msg);
315 --left;
318 if (ct.unreadCount != 0) { ct.unreadCount = 0; ct.saveUnreadCount(); }
321 fixTrayIcon();
325 //FIXME: scan all accounts
326 void fixTrayIcon () {
327 if (clist is null) return;
328 auto acc = clist.mainAccount;
329 if (acc is null) return;
330 int unc = 0;
331 foreach (Contact ct; acc) unc += ct.unreadCount;
332 if (unc) {
333 import std.format : format;
334 setTrayUnread();
335 setHint("unread: %d".format(unc));
336 } else {
337 setTrayStatus(acc.status);
338 final switch (acc.status) {
339 case ContactStatus.Connecting: setHint("connecting..."); break;
340 case ContactStatus.Offline: setHint("offline"); break;
341 case ContactStatus.Online: setHint("online"); break;
342 case ContactStatus.Away: setHint("away"); break;
343 case ContactStatus.Busy: setHint("busy"); break;
349 void fixUnreadIndicators () {
350 if (!mainWindowVisible || !mainWindowActive) return; // nothing to do
351 if (activeContact is null || activeContact.unreadCount == 0) return; // nothing to do
352 activeContact.unreadCount = 0;
353 activeContact.unreadCount = 0;
354 activeContact.saveUnreadCount();
355 fixTrayIcon();
359 // ////////////////////////////////////////////////////////////////////////// //
360 void addContactCommands () {
361 conRegFunc!((ConString grpname) {
362 if (clist is null || sdmain is null || sdmain.closed) return;
363 auto acc = clist.mainAccount;
364 if (acc is null) return;
365 if (grpname.length == 0) { conwriteln("group name?"); return; }
366 if (acc.findGroupByName(grpname) != uint.max) { conwriteln("group <", grpname, "> already exists"); return; }
367 auto gid = acc.createGroup(grpname);
368 if (gid == uint.max) { conwriteln("cannot create group <", grpname, ">"); return; }
369 clist.buildAccount(acc);
370 glconPostScreenRepaint();
371 })("group_create", "create group");
374 conRegFunc!((ConString grpname) {
375 if (clist is null || sdmain is null || sdmain.closed) return;
376 if (activeContact is null) { conwriteln("please, select contact first"); return; }
377 if (grpname.length == 0) { conwriteln("group name?"); return; }
378 auto gid = activeContact.acc.createGroup(grpname);
379 if (gid == uint.max) { conwriteln("cannot create group <", grpname, ">"); return; }
380 if (activeContact.acc.moveContactToGroup(activeContact, gid)) {
381 clist.buildAccount(activeContact.acc);
382 glconPostScreenRepaint();
383 } else {
384 conwriteln("cannot move contact to new group");
386 })("move_to_group", "move current contact to named group");
389 conRegFunc!(() {
390 if (clist is null || sdmain is null || sdmain.closed) return;
391 if (activeContact is null) { conwriteln("please, select contact first"); return; }
392 auto acc = activeContact.acc;
393 if (!acc.isOnline) { conwriteln("you must be online to remove contacts"); return; }
394 if (!acc.removeContact(activeContact)) { conwriteln("cannot remove current contact"); return; }
395 wipeLog();
396 activeContact = null;
397 clist.buildAccount(acc);
398 glconPostScreenRepaint();
399 })("contact_remove", "unfriend and remove current contact");