some hacks for oftc reconnection
[miri.git] / xmiri.d
blob2c7c881b7c5df188e7e653c45605bdb91a6ec4c4
1 /* Invisible Vector IRC client
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
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 xmiri;
18 import std.datetime;
20 import iv.rawtty;
21 import iv.srex;
22 import iv.strex : indexOf, startsWith, xstrip;
23 import iv.utfutil;
25 import iv.eventbus;
26 import iv.egtui;
27 import iv.vfs.io;
28 import iv.tox;
30 import miri;
31 import miri.other.tox;
32 import tray.trayicon;
34 import iv.libnotify;
37 // ////////////////////////////////////////////////////////////////////////// //
38 bool checkMatchFile (const(char)[] msg, string fname) {
39 if (fname.length == 0 || msg.length == 0) return false;
40 try {
41 int maxlen = 0;
42 import std.file, std.path;
43 auto filterFilename = buildPath(configDir, fname);
44 foreach (auto ln; VFile(filterFilename).byLine) {
45 ln = ln.xstrip;
46 if (ln.length == 0 || ln[0] == '#') continue;
47 auto cpos = ln.indexOf(':');
48 if (cpos <= 0) continue;
49 auto name = ln[0..cpos].xstrip;
50 auto value = ln[cpos+1..$].xstrip;
51 if (name.length == 0 || value.length == 0) continue;
52 if (name == "maxlen") {
53 import std.conv : to;
54 try { maxlen = value.to!int; } catch (Exception) {}
55 if (maxlen > 0 && msg.length > maxlen) return true;
56 continue;
58 if (name == "substring") {
59 if (msg.indexOf(value) >= 0) return true;
60 continue;
62 if (name == "glob") {
63 import std.path : globMatch;
64 if (globMatch(msg, value)) return true;
65 continue;
68 } catch (Exception) {}
69 return false;
72 bool isSrvInfoIdiot (const(char)[] msg) {
73 if (!msg.startsWith("srvinfo:")) return false;
74 msg = msg[8..$].xstrip;
75 return checkMatchFile(msg, "srvinfo_idiots.rc");
79 bool isTgIgnore (const(char)[] msg) {
80 return checkMatchFile(msg, "tg_ignore.rc");
84 // ////////////////////////////////////////////////////////////////////////// //
85 void fuckStdErr () {
86 import core.sys.posix.unistd : dup2;
87 import core.sys.posix.fcntl /*: open*/;
88 auto xfd = open("/dev/null", O_WRONLY, 0x1b6/*0o600*/);
89 if (xfd < 0) return;
90 dup2(xfd, 2); // fuck stderr
94 void showNotify (cstring title, cstring text) {
95 Recoder rc;
96 rc.utfucked = true;
97 auto tt8 = rc.encodeBuf(title~"\0");
98 auto tx8 = rc.encodeBuf(text~"\0");
99 auto n = notify_notification_new(tt8.ptr, tx8.ptr, "none");
100 GError* ge;
101 notify_notification_show(n, &ge);
105 // ////////////////////////////////////////////////////////////////////////// //
106 __gshared TextPane syspane;
107 __gshared TtyEditor inputOneLine;
108 __gshared TtyEditor inputMultiLine;
109 __gshared TtyEditor inputUserFilter;
110 __gshared bool inputActive = true;
111 __gshared bool inputModeSingle = true;
112 __gshared bool doQuit = false;
113 __gshared bool userFilterFocused = false;
116 // ////////////////////////////////////////////////////////////////////////// //
118 * Build list of suitable autocompletions.
120 * Params:
121 * cmd = user-given command prefix
122 * cmdlist = list of all available commands
124 * Returns:
125 * null = no matches (empty array)
126 * array with one item: exactly one match
127 * array with more that one item = [0] is max prefix, then list of autocompletes
129 * Throws:
130 * Out of memory exception
132 cstring[] autocompleteFromList (cstring cmd, cstring[] cmdlist...) nothrow @trusted {
133 alias usize = size_t;
134 if (cmdlist.length == 0) return [cmd];
135 cstring found; // autoinit
136 usize foundlen, pfxcount; // autoinit
137 // first pass: count prefixes, remember command with longest prefix
138 foreach (cstring s; cmdlist) {
139 if (cmd.length <= s.length) {
140 usize pos = cmd.length;
141 foreach (immutable idx; 0..cmd.length) if (cmd[idx].irc2lower != s[idx].irc2lower) { pos = idx; break; }
142 if (pos == cmd.length) {
143 if (s.length > found.length) found = s;
144 ++pfxcount;
148 if (pfxcount == 0) return null; // nothing was found
149 if (pfxcount == 1) return [found]; // one match found
150 // we are too many, do something
151 cstring[] res;
152 res.length = pfxcount+1; // we know size beforehand
153 usize respos = 1; // res[0] -- longest prefix, start with [1]
154 usize slen = cmd.length; // not longer than found.length
155 foreach (cstring s; cmdlist) {
156 if (s.length >= slen) {
157 usize pos = slen;
158 foreach (immutable idx; 0..slen) if (found[idx].irc2lower != s[idx].irc2lower) { pos = idx; break; }
159 if (pos == slen) {
160 // i found her! remember (and fix) prefix
161 res[respos++] = s;
162 // do circimcision
163 for (; pos < found.length && pos < s.length; ++pos) if (found[pos].irc2lower != s[pos].irc2lower) break;
164 if (pos < found.length) {
165 found = found[0..pos];
166 if (slen > pos) slen = pos;
171 // set first item to longest prefix
172 res[0] = found;
173 return res;
177 // ////////////////////////////////////////////////////////////////////////// //
178 void autocomplete (TtyEditor ed, IRCChannel chan) {
179 if (ed is null) return;
180 auto pos = ed.curpos;
181 if (pos == 0) return;
182 if (pos < ed.textsize && ed[pos].ircisalnum) return;
183 if (!ed[pos-1].ircisalnum && ed[pos-1] != '/') return;
184 // collect word
185 int sp = pos;
186 while (sp > 0 && ed[sp-1].ircisalnum) --sp;
187 if (sp > 0 && ed[sp-1] == '/') --sp;
188 if (pos-sp > 64) return;
190 bool atLineStart = true;
191 for (int pp = sp; pp > 0; --pp) {
192 if (ed[pp-1] > ' ') { atLineStart = false; break; }
195 bool atCommand = false;
196 for (int pp = 0; pp < ed.textsize; ++pp) {
197 auto ch = ed[pp];
198 if (ch > ' ') {
199 atCommand = (ch == '/');
200 break;
204 auto startpos = sp; // we may need it later
205 char[128] wordbuf;
206 int wbpos;
207 while (sp < pos) wordbuf[wbpos++] = ed[sp++];
208 cstring word = wordbuf[0..wbpos];
209 cstring[] aclist;
210 // nicks
211 string lastText;
212 if (word[0] != '/') {
213 if (pos < ed.textsize && ed[pos] <= ' ') {
214 lastText = (atLineStart ? ":" : ",");
215 } else {
216 lastText = (atLineStart ? ": " : ", ");
218 if (atCommand) lastText = "";
219 if (chan !is null) {
220 aclist ~= chan.server.self.nick;
221 foreach (IRCUser u; chan.users) {
222 if (!u.ignored && !u.isme) aclist ~= u.nick;
225 } else {
226 // commands
227 void addCmd (cstring s) {
228 if (s.length == 0) return;
229 string xn = "/";
230 foreach (char ch; s) xn ~= ch.irc2lower;
231 foreach (cstring cmd; aclist) if (ircStrEquCI(cmd, xn)) return;
232 aclist ~= xn;
233 import std.algorithm : sort;
234 aclist.sort;
236 void addCommands () {
237 cstring[] args;
238 cstring origtext;
239 foreach (string mname; __traits(allMembers, mixin(__MODULE__))) {
240 static if (mname.length > 3 && mname[0..3] == "cmd") {
241 static if (is(typeof(__traits(getMember, mixin(__MODULE__), mname)) == function)) {
242 static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$], origtext);}))) {
243 addCmd(mname[3..$]);
244 } else static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$]);}))) {
245 addCmd(mname[3..$]);
251 // append server aliases
252 void addAliases () {
253 import std.file, std.path;
254 foreach (DirEntry de; dirEntries(configDir, "*.config.rc", SpanMode.shallow)) {
255 if (!de.isFile) continue;
256 aclist ~= "/"~de.baseName(".config.rc");
259 addCommands();
260 addAliases();
261 lastText = " ";
263 aclist = autocompleteFromList(word, aclist);
264 if (aclist.length == 0) { ttyBeep; return; }
265 if (aclist.length == 1 || aclist[0].length > word.length) {
266 //ed.insertText!("end", false)(pos, aclist[0][word.length..$]);
267 ed.replaceText!("end", false)(startpos, cast(int)word.length, aclist[0]);
268 // add undoable space if this is only completion
269 if (aclist.length == 1) {
270 ed.insertText!("end", false)(ed.curpos, lastText);
271 return;
274 // write variants
275 chatlist.textpane.addLine(null, "autocomplete options:", TextLine.Hi.Service);
276 foreach (cstring s; aclist[1..$]) chatlist.textpane.addLine(null, " "~s, TextLine.Hi.Service);
280 // ////////////////////////////////////////////////////////////////////////// //
281 @property TextLine.Hi hiType (IRCUser user) {
282 if (user is null) return TextLine.Hi.Normal;
283 if (user.ignored) return TextLine.Hi.Ignored;
284 if (user.isme) return TextLine.Hi.Mine;
285 return TextLine.Hi.Normal;
289 private bool containsCI (const(char)[] str, const(char)[] pat) {
290 if (pat.length == 0 || str.length < pat.length) return false;
291 foreach (usize pos; 0..str.length-pat.length+1) {
292 auto s = str[pos..pos+pat.length];
293 bool ok = true;
294 foreach (usize n; 0..pat.length) {
295 import iv.strex : tolower;
296 if (s[n].tolower != pat[n].tolower) {
297 ok = false;
298 break;
301 if (ok) return true;
303 return false;
307 // ////////////////////////////////////////////////////////////////////////// //
308 class ChatList {
309 static struct Item {
310 // only one of this can be set
311 IRCServer srv;
312 IRCChannel chan;
313 IRCUser user;
314 TextPane textpane;
316 this (IRCServer asrv) { textpane = new TextPane(); srv = asrv; }
317 this (IRCChannel achan) { textpane = new TextPane(); chan = achan; }
318 this (IRCUser auser) { textpane = new TextPane(); user = auser; }
320 @property bool system () { return (srv is null && chan is null && user is null); }
321 @property bool server () { return (srv !is null); }
322 @property bool channel () { return (chan !is null); }
323 @property bool privchat () { return (user !is null); }
324 @property string text () {
325 import std.format : format;
326 return
327 srv !is null ? (srv.srvalias.length ? srv.srvalias : srv.address) :
328 chan !is null ? "%s (%s)".format(chan.name, chan.users.length) :
329 user !is null ? user.nick :
330 "*system*";
334 int mCurItem;
335 int topitem;
336 Item[] items;
337 bool focused = true;
339 this () {
340 items.length = 1; // system pane
341 items[0].textpane = syspane;
342 syspane.active = true;
343 this.connectListeners();
346 void moveItemUp (int idx) {
347 if (idx < 1 || idx >= items.length || items.length < 2) return;
348 Item it = items[idx];
349 if (it.system) return;
350 int newidx = idx-1;
351 if (it.privchat) {
352 // privchat
353 // move up to channel/server/system
354 while (newidx >= 0) {
355 Item prevIt = items[newidx];
356 if (!prevIt.privchat) { ++newidx; break; }
357 --newidx;
359 } else if (it.channel) {
360 // channel
361 // move up to server/system
362 while (newidx >= 0) {
363 Item prevIt = items[newidx];
364 if (!prevIt.channel) { ++newidx; break; }
365 --newidx;
367 } else if (it.server) {
368 // server
369 // move this server and all its children before previous server
370 while (newidx >= 0) {
371 Item prevIt = items[newidx];
372 if (prevIt.server) break;
373 --newidx;
375 } else {
376 return;
378 if (newidx < 0 || newidx == idx) return; // just in case
379 // server move is complex
380 if (it.server) {
381 // collect server
382 Item[] sls;
383 int sidx = idx;
384 sls ~= items[sidx++];
385 while (sidx < items.length) {
386 if (items[sidx].server) break;
387 sls ~= items[sidx++];
389 // remove server
390 Item[] newlist = items[0..idx]~items[sidx..$];
391 // insert server
392 items = newlist[0..newidx]~sls~newlist[newidx..$];
393 // fix cursor
394 if (mCurItem >= idx && mCurItem < idx+sls.length) {
395 mCurItem = mCurItem-idx+newidx;
396 if (topitem > mCurItem) topitem = mCurItem;
398 } else {
399 for (usize n = items.length-2; n >= newidx; --n) items[n+1] = items[n];
400 items[newidx] = it;
401 if (mCurItem == idx) {
402 mCurItem = newidx;
403 if (topitem > mCurItem) topitem = mCurItem;
406 normCurItem();
409 TextPane paneForSystem () {
410 foreach (ref Item it; items) if (it.system) return it.textpane;
411 return null;
414 TextPane paneFor (IRCServer asrv) {
415 if (asrv is null) return null;
416 foreach (ref Item it; items) if (it.srv is asrv) return it.textpane;
417 return null;
420 TextPane paneFor (IRCChannel achan) {
421 if (achan is null) return null;
422 foreach (ref Item it; items) if (it.chan is achan) return it.textpane;
423 return null;
426 TextPane paneFor (IRCUser auser) {
427 if (auser is null) return null;
428 foreach (ref Item it; items) if (it.user is auser) return it.textpane;
429 return null;
432 public {
433 void onEvent (EventUserNickChanged evt) {
434 foreach (ref Item it; items) {
435 bool putNotice = false;
436 if (it.channel && it.chan.hasUser(evt.user)) {
437 putNotice = true;
438 } else if (it.privchat && it.user is evt.user) {
439 putNotice = true;
441 if (putNotice) {
442 it.textpane.addLine(evt.user.visNick, "changed nick from '"~evt.oldnick~"' to '"~evt.user.nick~"'", TextLine.Hi.Service);
447 void onEvent (EventJoin/*Part*/ evt) {
448 auto idx = ensureChannel(evt.chan);
449 if (idx < 0) return;
452 void onEvent (EventChanEnter evt) {
453 if (evt.user.ignored) return; // don't show enter/leave messages for ignored users
454 if (auto pane = paneFor(evt.chan)) {
455 string msg = "enters";
456 if (evt.msg.length) msg ~= ": "~evt.msg;
457 pane.addLine(evt.user.visNick, msg, TextLine.Hi.Service);
461 void onEvent (EventChanLeave evt) {
462 if (evt.user.ignored) return; // don't show enter/leave messages for ignored users
463 if (auto pane = paneFor(evt.chan)) {
464 string msg = "leaves";
465 if (evt.msg.length) msg ~= ": "~evt.msg;
466 pane.addLine(evt.user.visNick, msg, TextLine.Hi.Service);
470 private T extractCTCP(T:cstring) (ref T str) {
471 auto pos = str.indexOf('\x01');
472 if (pos < 0) return null;
473 auto epos = pos+1;
474 while (epos < str.length) {
475 // quoting?
476 if (str.ptr[epos] == 0x10) {
477 if (str.length-epos < 2) return null;
478 epos += 2;
479 } else if (str.ptr[epos] == 0x01) {
480 break;
481 } else {
482 ++epos;
485 if (epos >= str.length && str.ptr[epos] != 0x01) return null;
486 auto res = str[pos+1..epos];
487 if (pos == 0) {
488 // easy deal
489 str = str[epos+1..$];
490 } else {
491 str = str[0..pos]~str[epos+1..$];
493 return res;
496 private T extractCTCPKeyword(T:cstring) (ref T str) {
497 str = str.ircstrip;
498 usize pos = 0;
499 while (pos < str.length && str[pos] > ' ') ++pos;
500 auto res = str[0..pos];
501 str = str[pos..$].ircstrip;
502 return res;
505 void onEvent (EventChanSysNotice evt) {
506 if (evt.chan is null) return;
507 foreach (ref Item it; items) {
508 if (it.chan is evt.chan) {
509 it.textpane.putMessage(evt.time, evt.user.visNick, evt.msg, TextLine.Hi.Service);
514 void onEvent (EventPrivSysNotice evt) {
515 if (evt.user is null) return;
516 foreach (ref Item it; items) {
517 if (it.user is evt.user) {
518 it.textpane.putMessage(evt.time, evt.user.visNick, evt.msg, TextLine.Hi.Service);
523 void onEvent (EventPrivChat evt) {
524 if (!hasPrivChat(evt.user)) {
525 if (evt.user.ignored) {
526 auto ii = evt.user.server.findIgnoreInfo(evt.user.nick);
527 if (ii.valid && !ii.allowprivchat) return;
531 TextPane upane;
533 void openChat (bool forced=false) {
534 if (upane !is null) return;
535 if (!forced && evt.user.ignored) return;
536 ensurePrivChat(evt.user);
537 upane = paneFor(evt.user);
540 auto msg = evt.msg;
541 // CTCP processor
542 for (;;) {
543 auto ctcp = extractCTCP(msg);
544 if (ctcp.ptr is null) break;
545 ctcp = ctcp.ircstrip;
546 auto kw = extractCTCPKeyword(ctcp);
547 if (kw.ircStrCmpCI("ACTION")) {
548 if (ctcp.length) {
549 openChat(!evt.hisname);
550 if (evt.hisname) {
551 upane.addLine(evt.time, evt.user.visNick, ctcp, TextLine.Hi.Action);
552 } else {
553 upane.addLine(evt.time, evt.user.server.self.visNick, ctcp, TextLine.Hi.Action);
556 } else if (kw.ircStrCmpCI("VERSION")) {
557 if (evt.hisname) {
558 logwritefln("*** sending version response to %s", evt.user.nick);
559 evt.user.server.sendf("NOTICE %s :\x01VERSION Miriel, The IRC Goddess\x01", evt.user.nick);
560 } else {
561 logwritefln("*** version query to %s -- echo", evt.user.nick);
563 } else if (kw.ircStrCmpCI("CLIENTINFO")) {
564 if (evt.hisname) {
565 logwritefln("*** sending CLIENTINFO response to %s", evt.user.nick);
566 evt.user.server.sendf("NOTICE %s :\x01CLIENTINFO ACTION CLIENTINFO PING SOURCE VERSION\x01", evt.user.nick);
567 } else {
568 logwritefln("*** CLIENTINFO query to %s -- echo", evt.user.nick);
570 } else if (kw.ircStrCmpCI("PING")) {
571 if (evt.hisname) {
572 logwritefln("*** sending PING response to %s", evt.user.nick);
573 evt.user.server.sendf("NOTICE %s :\x01PING %s\x01", evt.user.nick, ctcp);
574 } else {
575 logwritefln("*** PING query to %s -- echo", evt.user.nick);
577 } else if (kw.ircStrCmpCI("SOURCE")) {
578 if (evt.hisname) {
579 logwritefln("*** sending SOURCE response to %s", evt.user.nick);
580 evt.user.server.sendf("NOTICE %s :\x01SOURCE %s\x01", evt.user.nick, "https://repo.or.cz/miri.git");
581 } else {
582 logwritefln("*** SOURCE query to %s -- echo", evt.user.nick);
587 if (msg.ircstrip.length) {
588 openChat();
589 if (!focused) {
590 // blink only for interesting messages
591 if (evt.hisname && !evt.user.ignored) {
592 showNotify(evt.user.visNick, msg);
593 (new EventTrayBlink()).post;
596 if (!evt.hisname) {
597 // my echo
598 if (upane !is null) upane.putMessage(evt.time, evt.user.server.self.visNick, msg, TextLine.Hi.Mine);
599 } else {
600 if (upane !is null) upane.putMessage(evt.time, evt.user.visNick, msg);
605 // put highlighted messages to server pane too
606 void onEvent (EventChanChat evt) {
607 if (evt.chan.server.dead) return;
608 if (evt.chan.joined) ensureChannel(evt.chan);
609 auto pane = paneFor(evt.chan);
610 auto hi = (evt.hasme ? TextLine.Hi.ToMe : evt.user.hiType);
612 auto msg = evt.msg;
613 for (;;) {
614 auto ctcp = extractCTCP(msg);
615 if (ctcp.ptr is null) break;
616 ctcp = ctcp.ircstrip;
617 auto kw = (ctcp.indexOf(' ') >= 0 ? ctcp[0..ctcp.indexOf(' ')] : ctcp);
618 ctcp = ctcp[kw.length..$].ircstrip;
619 // ignore VERSION request sent to channel
620 if (kw == "ACTION") {
621 if (pane !is null && ctcp.length) {
622 if (evt.user.ignored) ctcp = "*** "~ctcp;
623 pane.addLine(evt.time, evt.user.visNick, ctcp, (evt.user.ignored ? TextLine.Hi.Ignored : TextLine.Hi.Action));
628 bool srvinfo = msg.startsWith("srvinfo:");
630 // idiots
631 if (srvinfo && isSrvInfoIdiot(msg)) return;
633 // convert "tg: <nick> text"
634 string visNick = evt.user.visNick;
635 if (!srvinfo && msg.startsWith("tg: <")) {
636 auto epos = msg.indexOf('>');
637 if (epos >= 0) {
638 string nn = msg[5..epos].xstrip;
639 msg = (epos+1 < msg.length ? msg[epos+1..$].xstrip : null);
640 if (msg.length == 0) msg = "[fooboo]";
641 if (nn.length != 0) {
642 visNick = "^";
643 foreach (immutable idx, char ch; nn) {
644 if (!ircisalnum(ch)) {
645 if (visNick.length == 1) continue;
646 if (visNick[$-1] == '_') continue;
647 visNick ~= '_';
648 } else {
649 visNick ~= ch;
652 while (visNick.length > 1 && visNick[$-1] == '_') visNick = visNick[0..$-1];
653 if (visNick.length == 1) visNick ~= "dumbass^";
658 if (msg.indexOf("tg: ") >= 0 && isTgIgnore(msg)) return;
660 // custom ignore
661 if (evt.chan.server.checkIgnoreMask(visNick)) return;
663 if (pane !is null && msg.ircstrip.length) pane.putMessage(evt.time, visNick, msg, hi);
665 // ignore "srvinfo:" messages
666 if (srvinfo) return;
668 // if this is highlighted message to inactive channel, put it to server text pane
669 if (evt.user.isme) return;
670 if (evt.user.ignored) return;
672 if (!focused && pane !is null && !pane.active) {
673 if ((evt.chan.blink && !evt.user.nonotify) || hi == TextLine.Hi.ToMe) {
674 // blink only for interesting messages
675 switch (hi) {
676 case TextLine.Hi.Normal:
677 case TextLine.Hi.ToMe:
678 (new EventTrayBlink()).post;
679 break;
680 default:
683 if ((evt.chan.popup && !evt.user.nonotify) || hi == TextLine.Hi.ToMe) {
684 // blink only for interesting messages
685 switch (hi) {
686 case TextLine.Hi.Normal:
687 case TextLine.Hi.ToMe:
688 if (msg.ircstrip.length) showNotify(evt.chan.name~" -- "~visNick, msg);
689 break;
690 default:
694 if (pane !is null && pane.active /*&& !pane.hasmark*/) return;
695 if (!evt.hasme) return;
696 if (auto spane = paneFor(evt.user.server)) {
697 spane.putMessage(evt.time, visNick, evt.msg, TextLine.Hi.ToMe);
701 void onEvent (EventChanTopic evt) {
702 if (evt.chan.server.dead) return;
703 if (evt.chan.joined) ensureChannel(evt.chan);
704 auto pane = paneFor(evt.chan);
705 if (pane is null || pane !is textpane) return;
706 // do something here, but for now topic will be drawn automatically
709 void onEvent (EventNickServNotice evt) {
710 if (evt.server.dead) return;
711 if (auto pane = paneFor(evt.server)) {
712 pane.putMessage(evt.time, "NickServ", evt.msg);
716 void onEvent (EventOtherNotice evt) {
717 if (evt.server.dead) return;
718 if (auto pane = paneFor(evt.server)) {
719 pane.putMessage(evt.time, null, evt.msg, (evt.hasme ? TextLine.Hi.ToMe : TextLine.Hi./*Normal*/Service), evt.unimportant);
723 void onEvent (EventUserNotice evt) {
724 if (evt.user.server.dead) return;
725 if (auto pane = paneFor(evt.user.server)) {
726 pane.putMessage(evt.time, evt.user.visNick, evt.msg, (evt.hasme ? TextLine.Hi.ToMe : evt.user.hiType));
730 void onEvent (EventFocusOut evt) {
731 focused = false;
732 textpane.active = false;
733 //textpane.addLine(null, "focusout", TextLine.Hi.Service);
736 void onEvent (EventFocusIn evt) {
737 focused = true;
738 if (!textpane.active) {
739 if (!textpane.wasLogMove) textpane.doCenterMark(true);
741 textpane.active = true;
742 (new EventTrayUnBlink()).post;
743 //textpane.addLine(null, "focusout", TextLine.Hi.Service);
747 @property TextPane textpane () {
748 if (items.length == 0) return syspane;
749 if (mCurItem < 0) return items[0].textpane;
750 if (mCurItem >= items.length) return items[$-1].textpane;
751 return items[mCurItem].textpane;
754 @property IRCServer cursrv(bool donorm=true) () {
755 static if (donorm) normCurItem();
756 if (mCurItem < 0 || mCurItem >= items.length) return null;
757 if (items[mCurItem].server) return items[mCurItem].srv;
758 if (items[mCurItem].channel) return items[mCurItem].chan.server;
759 if (items[mCurItem].privchat) return items[mCurItem].user.server;
760 return null;
763 @property IRCChannel curchan () {
764 normCurItem();
765 if (mCurItem < 0 || mCurItem >= items.length) return null;
766 if (items[mCurItem].channel) return items[mCurItem].chan;
767 return null;
770 @property IRCUser curpriv () {
771 normCurItem();
772 if (mCurItem < 0 || mCurItem >= items.length) return null;
773 if (items[mCurItem].user) return items[mCurItem].user;
774 return null;
777 bool sendMsg (cstring msg) {
778 normCurItem();
779 if (mCurItem < 0 || mCurItem >= items.length) return false;
780 // send to channel?
781 IRCServer srv;
782 IRCChannel dchan;
783 IRCUser duser;
784 cstring unick;
785 if (items[mCurItem].channel) {
786 dchan = items[mCurItem].chan;
787 if (!dchan.joined) return false;
788 srv = dchan.server;
789 unick = dchan.name;
790 } else if (items[mCurItem].privchat) {
791 duser = items[mCurItem].user;
792 if (duser.dead) {
793 sysmsgf("can't send message (user '%s' is dead):\n%s", duser.nick, msg);
794 return false;
796 srv = duser.server;
797 unick = duser.nick;
799 if (srv is null) {
800 sysmsgf("can't send message (no server):\n%s", msg);
801 return false;
803 if (!srv.alive) {
804 sysmsgf("can't send message ([%s] is dead):\n%s", srv.address, msg);
805 return false;
808 bool doWrap = true;
809 if (msg.length >= 2 && msg[0..2] == "::") {
810 doWrap = false;
811 msg = msg[2..$];
812 while (msg.length && msg[0] == ' ') msg = msg[1..$];
815 //if (dchan !is null) dchan.logChatMessageF(Clock.currTime, "<%s> %s", srv.self.nick, msg);
816 //if (duser !is null) duser.logChatMessageF(Clock.currTime, true, "%s", msg);
817 if (dchan !is null) dchan.say(msg, doWrap);
818 if (duser !is null) duser.say(msg, doWrap);
819 return true;
822 // insert before pos
823 private void insertItemBefore (Item it, int pos) {
824 if (pos < 0) pos = 0;
825 if (pos >= items.length) {
826 items.arrayAppend(it);
827 } else {
828 if (mCurItem >= pos) ++mCurItem;
829 items.arrayGrow;
830 foreach_reverse (immutable c; pos..items.length-1) items[c+1] = items[c];
831 items[pos] = it;
833 normCurItem();
836 IRCServer findAndActivateServerByAlias (IRCServer xsrv) {
837 IRCServer res;
838 activateServerByAlias!true(xsrv, true, &res);
839 return res;
842 bool activateServerByAlias(bool addit=false) (IRCServer xsrv, bool activate=true, IRCServer* sout=null) {
843 if (sout !is null) *sout = null;
844 if (xsrv is null) return false;
845 foreach (immutable idx, ref Item it; items) {
846 if (it.server && it.srv.srvalias == xsrv.srvalias) {
847 if (activate) {
848 if (mCurItem >= 0 && mCurItem < items.length) items[mCurItem].textpane.active = false;
849 mCurItem = cast(int)idx;
850 items[mCurItem].textpane.active = true;
851 //(new EventXKbSetLayout(items[mCurItem].srv.forceXKbLayout)).post;
853 if (sout !is null) *sout = it.srv;
854 normCurItem();
855 return true;
858 static if (addit) {
859 insertItemBefore(Item(xsrv), int.max);
860 if (activate) {
861 if (mCurItem >= 0 && mCurItem < items.length) items[mCurItem].textpane.active = false;
862 mCurItem = cast(int)items.length-1;
863 items[mCurItem].textpane.active = true;
864 //(new EventXKbSetLayout(items[mCurItem].srv.forceXKbLayout)).post;
865 normCurItem();
867 if (sout !is null) *sout = xsrv;
869 return false;
872 void doCloseTab (int tabidx) {
873 if (tabidx < 1 || tabidx >= items.length) return;
874 if (items[tabidx].server) {
875 auto srv = items[tabidx].srv;
876 // close all channels and private chats for this server
877 for (;;) {
878 bool again = false;
879 foreach (immutable idx, ref it; items) {
880 if (it.channel && it.chan.server is srv) {
881 doCloseTab(cast(int)idx);
882 again = true;
883 break;
886 if (!again) break;
888 srv.disconnect();
889 srv.markDead();
890 tabidx = 1;
891 while (tabidx < items.length && items[tabidx].srv !is srv) ++tabidx;
892 if (tabidx >= items.length) return;
893 } else if (items[tabidx].chan) {
894 if (items[tabidx].chan.joined) {
895 items[tabidx].chan.server.sendf("PART %s", items[tabidx].chan.name);
897 } else if (items[tabidx].privchat) {
898 } else {
899 return;
901 items[tabidx].textpane.active = false;
902 foreach (immutable c; tabidx+1..items.length) items[c-1] = items[c];
903 items[$-1] = Item.init;
904 items.length -= 1;
905 items.assumeSafeAppend;
906 if (mCurItem >= tabidx) {
907 --mCurItem;
908 normCurItem();
909 items[mCurItem].textpane.active = true;
913 void doCloseCurrentTab () {
914 doCloseTab(mCurItem);
917 private int ensureChannel (IRCChannel xchan) {
918 if (xchan is null) return -1;
919 foreach (immutable idx, ref Item it; items) {
920 if (it.channel && it.chan is xchan) return cast(int)idx;
922 // not found, create new channel item in current server
923 activateServerByAlias!true(xchan.server, false);
924 // find server item
925 int srvidx = 0;
926 while (srvidx < items.length) {
927 if (items[srvidx].server && items[srvidx].srv is xchan.server) break;
928 ++srvidx;
930 if (srvidx >= items.length) assert(0, "wtf?!");
931 // add channel, sorted
932 int pos = srvidx+1;
933 while (pos < items.length) {
934 if (!items[pos].channel) break;
935 if (items[pos].chan.name.ircStrCmpCI(xchan.name) > 0) break;
936 ++pos;
938 insertItemBefore(Item(xchan), pos);
939 return pos;
942 private bool hasPrivChat (IRCUser xuser) {
943 if (xuser is null) return false;
944 foreach (immutable idx, ref Item it; items) {
945 if (it.privchat && it.user is xuser) return true;
947 return false;
950 private int ensurePrivChat(bool activate=false) (IRCUser xuser) {
951 if (xuser is null) return -1;
952 foreach (immutable idx, ref Item it; items) {
953 if (it.privchat && it.user is xuser) return cast(int)idx;
955 // not found, create new channel item in current server
956 activateServerByAlias!true(xuser.server, false);
957 // find server item
958 int srvidx = 0;
959 while (srvidx < items.length) {
960 if (items[srvidx].server && items[srvidx].srv is xuser.server) break;
961 ++srvidx;
963 if (srvidx >= items.length) assert(0, "wtf?!");
964 ++srvidx;
965 // skip channels
966 while (srvidx < items.length && items[srvidx].channel) ++srvidx;
967 // add privchat, sorted
968 int pos = srvidx;
969 while (pos < items.length) {
970 if (!items[pos].privchat) break;
971 if (items[pos].user.nick.ircStrCmpCI(xuser.nick) > 0) break;
972 ++pos;
974 insertItemBefore(Item(xuser), pos);
975 if (activate && mCurItem != pos) {
976 if (mCurItem >= 0 && mCurItem < items.length) items[mCurItem].textpane.active = false;
977 mCurItem = pos;
978 items[mCurItem].textpane.active = true;
979 normCurItem();
981 return pos;
984 // for layouts
985 private IRCServer lastActiveServer = null;
986 private Object lastActiveChanUser = null;
988 void fixLayout () {
989 if (mCurItem < 0 || mCurItem >= items.length) {
990 lastActiveServer = null;
991 lastActiveChanUser = null;
992 return;
994 auto it = &items[mCurItem];
995 // system pane?
996 if (it.system) {
997 if (lastActiveServer !is null || lastActiveChanUser !is null) {
998 lastActiveServer = null;
999 lastActiveChanUser = null;
1000 (new EventXKbSetLayout(0)).post;
1002 return;
1004 // server pane?
1005 if (it.server) {
1006 if (lastActiveServer !is it.srv || lastActiveChanUser !is null) {
1007 lastActiveServer = it.srv;
1008 lastActiveChanUser = null;
1009 (new EventXKbSetLayout(0)).post;
1011 return;
1013 // channel pane?
1014 if (it.channel) {
1015 if (lastActiveChanUser !is it.chan) {
1016 int doChange = it.chan.server.forceXKbLayout;
1017 lastActiveServer = it.chan.server;
1018 lastActiveChanUser = it.chan;
1019 if (doChange >= 0) (new EventXKbSetLayout(doChange)).post;
1021 return;
1023 // private chat pane?
1024 if (it.privchat) {
1025 if (lastActiveChanUser !is it.user) {
1026 int doChange = it.user.server.forceXKbLayout;
1027 lastActiveServer = it.user.server;
1028 lastActiveChanUser = it.user;
1029 if (doChange >= 0) (new EventXKbSetLayout(doChange)).post;
1031 return;
1033 // something unknown, reset flags
1034 lastActiveServer = null;
1035 lastActiveChanUser = null;
1038 void normCurItem () {
1039 if (mCurItem < 0) mCurItem = 0;
1040 if (mCurItem >= items.length) mCurItem = cast(int)items.length-1;
1041 if (topitem < mCurItem) topitem = mCurItem;
1042 if (topitem+ttyh >= mCurItem) {
1043 topitem = mCurItem-ttyh+1;
1044 if (topitem < 0) topitem = 0;
1046 textpane.doCenterMark();
1047 fixLayout();
1050 void goUp () {
1051 if (mCurItem > 0) {
1052 textpane.active = false;
1053 --mCurItem;
1054 normCurItem();
1055 textpane.active = true;
1059 void goDown () {
1060 if (mCurItem+1 < items.length) {
1061 textpane.active = false;
1062 ++mCurItem;
1063 normCurItem();
1064 textpane.active = true;
1068 void draw () {
1069 import miri.textpane : NickBG;
1070 auto win = XtWindow(0, 0, ChanTabWidth, ttyh);
1071 win.fg = 15;
1072 win.bg = NickBG;
1073 win.fill(0, 0, win.width, win.height, ' ');
1074 win.vline(win.width-1, 0, win.height);
1075 win.width = win.width-1;
1076 normCurItem(); // just in case
1077 int y = 0;
1078 int it = topitem;
1079 while (y < win.height && it < items.length) {
1080 win.fg = 7;
1081 win.bg = NickBG;
1082 auto item = &items[it];
1083 if (it == mCurItem) {
1084 win.fg = 15;
1085 win.bg = TtyRgb2Color!(0x00, 0x5f, 0xff);
1086 win.writeCharsAt(0, y, win.width, ' ');
1087 } else {
1088 win.fg = (item.server ? 15 : 7);
1089 win.bg = NickBG;
1090 if (!item.textpane.active && item.textpane.hasmark) {
1091 win.fg = TtyRgb2Color!(0x00, 0xff, 0x00);
1092 } else {
1093 if (item.channel && !item.chan.joined) win.fg = 6;
1094 if (item.server && !item.srv.alive) win.fg = 2;
1095 if (item.privchat && item.user.dead) win.fg = 8;
1098 win.writeStrAt((item.privchat ? 1 : item.channel ? 1 : 0), y, item.text);
1099 ++it;
1100 ++y;
1105 __gshared ChatList chatlist;
1108 // ////////////////////////////////////////////////////////////////////////// //
1109 void curpanemsgf(A...) (cstring fmt, A args) {
1110 import std.format : formattedWrite;
1111 char[] st;
1112 struct Writer { void put (cstring s...) { st ~= s[]; } }
1113 Writer wr;
1114 formattedWrite(wr, fmt, args);
1115 (new EventSysMsg(cast(string)st)).post; // it is safe to cast here
1116 if (st.length) chatlist.textpane.addLine(null, st, TextLine.Hi.Service);
1120 void curpanemsgfnosys(A...) (cstring fmt, A args) {
1121 import std.format : formattedWrite;
1122 char[] st;
1123 struct Writer { void put (cstring s...) { st ~= s[]; } }
1124 Writer wr;
1125 formattedWrite(wr, fmt, args);
1126 (new EventSysMsg(cast(string)st)).post; // it is safe to cast here
1127 if (st.length) chatlist.textpane.addLine(null, st, TextLine.Hi.Service);
1131 void doSendText (cstring text) {
1132 if (text.ircstrip.length == 0) return;
1133 if (!chatlist.sendMsg(text)) ttyBeep;
1137 T getNickFromArg(T : cstring) (T arg) {
1138 auto nk = arg.ircstrip;
1139 if (nk.length && (nk[$-1] == ':' || nk[$-1] == ',')) nk = nk[0..$-1].ircstrip;
1140 return nk;
1144 char[] argsJoin (cstring[] args) {
1145 char[] tlong;
1146 foreach (cstring s; args[]) {
1147 s = s.xstrip;
1148 if (s.length == 0) continue;
1149 if (tlong.length) tlong ~= ' ';
1150 tlong ~= s;
1152 return tlong;
1156 // ////////////////////////////////////////////////////////////////////////// //
1157 // optional cmdXXX arg: `cstring text`: text *WITHOUT* command (i.e. only args), unparsed
1159 // move current item up
1160 void cmdMoveUp (cstring cmd, cstring[] args/*, cstring text*/) {
1161 if (!chatlist) return;
1162 chatlist.moveItemUp(chatlist.mCurItem);
1166 // salias alias server[:port] nick[:password] [options]
1167 // utfuck
1168 // koi8/cp1251/cp866
1169 // sendcodepage
1170 void cmdSAlias (cstring cmd, cstring[] args/*, cstring text*/) {
1171 if (args.length < 3) {
1172 sysmsgf("%s",
1173 "salias alias proto:server[:port] nick[:password] [options]\n"~
1174 " utfuck\n"~
1175 " koi8/cp1251/cp866\n"~
1176 " sendcodepage"
1178 return;
1180 IRCServer srv;
1181 try {
1182 import iv.strex : startsWith, lastIndexOf;
1183 if (args[1].startsWith("tox:")) {
1184 srv = new IRCServer(args[1], 0);
1185 } else if (args[1].indexOf(':') >= 0) {
1186 import std.conv : to;
1187 auto idx = args[1].lastIndexOf(':');
1188 auto port = args[1][idx+1..$].to!ushort;
1189 srv = new IRCServer(args[1][0..idx], port);
1190 } else {
1191 srv = new IRCServer(args[1], 6667);
1193 } catch (Exception e) {
1194 curpanemsgf("error parsing server address: %s", e.msg);
1195 return;
1197 srv.srvalias = args[0].idup;
1198 if (args[2].indexOf(':') >= 0) {
1199 auto idx = args[2].indexOf(':');
1200 srv.self.nick = args[2][0..idx].idup;
1201 srv.password = args[2][idx+1..$].idup;
1202 } else {
1203 srv.self.nick = args[2].idup;
1205 if (srv.self.nick.length == 0) {
1206 curpanemsgf("empty nick!");
1207 return;
1209 foreach (cstring opt; args[3..$]) {
1210 if ("utfuck".ircStrEquCI(opt)) srv.utfucked = true;
1211 else if ("koi8".ircStrEquCI(opt)) { srv.utfucked = false; srv.codepage = srv.CodePage.koi8u; }
1212 else if ("cp1251".ircStrEquCI(opt)) { srv.utfucked = false; srv.codepage = srv.CodePage.cp1251; }
1213 else if ("cp866".ircStrEquCI(opt)) { srv.utfucked = false; srv.codepage = srv.CodePage.cp866; }
1214 else if ("sendcp".ircStrEquCI(opt)) srv.sendCodepage = true;
1215 else if ("sendcodepage".ircStrEquCI(opt)) srv.sendCodepage = true;
1216 else {
1217 curpanemsgf("invalid option: %s", opt);
1218 return;
1221 if (srv.saveConfig()) {
1222 curpanemsgf("alias '%s' added", srv.srvalias);
1223 } else {
1224 curpanemsgf("ERROR adding alias '%s'", srv.srvalias);
1228 void cmdXCmd (cstring cmd, cstring[] args, cstring text) {
1229 if (args.length == 0 || text.length == 0) {
1230 curpanemsgf("cmd what?");
1231 return;
1233 auto srv = chatlist.cursrv;
1234 if (srv is null) {
1235 curpanemsgf("command to which server?");
1236 return;
1238 if (!srv.alive) {
1239 curpanemsgf("can't command dead server");
1240 return;
1242 srv.sendf("%s", text);
1245 void cmdMsg (cstring cmd, cstring[] args, cstring text) {
1246 if (args.length < 2 || text.length == 0) {
1247 curpanemsgf("msg whom?");
1248 return;
1250 auto srv = chatlist.cursrv;
1251 if (srv is null) {
1252 curpanemsgf("command to which server?");
1253 return;
1255 if (!srv.alive) {
1256 curpanemsgf("can't command dead server");
1257 return;
1259 // get name
1260 text = text.xstrip();
1261 usize sppos = 0;
1262 while (sppos < text.length && text[sppos] > ' ') ++sppos;
1263 if (sppos >= text.length) {
1264 curpanemsgf("msg what?");
1265 return;
1267 cstring who = text[0..sppos];
1268 text = text[sppos..$].xstrip();
1269 if (text.length == 0) {
1270 curpanemsgf("msg what?");
1271 return;
1273 srv.sendf("PRIVMSG %s :%s", who, text);
1277 void cmdQuit (cstring cmd, cstring[] args) {
1278 (new EventQuitRun()).post;
1281 void cmdClose (cstring cmd, cstring[] args) {
1282 chatlist.doCloseCurrentTab();
1285 void cmdJoin (cstring cmd, cstring[] args) {
1286 if (args.length == 0) {
1287 curpanemsgf("join to?");
1288 return;
1290 auto srv = chatlist.cursrv;
1291 if (srv is null) {
1292 curpanemsgf("join on which server?");
1293 return;
1295 foreach (cstring cname; args) {
1296 if (cname.length == 0) continue;
1297 if (cname[0] != '#' || cname.length < 2) {
1298 curpanemsgf("invalid channel name: '%s'", cname);
1299 } else {
1300 srv.sendf("JOIN %s", cname);
1305 void cmdPart (cstring cmd, cstring[] args) {
1306 if (args.length != 0) {
1307 curpanemsgf("/part cannot accept args");
1308 return;
1310 if (auto chan = chatlist.curchan) {
1311 if (chan.joined) {
1312 chan.server.sendf("PART %s", chan.name);
1313 } else {
1314 curpanemsgf("you aren't joined %s", chan.name);
1316 } else {
1317 curpanemsgf("part from?");
1321 void cmdNick (cstring cmd, cstring[] args) {
1322 if (args.length != 1) {
1323 curpanemsgf("change nick to?");
1324 return;
1326 auto srv = chatlist.cursrv;
1327 if (srv is null) {
1328 curpanemsgf("change nick on which server?");
1329 return;
1331 auto nk = getNickFromArg(args[0]);
1332 if (nk.length == 0 || nk[0] == '#') { ttyBeep; return; }
1333 srv.sendf("NICK %s", nk);
1336 void cmdAuthorizeNick (cstring cmd, cstring[] args) {
1337 auto srv = chatlist.cursrv;
1338 if (srv is null) {
1339 curpanemsgf("authorize on which server?");
1340 return;
1342 srv.authorize();
1345 // start private chat
1346 void cmdPrivate (cstring cmd, cstring[] args) {
1347 if (args.length != 1) {
1348 curpanemsgf("private chat with?");
1349 return;
1351 auto srv = chatlist.cursrv;
1352 if (srv is null) {
1353 curpanemsgf("change nick on which server?");
1354 return;
1356 auto nk = getNickFromArg(args[0]);
1357 if (nk.length == 0 || nk[0] == '#') { ttyBeep; return; }
1358 if (auto user = srv.findUser(nk)) {
1359 chatlist.ensurePrivChat!true(user);
1360 } else {
1361 curpanemsgf("opening chat with non-channel nick '%s'", nk);
1362 auto newusr = srv.findUser!true(nk);
1363 if (newusr) chatlist.ensurePrivChat!true(newusr);
1367 void cmdMe (cstring cmd, cstring[] args, cstring text) {
1368 if (args.length == 0 || text.length == 0) {
1369 curpanemsgf("me what?");
1370 return;
1372 string act = "\x01ACTION "~text.idup~"\x01";
1373 if (!chatlist.sendMsg(act)) ttyBeep;
1376 void cmdVersion (cstring cmd, cstring[] args, cstring text) {
1377 auto srv = chatlist.cursrv;
1378 if (srv is null) {
1379 curpanemsgf("VERSION on which server?");
1380 return;
1382 if (args.length == 0 || text.length == 0) {
1383 curpanemsgf("version who?");
1384 return;
1386 if (args.length != 1) {
1387 curpanemsgf("which nick?");
1388 return;
1390 cstring nick = args[0].xstrip();
1391 while (nick.length && (nick[$-1] == ',' || nick[$-1] == ':' || nick[$-1] == '.' || nick[$-1] == ';')) nick = nick[0..$-1];
1392 if (nick.length == 0) {
1393 curpanemsgf("which nick?");
1394 return;
1396 srv.sendf("PRIVMSG %s :\x01VERSION\x01", nick);
1399 // /allowchat nick
1400 void cmdAllowChat (cstring cmd, cstring[] args) {
1401 auto srv = chatlist.cursrv;
1402 if (srv is null) {
1403 curpanemsgf("ignore on which server?");
1404 return;
1406 if (args.length == 0) return;
1407 auto unick = getNickFromArg(args[0]);
1408 if (unick.length == 0) { ttyBeep; return; }
1409 srv.allowIngoredPrivChat(unick, true);
1412 // /nochat nick
1413 void cmdNoChat (cstring cmd, cstring[] args) {
1414 auto srv = chatlist.cursrv;
1415 if (srv is null) {
1416 curpanemsgf("ignore on which server?");
1417 return;
1419 if (args.length == 0) return;
1420 auto unick = getNickFromArg(args[0]);
1421 if (unick.length == 0) { ttyBeep; return; }
1422 srv.allowIngoredPrivChat(unick, false);
1425 // /ignore [nick] [dsc] [longdsc]
1426 void cmdIgnore (cstring cmd, cstring[] args) {
1427 auto srv = chatlist.cursrv;
1428 if (srv is null) {
1429 curpanemsgf("ignore on which server?");
1430 return;
1432 // list ignores?
1433 if (args.length == 0) {
1434 auto list = srv.allIgnores();
1435 if (list.length == 0) {
1436 curpanemsgfnosys("you are ignoring noone");
1437 } else {
1438 import std.algorithm : sort;
1439 import std.string : format;
1440 list.sort!((ref i0, ref i1) => (ircStrCmpCI(i0.nick, i1.nick) < 0));
1441 string rep = "you are ignoring %s people".format(list.length);
1442 foreach (ref ii; list) {
1443 rep ~= "\n ";
1444 if (ii.allowprivchat) rep ~= "*"; else rep ~= " ";
1445 rep ~= ii.nick;
1446 if (ii.ignoreShort.length > 0) rep ~= " <"~ii.ignoreShort~">";
1447 if (ii.ignoreLong.length > 0) rep ~= " : "~ii.ignoreLong;
1449 curpanemsgfnosys("%s", rep);
1451 return;
1453 // add new ignore
1454 auto unick = getNickFromArg(args[0]);
1455 if (unick.length == 0) { ttyBeep; return; }
1456 cstring tshort;
1457 char[] tlong;
1458 if (args.length > 1) tshort = args[1];
1459 if (args.length > 2) tlong = argsJoin(args[2..$]);
1460 auto ii = srv.findIgnoreInfo(unick);
1461 if (ii.nick.length != 0) {
1462 curpanemsgfnosys("you are already ignoring %s", ii.nick);
1463 } else {
1464 srv.ignoreUser(unick, tshort, tlong);
1465 ii = srv.findIgnoreInfo(unick);
1466 if (ii.nick.length > 0) curpanemsgfnosys("you are now ignoring %s", ii.nick);
1470 // /unignore nick
1471 void cmdUnignore (cstring cmd, cstring[] args) {
1472 auto srv = chatlist.cursrv;
1473 if (srv is null) {
1474 curpanemsgf("unignore on which server?");
1475 return;
1477 if (args.length != 1) {
1478 curpanemsgfnosys("unignore who?");
1479 return;
1481 auto unick = getNickFromArg(args[0]);
1482 if (unick.length == 0) { ttyBeep; return; }
1483 auto ii = srv.findIgnoreInfo(unick);
1484 if (ii.nick.length == 0) {
1485 curpanemsgfnosys("you are not ignoring %s", unick);
1486 } else {
1487 srv.unignoreUser(unick);
1488 ii = srv.findIgnoreInfo(unick);
1489 if (ii.nick.length == 0) curpanemsgfnosys("you are no longer ignoring %s", unick);
1493 // /nonotify [nick]
1494 void cmdNonotify (cstring cmd, cstring[] args) {
1495 auto srv = chatlist.cursrv;
1496 if (srv is null) {
1497 curpanemsgf("nonotify on which server?");
1498 return;
1500 // list ignores?
1501 if (args.length == 0) {
1502 auto list = srv.allNonitify();
1503 if (list.length == 0) {
1504 curpanemsgfnosys("nonotify list is empty");
1505 } else {
1506 import std.algorithm : sort;
1507 import std.string : format;
1508 list.sort!((ref i0, ref i1) => (ircStrCmpCI(i0.nick, i1.nick) < 0));
1509 string rep = "nonotify %s people".format(list.length);
1510 foreach (ref ii; list) {
1511 rep ~= "\n ";
1512 if (ii.ignore) rep ~= "*"; else rep ~= " ";
1513 rep ~= ii.nick;
1515 curpanemsgfnosys("%s", rep);
1517 return;
1519 // add new nonotify
1520 auto unick = getNickFromArg(args[0]);
1521 if (unick.length == 0) { ttyBeep; return; }
1522 auto ii = srv.findNonotifyInfo(unick);
1523 if (ii.nick.length != 0) {
1524 curpanemsgfnosys("%s is already nonotify", ii.nick);
1525 } else {
1526 srv.nonotifyUser(unick);
1527 ii = srv.findNonotifyInfo(unick);
1528 if (ii.nick.length > 0) curpanemsgfnosys("%s is nonotify now", ii.nick);
1532 // /donotify nick
1533 void cmdDonotify (cstring cmd, cstring[] args) {
1534 auto srv = chatlist.cursrv;
1535 if (srv is null) {
1536 curpanemsgf("donotify on which server?");
1537 return;
1539 if (args.length != 1) {
1540 curpanemsgfnosys("donotify who?");
1541 return;
1543 auto unick = getNickFromArg(args[0]);
1544 if (unick.length == 0) { ttyBeep; return; }
1545 auto ii = srv.findNonotifyInfo(unick);
1546 if (ii.nick.length == 0) {
1547 curpanemsgfnosys("%s is not nonotify", unick);
1548 } else {
1549 srv.donotifyUser(unick);
1550 ii = srv.findNonotifyInfo(unick);
1551 if (ii.nick.length == 0) curpanemsgfnosys("%s is donotify now", unick);
1555 void cmdBlink (cstring cmd, cstring[] args) {
1556 auto chan = chatlist.curchan;
1557 if (chan is null) {
1558 curpanemsgf("what chan?");
1559 return;
1561 if (args.length == 1 && args[0] == "?") {
1562 curpanemsgf("blink status: %s", (chan.blink ? "on" : "off"));
1563 return;
1565 if (args.length != 0) {
1566 curpanemsgf("/blink doesn't like args");
1567 return;
1569 chan.server.setChanBlink(chan, true);
1572 void cmdUnblink (cstring cmd, cstring[] args) {
1573 auto chan = chatlist.curchan;
1574 if (chan is null) {
1575 curpanemsgf("what chan?");
1576 return;
1578 if (args.length == 1 && args[0] == "?") {
1579 curpanemsgf("blink status: %s", (chan.blink ? "on" : "off"));
1580 return;
1582 if (args.length != 0) {
1583 curpanemsgf("/unblink doesn't like args");
1584 return;
1586 chan.server.setChanBlink(chan, false);
1589 void cmdPopup (cstring cmd, cstring[] args) {
1590 auto chan = chatlist.curchan;
1591 if (chan is null) {
1592 curpanemsgf("what chan?");
1593 return;
1595 if (args.length == 1 && args[0] == "?") {
1596 curpanemsgf("popup status: %s", (chan.blink ? "on" : "off"));
1597 return;
1599 if (args.length != 0) {
1600 curpanemsgf("/popup doesn't like args");
1601 return;
1603 chan.server.setChanPopup(chan, true);
1606 void cmdUnpopup (cstring cmd, cstring[] args) {
1607 auto chan = chatlist.curchan;
1608 if (chan is null) {
1609 curpanemsgf("what chan?");
1610 return;
1612 if (args.length == 1 && args[0] == "?") {
1613 curpanemsgf("popup status: %s", (chan.blink ? "on" : "off"));
1614 return;
1616 if (args.length != 0) {
1617 curpanemsgf("/unpopup doesn't like args");
1618 return;
1620 chan.server.setChanPopup(chan, false);
1623 void cmdDisconnect (cstring cmd, cstring[] args) {
1624 auto srv = chatlist.cursrv;
1625 if (srv is null) {
1626 curpanemsgf("disconnect from which server?");
1627 return;
1629 srv.disconnect(); // this will stop reconnection attempts
1632 void cmdReconnect (cstring cmd, cstring[] args) {
1633 auto srv = chatlist.cursrv;
1634 if (srv is null) {
1635 curpanemsgf("reconnect to which server?");
1636 return;
1638 srv.ForceReconnect();
1642 void cmdReconnect (cstring cmd, cstring[] args) {
1643 auto srv = chatlist.cursrv;
1644 if (srv is null) {
1645 curpanemsgf("reconnect to which server?");
1646 return;
1648 if (!srv.alive) srv.connect();
1652 void cmdVersion (cstring cmd, cstring[] args) {
1653 if (args.length == 0) {
1654 if (auto user = chatlist.curpriv) {
1655 user.server.sendf("PRIVMSG %s :\x01VERSION\x01", user.nick);
1656 } else {
1657 curpanemsgf("version of what?");
1659 return;
1661 if (args.length != 1) {
1662 curpanemsgf("version of what?");
1663 return;
1665 auto srv = chatlist.cursrv;
1666 if (srv is null) {
1667 curpanemsgf("on which server?");
1668 return;
1670 auto nk = getNickFromArg(args[0]);
1671 if (nk.length == 0) { ttyBeep; return; }
1672 if (auto user = srv.findUser(nk)) {
1673 srv.sendf("PRIVMSG %s :\x01VERSION\x01", user.nick);
1677 void cmdWhoIs (cstring cmd, cstring[] args) {
1678 if (args.length == 0) {
1679 if (auto user = chatlist.curpriv) {
1680 user.server.sendf("WHOIS %s", user.nick);
1681 } else {
1682 curpanemsgf("whois of what?");
1684 return;
1686 if (args.length != 1) {
1687 curpanemsgf("whois of what?");
1688 return;
1690 auto srv = chatlist.cursrv;
1691 if (srv is null) {
1692 curpanemsgf("on which server?");
1693 return;
1695 auto nk = getNickFromArg(args[0]);
1696 if (nk.length == 0) { ttyBeep; return; }
1697 if (auto user = srv.findUser(nk)) {
1698 srv.sendf("WHOIS %s", user.nick);
1702 // /topic [topic]
1703 void cmdTopic (cstring cmd, cstring[] args, cstring text) {
1704 text = text.xstrip;
1705 auto chan = chatlist.curchan;
1706 if (chan is null) {
1707 curpanemsgf("topic for what?");
1708 return;
1710 chan.server.sendf("TOPIC %s :%s", chan.name, text);
1713 void cmdForceLayout (cstring cmd, cstring[] args) {
1714 if (args.length != 1) {
1715 curpanemsgf("which layout?");
1716 return;
1718 auto srv = chatlist.cursrv;
1719 if (srv is null) {
1720 curpanemsgf("for which server?");
1721 return;
1723 int lay = -1;
1724 try {
1725 import std.conv : to; lay = args[0].to!int;
1726 } catch (Exception e) {
1727 curpanemsgf("invalid layout number");
1728 return;
1730 if (lay < 0) lay = -1;
1731 if (srv.forceXKbLayout != lay) {
1732 srv.forceXKbLayout = lay;
1733 srv.saveConfig();
1738 // ////////////////////////////////////////////////////////////////////////// //
1739 void doCommand (cstring text) {
1740 if (text.ircstrip.length == 0) return;
1741 if (text.ptr[0] == '/') {
1742 // command
1743 auto origtext = text;
1744 while (origtext.length && origtext.ptr[0] > ' ') origtext = origtext[1..$];
1745 while (origtext.length && (origtext.ptr[0] == ' ' || origtext.ptr[0] == '\t')) origtext = origtext[1..$];
1746 // parse
1747 cstring[] args;
1748 text = text[1..$];
1749 while (text.length > 0) {
1750 if (text.ptr[0] <= ' ') { text = text[1..$]; continue; }
1751 typeof(text.length) pos = 0;
1752 while (pos < text.length && text.ptr[pos] > ' ') ++pos;
1753 args ~= text[0..pos];
1754 text = text[pos..$];
1756 if (args.length == 0) {
1757 ttyBeep();
1758 sysmsgf("%s", "empty command");
1759 return;
1761 foreach (string mname; __traits(allMembers, mixin(__MODULE__))) {
1762 static if (mname.length > 3 && mname[0..3] == "cmd") {
1763 static if (is(typeof(__traits(getMember, mixin(__MODULE__), mname)) == function)) {
1764 static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$], origtext);}))) {
1765 if (mname.length == args[0].length+3 && mname[3..$].ircStrEquCI(args[0])) {
1766 __traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$], origtext);
1767 return;
1769 } else static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$]);}))) {
1770 if (mname.length == args[0].length+3 && mname[3..$].ircStrEquCI(args[0])) {
1771 __traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$]);
1772 return;
1778 // try server alias
1779 if (args.length == 1) {
1780 try {
1781 auto srv = new IRCServer(args[0]);
1782 // i found her!
1783 srv = chatlist.findAndActivateServerByAlias(srv);
1784 if (srv !is null) srv.connect();
1785 return;
1786 } catch (Exception e) {}
1788 // just send it verbatim
1789 import std.array : join;
1790 string cmd;
1791 foreach (char ch; args[0]) cmd ~= irc2upper(ch);
1792 if (auto user = chatlist.curpriv) {
1793 if (args.length > 1) {
1794 user.server.sendf("%s %s %s", cmd, user.nick, args[1..$].join(" "));
1795 } else {
1796 user.server.sendf("%s %s", cmd, user.nick);
1798 } else if (auto chan = chatlist.curchan) {
1799 if (args.length > 1) {
1800 chan.server.sendf("%s %s %s", cmd, chan.name, args[1..$].join(" "));
1801 //chan.server.sendf("%s %s", cmd, args[1..$].join(" "));
1802 } else {
1803 chan.server.sendf("%s %s", cmd, chan.name);
1804 //chan.server.sendf("%s", cmd);
1806 } else if (auto srv = chatlist.cursrv) {
1807 if (args.length > 1) {
1808 srv.sendf("%s %s", cmd, args[1..$].join(" "));
1809 } else {
1810 srv.sendf("%s", cmd);
1812 } else {
1813 sysmsgf("unknown command: '%s'", args[0]);
1815 return;
1817 doSendText(text);
1821 // ////////////////////////////////////////////////////////////////////////// //
1822 void sizecb () {
1823 TextPaneWidth = ttyw-ChanTabWidth-1-NickTabWidth-1;
1824 if (TextPaneWidth < 30) TextPaneWidth = 30;
1828 // ////////////////////////////////////////////////////////////////////////// //
1829 int userPaneX0 () { return ChanTabWidth+TextPaneWidth; }
1830 int userPaneY0 () {
1831 int y = ttyh-userPaneHeight;
1832 if (y < 1) y = 1;
1833 return y;
1836 int userPaneWidth () {
1837 int w = ttyw-ChanTabWidth+TextPaneWidth;
1838 if (w < 17) w = 17;
1839 return w;
1842 int userPaneHeight () {
1843 int h = ttyh-15;
1844 if (h < 4) h = 4;
1845 return h;
1849 void fixWidgetSizes () {
1850 int tphgt = ttyh-1-(inputModeSingle ? inputOneLine.height : inputMultiLine.height);
1851 string mynick;
1853 foreach (ref it; chatlist.items) {
1854 if (auto tp = it.textpane) {
1855 tp.x0 = ChanTabWidth;
1856 tp.y0 = 1;
1857 tp.height = tphgt;
1858 tp.width = TextPaneWidth;
1859 if (!tp.active) {
1860 if (!tp.wasLogMove) tp.doCenterMark(true);
1865 if (auto srv = chatlist.cursrv) mynick = srv.self.nick;
1867 inputOneLine.x0 = ChanTabWidth+(mynick.length ? cast(int)mynick.length+1 : 0);
1868 inputOneLine.y0 = ttyh-1;
1869 inputOneLine.width = TextPaneWidth-(mynick.length ? cast(int)mynick.length+1 : 0);
1871 inputMultiLine.x0 = ChanTabWidth+(mynick.length ? cast(int)mynick.length+1 : 0);
1872 inputMultiLine.y0 = ttyh-inputMultiLine.height;
1873 inputMultiLine.width = TextPaneWidth-(mynick.length ? cast(int)mynick.length+1 : 0);
1875 inputUserFilter.x0 = userPaneX0+1;
1876 inputUserFilter.y0 = userPaneY0-1;
1877 inputUserFilter.width = userPaneWidth-1;
1881 char[] getEditorText (EditorEngine ed) {
1882 if (ed is null) return null;
1883 char[] text;
1884 text.reserve(ed.textsize);
1885 foreach (char ch; ed[]) text ~= ch;
1886 return text;
1890 void switchInputMode (bool multi) {
1891 if (inputModeSingle == !multi) return;
1892 if (multi) {
1893 // switch to multiline
1894 auto text = getEditorText(inputOneLine);
1895 inputOneLine.clear();
1896 inputMultiLine.clear();
1897 inputMultiLine.insertText!"end"(0, text);
1898 inputMultiLine.clearUndo();
1899 inputModeSingle = false;
1900 } else {
1901 // switch to single line
1902 auto text = getEditorText(inputMultiLine);
1903 while (text.length > 0 && text[$-1] <= ' ') text = text[0..$-1];
1904 if (text.indexOf('\n') >= 0) return; // can't
1905 inputMultiLine.clear();
1906 inputOneLine.clear();
1907 inputOneLine.insertText!"end"(0, text);
1908 inputOneLine.clearUndo();
1909 inputModeSingle = true;
1914 bool keycb (TtyEvent key) {
1915 //if (key == "^C") return -1; // abort
1916 fixWidgetSizes();
1918 if (key == "FocusOut") { (new EventFocusOut()).post; return true; }
1919 if (key == "FocusIn") { (new EventFocusIn()).post; return true; }
1921 if (key == "^L") { xtFullRefresh(); return true; }
1923 if (key == "M-L") { chatlist.textpane.doCenterMark(true); return true; }
1925 if (key == "^PageUp") { chatlist.goUp(); return true; }
1926 if (key == "^PageDown") { chatlist.goDown(); return true; }
1928 if (key.mouse) {
1929 if (key.key == TtyEvent.Key.MLeftUp) {
1930 if (auto tp = chatlist.textpane) tp.clicked(key.x, key.y);
1932 return true;
1935 if (inputActive) {
1936 if (inputModeSingle) {
1937 if (key == "PageUp") { if (auto tp = chatlist.textpane) tp.doPageUp(); return true; }
1938 if (key == "PageDown") { if (auto tp = chatlist.textpane) tp.doPageDown(); return true; }
1939 if (key == "S-Up") { if (auto tp = chatlist.textpane) tp.doLineUp(); return true; }
1940 if (key == "S-Down") { if (auto tp = chatlist.textpane) tp.doLineDown(); return true; }
1942 if (key == "M-PageUp") { if (auto tp = chatlist.textpane) tp.doPageUp(); return true; }
1943 if (key == "M-PageDown") { if (auto tp = chatlist.textpane) tp.doPageDown(); return true; }
1944 if (key == "M-Up") { if (auto tp = chatlist.textpane) tp.doLineUp(); return true; }
1945 if (key == "M-Down") { if (auto tp = chatlist.textpane) tp.doLineDown(); return true; }
1948 // go to input line
1949 if (!inputActive && key == "F8") {
1950 inputActive = true;
1951 userFilterFocused = false;
1952 return true;
1955 // go to userlist filter
1956 if (!userFilterFocused && key == "S-F8") {
1957 inputActive = false;
1958 userFilterFocused = true;
1959 return true;
1962 if (key == "M-M") { switchInputMode(inputModeSingle); return true; }
1963 if (inputActive && key == "^Up") { switchInputMode(true); return true; }
1964 if (inputActive && key == "^Down") { switchInputMode(false); return true; }
1966 if (inputActive) {
1967 TtyEditor cured = (inputModeSingle ? inputOneLine : inputMultiLine);
1968 if (cured is null) return false; // just in case
1969 // send?
1970 if ((inputModeSingle && key == "Enter") || (!inputModeSingle && key == "M-Enter")) {
1971 import core.time;
1972 static lastSendTime = MonoTime.zero;
1973 auto curtime = MonoTime.currTime;
1974 if ((curtime-lastSendTime).total!"msecs" < 400) {
1975 ttyBeep();
1976 } else {
1977 auto text = getEditorText(cured);
1978 cured.clear();
1979 switchInputMode(false);
1980 doCommand(text);
1982 lastSendTime = curtime;
1983 return true;
1985 // autocompletion
1986 if (key == "Tab") {
1987 cured.autocomplete(chatlist.curchan);
1988 return true;
1990 // normal input
1991 auto tlen = cured.textsize;
1992 if (cured.processKey(key)) {
1993 // switch to eng if we're tying a command
1994 if (tlen == 0 && cured.textsize == 1 && cured[0] == '/') {
1995 (new EventXKbSetLayout(0)).post;
2000 if (userFilterFocused) {
2001 return inputUserFilter.processKey(key);
2004 return false;
2008 // ////////////////////////////////////////////////////////////////////////// //
2009 void drawScreen () {
2010 void drawUsers () {
2011 import miri.textpane : NickBG;
2013 // clear whole pane and draw vline
2014 auto win = XtWindow(userPaneX0, 0, ttyw, ttyh);
2015 win.fg = 15;
2016 win.bg = NickBG;
2017 win.fill(0, 0, win.width, win.height, ' ');
2018 win.vline(0, 0, win.height);
2020 inputUserFilter.fullDirty();
2021 inputUserFilter.drawPage();
2023 auto chan = chatlist.curchan;
2024 if (chan is null || chan.users.length == 0) return;
2026 RegExp userFilterRE;
2028 if (inputUserFilter.textsize > 0) {
2029 import iv.strex : xstrip;
2030 auto text = inputUserFilter.getEditorText().xstrip;
2031 if (text.length) {
2032 import std.utf : byChar;
2033 userFilterRE = RegExp.create(text.byChar, SRFlags.CaseInsensitive);
2037 win = XtWindow(userPaneX0+1, userPaneY0, userPaneWidth-1, userPaneHeight);
2038 win.fg = 15;
2039 win.bg = NickBG;
2041 int y = 0;
2042 int it = 0;
2043 while (y < win.height && it < chan.users.length) {
2044 auto user = chan.users[it];
2045 // do filtering
2046 if (userFilterRE.valid) {
2047 auto rectx = Thompson.create(userFilterRE);
2048 if (rectx.exec(user.nick) != SRes.Ok) { ++it; continue; } // not matched
2050 win.fg = 7;
2051 win.bg = NickBG;
2052 char mode = ' ';
2053 if (chan.isOp(user)) { mode = '@'; win.fg = 15; }
2054 if (chan.isVoiced(user)) { mode = '+'; win.fg = 3+8; }
2055 if (user.ignored) {
2056 win.fg = 238;
2057 } else {
2058 final switch (user.status) {
2059 case IRCUser.Status.Offline: win.fg = 239; break;
2060 case IRCUser.Status.Online: break;
2061 case IRCUser.Status.Away: win.fg = 6; break;
2064 win.writeCharsAt(0, y, 1, mode);
2065 win.writeStrAt(1, y, user.visNick);
2066 ++it;
2067 ++y;
2071 scope(exit) xtFlush();
2073 fixWidgetSizes();
2075 chatlist.draw();
2076 chatlist.textpane.draw();
2078 // draw channel topic
2080 import miri.textpane : TextBG;
2081 auto win = XtWindow(chatlist.textpane.x0, 0, chatlist.textpane.width, 1);
2082 win.fg = 7;
2083 win.bg = TtyRgb2Color!(0x20, 0x20, 0x20);
2084 win.writeCharsAt(0, 0, win.width, ' ');
2085 if (auto cc = chatlist.curchan) win.writeStrAt(0, 0, cc.topic);
2088 drawUsers();
2090 string mynick;
2091 if (auto srv = chatlist.cursrv) mynick = srv.self.nick;
2092 TtyEditor ied = (inputModeSingle ? inputOneLine : inputMultiLine);
2094 if (mynick.length) {
2095 import miri.textpane : TextBG;
2096 auto win = XtWindow(ied.x0-cast(int)mynick.length-1, ied.y0, cast(int)mynick.length+1, ied.height);
2097 win.fg = 4+8;
2098 win.bg = TextBG;
2099 win.fill(0, 0, win.width, win.height, ' ');
2100 win.writeCharsAt(win.width-1, 0, 1, ':');
2101 win.writeStrAt(0, 0, mynick);
2103 ied.fullDirty();
2105 // uncomment the following to disable scrollbar
2106 //auto scs = ttyScissor;
2107 //scope(exit) ttyScissor = scs;
2108 //ttyScissor = scs.crop(ied.x0, ied.y0, ied.width, ied.height);
2109 ied.drawPage();
2112 if (userFilterFocused) {
2113 inputUserFilter.drawCursor();
2118 // ////////////////////////////////////////////////////////////////////////// //
2119 void main (string[] args) {
2120 version(none) {
2121 void test () {
2122 cstring[] args;
2123 cstring origtext;
2124 foreach (string mname; __traits(allMembers, mixin(__MODULE__))) {
2125 static if (mname.length > 3 && mname[0..3] == "cmd") {
2126 //pragma(msg, is(typeof(__traits(getMember, mixin(__MODULE__), mname)) == function));
2127 static if (is(typeof(__traits(getMember, mixin(__MODULE__), mname)) == function)) {
2128 static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$], origtext);}))) {
2129 writeln("0: ", mname);
2130 } else static if (is(typeof({__traits(getMember, mixin(__MODULE__), mname)(args[0], args[1..$]);}))) {
2131 writeln("1: ", mname);
2137 test();
2138 assert(0);
2141 if (ttyIsRedirected) assert(0, "no redirections, please");
2142 xtInit();
2143 ttyEnableMouseReports();
2144 if (ttyw < ChanTabWidth+NickTabWidth+30 || ttyh < 20) assert(0, "tty is too small");
2145 TextPaneWidth = ttyw-ChanTabWidth-1-NickTabWidth-1;
2147 foreach (string s; args[1..$]) {
2148 import iv.strex : startsWith;
2149 if (s == "--log") {
2150 logOpenFile("zlog.log");
2151 } else if (s.startsWith("--log=")) {
2152 logOpenFile(s[6..$]);
2156 initConfig();
2158 setupChatLogger();
2160 inputOneLine = new TtyEditor(ChanTabWidth, ttyh-1, TextPaneWidth, 1, true);
2161 inputUserFilter = new TtyEditor(ChanTabWidth, ttyh-1, TextPaneWidth, 1, true);
2163 inputMultiLine = new TtyEditor(ChanTabWidth, ttyh-8, TextPaneWidth, 8, false);
2164 inputMultiLine.hideStatus = true;
2166 fuckStdErr(); // for libnotify
2167 notify_init("Miri");
2168 scope(exit) notify_uninit();
2170 auto ttymode = ttyGetMode();
2171 scope(exit) {
2172 normalScreen();
2173 ttyDisableFocusReports();
2174 ttySetMode(ttymode);
2176 ttySetRaw();
2177 altScreen();
2178 ttyEnableFocusReports();
2180 syspane = new TextPane();
2181 syspane.active = true;
2184 syspane.addLine("ketmar", "** hello", TextLine.Hi.Action);
2185 syspane.addLine("ketmar", "hello", TextLine.Hi.Mine);
2189 syspane.addLine("ketmar", "hello", TextLine.Hi.Mine);
2190 syspane.addMark();
2191 syspane.addLine("ketmar1", "fuck you");
2192 syspane.addLine("ketmar2", " and fuck you too!", TextLine.Hi.ToMe);
2193 syspane.addLine("asshole[idiot]", "and you all!", TextLine.Hi.Ignored);
2194 foreach (int n; 0..128) {
2195 import std.format : format;
2196 syspane.addLine("line#%s".format(n), "item #%s".format(n));
2198 //syspane.doCenterMark();
2202 addEventListener((EventSysMsg evt) {
2203 syspane.addLine("<system>", evt.msg);
2206 sysmsgf("%s", "welcome...");
2208 //(new EventSysMsg("postponed event...")).later(5000);
2210 chatlist = new ChatList();
2212 addEventListener((EventTtyResized evt) { sizecb(); });
2213 addEventListener((EventTtyKey evt) { if (keycb(evt.key)) evt.eat(); });
2215 { import etc.linux.memoryerror; registerMemoryErrorHandler(); }
2218 import core.stdc.stdio;
2219 FILE* errfl = fopen("/home/ketmar/back/D/prj/miri/zerr.err", "a");
2220 if (errfl is null) assert(0);
2221 errfl.fprintf("**********************************\n");
2222 errfl.fflush();
2223 try {
2224 netioRun(
2225 () => drawScreen(),
2227 errfl.fclose();
2228 } catch (Throwable e) {
2229 import core.stdc.stdlib : abort;
2230 import core.memory : GC;
2231 import core.thread;
2232 GC.disable();
2233 ttyBeep();
2234 thread_suspendAll(); // stop right here, you criminal scum!
2235 auto enm = typeid(e).name;
2236 errfl.fprintf("\n******** FATAL %.*s: %.*s (%.*s:%u)\n", cast(uint)enm.length, enm.ptr, cast(uint)e.msg.length, e.msg.ptr, cast(uint)e.file.length, e.file.ptr, cast(uint)e.line);
2237 errfl.fflush();
2238 auto se = e.toString;
2239 errfl.fprintf("********\n%.*s\n********\n", cast(uint)se.length, se.ptr);
2240 errfl.fflush();
2241 errfl.fclose();
2242 //assert(0);
2243 normalScreen();
2244 ttyDisableFocusReports();
2245 ttySetMode(ttymode);
2246 abort();