C-3 to set "busy" status
[bioacid.git] / tkmain.d
blob926ec7e02ad239a38413233d7b3fb99209c46589
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 tkmain 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.gxx;
28 import iv.meta;
29 import iv.nanovega;
30 import iv.nanovega.blendish;
31 import iv.nanovega.textlayouter;
32 import iv.strex;
33 import iv.tox;
34 import iv.txtser;
35 import iv.sdpyutil;
36 import iv.unarray;
37 import iv.utfutil;
38 import iv.vfs.io;
40 version(sfnt_test) import iv.nanovega.simplefont;
42 import accdb;
43 import accobj;
44 import fonts;
45 import icondata;
46 import notifyicon;
47 import toxproto;
49 import tkclist;
50 import tklog;
53 // ////////////////////////////////////////////////////////////////////////// //
54 __gshared bool mainWindowActive = false;
55 __gshared bool mainWindowVisible = false;
56 __gshared SimpleWindow sdmain;
57 __gshared CList clist;
60 void setupToxCoreSender () {
61 toxCoreSendEvent = delegate (Object msg) {
62 if (msg is null) return; // just in case
63 try {
64 if (glconCtlWindow is null || glconCtlWindow.closed) return;
65 glconCtlWindow.postEvent(msg);
66 } catch (Exception e) {}
71 // ////////////////////////////////////////////////////////////////////////// //
72 string getBrowserCommand (bool forceOpera=false) {
73 __gshared string browser;
74 if (forceOpera) return "opera";
75 if (browser.length == 0) {
76 import core.stdc.stdlib : getenv;
77 const(char)* evar = getenv("BROWSER");
78 if (evar !is null && evar[0]) {
79 import std.string : fromStringz;
80 browser = evar.fromStringz.idup;
81 } else {
82 browser = "opera";
85 return browser;
89 void openUrl (ConString url, bool forceOpera=false) {
90 if (url.length) {
91 import std.stdio : File;
92 import std.process;
93 try {
94 auto frd = File("/dev/null");
95 auto fwr = File("/dev/null", "w");
96 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
97 } catch (Exception e) {
98 conwriteln("ERROR executing URL viewer (", e.msg, ")");
104 // ////////////////////////////////////////////////////////////////////////// //
105 struct UrlInfo {
106 int pos = -1, len = 0;
108 @property bool valid () const pure nothrow @safe @nogc => (pos >= 0 && len > 0);
109 @property int end () const pure nothrow @safe @nogc => (pos >= 0 && len > 0 ? pos+len : 0);
111 static UrlInfo Invalid () pure nothrow @safe @nogc => UrlInfo.init;
115 UrlInfo urlDetect (const(char)[] text) nothrow @trusted @nogc {
116 import iv.strex;
117 UrlInfo res;
118 auto dlpos = text.indexOf("://");
119 if (dlpos < 3) return res;
121 //{ import core.stdc.stdio; printf("det: <%.*s>\n", cast(uint)text.length, text.ptr); }
123 bool isProto (const(char)[] prt) nothrow @trusted @nogc {
124 if (dlpos < prt.length) return false;
125 if (!strEquCI(prt, text[dlpos-prt.length..dlpos])) return false;
126 // check word boundary
127 if (dlpos == prt.length) return true;
128 return !isalpha(text[dlpos-prt.length-1]);
131 if (isProto("ftp")) res.pos = cast(int)(dlpos-3);
132 else if (isProto("http")) res.pos = cast(int)(dlpos-4);
133 else if (isProto("https")) res.pos = cast(int)(dlpos-5);
134 else return res;
136 dlpos += 3; // skip "://"
138 // skip host name
139 for (; dlpos < text.length; ++dlpos) {
140 char ch = text[dlpos];
141 if (ch == '/') break;
142 if (!(isalnum(ch) || ch == '.' || ch == '-' || ch == ':' || ch == '@')) break;
145 // skip path
146 char[64] brcStack;
147 int brcSP = 0;
148 bool wasSharp = false;
150 for (; dlpos < text.length; ++dlpos) {
151 char ch = text[dlpos];
152 // hash
153 if (ch == '#') {
154 if (wasSharp) break;
155 wasSharp = true;
156 brcSP = 0;
157 continue;
159 // opening bracket
160 if (ch == '(' || ch == '[' || ch == '{') {
161 final switch (ch) {
162 case '(': ch = ')'; break;
163 case '[': ch = ']'; break;
164 case '{': ch = '}'; break;
166 if (brcSP < brcStack.length) brcStack[brcSP++] = ch;
167 continue;
169 // closing bracket
170 if (ch == ')' || ch == ']' || ch == '}') {
171 if (brcSP == 0 || brcStack[brcSP-1] != ch) break;
172 --brcSP;
173 continue;
175 if (ch == '.') {
176 if (brcSP == 0) {
177 if (dlpos == text.length || (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_')) break;
179 continue;
181 if (ch <= ' ' || ch >= 127) break;
184 res.len = cast(int)(dlpos-res.pos);
186 return res;
190 // ////////////////////////////////////////////////////////////////////////// //
191 __gshared NVGContext nvg = null;
192 __gshared NVGImage nvgSkullsImg;
193 __gshared NVGImage kittyOut, kittyMsg, kittyFish;
194 __gshared NVGImage[5] statusImgId;
196 __gshared int lastWindowWidth = -1;
199 shared static ~this () {
200 //{ import core.stdc.stdio; printf("******************************\n"); }
201 nvgSkullsImg.clear();
202 //{ import core.stdc.stdio; printf("---\n"); }
203 foreach (ref img; statusImgId[]) img.clear();
204 kittyOut.clear();
205 kittyMsg.clear();
206 kittyFish.clear();
210 void buildStatusImages () {
211 version(none) {
212 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
213 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, ctiOnline[], NVGImageFlags.NoFiltering);
214 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, ctiAway[], NVGImageFlags.NoFiltering);
215 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
216 } else {
217 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
218 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, baph16Gray[], NVGImageFlags.NoFiltering);
219 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
220 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, baph16Online[], NVGImageFlags.NoFiltering);
221 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
222 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, baph16Away[], NVGImageFlags.NoFiltering);
223 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
224 statusImgId[ContactStatus.Busy] = nvg.createImageRGBA(16, 16, baph16Busy[], NVGImageFlags.NoFiltering);
225 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
226 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, baph16Orange[], NVGImageFlags.NoFiltering);
227 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
228 kittyOut = nvg.createImageRGBA(16, 16, kittyOutgoing[], NVGImageFlags.NoFiltering);
229 kittyMsg = nvg.createImageRGBA(16, 16, kittyMessage[], NVGImageFlags.NoFiltering);
230 kittyFish = nvg.createImageRGBA(16, 16, kittyFish16[], NVGImageFlags.NoFiltering);
235 // ////////////////////////////////////////////////////////////////////////// //
236 void loadFonts (NVGContext vg) {
237 vg.fonsContext.fonsAddStashFonts(fstash);
238 bndSetFont(vg.findFont("ui"));
242 // ////////////////////////////////////////////////////////////////////////// //
243 void loadAccount (string nick) {
244 Account acc;
246 try {
247 acc = new Account(nick);
248 } catch (Exception e) {
249 conwriteln("creating account...");
250 assert(0, "not yet");
251 acc = Account.CreateNew("_fakeacc", "ketmar");
253 // create fake contact
254 if (acc.contacts.length == 0 && nick == "_fakeacc") {
255 conwriteln("creating fake contact...");
256 auto c = acc.createEmptyContact();
257 c.info.nick = "test contact";
258 c.info.pubkey[] = 0x55;
259 c.save();
262 clist.buildAccount(acc);
266 // ////////////////////////////////////////////////////////////////////////// //
267 // null: deactivate
268 void doActivateContact (Contact ct) {
269 if (sdmain is null || sdmain.closed) return;
270 if (activeContact is ct) return;
272 activeContact = ct;
273 wipeLog();
274 if (ct is null) {
275 //conwriteln("clear log");
276 if (clist !is null) clist.resetActiveItem();
277 sdmain.title = "BioAcid";
278 glconPostScreenRepaint();
279 } else if (ct.acceptPending) {
280 addTextToLog(ct.acc, ct, LogFile.Msg.Kind.Notification, false, (ct.statusmsg.length ? ct.statusmsg : "I brought you a tasty fish!"), systimeNow);
281 } else {
282 import std.format : format;
283 sdmain.title = "%s [%s] -- BioAcid".format(ct.info.nick, tox_hex(ct.info.pubkey));
284 LogFile log;
285 ct.loadLogInto(log);
286 auto mcount = cast(int)log.messages.length;
287 int left = ct.hmcOnOpen;
288 if (left < ct.unreadCount) left = ct.unreadCount;
289 if (left > mcount) left = mcount;
290 if (mcount > left) mcount = left;
291 if (mcount > 0) {
292 foreach (const ref msg; log.messages[$-mcount..$]) {
293 if (left == ct.unreadCount) addDividerLine();
294 addTextToLog(ct.acc, ct, msg);
295 --left;
298 if (ct.unreadCount != 0) { ct.unreadCount = 0; ct.saveUnreadCount(); }
301 fixTrayIcon();
305 //FIXME: scan all accounts
306 void fixTrayIcon () {
307 if (clist is null) return;
308 auto acc = clist.mainAccount;
309 if (acc is null) return;
310 int unc = 0;
311 foreach (Contact ct; acc) unc += ct.unreadCount;
312 if (unc) {
313 import std.format : format;
314 setTrayUnread();
315 setHint("unread: %d".format(unc));
316 } else {
317 setTrayStatus(acc.status);
318 final switch (acc.status) {
319 case ContactStatus.Connecting: setHint("connecting..."); break;
320 case ContactStatus.Offline: setHint("offline"); break;
321 case ContactStatus.Online: setHint("online"); break;
322 case ContactStatus.Away: setHint("away"); break;
323 case ContactStatus.Busy: setHint("busy"); break;
329 void fixUnreadIndicators () {
330 if (!mainWindowVisible || !mainWindowActive) return; // nothing to do
331 if (activeContact is null || activeContact.unreadCount == 0) return; // nothing to do
332 activeContact.unreadCount = 0;
333 activeContact.unreadCount = 0;
334 activeContact.saveUnreadCount();
335 fixTrayIcon();
339 // ////////////////////////////////////////////////////////////////////////// //
340 void addContactCommands () {
341 conRegFunc!((ConString grpname) {
342 if (clist is null || sdmain is null || sdmain.closed) return;
343 auto acc = clist.mainAccount;
344 if (acc is null) return;
345 if (grpname.length == 0) { conwriteln("group name?"); return; }
346 if (acc.findGroupByName(grpname) != uint.max) { conwriteln("group <", grpname, "> already exists"); return; }
347 auto gid = acc.createGroup(grpname);
348 if (gid == uint.max) { conwriteln("cannot create group <", grpname, ">"); return; }
349 clist.buildAccount(acc);
350 glconPostScreenRepaint();
351 })("group_create", "create group");
354 conRegFunc!((ConString grpname) {
355 if (clist is null || sdmain is null || sdmain.closed) return;
356 if (activeContact is null) { conwriteln("please, select contact first"); return; }
357 if (grpname.length == 0) { conwriteln("group name?"); return; }
358 auto gid = activeContact.acc.createGroup(grpname);
359 if (gid == uint.max) { conwriteln("cannot create group <", grpname, ">"); return; }
360 if (activeContact.acc.moveContactToGroup(activeContact, gid)) {
361 clist.buildAccount(activeContact.acc);
362 glconPostScreenRepaint();
363 } else {
364 conwriteln("cannot move contact to new group");
366 })("move_to_group", "move current contact to named group");
369 conRegFunc!(() {
370 if (clist is null || sdmain is null || sdmain.closed) return;
371 if (activeContact is null) { conwriteln("please, select contact first"); return; }
372 auto acc = activeContact.acc;
373 if (!acc.isOnline) { conwriteln("you must be online to remove contacts"); return; }
374 if (!acc.removeContact(activeContact)) { conwriteln("cannot remove current contact"); return; }
375 wipeLog();
376 activeContact = null;
377 clist.buildAccount(acc);
378 glconPostScreenRepaint();
379 })("contact_remove", "unfriend and remove current contact");