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/>.
22 import iv
.strex
: indexOf
, startsWith
, xstrip
;
31 import miri
.other
.tox
;
37 // ////////////////////////////////////////////////////////////////////////// //
38 bool checkMatchFile (const(char)[] msg
, string fname
) {
39 if (fname
.length
== 0 || msg
.length
== 0) return false;
42 import std
.file
, std
.path
;
43 auto filterFilename
= buildPath(configDir
, fname
);
44 foreach (auto ln
; VFile(filterFilename
).byLine
) {
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") {
54 try { maxlen
= value
.to
!int; } catch (Exception
) {}
55 if (maxlen
> 0 && msg
.length
> maxlen
) return true;
58 if (name
== "substring") {
59 if (msg
.indexOf(value
) >= 0) return true;
63 import std
.path
: globMatch
;
64 if (globMatch(msg
, value
)) return true;
68 } catch (Exception
) {}
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 // ////////////////////////////////////////////////////////////////////////// //
86 import core
.sys
.posix
.unistd
: dup2
;
87 import core
.sys
.posix
.fcntl
/*: open*/;
88 auto xfd
= open("/dev/null", O_WRONLY
, 0x1b6/*0o600*/);
90 dup2(xfd
, 2); // fuck stderr
94 void showNotify (cstring title
, cstring text
) {
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");
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.
121 * cmd = user-given command prefix
122 * cmdlist = list of all available commands
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
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
;
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
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
) {
158 foreach (immutable idx
; 0..slen
) if (found
[idx
].irc2lower
!= s
[idx
].irc2lower
) { pos
= idx
; break; }
160 // i found her! remember (and fix) prefix
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
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;
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
) {
199 atCommand
= (ch
== '/');
204 auto startpos
= sp
; // we may need it later
207 while (sp
< pos
) wordbuf
[wbpos
++] = ed
[sp
++];
208 cstring word
= wordbuf
[0..wbpos
];
212 if (word
[0] != '/') {
213 if (pos
< ed
.textsize
&& ed
[pos
] <= ' ') {
214 lastText
= (atLineStart ?
":" : ",");
216 lastText
= (atLineStart ?
": " : ", ");
218 if (atCommand
) lastText
= "";
220 aclist
~= chan
.server
.self
.nick
;
221 foreach (IRCUser u
; chan
.users
) {
222 if (!u
.ignored
&& !u
.isme
) aclist
~= u
.nick
;
227 void addCmd (cstring s
) {
228 if (s
.length
== 0) return;
230 foreach (char ch
; s
) xn
~= ch
.irc2lower
;
231 foreach (cstring cmd
; aclist
) if (ircStrEquCI(cmd
, xn
)) return;
233 import std
.algorithm
: sort
;
236 void addCommands () {
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
);}))) {
244 } else static if (is(typeof({__traits(getMember
, mixin(__MODULE__
), mname
)(args
[0], args
[1..$]);}))) {
251 // append server aliases
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");
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
);
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
];
294 foreach (usize n
; 0..pat
.length
) {
295 import iv
.strex
: tolower
;
296 if (s
[n
].tolower
!= pat
[n
].tolower
) {
307 // ////////////////////////////////////////////////////////////////////////// //
310 // only one of this can be set
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
;
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
:
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;
353 // move up to channel/server/system
354 while (newidx
>= 0) {
355 Item prevIt
= items
[newidx
];
356 if (!prevIt
.privchat
) { ++newidx
; break; }
359 } else if (it
.channel
) {
361 // move up to server/system
362 while (newidx
>= 0) {
363 Item prevIt
= items
[newidx
];
364 if (!prevIt
.channel
) { ++newidx
; break; }
367 } else if (it
.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;
378 if (newidx
< 0 || newidx
== idx
) return; // just in case
379 // server move is complex
384 sls
~= items
[sidx
++];
385 while (sidx
< items
.length
) {
386 if (items
[sidx
].server
) break;
387 sls
~= items
[sidx
++];
390 Item
[] newlist
= items
[0..idx
]~items
[sidx
..$];
392 items
= newlist
[0..newidx
]~sls
~newlist
[newidx
..$];
394 if (mCurItem
>= idx
&& mCurItem
< idx
+sls
.length
) {
395 mCurItem
= mCurItem
-idx
+newidx
;
396 if (topitem
> mCurItem
) topitem
= mCurItem
;
399 for (usize n
= items
.length
-2; n
>= newidx
; --n
) items
[n
+1] = items
[n
];
401 if (mCurItem
== idx
) {
403 if (topitem
> mCurItem
) topitem
= mCurItem
;
409 TextPane
paneForSystem () {
410 foreach (ref Item it
; items
) if (it
.system
) return it
.textpane
;
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
;
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
;
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
;
433 void onEvent (EventUserNickChanged evt
) {
434 foreach (ref Item it
; items
) {
435 bool putNotice
= false;
436 if (it
.channel
&& it
.chan
.hasUser(evt
.user
)) {
438 } else if (it
.privchat
&& it
.user
is evt
.user
) {
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
);
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;
474 while (epos
< str.length
) {
476 if (str.ptr
[epos
] == 0x10) {
477 if (str.length
-epos
< 2) return null;
479 } else if (str.ptr
[epos
] == 0x01) {
485 if (epos
>= str.length
&& str.ptr
[epos
] != 0x01) return null;
486 auto res
= str[pos
+1..epos
];
489 str = str[epos
+1..$];
491 str = str[0..pos
]~str[epos
+1..$];
496 private T
extractCTCPKeyword(T
:cstring
) (ref T
str) {
499 while (pos
< str.length
&& str[pos
] > ' ') ++pos
;
500 auto res
= str[0..pos
];
501 str = str[pos
..$].ircstrip
;
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;
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
);
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")) {
549 openChat(!evt
.hisname
);
551 upane
.addLine(evt
.time
, evt
.user
.visNick
, ctcp
, TextLine
.Hi
.Action
);
553 upane
.addLine(evt
.time
, evt
.user
.server
.self
.visNick
, ctcp
, TextLine
.Hi
.Action
);
556 } else if (kw
.ircStrCmpCI("VERSION")) {
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
);
561 logwritefln("*** version query to %s -- echo", evt
.user
.nick
);
563 } else if (kw
.ircStrCmpCI("CLIENTINFO")) {
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
);
568 logwritefln("*** CLIENTINFO query to %s -- echo", evt
.user
.nick
);
570 } else if (kw
.ircStrCmpCI("PING")) {
572 logwritefln("*** sending PING response to %s", evt
.user
.nick
);
573 evt
.user
.server
.sendf("NOTICE %s :\x01PING %s\x01", evt
.user
.nick
, ctcp
);
575 logwritefln("*** PING query to %s -- echo", evt
.user
.nick
);
577 } else if (kw
.ircStrCmpCI("SOURCE")) {
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");
582 logwritefln("*** SOURCE query to %s -- echo", evt
.user
.nick
);
587 if (msg
.ircstrip
.length
) {
590 // blink only for interesting messages
591 if (evt
.hisname
&& !evt
.user
.ignored
) {
592 showNotify(evt
.user
.visNick
, msg
);
593 (new EventTrayBlink()).post
;
598 if (upane
!is null) upane
.putMessage(evt
.time
, evt
.user
.server
.self
.visNick
, msg
, TextLine
.Hi
.Mine
);
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
);
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:");
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('>');
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) {
643 foreach (immutable idx
, char ch
; nn
) {
644 if (!ircisalnum(ch
)) {
645 if (visNick
.length
== 1) continue;
646 if (visNick
[$-1] == '_') continue;
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;
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
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
676 case TextLine
.Hi
.Normal
:
677 case TextLine
.Hi
.ToMe
:
678 (new EventTrayBlink()).post
;
683 if ((evt
.chan
.popup
&& !evt
.user
.nonotify
) || hi
== TextLine
.Hi
.ToMe
) {
684 // blink only for interesting messages
686 case TextLine
.Hi
.Normal
:
687 case TextLine
.Hi
.ToMe
:
688 if (msg
.ircstrip
.length
) showNotify(evt
.chan
.name
~" -- "~visNick
, msg
);
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
) {
732 textpane
.active
= false;
733 //textpane.addLine(null, "focusout", TextLine.Hi.Service);
736 void onEvent (EventFocusIn evt
) {
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
;
763 @property IRCChannel
curchan () {
765 if (mCurItem
< 0 || mCurItem
>= items
.length
) return null;
766 if (items
[mCurItem
].channel
) return items
[mCurItem
].chan
;
770 @property IRCUser
curpriv () {
772 if (mCurItem
< 0 || mCurItem
>= items
.length
) return null;
773 if (items
[mCurItem
].user
) return items
[mCurItem
].user
;
777 bool sendMsg (cstring msg
) {
779 if (mCurItem
< 0 || mCurItem
>= items
.length
) return false;
785 if (items
[mCurItem
].channel
) {
786 dchan
= items
[mCurItem
].chan
;
787 if (!dchan
.joined
) return false;
790 } else if (items
[mCurItem
].privchat
) {
791 duser
= items
[mCurItem
].user
;
793 sysmsgf("can't send message (user '%s' is dead):\n%s", duser
.nick
, msg
);
800 sysmsgf("can't send message (no server):\n%s", msg
);
804 sysmsgf("can't send message ([%s] is dead):\n%s", srv
.address
, msg
);
809 if (msg
.length
>= 2 && msg
[0..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
);
823 private void insertItemBefore (Item it
, int pos
) {
824 if (pos
< 0) pos
= 0;
825 if (pos
>= items
.length
) {
826 items
.arrayAppend(it
);
828 if (mCurItem
>= pos
) ++mCurItem
;
830 foreach_reverse (immutable c
; pos
..items
.length
-1) items
[c
+1] = items
[c
];
836 IRCServer
findAndActivateServerByAlias (IRCServer xsrv
) {
838 activateServerByAlias
!true(xsrv
, true, &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
) {
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
;
859 insertItemBefore(Item(xsrv
), int.max
);
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;
867 if (sout
!is null) *sout
= xsrv
;
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
879 foreach (immutable idx
, ref it
; items
) {
880 if (it
.channel
&& it
.chan
.server
is srv
) {
881 doCloseTab(cast(int)idx
);
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
) {
901 items
[tabidx
].textpane
.active
= false;
902 foreach (immutable c
; tabidx
+1..items
.length
) items
[c
-1] = items
[c
];
903 items
[$-1] = Item
.init
;
905 items
.assumeSafeAppend
;
906 if (mCurItem
>= tabidx
) {
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);
926 while (srvidx
< items
.length
) {
927 if (items
[srvidx
].server
&& items
[srvidx
].srv
is xchan
.server
) break;
930 if (srvidx
>= items
.length
) assert(0, "wtf?!");
931 // add channel, sorted
933 while (pos
< items
.length
) {
934 if (!items
[pos
].channel
) break;
935 if (items
[pos
].chan
.name
.ircStrCmpCI(xchan
.name
) > 0) break;
938 insertItemBefore(Item(xchan
), 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;
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);
959 while (srvidx
< items
.length
) {
960 if (items
[srvidx
].server
&& items
[srvidx
].srv
is xuser
.server
) break;
963 if (srvidx
>= items
.length
) assert(0, "wtf?!");
966 while (srvidx
< items
.length
&& items
[srvidx
].channel
) ++srvidx
;
967 // add privchat, sorted
969 while (pos
< items
.length
) {
970 if (!items
[pos
].privchat
) break;
971 if (items
[pos
].user
.nick
.ircStrCmpCI(xuser
.nick
) > 0) break;
974 insertItemBefore(Item(xuser
), pos
);
975 if (activate
&& mCurItem
!= pos
) {
976 if (mCurItem
>= 0 && mCurItem
< items
.length
) items
[mCurItem
].textpane
.active
= false;
978 items
[mCurItem
].textpane
.active
= true;
985 private IRCServer lastActiveServer
= null;
986 private Object lastActiveChanUser
= null;
989 if (mCurItem
< 0 || mCurItem
>= items
.length
) {
990 lastActiveServer
= null;
991 lastActiveChanUser
= null;
994 auto it
= &items
[mCurItem
];
997 if (lastActiveServer
!is null || lastActiveChanUser
!is null) {
998 lastActiveServer
= null;
999 lastActiveChanUser
= null;
1000 (new EventXKbSetLayout(0)).post
;
1006 if (lastActiveServer
!is it
.srv || lastActiveChanUser
!is null) {
1007 lastActiveServer
= it
.srv
;
1008 lastActiveChanUser
= null;
1009 (new EventXKbSetLayout(0)).post
;
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
;
1023 // private chat pane?
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
;
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();
1052 textpane
.active
= false;
1055 textpane
.active
= true;
1060 if (mCurItem
+1 < items
.length
) {
1061 textpane
.active
= false;
1064 textpane
.active
= true;
1069 import miri
.textpane
: NickBG
;
1070 auto win
= XtWindow(0, 0, ChanTabWidth
, ttyh
);
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
1079 while (y
< win
.height
&& it
< items
.length
) {
1082 auto item
= &items
[it
];
1083 if (it
== mCurItem
) {
1085 win
.bg
= TtyRgb2Color
!(0x00, 0x5f, 0xff);
1086 win
.writeCharsAt(0, y
, win
.width
, ' ');
1088 win
.fg
= (item
.server ?
15 : 7);
1090 if (!item
.textpane
.active
&& item
.textpane
.hasmark
) {
1091 win
.fg
= TtyRgb2Color
!(0x00, 0xff, 0x00);
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
);
1105 __gshared ChatList chatlist
;
1108 // ////////////////////////////////////////////////////////////////////////// //
1109 void curpanemsgf(A
...) (cstring fmt
, A args
) {
1110 import std
.format
: formattedWrite
;
1112 struct Writer
{ void put (cstring s
...) { st
~= s
[]; } }
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
;
1123 struct Writer
{ void put (cstring s
...) { st
~= s
[]; } }
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
;
1144 char[] argsJoin (cstring
[] args
) {
1146 foreach (cstring s
; args
[]) {
1148 if (s
.length
== 0) continue;
1149 if (tlong
.length
) 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]
1168 // koi8/cp1251/cp866
1170 void cmdSAlias (cstring cmd
, cstring
[] args
/*, cstring text*/) {
1171 if (args
.length
< 3) {
1173 "salias alias proto:server[:port] nick[:password] [options]\n"~
1175 " koi8/cp1251/cp866\n"~
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
);
1191 srv
= new IRCServer(args
[1], 6667);
1193 } catch (Exception e
) {
1194 curpanemsgf("error parsing server address: %s", e
.msg
);
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
;
1203 srv
.self
.nick
= args
[2].idup
;
1205 if (srv
.self
.nick
.length
== 0) {
1206 curpanemsgf("empty nick!");
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;
1217 curpanemsgf("invalid option: %s", opt
);
1221 if (srv
.saveConfig()) {
1222 curpanemsgf("alias '%s' added", srv
.srvalias
);
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?");
1233 auto srv
= chatlist
.cursrv
;
1235 curpanemsgf("command to which server?");
1239 curpanemsgf("can't command dead server");
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?");
1250 auto srv
= chatlist
.cursrv
;
1252 curpanemsgf("command to which server?");
1256 curpanemsgf("can't command dead server");
1260 text
= text
.xstrip();
1262 while (sppos
< text
.length
&& text
[sppos
] > ' ') ++sppos
;
1263 if (sppos
>= text
.length
) {
1264 curpanemsgf("msg what?");
1267 cstring who
= text
[0..sppos
];
1268 text
= text
[sppos
..$].xstrip();
1269 if (text
.length
== 0) {
1270 curpanemsgf("msg what?");
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?");
1290 auto srv
= chatlist
.cursrv
;
1292 curpanemsgf("join on which server?");
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
);
1300 srv
.sendf("JOIN %s", cname
);
1305 void cmdPart (cstring cmd
, cstring
[] args
) {
1306 if (args
.length
!= 0) {
1307 curpanemsgf("/part cannot accept args");
1310 if (auto chan
= chatlist
.curchan
) {
1312 chan
.server
.sendf("PART %s", chan
.name
);
1314 curpanemsgf("you aren't joined %s", chan
.name
);
1317 curpanemsgf("part from?");
1321 void cmdNick (cstring cmd
, cstring
[] args
) {
1322 if (args
.length
!= 1) {
1323 curpanemsgf("change nick to?");
1326 auto srv
= chatlist
.cursrv
;
1328 curpanemsgf("change nick on which server?");
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
;
1339 curpanemsgf("authorize on which server?");
1345 // start private chat
1346 void cmdPrivate (cstring cmd
, cstring
[] args
) {
1347 if (args
.length
!= 1) {
1348 curpanemsgf("private chat with?");
1351 auto srv
= chatlist
.cursrv
;
1353 curpanemsgf("change nick on which server?");
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
);
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?");
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
;
1379 curpanemsgf("VERSION on which server?");
1382 if (args
.length
== 0 || text
.length
== 0) {
1383 curpanemsgf("version who?");
1386 if (args
.length
!= 1) {
1387 curpanemsgf("which nick?");
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?");
1396 srv
.sendf("PRIVMSG %s :\x01VERSION\x01", nick
);
1400 void cmdAllowChat (cstring cmd
, cstring
[] args
) {
1401 auto srv
= chatlist
.cursrv
;
1403 curpanemsgf("ignore on which server?");
1406 if (args
.length
== 0) return;
1407 auto unick
= getNickFromArg(args
[0]);
1408 if (unick
.length
== 0) { ttyBeep
; return; }
1409 srv
.allowIngoredPrivChat(unick
, true);
1413 void cmdNoChat (cstring cmd
, cstring
[] args
) {
1414 auto srv
= chatlist
.cursrv
;
1416 curpanemsgf("ignore on which server?");
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
;
1429 curpanemsgf("ignore on which server?");
1433 if (args
.length
== 0) {
1434 auto list
= srv
.allIgnores();
1435 if (list
.length
== 0) {
1436 curpanemsgfnosys("you are ignoring noone");
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
) {
1444 if (ii
.allowprivchat
) rep ~= "*"; else rep ~= " ";
1446 if (ii
.ignoreShort
.length
> 0) rep ~= " <"~ii
.ignoreShort
~">";
1447 if (ii
.ignoreLong
.length
> 0) rep ~= " : "~ii
.ignoreLong
;
1449 curpanemsgfnosys("%s", rep);
1454 auto unick
= getNickFromArg(args
[0]);
1455 if (unick
.length
== 0) { ttyBeep
; return; }
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
);
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
);
1471 void cmdUnignore (cstring cmd
, cstring
[] args
) {
1472 auto srv
= chatlist
.cursrv
;
1474 curpanemsgf("unignore on which server?");
1477 if (args
.length
!= 1) {
1478 curpanemsgfnosys("unignore who?");
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
);
1487 srv
.unignoreUser(unick
);
1488 ii
= srv
.findIgnoreInfo(unick
);
1489 if (ii
.nick
.length
== 0) curpanemsgfnosys("you are no longer ignoring %s", unick
);
1494 void cmdNonotify (cstring cmd
, cstring
[] args
) {
1495 auto srv
= chatlist
.cursrv
;
1497 curpanemsgf("nonotify on which server?");
1501 if (args
.length
== 0) {
1502 auto list
= srv
.allNonitify();
1503 if (list
.length
== 0) {
1504 curpanemsgfnosys("nonotify list is empty");
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
) {
1512 if (ii
.ignore
) rep ~= "*"; else rep ~= " ";
1515 curpanemsgfnosys("%s", rep);
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
);
1526 srv
.nonotifyUser(unick
);
1527 ii
= srv
.findNonotifyInfo(unick
);
1528 if (ii
.nick
.length
> 0) curpanemsgfnosys("%s is nonotify now", ii
.nick
);
1533 void cmdDonotify (cstring cmd
, cstring
[] args
) {
1534 auto srv
= chatlist
.cursrv
;
1536 curpanemsgf("donotify on which server?");
1539 if (args
.length
!= 1) {
1540 curpanemsgfnosys("donotify who?");
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
);
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
;
1558 curpanemsgf("what chan?");
1561 if (args
.length
== 1 && args
[0] == "?") {
1562 curpanemsgf("blink status: %s", (chan
.blink ?
"on" : "off"));
1565 if (args
.length
!= 0) {
1566 curpanemsgf("/blink doesn't like args");
1569 chan
.server
.setChanBlink(chan
, true);
1572 void cmdUnblink (cstring cmd
, cstring
[] args
) {
1573 auto chan
= chatlist
.curchan
;
1575 curpanemsgf("what chan?");
1578 if (args
.length
== 1 && args
[0] == "?") {
1579 curpanemsgf("blink status: %s", (chan
.blink ?
"on" : "off"));
1582 if (args
.length
!= 0) {
1583 curpanemsgf("/unblink doesn't like args");
1586 chan
.server
.setChanBlink(chan
, false);
1589 void cmdPopup (cstring cmd
, cstring
[] args
) {
1590 auto chan
= chatlist
.curchan
;
1592 curpanemsgf("what chan?");
1595 if (args
.length
== 1 && args
[0] == "?") {
1596 curpanemsgf("popup status: %s", (chan
.blink ?
"on" : "off"));
1599 if (args
.length
!= 0) {
1600 curpanemsgf("/popup doesn't like args");
1603 chan
.server
.setChanPopup(chan
, true);
1606 void cmdUnpopup (cstring cmd
, cstring
[] args
) {
1607 auto chan
= chatlist
.curchan
;
1609 curpanemsgf("what chan?");
1612 if (args
.length
== 1 && args
[0] == "?") {
1613 curpanemsgf("popup status: %s", (chan
.blink ?
"on" : "off"));
1616 if (args
.length
!= 0) {
1617 curpanemsgf("/unpopup doesn't like args");
1620 chan
.server
.setChanPopup(chan
, false);
1623 void cmdDisconnect (cstring cmd
, cstring
[] args
) {
1624 auto srv
= chatlist
.cursrv
;
1626 curpanemsgf("disconnect from which server?");
1629 srv
.disconnect(); // this will stop reconnection attempts
1632 void cmdReconnect (cstring cmd
, cstring
[] args
) {
1633 auto srv
= chatlist
.cursrv
;
1635 curpanemsgf("reconnect to which server?");
1638 srv
.ForceReconnect();
1642 void cmdReconnect (cstring cmd, cstring[] args) {
1643 auto srv = chatlist.cursrv;
1645 curpanemsgf("reconnect to which server?");
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
);
1657 curpanemsgf("version of what?");
1661 if (args
.length
!= 1) {
1662 curpanemsgf("version of what?");
1665 auto srv
= chatlist
.cursrv
;
1667 curpanemsgf("on which server?");
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
);
1682 curpanemsgf("whois of what?");
1686 if (args
.length
!= 1) {
1687 curpanemsgf("whois of what?");
1690 auto srv
= chatlist
.cursrv
;
1692 curpanemsgf("on which server?");
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
);
1703 void cmdTopic (cstring cmd
, cstring
[] args
, cstring text
) {
1705 auto chan
= chatlist
.curchan
;
1707 curpanemsgf("topic for what?");
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?");
1718 auto srv
= chatlist
.cursrv
;
1720 curpanemsgf("for which server?");
1725 import std
.conv
: to
; lay
= args
[0].to
!int;
1726 } catch (Exception e
) {
1727 curpanemsgf("invalid layout number");
1730 if (lay
< 0) lay
= -1;
1731 if (srv
.forceXKbLayout
!= lay
) {
1732 srv
.forceXKbLayout
= lay
;
1738 // ////////////////////////////////////////////////////////////////////////// //
1739 void doCommand (cstring text
) {
1740 if (text
.ircstrip
.length
== 0) return;
1741 if (text
.ptr
[0] == '/') {
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..$];
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) {
1758 sysmsgf("%s", "empty command");
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
);
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..$]);
1779 if (args
.length
== 1) {
1781 auto srv
= new IRCServer(args
[0]);
1783 srv
= chatlist
.findAndActivateServerByAlias(srv
);
1784 if (srv
!is null) srv
.connect();
1786 } catch (Exception e
) {}
1788 // just send it verbatim
1789 import std
.array
: join
;
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(" "));
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(" "));
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(" "));
1810 srv
.sendf("%s", cmd
);
1813 sysmsgf("unknown command: '%s'", args
[0]);
1821 // ////////////////////////////////////////////////////////////////////////// //
1823 TextPaneWidth
= ttyw
-ChanTabWidth
-1-NickTabWidth
-1;
1824 if (TextPaneWidth
< 30) TextPaneWidth
= 30;
1828 // ////////////////////////////////////////////////////////////////////////// //
1829 int userPaneX0 () { return ChanTabWidth
+TextPaneWidth
; }
1831 int y
= ttyh
-userPaneHeight
;
1836 int userPaneWidth () {
1837 int w
= ttyw
-ChanTabWidth
+TextPaneWidth
;
1842 int userPaneHeight () {
1849 void fixWidgetSizes () {
1850 int tphgt
= ttyh
-1-(inputModeSingle ? inputOneLine
.height
: inputMultiLine
.height
);
1853 foreach (ref it
; chatlist
.items
) {
1854 if (auto tp
= it
.textpane
) {
1855 tp
.x0
= ChanTabWidth
;
1858 tp
.width
= TextPaneWidth
;
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;
1884 text
.reserve(ed
.textsize
);
1885 foreach (char ch
; ed
[]) text
~= ch
;
1890 void switchInputMode (bool multi
) {
1891 if (inputModeSingle
== !multi
) return;
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;
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
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; }
1929 if (key
.key
== TtyEvent
.Key
.MLeftUp
) {
1930 if (auto tp
= chatlist
.textpane
) tp
.clicked(key
.x
, key
.y
);
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; }
1949 if (!inputActive
&& key
== "F8") {
1951 userFilterFocused
= false;
1955 // go to userlist filter
1956 if (!userFilterFocused
&& key
== "S-F8") {
1957 inputActive
= false;
1958 userFilterFocused
= 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; }
1967 TtyEditor cured
= (inputModeSingle ? inputOneLine
: inputMultiLine
);
1968 if (cured
is null) return false; // just in case
1970 if ((inputModeSingle
&& key
== "Enter") ||
(!inputModeSingle
&& key
== "M-Enter")) {
1972 static lastSendTime
= MonoTime
.zero
;
1973 auto curtime
= MonoTime
.currTime
;
1974 if ((curtime
-lastSendTime
).total
!"msecs" < 400) {
1977 auto text
= getEditorText(cured
);
1979 switchInputMode(false);
1982 lastSendTime
= curtime
;
1987 cured
.autocomplete(chatlist
.curchan
);
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
);
2008 // ////////////////////////////////////////////////////////////////////////// //
2009 void drawScreen () {
2011 import miri
.textpane
: NickBG
;
2013 // clear whole pane and draw vline
2014 auto win
= XtWindow(userPaneX0
, 0, ttyw
, ttyh
);
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
;
2032 import std
.utf
: byChar
;
2033 userFilterRE
= RegExp
.create(text
.byChar
, SRFlags
.CaseInsensitive
);
2037 win
= XtWindow(userPaneX0
+1, userPaneY0
, userPaneWidth
-1, userPaneHeight
);
2043 while (y
< win
.height
&& it
< chan
.users
.length
) {
2044 auto user
= chan
.users
[it
];
2046 if (userFilterRE
.valid
) {
2047 auto rectx
= Thompson
.create(userFilterRE
);
2048 if (rectx
.exec(user
.nick
) != SRes
.Ok
) { ++it
; continue; } // not matched
2053 if (chan
.isOp(user
)) { mode
= '@'; win
.fg
= 15; }
2054 if (chan
.isVoiced(user
)) { mode
= '+'; win
.fg
= 3+8; }
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
);
2071 scope(exit
) xtFlush();
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);
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
);
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
);
2099 win
.fill(0, 0, win
.width
, win
.height
, ' ');
2100 win
.writeCharsAt(win
.width
-1, 0, 1, ':');
2101 win
.writeStrAt(0, 0, mynick
);
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);
2112 if (userFilterFocused
) {
2113 inputUserFilter
.drawCursor();
2118 // ////////////////////////////////////////////////////////////////////////// //
2119 void main (string
[] args
) {
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
);
2141 if (ttyIsRedirected
) assert(0, "no redirections, please");
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
;
2150 logOpenFile("zlog.log");
2151 } else if (s
.startsWith("--log=")) {
2152 logOpenFile(s
[6..$]);
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();
2173 ttyDisableFocusReports();
2174 ttySetMode(ttymode
);
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);
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");
2228 } catch (Throwable e
) {
2229 import core
.stdc
.stdlib
: abort
;
2230 import core
.memory
: GC
;
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
);
2238 auto se
= e
.toString
;
2239 errfl
.fprintf("********\n%.*s\n********\n", cast(uint)se
.length
, se
.ptr
);
2244 ttyDisableFocusReports();
2245 ttySetMode(ttymode
);