From a4c4066fb785101316573c6b35281f2950aa9672 Mon Sep 17 00:00:00 2001 From: yannickreiss Date: Mon, 7 Aug 2023 08:44:59 +0200 Subject: [PATCH] Start spell check on enter --- viml/legacyconf.vim | 10 +- viml/vimirc.vim | 8666 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 8674 insertions(+), 2 deletions(-) create mode 100644 viml/vimirc.vim diff --git a/viml/legacyconf.vim b/viml/legacyconf.vim index 472c219..0036143 100644 --- a/viml/legacyconf.vim +++ b/viml/legacyconf.vim @@ -5,7 +5,7 @@ nnoremap :NERDTreeToggle " open builtin terminal function OpenTerm() - vsplit + vsplit terminal endfunction nnoremap :call OpenTerm() @@ -25,6 +25,12 @@ augroup END " Update Plugins and Treesitter languages autocmd VimLeave * mksession! ~/.config/nvim/lastSession.vi +" set spellcheck according to Filetype +autocmd VimEnter * set spell spelllang=en_us +autocmd Vimenter *.tex set spell spellang=de_de +autocmd Vimenter *.txt set spell spellang=de_de +autocmd Vimenter *.md set spell spellang=de_de + " autosave for Markdown and Latex set updatetime=800 autocmd CursorHold *.md w @@ -38,7 +44,7 @@ function RestoreSession() endif endfunction -" Git-Blame config +" Git-Blame configuration let g:gitblame_message_template = ' => ' let g:gitblame_date_format = '%r' diff --git a/viml/vimirc.vim b/viml/vimirc.vim new file mode 100644 index 0000000..e8d1fcc --- /dev/null +++ b/viml/vimirc.vim @@ -0,0 +1,8666 @@ +" An IRC client plugin for Vim +" Maintainer: Madoka Machitani +" Created: Tue, 24 Feb 2004 +" Last Change: Sat, 16 Apr 2005 09:57:57 +0900 (JST) +" License: Distributed under the same terms as Vim itself +" +" Credits: +" ircII the basics of IRC +" X-Chat ideas for DCC implementation specifically +" ERC ideas for netsplit handling, auto-away feature etc. +" KoRoN creator of a BBS viewer for Vim, Chalice (very popular +" among Japanese vim community) +" Ilya Sher an idea for mini-buffers for cmdline editing +" morbuz pointing out the error "E28: No such highlight group" +" with s:Hilite* functions +" alexander pointing out the excessive CPU-time consumption, +" feature of multiple server-connection on startup, etc +" +" Features: +" * real-time message receiving with user interaction (many of the normal +" mode commands available) +" * multiple server/channel connectivity +" * DCC SEND/CHAT functionalities +" * logging +" * command aliasing +" * auto-join channels on logon +" * auto-reconnect/rejoin after disconnect +" * auto-away +" * netsplit detection +" +" A Drawback: +" VimIRC achieves real-time message reception by implementing its own main +" loop. Therefore, while you are out of it, VimIRC has no way to get new +" messages, unless you move the cursor periodically. +" +" So my recommendation is, use a shortcut which creates a VimIRC-dedicated +" instance of Vim, where you do not initiate normal editing sessions (See +" Tip 1, located far below, for how to do this). +" +" Requirements: +" * Vim 6.2 or later with perl interface enabled +" * Perl 5.6 or later (5.8 or later if you want multibyte feature) +" +" Options: +" +" Basic settings: +" let g:vimirc_nick ="nickname" +" let g:vimirc_user ="username" +" let g:vimirc_realname ="full name" +" let g:vimirc_server ="irc.foobar.com:6667" +" (default: irc.freenode.net) +" Your favorite IRC server. Can be a +" comma-separated list. +" let g:vimirc_umode ="user modes" set upon logon (e.g.: "+i") +" let g:vimirc_pass ="password" +" ="password1@server1,password2@server2" +" Set this only if server requires +" authentication. +" +" Misc.: +" (Some of these options are configurable while running, with "/SET" +" command) +" +" let g:vimirc_partmsg ="message sent with QUIT/PART" +" +" let g:vimirc_autojoin ="#chan1,#chan2" +" ="#chan1|#chan2@irc.foo.com,#chan3@irc.bar.com" +" +" List of channels to join upon logon. +" +" You can specify a password by appending +" ":pass" to the channel, if it requires one. +" +" Special keyword "list" is also accepted, in +" which case the list of channels will be +" displayed upon logon. +" +" let g:vimirc_nickpass ="pass@irc.foo.com" +" ="nick:pass@irc.foo.com" +" ="nick1:pass1|nick2:pass2@irc.foo.com" +" +" List of passwords to identify yourself to +" NickServ. +" +" The first form can be used if you are using +" the same pass for different nicks, or you +" have only one nick registered. +" +" Use commas to separate settings for each +" server. (Only irc.freenode.net is +" supported currently) +" +" let g:vimirc_log =NUMBER +" Set this to non-zero to enable logging +" feature. (default: zero) +" Logs will be taken for each channel and +" server independently. +" +" Since server buffers are usually not +" interesting (is it?), they'll be logged only +" if NUMBER is greater than 1. +" let g:vimirc_logdir ="where log files are saved" +" (default: ~/.vimirc) +" The specified directory will be created +" automatically. +" +" let g:vimirc_listexpire =NUMBER +" (default: 604800 (a week)) +" Threshold time in seconds after which VimIRC +" discards cached channels lists. +" +" Channels list is cached always (from +" 0.8.12), since obtaining one (command: /list) +" is rather a hard work for both server and +" client. Pressing "R" on the list buffer +" refreshes it anyway regardless of the value. +" +" let g:vimirc_browser ="web-browser" +" (default: see GetUserBrowser()) +" The name of the web browser program which is +" to be invoked when hitting "" near a +" URL-like string. +" +" You can insert the special argument "%URL%", +" which will be replaced with the actual URL, +" so that you can add other arguments after it +" like this: +" "kterm -rv -e lynx %URL% &" +" +" let g:vimirc_winmode (abolished) +" +" let g:vimirc_infowidth =NUMBER +" (default: 20) +" Width of the info-bar (area to indicate +" which hidden buffers have new messages). +" +" Auto-away feature: +" let g:vimirc_autoaway =NUMBER +" Set to non-zero to enable the feature. +" (default: zero) +" let g:vimirc_autoawaytime =NUMBER +" Threshold time in seconds after which you +" will be marked as `away' +" (default: 1800 (30 minutes)) +" +" DCC-related: +" let g:vimirc_dccdir ="where files are downloaded" +" (default: ~/.vimirc/dcc) +" let g:vimirc_dccport =NUMBER +" Port-number to watch at when you set up +" a dcc server. maybe necessary to set if you +" are behind firewall or something. +" +" Runtime Options: +" You can pass following options to the command :VimIRC, overriding the +" vim variables above +" +" -n nickname +" -u username +" -s server[:port] +" -p password +" +" Long option(s): +" +" --real[name]="full name" +" +" Startup: +" Type +" :VimIRC [runtime options] +" +" You will be prompted for several user information (nick etc.) if you have +" not set options listed above. +" +" Usage: +" +" Normal mode: This is a pseudo normal mode. Try your favorite Vim normal +" mode commands as usual. +" +" Hitting "i" or "I" will let you in the `command mode' +" described below, just as you do in Vim to go insert mode. +" +" ":", "/" and "?" keys will prompt you to enter ex-commands +" or search strings as usual. +" +" Hit to get out of control and freely move around +" or do ex commands. Hit to re-enter the normal +" (online) mode again. +" +" Hitting "q" will quit the current channel, chat, server, or +" VimIRC itself, depending on the context ("Q" does the last +" forcibly). Contrarily, "r" and "R" keys would reconnect to +" servers, if they were disconnected. +" +" Special cases: +" +" * In a channels list window (which should open up with /list +" command), you can type "o" to sort the list, "O" to +" reverse it (I took these mappings from Mutt the e-mail +" client). "R" will refresh the list. +" Hitting "" will prompt you whether to join the channel +" where the cursor is. +" +" * In a nicks window, you can hit "" to choose an action +" to take against the one whom the cursor is on. +" +" * "" / "" keys are available in all buffers, for +" cycling forward/backward through channel/server buffers. +" "" / "" do the same, with windows +" split open. +" +" * Hit "" if you find some windows have got corrupt +" window sizes. It will (hopefully) restore them. +" +" Command mode: This is just a normal buffer opened (at the bottom of the +" screen|below the current window). Enter IRC commands here. +" Hitting "", both in insert and normal mode, will send out +" the cursor line instantly either as a command or a message. +" +" Every IRC command starts with "/". E.g.: /join #vim,#c +" +" Line without a leading slash will be sent as a message, +" normaly to the current channel. +" +" Type /help to see the list of available commands. It's +" far from complete, though. +" +" Quit: +" Type +" /quit +" in the IRC command line to disconnect with the current server. +" +" To totally exit from VimIRC, press "Q" in normal mode, or type +" VimIRCQuit +" on the VIM command line. +" +" TODOs: +" * handling of control characters (bold, underline etc.) +" * multibyte support (done? I don't think so) +" * flood protection +" * IPv6 +" * SSL +" * nicks auto-identification (done for freenode) +" * command-line completion (with tab, arrows etc.) +" * scripting (?) +" * help (both /help command and local help file) +" * menus (I personally never use menus) +" * etc. etc. +" +" Done: +" - command-line history (?) +" - handling of channel-mode changes +" - separate listing of channels with sorting facilities +" - auto reconnect/rejoin +" - ctcp, including dcc stuffs (well, mostly) +" - timer (it hasn't been in todo list though) +" - auto-away +" - logging +" - netsplit detection +" - command abbreviation (aliasing) +" - authentication (just add one line to send PASS) +" +" Tips: +" 1. If you see extreme slowness of vim's startup due to this plugin, put +" "let loaded_vimirc=1" in your .vimrc to avoid loading this in your +" everyday editing life. Create a VimIRC-dedicated rc file (.vimircrc +" or something) and put necessary settings into it. Then set up an +" alias (shortcut) which runs VimIRC, specifying the rc file you've just +" created. Like this: +" alias irc='gvim -i NONE -u ~/.vimircrc -c VimIRC' +" +" 2. In the `info-bar' on the left, you'll see buffer numbers next to the +" server/channel names. With them, you can easily navigate +" servers/channels like this: +" :sb1 +" +" 3. You can send multiple messages/commands at a time. Prepare lines of +" messages elsewhere and copy/paste them into the command buffer. Then +" visually select the lines and press . (Do with caution so as not +" to be kicked off!) +" +" 4. How to send a message starting with a forward-slash? Just precede it +" with slash-space like this: "/ /message" (without quotes) +" +" 5. When writing in a channel, you can use '%' where you have to type the +" name of the current channel. Examples: +" +" /msg % You wouldn't normally write this way though. +" /msg %,nick This also works, at least theoretically. +" /notice % '%'s in the message won't be expanded. +" /topic % New Topic +" /invite nick % +" +" You can even omit '%' in the last two examples. Note also that '%' +" will be expanded to the nick, if you are on a chat window. +" +" 6. There is a special command prefix "SPLIT", which executes commands +" after it in new windows: +" +" /split /join #chan +" /sp freenode +" +" NOTE: The leading slash of the following command is optional. Note +" also that "split" can be abbreviated in the same manner as Vim's +" ":split". +" +" The second example shows that you can prepend it to aliases, too +" ("freenode" should expand into "/server irc.freenode.net" in this +" case). +" +" 7. Type shorter by utilizing aliases. Example: +" +" /alias sj split join +" +" registers new alias "sj", expanding to "split join". Now you can +" type: +" +" /sj #chan1,#chan2 +" +" to open the channels in separate windows. +" +" 8. Many IRC commands are abbreviatable by default. E.g., "/join" can be +" typed in any way as "/joi", "jo", or "/j". +" +" Do "/h" to see how you can abbreviate commands. +" +" NOTE: ALIASes take precedence over ABBREVIATIONs. E.g., "/q" is an +" abbreviation of "/query", but you can (re)define "/q" as "/quit", if +" you prefer. +" +" 9. There are several ways to open (hidden) channel/chat/server buffers: +" (1) In normal mode (both online and offline), hit "" and "" +" to cycle through the buffers. It will open buffers in the same +" order shown in the info-bar on the left. +" You can prepend count: "2" goes to the second next buffer +" counted from the current one. +" You can also prepend "", to open the buffer in a new window +" (e.g., "", "2", "2"). +" NOTE: Count will be multiplied if used both before and after +" "". +" (2) Use ":buffer" command, as described in Tip 2. +" (3) Visit the info-bar (with "t") and select the one you want to +" open, then hit "". "" will open it in a new window. +" (4) If you try to join already joined channels, either via "/join" or +" through channels-list, they'll just safely be opened. + +if exists('g:loaded_vimirc') || &compatible + finish +endif +let s:version = '0.9.28' + +let s:debug = (s:version =~# '-devel$') +if !s:debug + let g:loaded_vimirc = 1 +endif + +let s:save_cpoptions = &cpoptions +set cpoptions& + +" +" Developing functions +" + +if 0 + +" Truncate too long buffers (upon logging) +function! s:TruncateBuf() + " suggested option: vimirc_maxlines +endfunction + +endif + +" +" Start/Exit +" + +function! s:GetUserInfo(args) + " NOTE: This may be called more than once + if !strlen(a:args) + \ && (exists('s:nick') && exists('s:user') && exists('s:realname')) + " Already set + return 1 + endif + + let s:nick = s:StrMatch(a:args, '-n\s*\(\S\+\)', '\1') + if !strlen(s:nick) + let s:nick = s:GetVimVar('g:vimirc_nick') + if !strlen(s:nick) + let s:nick = s:GetEnv('$IRCNICK') + if !strlen(s:nick) + let s:nick = s:Input('Enter your nickname') + endif + endif + endif + + let s:user = s:StrMatch(a:args, '-u\s*\(\S\+\)', '\1') + if !strlen(s:user) + let s:user = s:GetVimVar('g:vimirc_user') + if !strlen(s:user) + let s:user = s:GetEnv('$USER') + if !strlen(s:user) + let s:user = s:Input('Enter your username') + endif + endif + endif + + let s:pass = s:StrMatch(a:args, '-p\s*\(\S\+\)', '\1') + if !strlen(s:pass) + let s:pass = s:GetVimVar('g:vimirc_pass') + endif + + let s:realname = s:StrMatch(a:args, + \"--real\\%(name\\)\\==\\(['\"]\\)\\(.\\{-\\}\\)\\1", '\2') + if !strlen(s:realname) + let s:realname = s:GetVimVar('g:vimirc_realname') + if !strlen(s:realname) + let s:realname = s:GetEnv('$NAME') + if !strlen(s:realname) + let s:realname = s:GetEnv('$IRCNAME') + if !strlen(s:realname) + let s:realname = s:Input('Enter your full name') + endif + endif + endif + endif + + let s:umode = s:StrMatch(a:args, '-m\s*\(\S\+\)', '\1') + if !strlen(s:umode) + let s:umode = s:GetVimVar('g:vimirc_umode') + if !strlen(s:umode) + let s:umode = s:GetEnv('$IRCUMODE') + endif + endif + + if !(strlen(s:nick) && strlen(s:user) && strlen(s:realname)) + unlet s:nick s:user s:pass s:realname s:umode + return 0 + endif + + let s:server = s:StrMatch(a:args, '-s\s*\(\S\+\)', '\1') + if !strlen(s:server) + let s:server = s:GetVimVar('g:vimirc_server') + if !strlen(s:server) + let s:server = 'irc.freenode.net:6667' + endif + endif + + return 1 +endfunction + +function! s:GetUserBrowser() + if !strlen(s:browser) + if s:IsWin3264() + let s:browser = 'start explorer' + elseif has('mac') + let s:browser = "osascript -e 'open location %URL%'" + elseif has('unix') + let s:browser = executable('mozilla') + \ ? 'mozilla' + \ : executable('netscape') + \ ? 'netscape' + \ : executable('lynx') + \ ? 'lynx' + \ : executable('w3m') ? 'w3m' : '' + endif + + if !strlen(s:browser) + let s:browser = s:Input('Enter the name of your web browser') + call s:OptValidate('browser') + endif + endif + return s:browser +endfunction + +function! s:GetServerOpt(option, server) + let option = a:option + " option1@server1,option2@server2 + if a:option =~ '@' + let option = matchstr(a:option, + \ '\m[^,]\+\%(@\V'.a:server.'\m\%([,:]\|$\)\)\@=') + let option = substitute(option, '|', ',', 'g') + endif + return option +endfunction + +function! s:GetServerUMODE(...) + let umode = s:GetServerOpt(s:umode, (a:0 ? a:1 : s:server)) + if !strlen(umode) + " NOTE: This cannot be an empty string: required as a second parameter of + " USER command. + let umode = '0' + endif + return umode +endfunction + +function! s:GetServerPASS(...) + return s:GetServerOpt(s:pass, (a:0 ? a:1 : s:server)) +endfunction + +function! s:InitVars() + if exists('s:sid') " already init'ed + return + endif + + " Obtain the script ID + map xx xx + let s:sid = substitute(maparg('xx'), 'xx$', '', '') + unmap xx + + let s:client = 'VimIRC '.s:version + " Set up the names of buffers we use + call s:InitBufNames() + " Init system variables + call s:ResetSysVars() + + " User-defined options + + " Favorite farewell message + call s:OptSet('vimirc_partmsg', (s:debug ? 'Testing ' : '').s:client. + \' (IRC client for Vim)') + " Preferred language. Encoding name which Perl's Encode module can accept + call s:OptSet('vimirc_preflang') + " On/off logging feature + call s:OptSet('vimirc_log', 0) + " Log directory + call s:OptSet('vimirc_logdir', expand('$HOME').'/.vimirc') + " Setings like aliases will go here + call s:OptSet('vimirc_rcfile', s:logdir.'/.vimircrc') + " Channels list will be refreshed after these amount of seconds have passed + call s:OptSet('vimirc_listexpire', (60 * 60 * 24 * 7)) + " External Web browser + call s:OptSet('vimirc_browser') + " Directory where incoming dcc files should go + call s:OptSet('vimirc_dccdir', s:logdir.'/dcc') + " Port you want to listen to + call s:OptSet('vimirc_dccport') + + " Window-related options + " Width of info-bar + call s:OptSet('vimirc_infowidth', 20) + " Width of nicks-window + call s:OptSet('vimirc_nickswidth', 12) + " Height of command-line buffer + call s:OptSet('vimirc_cmdheight', 1) + + " Timer-related + " On/off auto-away feature + call s:OptSet('vimirc_autoaway', 0) + " Threshold time for you to be marked `away'. MUST be in seconds + call s:OptSet('vimirc_autoawaytime', (60 * 30)) +endfunction + +function! s:SetSysVars() + call s:ResetSysVars() + let s:opened = 1 + " When the timer was last triggered + let s:lasttime = localtime() + " When user did some action most recently + let s:lastactive = s:lasttime + let s:lastbeep = s:lasttime +endfunction + +function! s:ResetSysVars() + let s:opened = 0 + let s:in_loop = 0 + let s:autocmd_disabled = 0 + let s:current_changed = 0 + let s:split = 0 +endfunction + +function! s:SetGlobVars() + let s:eadirection = &eadirection + set eadirection=ver + let s:equalalways = &equalalways + let s:lazyredraw = &lazyredraw + set lazyredraw + let s:showbreak = &showbreak + let &showbreak = ' ' + let s:statusline = &statusline + let &statusline = '%{'.s:sid.'GetBufTitle()}%=%l/%L' + let s:titlestring = &titlestring + let s:winminheight = &winminheight + set winminheight=1 + let s:winwidth = &winwidth + set winwidth=12 +endfunction + +function! s:ResetGlobVars() + let &eadirection = s:eadirection + unlet s:eadirection + let &equalalways = s:equalalways + unlet s:equalalways + let &lazyredraw = s:lazyredraw + unlet s:lazyredraw + let &showbreak = s:showbreak + unlet s:showbreak + let &statusline = s:statusline + unlet s:statusline + let &titlestring = s:titlestring + unlet s:titlestring + let &winminheight = s:winminheight + unlet s:winminheight + let &winwidth = s:winwidth + unlet s:winwidth +endfunction + +function! s:SetCmds() + if exists(':VimIRC') + delcommand VimIRC + endif + command! VimIRCQuit :call s:QuitVimIRC() +endfunction + +function! s:ResetCmds() + if exists(':VimIRCQuit') + delcommand VimIRCQuit + endif + command! -nargs=* VimIRC :call s:StartVimIRC() +endfunction +if !exists(':VimIRC') + call s:ResetCmds() +endif + +function! s:SetAutocmds() + augroup VimIRC + autocmd! + " NOTE: Cannot use CursorHold to auto re-enter the loop: getchar() won't + " get a char since key inputs will never be waited after that event. + execute 'autocmd CursorHold * call s:NotifyOffline()' + execute 'autocmd BufHidden' s:bufname_channel.'* call s:PreCloseBuf_Channel()' + execute 'autocmd BufHidden' s:bufname_chat.'* call s:PreCloseBuf_Chat()' + execute 'autocmd BufHidden' s:bufname_list.'* call s:PreCloseBuf_List()' + execute 'autocmd BufHidden' s:bufname_server.'* call s:PreCloseBuf_Server()' + execute 'autocmd BufUnload' s:bufname.'* call s:DelChanServ()' + execute 'autocmd BufWinEnter' s:bufname_channel.'* call s:PostOpenBuf_Channel()' + execute 'autocmd BufWinEnter' s:bufname_chat.'* call s:PostOpenBuf_Chat()' + execute 'autocmd BufWinEnter' s:bufname_list.'* call s:PostOpenBuf_List()' + execute 'autocmd BufWinEnter' s:bufname_server.'* call s:PostOpenBuf_Server()' + execute 'autocmd BufWinLeave' s:bufname.'* call s:ChangeChanServ(1)' + execute 'autocmd WinEnter' s:bufname.'* call s:ChangeChanServ(1)' + augroup END +endfunction + +function! s:ResetAutocmds() + autocmd! VimIRC +endfunction + +function! s:StartVimIRC(...) + if !has('perl') + echoerr 'To use this, you have to build vim with perl interface. Exiting.' + return + endif + if s:GetVimVar('s:opened') + return + endif + + " Initialize most of the internal variables. User configurations will be + " dealt with here, too. + call s:InitVars() + " Parse command-line arguments + if !s:GetUserInfo(a:0 ? a:1 : '') + return + endif + + " Load system .vimircrc + call s:RC_Load() + + " Adjust some Vim's global variables to match VimIRC's need + call s:SetGlobVars() + " Set up system variables + call s:SetSysVars() + + " Remove ":VimIRC" so this won't be called twice, etc. + call s:SetCmds() + call s:SetAutocmds() + + " Set VimIRC's cursor color + call s:SetHlCursor() + call s:SetEncoding() + + " Initialize perl codes + call s:PerlIRC() + " Now it is OK to commence connections + call s:StartServer() + + call s:MainLoop() +endfunction + +function! s:QuitVimIRC() + if !s:opened + return + endif + + try + let s:autocmd_disabled = 1 + + call s:Send_GQUIT('QUIT', '') + + call s:ResetCmds() + call s:ResetAutocmds() + call s:ResetGlobVars() + call s:ResetPerlVars() + + call s:CloseWin_IRC() + if s:in_loop + throw 'IMGONNAQUIT' + endif + finally + call s:PromptKey(0, ' Thanks for flying VimIRC', 'Title') + call s:ResetSysVars() + endtry +endfunction + +function! s:QuitWhat(severe) + if a:severe + " Don't confirm when quitting with "Q" + return s:QuitVimIRC() + endif + + let canceld= 0 + " NOTE: `server' could get invalid value + let server = s:GetVimVar('b:server') + let channel= s:GetVimVar('b:channel') + call s:SetCurServer(server) + + if s:IsChannel(channel) + let canceld = !s:Confirm_YN('Really close channel '.channel) + if !canceld + return s:Send_PART('PART', channel) + endif + elseif s:IsNick(channel) + let canceld = !s:Confirm_YN('Really quit chat with '.channel) + if !canceld + return s:QuitChat(channel) + endif + endif + + if s:IsConnected() + if s:Confirm_YN((canceld ? 'Then,' + \ : 'Really').' disconnect with server '.s:server) + call s:Send_QUIT('QUIT', '') + endif + else + if s:Confirm_YN((canceld ? 'Then,' : 'Really').' quit VimIRC') + call s:QuitVimIRC() + endif + endif +endfunction + +function! s:MainLoop() + " NOTE: Be careful of the recursion, it may cause some obscure troubles + if !(!s:in_loop && s:opened && s:IsSockOpen()) + return + endif + + try + call s:PreMainLoop() + while 1 + if getchar(1) + try + call s:HandleKey(getchar(0)) + catch /^\S\+:E/ + " Catch some familiar, or low severity errors + call s:IgnoreException(v:exception) + endtry + endif + if !s:DoTimer() + break + endif + endwhile + catch /^IMGONNA/ + " Get out of the loop + " NOTE: You cannot see new messages posted while posting + catch /^Vim:Interrupt$/ + if 1 && s:IsBufType_Command() + call s:DoInsert(0) + endif + catch + echoerr v:exception + finally + call s:PostMainLoop() + endtry +endfunction + +function! s:IgnoreException(exception) + let errno = s:StrMatch(a:exception, ':E\(\d\+\):', '\1') + let ignore= (errno =~ '^\%(2[01]\|35\|78\|132\|4\%(43\|86\|92\)\)$') + if ignore + if errno != 132 " pressed the same key for a long time; ignore it + call s:EchoError('VimIRC: '.s:StrDivide(a:exception, 2)) + endif + else + echoerr v:exception + endif +endfunction + +function! s:PreMainLoop() + let s:in_loop = 1 + let s:current_changed = 1 + call s:ToggleCursor(1) + call s:ClearCommand(0) + " Show line-cursor, so the user can easily recognize that she is online + call s:HiliteLine('.') +endfunction + +function! s:PostMainLoop() + call s:HiliteClear() + call s:ToggleCursor(0) + let s:in_loop = 0 +endfunction + +" +" .vimircrc manipulation +" + +function! s:RC_Load() + if filereadable(s:rcfile) + execute 'source' s:rcfile + endif +endfunction + +function! s:RC_Open(force) + return (a:force || filereadable(s:rcfile)) && s:OpenBuf('1split', s:rcfile) +endfunction + +function! s:RC_Close() + if s:BufVisit(bufnr(s:rcfile)) + if &modified + call s:ExecuteSilent('write!') + endif + call s:ExecuteSilent('bdelete!') + endif +endfunction + +function! s:RC_Varname(type, name) + return 'vimirc_'.a:type.'_'.(a:name =~ '[^_[:alnum:]]' + \ ? '{"'.s:EscapeQuote(a:name).'"}' : a:name) +endfunction + +function! s:RC_Section(section) + if !search('^" '.a:section, 'w') + call s:OpenNewLine() + call append('.', '" '.a:section) + $ + endif +endfunction + +function! s:RC_Set(type, name, value) + call s:RC_Unset(a:type, a:name) + let varname = s:RC_Varname(a:type, a:name) + call append('.', 'let '.varname.' = "'.s:EscapeQuote(a:value).'"') + let g:{varname} = a:value +endfunction + +function! s:RC_Unset(type, name) + let varname = s:RC_Varname(a:type, a:name) + while search('\m'.s:EscapeMagic(varname).'\s*=', 'w') + call s:DelLine() + endwhile + unlet! g:{varname} +endfunction + +" +" Buffer manipulation +" + +function! s:InitBufNames() + let s:bufname = '_VimIRC_' + let list = 'info server list channel nicks command chat' + while strlen(list) + let type = s:StrDivide(list, 1) + let s:bufname_{type} = s:bufname.toupper(type).'_' + let list = s:StrDivide(list, 2) + endwhile +endfunction + +" I'm using buffer numbers to access buffers: accessing by name will soon fail +" if user changes directory or something. +" NOTE: I removed the `server' argument from the functions below, just for +" ease of typing (esp. on the perl's side). + +function! s:GetBufNum(bufname) + let bufnum = -1 + let varname = 's:bufnum_'.a:bufname + if exists('{varname}') + if bufloaded({varname}) + let bufnum = {varname} + else + unlet {varname} + endif + endif + return bufnum +endfunction + +function! s:GetBufNum_Info() + return s:GetBufNum(s:GenBufName_Info()) +endfunction + +function! s:GetBufNum_Server(...) + return s:GetBufNum(s:GenBufName_Server(a:0 ? a:1 : s:server)) +endfunction + +function! s:GetBufNum_List(...) + return s:GetBufNum(s:GenBufName_List(a:0 ? a:1 : s:server)) +endfunction + +function! s:GetBufNum_Channel(channel, ...) + return s:GetBufNum(s:GenBufName_Channel(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +function! s:GetBufNum_Nicks(channel, ...) + return s:GetBufNum(s:GenBufName_Nicks(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +function! s:GetBufNum_Command(channel, ...) + return s:GetBufNum(s:GenBufName_Command(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +function! s:GetBufNum_Chat(nick, server) + return s:GetBufNum(s:GenBufName_Chat(a:nick, a:server)) +endfunction + +function! s:SetBufNum(bufname) + let s:bufnum_{a:bufname} = bufnr('%') + " Need to set this buffer as `current' if it is channel/server + call s:ChangeChanServ(0) + return s:bufnum_{a:bufname} +endfunction + +function! s:DelBufNum(bufnum) + unlet! s:bufnum_{bufname(a:bufnum)} +endfunction + +function! s:GenBufName_Info() + return s:bufname_info +endfunction + +function! s:GenBufName_Server(...) + return s:bufname_server.(a:0 ? a:1 : s:server) +endfunction + +function! s:GenBufName_List(...) + return s:bufname_list.(a:0 ? a:1 : s:server) +endfunction + +function! s:GenBufName_Channel(channel, ...) + return s:bufname_channel.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server) +endfunction + +function! s:GenBufName_Nicks(channel, ...) + return s:bufname_nicks.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server) +endfunction + +function! s:GenBufName_Command(channel, ...) + return s:bufname_command.s:SecureChannel(a:channel).'@'.(a:0 ? a:1 : s:server) +endfunction + +function! s:GenBufName_Chat(nick, server) + return s:bufname_chat.s:EscapeFName(a:nick).'@'.a:server +endfunction + +function! s:SecureChannel(channel) + return (match(a:channel, '*') >= 0) ? '' : s:EscapeFName(tolower(a:channel)) +endfunction + +function! s:IsBufType_IRC(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (!match(bufname(bufnum), s:bufname) + \ && s:ExistsBufVar(bufnum, 'server'))) +endfunction + +function! s:IsBufType_ChanChat(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && ( s:IsBufType_Channel(bufnum) + \ || s:IsBufType_Chat(bufnum))) +endfunction + +" Whether this is an appropriate place over which to open another +" channel/server +function! s:IsBufType_ChanServ(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 + \ && ( s:IsBufType_Channel(bufnum) || s:IsBufType_Chat(bufnum) + \ || s:IsBufType_Server(bufnum) || s:IsBufType_List(bufnum))) +endfunction + +function! s:IsBufType_Info(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_info))) +endfunction + +function! s:IsBufType_Server(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_server))) +endfunction + +function! s:IsBufType_List(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_list))) +endfunction + +function! s:IsBufType_Channel(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_channel))) +endfunction + +function! s:IsBufType_Nicks(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_nicks))) +endfunction + +function! s:IsBufType_Command(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_command))) +endfunction + +function! s:IsBufType_Chat(...) + let bufnum = a:0 ? a:1 : bufnr('%') + return (bufnum >= 0 && (s:IsBufType_IRC(bufnum) + \ && !match(bufname(bufnum), s:bufname_chat))) +endfunction + +function! s:IsBufType_ServerDead() + return (s:IsBufType_IRC() && !(s:IsBufType_Chat() && b:channel[0] == '=') + \ && getbufvar(s:GetBufNum_Server(b:server), 'dead') + 0) +endfunction + +function! s:IsServer(server) + " TODO: Be more precise (see RFC3986) + return (a:server =~ '[.:]') +endfunction + +function! s:IsServerIRC(server) + return (a:server =~? + \'^\%(irc://\|\%(\w[-.[:alnum:]]\+[-.]\)\{-\}irc\)[^/]\+\%([/:]\S\+\)\=$') +endfunction + +function! s:IsChannel(channel) + return !match(a:channel, '[&#+!]') +endfunction + +function! s:IsNick(channel) + return (a:channel =~? '^=\=[-0-9\[-`a-z{-}]\+$') +endfunction + +function! s:IsChanChat(channel) + return (s:IsChannel(a:channel) || s:IsNick(a:channel)) +endfunction + +function! s:IsList(channel) + return (a:channel ==# '*list') +endfunction + +function! s:CanOpenChanServ() + let autocmd_disabled = s:autocmd_disabled + let s:autocmd_disabled = 1 + + if !(s:IsBufType_ChanServ() || s:VisitBuf_ChanServ(s:GetVimVar('b:channel'), + \ s:GetVimVar('b:server'))) + let i = 1 + while 1 + let bufnum = winbufnr(i) + if (bufnum < 0) || (s:IsBufType_ChanServ(bufnum) && s:BufVisit(bufnum)) + break + endif + let i = i + 1 + endwhile + endif + let s:autocmd_disabled = autocmd_disabled + + return s:IsBufType_ChanServ() +endfunction + +" Change highlighting of the current buffer (later) +function! s:ChangeChanServ(auto) + if !((a:auto && s:autocmd_disabled) || s:current_changed) + let s:current_changed = s:IsBufType_ChanServ(a:auto + \ ? expand('') + 0 + \ : bufnr('%')) + endif +endfunction + +function! s:EnterChanServ(split) + let line = getline('.') + let rx = '^\s*[-+*]\=\[-\=\d\+\]\(\S\+\)\%(\s\+\(\S\+\)\)\=$' + if line =~ rx + let channel= s:StrMatch(line, rx, '\1') + let server = s:StrMatch(line, rx, '\2') + call s:OpenChanServ(channel, (!strlen(server) ? channel : server), a:split) + endif +endfunction + +function! s:OpenChanServ(channel, server, split) + let s:server = a:server + let s:split = a:split + + if s:IsChannel(a:channel) + call s:OpenBuf_Channel(a:channel) + elseif s:IsNick(a:channel) + call s:OpenBuf_Chat(a:channel, a:server) + elseif s:IsList(a:channel) + call s:OpenBuf_List() + else + call s:OpenBuf_Server() + endif + + let s:split = 0 +endfunction + +function! s:OpenNextChanServ(forward, split, ...) + if s:CanOpenChanServ() + let bufnum = s:GetBufNum_Next(a:forward, (a:0 ? a:1 : v:count1)) + if bufnum + call s:OpenChanServ(getbufvar(bufnum, 'channel'), + \ getbufvar(bufnum, 'server'), + \ (a:split * (a:forward ? 1 : -1))) + endif + endif +endfunction + +function! s:VisitBuf_ChanChat(channel, ...) + return (strlen(a:channel) + \ && s:VisitBuf_Cha{s:IsNick(a:channel) + \ ? 't' : 'nnel'}(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +function! s:VisitBuf_ChanServ(channel, ...) + let server = a:0 ? a:1 : s:server + return s:IsChanChat(a:channel) + \ ? s:VisitBuf_ChanChat(a:channel, server) + \ : s:VisitBuf_ServList(server) +endfunction + +function! s:VisitBuf_ServList(...) + let server = a:0 ? a:1 : s:server + return (s:VisitBuf_Server(server) || s:VisitBuf_List(server)) +endfunction + +function! s:VisitBuf_Info() + return s:BufVisit(s:GetBufNum_Info()) +endfunction + +function! s:VisitBuf_Server(...) + return s:BufVisit(s:GetBufNum_Server(a:0 ? a:1 : s:server)) +endfunction + +function! s:VisitBuf_List(...) + return s:BufVisit(s:GetBufNum_List(a:0 ? a:1 : s:server)) +endfunction + +function! s:VisitBuf_Channel(channel, ...) + return s:BufVisit(s:GetBufNum_Channel(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +function! s:VisitBuf_Chat(nick, ...) + return s:BufVisit(s:GetBufNum_Chat(a:nick, (a:0 ? a:1 : s:server))) +endfunction + +function! s:VisitBuf_Nicks(channel, ...) + return s:BufVisit(s:GetBufNum_Nicks(a:channel, (a:0 ? a:1 : s:server))) +endfunction + +" +" Opening buffers +" + +function! s:OpenBuf(comd, ...) + try + let equalalways = &equalalways + let winminheight= &winminheight + let winminwidth = &winminwidth + + let curbuf = bufnr('%') + let winline = winline() + + let &equalalways = 0 + " Avoid "not enough room" error + set winminheight=0 winminwidth=0 + + call s:ExecuteSilent(a:comd.(a:0 && strlen(a:1) ? ' '.a:1 : '')) + + setlocal noswapfile modifiable + if !s:autocmd_disabled + call s:RestoreWinLine(curbuf, winline) + endif + catch + if s:debug + echoerr v:exception + endif + finally + let &equalalways = equalalways + let &winminheight = winminheight + let &winminwidth = winminwidth + return !strlen(v:exception) + endtry +endfunction + +function! s:OpenBufNum(comd, bufnum) + return (a:bufnum > 0 && s:OpenBuf(a:comd, '+'.a:bufnum.'buffer')) +endfunction + +function! s:OpenBuf_Info() + let bufnum = s:GetBufNum_Info() + let loaded = (bufnum >= 0) + let retval = (loaded && s:BufVisit(bufnum)) + + if !retval + let comd = 'vertical topleft '.s:infowidth.'split' + if loaded + call s:OpenBufNum(comd, bufnum) + else + let bufname = s:GenBufName_Info() + call s:OpenBuf(comd, bufname) + call s:InitBuf_Info(bufname) + endif + let retval = -1 + endif + return retval +endfunction + +function! s:OpenBuf_Server(...) + let bufnum = s:GetBufNum_Server() + let loaded = (bufnum >= 0) + + " No need to search the buffer when peeking: it's already done + if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum)) + " Just stay here if it is peek mode + if !s:autocmd_disabled + " Reuse channels list window, if found + if !s:BufVisit(s:GetBufNum_List()) + call s:CanOpenChanServ() + endif + if s:split + call s:SplitBuf_Channel() + endif + endif + + if loaded + call s:OpenBuf('buffer', bufnum) + else + let bufname = s:GenBufName_Server() + call s:OpenBuf('edit', bufname) + call s:InitBuf_Server(bufname, (a:0 ? a:1 : 0)) + endif + endif + + if loaded && !s:autocmd_disabled + if a:0 + " a:1 is given only when user executed /server command, in which case we + " must clear the dead state of this buffer. + call setbufvar(bufnum, 'dead', 0) + endif + call s:WinBottom('$') + endif +endfunction + +function! s:OpenBuf_List() + let bufnum = s:GetBufNum_List() + let loaded = (bufnum >= 0) + + if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum)) + " Open it next to, or on the server window + if !s:autocmd_disabled + if !s:BufVisit(s:GetBufNum_Server()) + call s:CanOpenChanServ() + endif + if s:split + " I don't like vertical split, which was the original behaviour + call s:SplitBuf_Channel() + endif + endif + + if loaded + call s:OpenBuf('buffer', bufnum) + else + let bufname = s:GenBufName_List() + call s:OpenBuf('edit', bufname) + call s:InitBuf_List(bufname) + endif + endif + + if loaded && !s:autocmd_disabled + call s:DoNormal('^') + endif +endfunction + +function! s:OpenBuf_Channel(channel) + let bufnum = s:GetBufNum_Channel(a:channel) + let loaded = (bufnum >= 0) + + if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum)) + if !s:autocmd_disabled + call s:CanOpenChanServ() + if s:split + call s:SplitBuf_Channel() + endif + endif + + if loaded + call s:OpenBuf('buffer', bufnum) + call s:RestorePrevLine() + else + let bufname = s:GenBufName_Channel(a:channel) + call s:OpenBuf('edit', bufname) + call s:InitBuf_Channel(bufname, a:channel) + endif + " XXX: Is this necessary? Or necessary to all other OpenBuf_ funcs? + if &l:winfixheight + let &l:winfixheight = 0 + endif + endif +endfunction + +function! s:OpenBuf_Chat(nick, server) + let bufnum = s:GetBufNum_Chat(a:nick, a:server) + let loaded = (bufnum >= 0) + + if s:autocmd_disabled || !(loaded && s:BufVisit(bufnum)) + if !s:autocmd_disabled + call s:CanOpenChanServ() + if s:split + call s:SplitBuf_Channel() + endif + endif + + if loaded + call s:OpenBuf('buffer', bufnum) + call s:RestorePrevLine() + else + let bufname = s:GenBufName_Chat(a:nick, a:server) + call s:OpenBuf('edit', bufname) + call s:InitBuf_Chat(bufname, a:nick, a:server) + endif + endif +endfunction + +function! s:OpenBuf_Nicks(channel, ...) + let server = a:0 ? a:1 : s:server + let retval = s:VisitBuf_ChanChat(a:channel, server) + if retval + let bufnum = s:GetBufNum_Nicks(a:channel, server) + let loaded = (bufnum >= 0) + + if !(loaded && s:BufVisit(bufnum)) + let comd = 'vertical belowright '.s:nickswidth.'split' + if loaded + call s:OpenBufNum(comd, bufnum) + else + let bufname = s:GenBufName_Nicks(a:channel, server) + call s:OpenBuf(comd, bufname) + call s:InitBuf_Nicks(bufname, a:channel, server) + endif + endif + endif + return retval +endfunction + +function! s:OpenBuf_Command(posting) + if !(s:opened && s:IsBufType_IRC()) + return + endif + + " Set the current server name here, so the buffer number/name will be + " generated properly + if a:posting + call s:SetCurServer(b:server) + endif + + let channel = s:GetVimVar('b:channel') + let bufnum = s:GetBufNum_Command(channel) + let loaded = (bufnum >= 0) + + if !s:IsBufCurrent(bufnum) + call s:VisitBuf_ChanServ(channel) + endif + + " This is necessary for opening it in a desired height + let &equalalways = 0 + + if !(loaded && s:BufVisit(bufnum)) + let comd = 'belowright '.s:cmdheight.'split' + if loaded + call s:OpenBufNum(comd, bufnum) + else + let bufname = s:GenBufName_Command(channel) + call s:OpenBuf(comd, bufname) + call s:InitBuf_Command(bufname, channel) + endif + endif + + if !&l:winfixheight + setlocal winfixheight + endif + + if a:posting + call s:OpenNewLine() + call s:DoInsert(0) + endif +endfunction + +function! s:PostOpenBuf_Server() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let server = getbufvar(abuf, 'server') + if strlen(server) + call s:ResetCurChanServ('', server) + endif +endfunction + +function! s:PostOpenBuf_List() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let channel = getbufvar(abuf, 'channel') + let server = getbufvar(abuf, 'server') + if strlen(server) + call s:ResetCurChanServ(channel, server) + endif +endfunction + +function! s:PostOpenBuf_Channel() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let channel = getbufvar(abuf, 'channel') + let server = getbufvar(abuf, 'server') + if strlen(channel) && strlen(server) + let donick = (s:OpenBuf_Nicks(channel, server) && s:IsBufEmpty()) + call s:BufVisit(abuf) + call s:ResetCurChanServ(channel, server, donick) + endif +endfunction + +function! s:PostOpenBuf_Chat() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let nick = getbufvar(abuf, 'channel') + let server= getbufvar(abuf, 'server') + if strlen(nick) && strlen(server) + call s:OpenBuf_Nicks(nick, server) + call s:OpenBuf_Command(0) + call s:BufVisit(abuf) + call s:ResetCurChanServ(nick, server) + endif +endfunction + +function! s:SplitBuf_Channel() + let autocmd_disabled = s:autocmd_disabled + let s:autocmd_disabled = 1 + + let below = (s:split > 0) + let hasnick = s:IsBufType_ChanChat() + + if hasnick + call s:CloseBuf_Nicks(b:channel, b:server) + call s:CloseBuf_Command(b:channel, b:server) + endif + if !&equalalways + let &equalalways = 1 + endif + + if s:OpenBuf((below ? 'belowright' : 'aboveleft').' split') + call s:DoWincmd(below ? 'k' : 'j') + if !s:IsBufType_List() + call s:WinScroll2('.') + endif + if hasnick + call s:OpenBuf_Nicks(b:channel, b:server) + endif + call s:DoWincmd(below ? 'j' : 'k') + " New channel will be opened here + endif + let s:autocmd_disabled = autocmd_disabled +endfunction + +function! s:ModifyBuf(modify, ...) + " XXX: This should NOT be called recursively + let bufnum = a:0 ? a:1 : bufnr('%') + if a:modify + if !exists('s:save_undolevels') + let s:save_undolevels = &undolevels + set undolevels=-1 + endif + call setbufvar(bufnum, '&modifiable', 1) + else + if exists('s:save_undolevels') + let &undolevels = s:save_undolevels + unlet s:save_undolevels + endif + if 0 && !s:IsBufType_Command(bufnum) + call setbufvar(bufnum, '&modifiable', 0) + endif + endif +endfunction + +function! s:PeekBuf(orgwin) + if !a:orgwin + call s:PrePeekBuf() + else + call s:PostPeekBuf(a:orgwin) + endif +endfunction + +function! s:PrePeekBuf() + let s:autocmd_disabled = 1 + let &equalalways = 0 + call s:CanOpenChanServ() " XXX: is this necessary? + call s:OpenBuf('vertical 1split') +endfunction + +function! s:PostPeekBuf(orgwin) + call s:ExecuteSilent('close!') + call s:WinVisit(a:orgwin) + let s:autocmd_disabled = 0 +endfunction + +" +" Buffer initilization +" + +function! s:RegistMap(key, func, ...) + let keylist = a:key + while strlen(keylist) + let key = s:StrDivide(keylist, 1) + let modelist = a:0 ? a:1 : 'n' + while strlen(modelist) + let mode = s:StrDivide(modelist, 1) + execute mode.'noremap ' (key) + \ (mode == 'i' ? '' : '').':'. + \ (mode != 'v' ? '' : '').'call '.a:func.'' + let modelist = s:StrDivide(modelist, 2) + endwhile + let keylist = s:StrDivide(keylist, 2) + endwhile +endfunction + +function! s:UnregistMap(key) + let keylist = a:key + while strlen(keylist) + call s:ExecuteSilent('unmap '.s:StrDivide(keylist, 1)) + let keylist = s:StrDivide(keylist, 2) + endwhile +endfunction + +function! s:DoSettings() + setlocal bufhidden=hide + setlocal buftype=nofile + setlocal nolist + setlocal nonumber + setlocal noswapfile + setlocal wrap + + call s:RegistMap('', 'MainLoop()') + call s:RegistMap('', 'HandleEnter(0)') + call s:RegistMap(',,,', 'HandleEnter(1)') + call s:RegistMap('a,A,i,I,o,O', 'OpenBuf_Command(1)') + call s:RegistMap('p,P', 'Beep(1)', 'n,v') + call s:RegistMap('q,ZZ', 'QuitWhat(0)') + call s:RegistMap('Q,ZQ', 'QuitWhat(1)') + call s:RegistMap('r', 'ReconnServer(0)') + call s:RegistMap('R', 'ReconnServer(1)') + call s:RegistMap('', 'RestoreWinSize(1)') + call s:RegistMap('', 'OpenNextChanServ(1, 0)') + call s:RegistMap('', 'OpenNextChanServ(0, 0)') + call s:RegistMap('', 'OpenNextChanServ(1, 1)') + call s:RegistMap('', 'OpenNextChanServ(0, 1)') + call s:RegistMap('', 'Cmd_HELP()') + + call s:HiliteClear() +endfunction + +function! s:DoSyntax() + " NOTE: I'm really bad at syntax highlighting. It's horrible + " NOTE: Do not overdo. It'll slow things down + syntax match VimIRCUserHead display "^\S\+\%( \S\+:\)\=" contains=@VimIRCUserName + syntax match VimIRCTime display "^\d\d:\d\d" containedin=VimIRCUserHead contained + syntax match VimIRCBullet display "[*!]" containedin=VimIRCUserHead contained + " User names + syntax cluster VimIRCUserName contains=VimIRCUserPrivMSG,VimIRCUserNotice,VimIRCUserAction,VimIRCUserQuery + syntax match VimIRCUserPrivMSG display "<\S\+>" contained + syntax match VimIRCUserNotice display "\[\S\+\]" contained + syntax match VimIRCUserAction display "\*\S\+\*" contained + syntax match VimIRCUserQuery display "?\S\+?" containedin=VimIRCUserHead contained + + syntax match VimIRCUnderline display ".\{-\}" contains=VimIRCIgnore + syntax match VimIRCBold display ".\{-\}" contains=VimIRCIgnore + syntax match VimIRCIgnore display "[]" contained + + highlight link VimIRCTime String + highlight link VimIRCUserHead PreProc + highlight link VimIRCBullet WarningMsg + highlight link VimIRCUserPrivMSG Identifier + highlight link VimIRCUserNotice Statement + highlight link VimIRCUserAction WarningMsg + highlight link VimIRCUserQuery Question + highlight link VimIRCUnderline Underlined + " FIXME: This doesn't work + highlight link VimIRCIgnore Ignore + highlight VimIRCBold gui=bold cterm=bold term=bold +endfunction + +function! s:DoSyntax_Info() + syntax match VimIRCInfoHasNew "^\s*+.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum + syntax match VimIRCInfoActive "^\s*\*.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum + syntax match VimIRCInfoDead "^\s*-.*$" contains=VimIRCInfoIndic,VimIRCInfoBufNum + syntax match VimIRCInfoIndic "^\s*[*+-]" contained + syntax match VimIRCInfoBufNum "\[-\=\d\+\]" + + highlight link VimIRCInfoHasNew DiffChange + highlight link VimIRCInfoDead Comment + highlight link VimIRCInfoIndic Ignore + highlight link VimIRCInfoBufNum Comment + execute 'highlight link VimIRCInfoActive '.s:hl_cursor +endfunction + +function! s:DoSyntax_Server() + call s:DoSyntax_Channel() + syntax match VimIRCWallop display "!\S\+!" contained containedin=VimIRCUserHead + + highlight link VimIRCWallop WarningMsg +endfunction + +function! s:DoSyntax_List() + syntax match VimIRCListChan "^[&#+!]\S\+\s\+\d\+" contains=VimIRCListMember + syntax match VimIRCListMember "\<\d\+\>" contained + + highlight link VimIRCListChan Identifier + highlight link VimIRCListMember Number +endfunction + +function! s:DoSyntax_Channel() + call s:DoSyntax() + " FIXME: Highlight for chanops disappears sometimes after buffer reopen, why? + syntax match VimIRCChanEnter display "->" contained containedin=VimIRCUserHead + syntax match VimIRCChanExit display "<[-=]" contained containedin=VimIRCUserHead + syntax match VimIRCChanPriv display "[@+]" contained containedin=@VimIRCUserName + + highlight link VimIRCChanEnter DiffChange + highlight link VimIRCChanExit DiffDelete + highlight link VimIRCChanPriv Statement +endfunction + +function! s:DoSyntax_Nicks() + syntax match VimIRCNicksChop "^@" + syntax match VimIRCNicksVoice "^+" + + highlight link VimIRCNicksChop Identifier + highlight link VimIRCNicksVoice Statement +endfunction + +function! s:DoSyntax_Chat() + call s:DoSyntax() + syntax match VimIRCUserChat display "=\S\+=" containedin=VimIRCUserHead contained + highlight link VimIRCUserChat Special +endfunction + +function! s:InitBuf_Info(bufname) + let b:server = 'Connections' + let b:title = ' '.b:server + + call s:DoSettings() + call s:DoSyntax_Info() + setlocal nowrap + + call s:SetBufNum(a:bufname) +endfunction + +function! s:InitBuf_Server(bufname, port) + let b:server = s:server + let b:port = a:port + + call setline(1, s:GetTime(1).' *: Connecting with '.s:server.'...') + call s:DoSettings() + call s:DoSyntax_Server() + + call s:SetBufNum(a:bufname) + call s:SetUserMode('') +endfunction + +function! s:InitBuf_List(bufname) + let b:server = s:server + let b:channel = '*list' + let b:title = ' List of channels @ '.s:server + let b:updated = 0 + let b:updating= 1 + + call s:DoSettings() + call s:DoSyntax_List() + setlocal nowrap + " I take Mutt's key-bindings for sorting + call s:RegistMap('o', 'SortSelect()') + call s:RegistMap('O', 'SortReverse()') + call s:RegistMap('R', 'UpdateList()') + + call s:SetBufNum(a:bufname) +endfunction + +function! s:InitBuf_Channel(bufname, channel) + let b:server = s:server + let b:channel = a:channel + let b:cmode = '' + let b:topic = '' + let b:title = ' '.a:channel.' @ '.s:server + + call setline(1, s:GetTime(1).' *: Now talking in '.a:channel) + call s:DoSettings() + call s:DoSyntax_Channel() + + call s:SetBufNum(a:bufname) +endfunction + +function! s:InitBuf_Chat(bufname, nick, server) + " NOTE: a:server is the name of the IRC server, not the dcc peer's + let b:server = a:server + let b:channel = a:nick + let b:title = ' Chatting with '.a:nick + + call setline(1, s:GetTime(1).' *: Now chatting with '.a:nick) + call s:DoSettings() + call s:DoSyntax_Chat() + + let bufnum = s:SetBufNum(a:bufname) + if !s:autocmd_disabled && s:OpenBuf_Nicks(a:nick, a:server) + call s:BufVisit(bufnum) + endif +endfunction + +function! s:InitBuf_Nicks(bufname, channel, server) + let b:server = a:server + let b:channel = a:channel + let b:title = a:channel + + call s:DoSettings() + call s:DoSyntax_Nicks() + setlocal nowrap + call s:RegistMap('', 'SelectNickAction()', 'n,v') + + call s:SetBufNum(a:bufname) + if s:IsNick(a:channel) + call s:FillBuf_Nicks(a:channel, a:server) + endif +endfunction + +function! s:InitBuf_Command(bufname, channel) + let b:server = s:server + let b:channel = a:channel + let b:title = ' '.(s:IsNick(a:channel) + \ ? 'Chatting with' + \ : 'Posting to').' '.(s:IsChanChat(a:channel) + \ ? a:channel.' @ ' : '').s:server + + call s:DoSettings() + setlocal expandtab + call s:RegistMap('', 'SendLines()', 'n,i,v') + call s:UnregistMap('a,A,i,I,o,O,p,P') + + call s:SetBufNum(a:bufname) +endfunction + +function! s:FillBuf_Nicks(nick, server) + call s:ModifyBuf(1) + call s:BufClear() + call setline(1, a:nick) + call append(1, s:GetCurNick(a:server)) + call s:ModifyBuf(0) +endfunction + +" +" Closing buffers +" + +function! s:PreCloseBuf_Server() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let server = getbufvar(abuf, 'server') + if strlen(server) " validity check + call s:CloseBuf_Command('', server) + endif +endfunction + +function! s:PreCloseBuf_List() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let server = getbufvar(abuf, 'server') + if strlen(server) && !s:VisitBuf_Server(server) + call s:CloseBuf_Command('', server) + endif +endfunction + +function! s:PreCloseBuf_Channel() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let channel = getbufvar(abuf, 'channel') + let server = getbufvar(abuf, 'server') + if strlen(channel) && strlen(server) + call s:CloseBuf_Nicks(channel, server) + call s:CloseBuf_Command(channel, server) + endif +endfunction + +function! s:PreCloseBuf_Chat() + if s:autocmd_disabled + return + endif + + let abuf = expand('') + 0 + let nick = getbufvar(abuf, 'channel') + let server= getbufvar(abuf, 'server') + if strlen(nick) && strlen(server) + call s:CloseBuf_Nicks(nick, server) + call s:CloseBuf_Command(nick, server) + endif +endfunction + +function! s:PreCloseBuf_Command(destbuf, purge) + " Do NOT define any autocmd events to trigger this + if s:autocmd_disabled + return + endif + + let cmdbuf = a:destbuf ? s:GetBufNum_Command(s:channel) + \ : expand('') + 0 + let opened = s:BufVisit(cmdbuf) + + if opened || s:OpenBufNum('belowright 1split', cmdbuf) + call s:NeatenBuf_Command(a:purge) + if a:destbuf + if opened && s:IsNick(s:channel) " keep it open when chatting + call s:RedrawScreen(0) + else + let s:autocmd_disabled = 1 + call s:BufClose(cmdbuf) + let s:autocmd_disabled = 0 + endif + call s:PostCloseBuf_Command(cmdbuf, a:destbuf) + endif + endif +endfunction + +function! s:CloseBuf_Server() + " Close associated windows before closing the server window + call s:CloseBuf_List() + + let bufnum = s:GetBufNum_Server() + if bufnum >= 0 + if s:log >= 2 + call s:LogBuffer(bufnum) + endif + " Mark this buffer as dead so that the next /SERVER invocation can close + " this + call setbufvar(bufnum, 'dead', 1) + endif +endfunction + +function! s:CloseBuf_List() + let bufnum = s:GetBufNum_List() + if bufnum >= 0 + call s:CacheList(bufnum) + call s:OpenBuf_Server() + call s:BufClose(bufnum) + endif +endfunction + +function! s:CloseBuf_Channel(channel) + let bufnum = s:GetBufNum_Channel(a:channel) + if bufnum >= 0 + call s:LogBuffer(bufnum) + if !s:VisitBuf_ServList() + call s:OpenBuf_Server() + endif + call s:BufClose(bufnum) + if !s:IsBufType_List() + call s:WinScroll2('.') + endif + endif +endfunction + +function! s:CloseBuf_Chat(nick, server) + let save_server = s:server + let s:server = a:server + + let bufnum = s:GetBufNum_Chat(a:nick, a:server) + if bufnum >= 0 + call s:LogBuffer(bufnum) + if !s:VisitBuf_ServList() + call s:OpenBuf_Server() + endif + call s:BufClose(bufnum) + if !s:IsBufType_List() + call s:WinScroll2('.') + endif + endif + + let s:server = save_server +endfunction + +function! s:CloseBuf_Nicks(channel, server) + call s:BufClose(s:GetBufNum_Nicks(a:channel, a:server)) +endfunction + +function! s:CloseBuf_Command(channel, server) + call s:BufClose(s:GetBufNum_Command(a:channel, a:server)) +endfunction + +function! s:PostCloseBuf_Command(cmdbuf, destbuf) + if a:destbuf != a:cmdbuf + " Move the cursor to the destined place + call s:BufVisit(a:destbuf) + else + " Move the cursor back onto the channel/server where command mode was + " (supposedly) triggered. + call s:VisitBuf_ChanServ(s:channel) + endif + if !s:IsBufType_List() + " XXX: I do this to prevent channels from unexpectedly scrolling UP due + " to BufClose above. + call s:WinScroll2('.') + endif +endfunction + +" Make the command buffer look like a command-line history +function! s:NeatenBuf_Command(purge) + call s:ModifyBuf(1) + call s:HiliteClear() + + if a:purge + " We move the input line to the bottom + let line = getline('.') + if strlen(line) + call s:DelLine() + " Remove duplicates, if any + while s:SearchLine(line) + call s:DelLine() + endwhile + call {strlen(getline('$')) ? 'append' : 'setline'}('$', line) + endif + endif + + call s:BufTrim() + call s:OpenNewLine() + call s:ModifyBuf(0) +endfunction + +" +" Closing windows +" + +function! s:CloseWin_What(cond) + let retval = 1 + let curbuf = bufnr('%') + + let s:autocmd_disabled = 1 + call s:DoWincmd('b') + while 1 + " TODO: Accept arguments and/or multiple conditions? + if s:{a:cond}() + " XXX: Vim crashes here sometimes + if !s:WinClose() + let retval = 0 + break + endif + elseif winnr() == 1 + break + else + call s:DoWincmd('W') + endif + endwhile + + call s:BufVisit(curbuf) + let s:autocmd_disabled = 0 + + return retval +endfunction + +function! s:CloseWin_IRC() + if !s:CloseWin_What('IsBufType_IRC') + enew! + endif + call s:RedrawScreen(0) +endfunction + +function! s:CloseWin_DeadServer() + call s:CloseWin_What('IsBufType_ServerDead') + call s:ClearCommand(0) + call s:RedrawScreen(0) +endfunction + +" +" Restoring windows +" + +function! s:GetPrevLine() + return line("'\"") +endfunction + +" Restore the previous cursor position after re-opening a buffer +function! s:RestorePrevLine() + let lnum = s:GetPrevLine() + if s:autocmd_disabled + if lnum > 0 && line('.') != lnum + execute lnum + endif + else + call s:WinScroll2(lnum) + endif +endfunction + +" Tackle the annoyance of unexpected window scrolling when opening another +" buffer +function! s:RestoreWinLine(bufnum, winline) + let curwin = winnr() + if s:BufVisit(a:bufnum) + call s:WinScroll(winline() - a:winline) + call s:WinVisit(curwin) + endif +endfunction + +function! s:RestoreWinSize(restore) + if !s:opened + return + endif + + let s:autocmd_disabled = 1 + if a:restore + let orgwin = winnr() + let prevwin = s:GetWinNum_Prev(0) + let equalalways = s:ToggleEqualWin(1) + endif + + let botwin = s:DoWincmd('b') + call s:DoWincmd('t') + while 1 + if s:IsBufType_Nicks() + call s:WinResize(s:nickswidth, 1) + elseif s:IsBufType_Command() + call s:WinResize(s:cmdheight, 0) + elseif s:IsBufType_Info() + call s:WinResize(s:infowidth, 1) + endif + if s:IsBufType_ChanServ() && !s:IsBufType_List() + " XXX: Window could unexpectedly scroll up due to resizing + call s:WinScroll2('.') + else + call s:DoNormal('^') + endif + if winnr() == botwin + break + endif + call s:DoWincmd('w') + endwhile + + if a:restore + call s:WinVisit(prevwin) + call s:WinVisit(orgwin) + call s:RedrawScreen(1) + let &equalalways = equalalways + endif + let s:autocmd_disabled = 0 +endfunction + +function! s:GetWinNum_Prev(restore) + let curwin = winnr() + call s:DoWincmd('p') + let prevwin = winnr() + if a:restore + call s:DoWincmd('p') + endif + return prevwin - ((curwin > 1 && curwin == prevwin) ? 1 : 0) +endfunction + +function! s:ToggleEqualWin(on) + let equalalways = &equalalways + if a:on + let &equalalways = !&equalalways + endif + let &equalalways = a:on + + return equalalways +endfunction + +" +" Providing some user interaction +" + +function! s:HandleAt(comd) + if a:comd =~ '[:=]$' + let ex = (a:comd =~ ':$') + if ex + let comd = histget('@', -1) + else + call s:RedrawScreen(0) + let comd = substitute(s:DoInput(0, '=', ''), "\\%(^[\"']\\|[\"']$\\)", + \ '', 'g') + endif + if s:DoIterate(ex, comd, s:ComputeCount(a:comd)) + call s:PromptEnter(2) + endif + else + call s:DoNormal(a:comd, 1) + endif +endfunction + +function! s:HandleEnter(shifted, ...) + if s:IsBufType_Info() + return s:EnterChanServ(a:shifted) + elseif !a:shifted + if s:IsBufType_Command() + return s:SendLines() + elseif s:IsBufType_Nicks() + return s:SelectNickAction() + endif + endif + return s:OpenLink(a:shifted, (a:0 ? a:1 : 1)) +endfunction + +function! s:HandleKey(key) + let char = s:Key2Char(a:key) + if !a:key + call s:UnstickKey(0) + endif + + call s:HiliteClear() + " TODO: User-configurable maps + " Place movement commands earlier, to get quick response + if (a:key >= 2 && a:key <= 10) + \ || char =~# '^[jkhlwbeWBE^$0*#nN+\-;,GHLM()]$' + \ || char == "\" || char == "\" || char == "\" + \ || char == "\" || char == "\" + " One char commands + if a:key == 9 " + let char = '1'.char + endif + call s:DoNormal(char) + elseif char =~# '^[ p]$' " scroll forward/backward + call s:DoNormal(nr2char(2 * (char == ' ' ? 3 : 1))) + elseif s:Is2KeyCmd(char) + " Commands which take a second char + call s:HandleMultiKey(char, 0) + elseif (char + 0) || char == "\" + " Accept things like "10G", "2k" + call s:HandleMultiKey(char, 1) + elseif char == "\" || char == "\" || char == "\" + call s:HandleEnter(!(char == "\")) + elseif char == "\" || char == "\" + call s:HandleNextChanServ(char) + elseif char == ':' + if s:Execute(s:DoInput(0, ':', '')) + " Pause after commands which produce outputs + return s:PromptEnter(2) + endif + elseif char =~# '^[/?]$' + call s:SearchWord(char) + elseif char == "\" + call s:RestoreWinSize(1) + elseif char =~# '^[ORo]$' && s:IsBufType_List() + if char ==# 'R' + call s:UpdateList() + else + call s:Sort{char ==# 'o' ? 'Select' : 'Reverse'}() + endif + elseif char =~? '^[AIO]$' + if s:IsBufType_Command() && char =~# '^[AIa]$' + " Position cursor at an appropriate place + if char ==# 'I' + call s:DoNormal('^') + endif + call s:DoInsert(char ==? 'a') + else + call s:OpenBuf_Command(1) + endif + call s:RedrawScreen(0) + throw 'IMGONNAPOST' + elseif char == "\" + call s:Cmd_HELP() + elseif char ==? 'r' + call s:ReconnServer(char ==# 'R') + elseif char ==? 'q' + call s:QuitWhat(char ==# 'Q') + endif + + call s:HiliteBuffer() + " Accept repetitive inputs + let key = s:ConsumeKey() + if !s:IsZero(key) + return s:HandleKey(key) + endif + + call s:SetLastActive() +endfunction + +function! s:HandleMultiKey(char, multi) + let char = '' + let comd = a:char + let cnt = (a:char + 0) + let ctrl_w = s:IsCtrl_W(a:char) + let multi = a:multi + + while 1 + call s:ShowCmd(comd) + let char = s:GetChar(0) + if (char =~# '^[[:escape:]]\=$') || (multi && !cnt && s:IsZero(char)) + let comd = '' + if s:IsZero(char) + call s:Beep(1) + endif + elseif char == "\" + let comd = (comd =~ '\d$') ? substitute(comd, '.$', '', '') : '' + if strlen(comd) + continue + endif + else + let comd = comd.char + endif + + " Continue if it is a number or + if !(multi && strlen(comd)) + break + endif + + if (char >= '0' && char <= '9') + " `cnt' is a mere indicator which shows that user is currently typing + " count + let cnt = 1 + else + if ctrl_w + break + elseif s:IsCtrl_W(char) + let cnt = 0 + let ctrl_w = 1 + elseif s:Is2KeyCmd(char) + let multi = 0 + else + break + endif + endif + endwhile + + call s:UnstickKey(0) + if !strlen(comd) + return + endif + + if char == "\" || char == "\" || char == "\" + call s:HandleEnter((ctrl_w || char != "\"), s:ComputeCount(comd)) + elseif char =~# '^[]$' || (ctrl_w && char =~? '^[NP]$') + " XXX: Overriding Vim's p, which isn't very useful because of + " redrawing of nicks and info-bar windows. + call s:HandleNextChanServ(comd) + elseif comd =~ '@.$' + call s:HandleAt(comd) + elseif comd =~# 'Z[ZQ]$' + call s:QuitWhat(comd =~# 'Q$') + else + call s:DoNormal(comd, 1) + endif +endfunction + +function! s:HandleNextChanServ(comd) + call s:OpenNextChanServ((a:comd =~? '[N]'), (a:comd =~# ''), + \ s:ComputeCount(a:comd)) +endfunction + +function! s:ComputeCount(comd) + let cnt = 1 + let nums = s:StrCompress(substitute(a:comd, '\D\+', ' ', 'g')) + while strlen(nums) + let cnt = cnt * s:StrDivide(nums, 1) + let nums = s:StrDivide(nums, 2) + endwhile + return cnt +endfunction + +" Make use of the key input for the 'Hit any key to continue' prompt +function! s:HandlePromptKey(timeout, mesg, ...) + let key = s:PromptKey((10 * a:timeout), a:mesg, (a:0 ? a:1 : 'Question')) + " Do nothing with space, etc. + if s:Key2Char(key) =~# '^[ [:return:][:escape:]]\=$' + call s:HiliteBuffer() + else + try + call s:HandleKey(key) + catch /^\S\+:E/ + call s:IgnoreException(v:exception) + endtry + endif +endfunction + +function! s:SendLines() range + " TODO: Do confirmation before sending (preferably optionally) + if !s:IsBufType_Command() + return + endif + + try + let cmdbuf = bufnr('%') + let destbuf= cmdbuf + + let i = a:firstline + while 1 + try + " Remember the context for a while in which the command/message was + " entered. + " NOTE: Current server/buffer may change each time after SendLine() + call s:SetCurServer(getbufvar(cmdbuf, 'server'), + \ getbufvar(cmdbuf, 'channel')) + + if i > a:lastline || !s:BufVisit(cmdbuf) + " NOTE: Do NOT put this in the `finally' clause: HandleKey() may + " want to reuse this command buffer + call s:PreCloseBuf_Command(destbuf, (a:firstline == a:lastline)) + break + endif + " Get the destination buffer + let destbuf = s:SendLine(s:ExpandAlias(s:StrTrim(getline(i)))) + " Prevent sending multiple lines too fast to the server + call s:DoWait(a:lastline, i) + catch /^SYNTAX ERROR/ + call s:EchoError(v:exception) + endtry + let i = i + 1 + endwhile + catch /^IMGONNA/ + if s:in_loop + throw v:exception + endif + return + finally + unlet! s:channel + endtry + + call s:SetLastActive() + call s:MainLoop() +endfunction + +function! s:SendLine(line) + if strlen(a:line) + let comd = '' + let args = a:line + + let rx = s:GetCmdRx(comd) + if a:line =~ rx + let s:split = strlen(s:StrMatch(a:line, rx, '\1')) + + let comd = s:ExpandCmd(s:StrMatch(a:line, rx, '\2')) + let args = s:ExpandArgs(comd, s:StrMatch(a:line, rx, '\3')) + + if strlen(args) + " This provision is only for removing a leading colon which user + " unnecessarily appended to the message and adding it back! Silly and + " redundant + let rx = s:GetCmdRx(comd) + " NOTE: Matching with empty string always results in true (!!) + if strlen(rx) && args =~ rx + let args = s:StrMatch(args, rx, '\1').s:StrMatch(args, rx, ' :\2') + endif + let args = s:StrTrim(args) + endif + endif + + " Values will be copied several times by value, which I'm pretending + " I don't mind now: I just felt like making this function look simple. + if s:PreDoSend(comd, args) + call s:PostDoSend(comd, strlen(args)) + endif + let s:split = 0 + endif + return bufnr('%') +endfunction + +function! s:PreSendMSG(mesg) + let send = 0 + let mesg = substitute(a:mesg, '^/\s*', '', '') + if strlen(mesg) + let send = strlen(s:channel) + if send + call s:SendMSG('PRIVMSG', s:channel.' :'.mesg) + else + call s:EchoError('You are not on a channel.') + endif + endif + return send +endfunction + +function! s:PreDoSend(comd, args) + let send = exists('*s:Cmd_{a:comd}') + if send + call s:Cmd_{a:comd}(a:args) + else + let send = (s:IsConnected() || a:comd =~# '^G\%(QUIT\|AWAY\)$') + if send + if !strlen(a:comd) + let send = s:PreSendMSG(a:args) + elseif exists('*s:Send_{a:comd}') + call s:Send_{a:comd}(a:comd, a:args) + else + call s:DoSend(a:comd, a:args) + endif + else + call s:EchoWarn('Do /SERVER first to get connected') + endif + endif + return send +endfunction + +" Move cursor to an appropriate window +function! s:PostDoSend(comd, arglen) + if a:comd =~# '^\%(JOIN\|LIST\|NOTICE\|PRIVMSG\)$' + return + endif + + if s:IsChanChat(s:channel) && a:comd !~# '^\%(INFO\|MOTD\|NAMES\|WHO\)$' + " If user entered the command from a channel, visit there + call s:VisitBuf_ChanChat(s:channel) + " Scroll to the bottom only if user is in talkative mood + if a:comd !~# '^\%(ACTION\)\=$' + return + endif + else + " When receiving a bunch of lines, visit the server window beforehand: + " avoid flicker caused by cursor movement between server and channel + " windows + if a:comd =~# '^\%(NAMES\)$' && a:arglen + " The message will be quite short. O.K. to exit here + return + endif + call s:VisitBuf_Server() + endif + + if s:IsBufType_ChanServ() + " Prepare receiving lines + call s:WinLine('$') + endif +endfunction + +function! s:SetLastActive() + if !s:autoaway || s:lastactive >= localtime() + return + endif + + let lastactive = s:lastactive + let s:lastactive = localtime() + " Clear the `away' status only if considered to be set... + if s:lastactive >= lastactive + s:autoawaytime + " since I don't want to call this perl code every time you type something + call s:Send_GAWAY('AWAY', '') + endif +endfunction + +function! s:SelectNickAction() range + if !(s:IsBufType_Nicks() && s:IsConnected(b:server)) + return + endif + + let nicks = '' + let i = a:firstline + while i <= a:lastline + let nick = substitute(getline(i), '^[@+]\+', '', '') + if strlen(nick) + let nicks = nicks.(strlen(nicks) ? ' ' : '').nick + endif + let i = i + 1 + endwhile + + let number= a:lastline - a:firstline + 1 + let comds = "&whois\n&query\n&control\n&dcc/ctcp" + let choice = s:Confirm('What do you do with '.nicks.'?', comds) + if !choice + return + endif + + if !(choice % 2) && number > 1 + " TODO: Do it. + return s:EchoError('Sorry, you cannot do it with mulitple persons at '. + \'the same time') + endif + + if (choice == 2 || choice == 3) && !s:IsChannel(b:channel) + return s:EchoError('You cannot do it unless you are on a channel.') + endif + + " Set the current server appropriately, so the command will be sent to the + " one user intended + call s:SetCurServer(b:server, b:channel) + try + if choice == 1 + call s:DoSend('WHOIS', substitute(nicks, ' ', ',', 'g')) + elseif choice == 2 + call s:StartChat(nicks) + elseif choice == 3 + call s:SelectNickOpVoice(nicks) + elseif choice == 4 + call s:SelectNickCTCP(nicks) + endif + catch /^IMGONNA/ + if s:in_loop + throw v:exception + endif + return + finally + unlet! s:channel + endtry + + if !s:in_loop + call s:SetLastActive() + endif +endfunction + +function! s:SelectNickOpVoice(nicks) + let choice = s:Confirm('Choose one of these:', + \ "&1 Op\n&2 Deop\n&3 Voice\n&4 Devoice") + if choice + let onoff = (choice % 2) ? '+' : '-' + let mode = choice >= 3 ? 'v' : 'o' + call s:SetOpVoice(onoff, mode, a:nicks) + endif +endfunction + +function! s:SelectNickCTCP(nicks) + let choice = s:Confirm('Choose one of these:', + \ "&send\n&chat\n&ping\n&time\n&version\nclient&info") + if choice + let type = 'CTCP' + let args = '' + + if choice >= 3 + if choice == 3 + let args = 'ping' + elseif choice == 4 + let args = 'time' + elseif choice == 5 + let args = 'version' + elseif choice == 6 + let args = 'clientinfo' + endif + let args = a:nicks.' '.args + else + let type = 'DCC' + let args = (choice == 1 ? 'send' : 'chat').' '.a:nicks + endif + call s:Send_{type}(type, args) + endif +endfunction + +" +" Selecting sort method +" + +function! s:SortSelect() + if !s:IsBufType_List() || s:GetVimVar('b:updating') + return + endif + + let choice = s:Confirm("Sort by what?", "&channel\n&member\n&topic") + if !choice + return + endif + + if choice == 1 + let cmp = 'channel' + elseif choice == 2 + let cmp = 'member' + elseif choice == 3 + let cmp = 'topic' + endif + + let orgln = getline('.') + call s:ModifyBuf(1) + call s:SortList(cmp, (s:GetVimVar('b:sortdir') + 0)) + call s:ModifyBuf(0) + call s:SearchLine(orgln) +endfunction + +function! s:SortReverse() + if !s:IsBufType_List() || s:GetVimVar('b:updating') + return + endif + + let orgln = line('.') + call s:ModifyBuf(1) + call s:BufReverse() + call s:ModifyBuf(0) + let b:sortdir = !(s:GetVimVar('b:sortdir')) + execute (line('$') - orgln + 1) +endfunction + +" +" Controlling VimIRC options +" + +function! s:OptList(list) + let list = a:list + + call s:EchoHL('Current setting'.(list =~ ' ' ? 's' : '').':', 'Title') + while strlen(list) + let opt = s:StrDivide(list, 1) + echo opt.':' s:GetVimVar('s:'.opt) + let list = s:StrDivide(list, 2) + endwhile + + call s:PromptEnter(3) +endfunction + +" Internalize user variables (for safety) +function! s:OptSet(opt, ...) + let opt = substitute(a:opt, '^vimirc_', '', '') + let s:{opt} = s:GetVimVar('g:'.a:opt) + unlet! g:{a:opt} + + " If it wasn't defined by the user, set it + if !strlen(s:{opt}) && a:0 + let s:{opt} = a:1 + endif + call s:OptValidate(opt) +endfunction + +function! s:OptValidate(opt) + let var = 's:{a:opt}' + " Fix irregularities if encountered + if a:opt =~# '^\%(autoaway\|autoawaytime\|cmdheight\|dccport\|infowidth\|listexpire\|log\|nickswidth\)$' + " Make sure the value is a valid numeral + let {var} = {var} + 0 + if a:opt ==# 'dccport' + " Avoid well-known ports + if {var} <= 1023 + let {var} = 1024 + endif + elseif a:opt =~# '\%(height\|width\)$' + if {var} <= 0 " not acceptable + let {var} = 1 + endif + call s:RestoreWinSize(1) + endif + else + let {var} = s:StrCompress({var}) + if a:opt ==# 'partmsg' + " Prepend a leading colon + let {var} = substitute({var}, '^[^:]', ':&', '') + elseif a:opt ==# 'browser' + let {var} = substitute({var}, '^!', '', '') + elseif a:opt =~# '\%(dir\|file\)$' + let {var} = s:ValidatePath({var}) + endif + endif +endfunction + +" "/SET option value" +function! s:Cmd_SET(optval, ...) + let unset = (a:0 && a:1) + " Settable options + let numopt = 'autoaway autoawaytime browser log dccport listexpire '. + \'infowidth nickswidth cmdheight' + let stropt = 'partmsg' + + let rx = '^\(\S\+\)\%(\s\+\(.\+\)\)\=$' + let opt = tolower(s:StrMatch(a:optval, rx, '\1')) + let val = s:StrMatch(a:optval, rx, '\2') + + if strlen(opt) + \ && ( opt =~# '^\%('.substitute(numopt, ' ', '\\|', 'g').'\)$' + \ || opt =~# '^\%('.substitute(stropt, ' ', '\\|', 'g').'\)$') + if strlen(val) || unset + let s:{opt} = val + call s:OptValidate(opt) + endif + call s:OptList(opt) + else + call s:OptList(numopt.' '.stropt) + endif +endfunction + +function! s:Cmd_UNSET(opt) + call s:Cmd_SET(a:opt, 1) +endfunction + +" +" Controlling user privileges +" + +function! s:SetOpVoice(onoff, mode, args) + let channel = s:StrDivide(a:args, 1) + let nicks = s:StrDivide(a:args, 2) + if !s:IsChannel(channel) + let channel = s:channel + let nicks = a:args + endif + + if !(s:IsChannel(channel) && strlen(nicks)) + return s:EchoError('Syntax: /op [channel] nick(s)') + endif + + let nicks = s:StrCompress(substitute(nicks, ',', ' ', 'g')) + let number= strlen(substitute(nicks, '\S\+', '', 'g')) + 1 + let modes = s:StrMultiply(a:mode, number) + + call s:DoSend('MODE', channel.' '.a:onoff.modes.' '.nicks) +endfunction + +function! s:Cmd_OP(args) + call s:SetOpVoice('+', 'o', a:args) +endfunction + +function! s:Cmd_DEOP(args) + call s:SetOpVoice('-', 'o', a:args) +endfunction + +function! s:Cmd_VOICE(args) + call s:SetOpVoice('+', 'v', a:args) +endfunction + +function! s:Cmd_DEVOICE(args) + call s:SetOpVoice('-', 'v', a:args) +endfunction + +" +" Aliasing +" + +function! s:ExpandAlias(line) + let line = a:line + let rx = s:GetCmdRx('') + if a:line =~ rx + let varname = s:RC_Varname('alias', toupper(s:StrMatch(a:line, rx, '\2'))) + if exists('g:{varname}') + let args = s:StrMatch(a:line, rx, '\3') + let line = g:{varname}.(strlen(args) ? ' '.args : '') + + if strlen(s:StrMatch(a:line, rx, '\1')) && match(line, '/\csplit') + let line = '/SPLIT '.line + endif + endif + endif + return line +endfunction + +function! s:Cmd_ALIAS(line) + let rx = '^/\=\(\S\+\)\s\+/\=\(.\+\)$' + if a:line =~ rx + if s:RC_Open(1) + let alias = toupper(s:StrMatch(a:line, rx, '\1')) + let comd = s:StrMatch(a:line, rx, '/\2') + call s:RC_Section('Aliases') + call s:RC_Set('alias', alias, comd) + else + call s:EchoError('Failed in registering alias') + endif + call s:RC_Close() + else + call s:EchoError('Syntax: /ALIAS alias command (with arguments)') + endif +endfunction + +function! s:Cmd_UNALIAS(alias) + if s:RC_Open(0) + call s:RC_Unset('alias', toupper(substitute(a:alias, '^/', '', ''))) + endif + call s:RC_Close() +endfunction + +" Expand command aliases (after s:ExpandAlias()) +function! s:ExpandCmd(comd) + " NOTE: Beware of collisions + let comd = toupper(a:comd) + if comd =~# '^\%(B\%[YE]\|E\%[XIT]\|SI\%[GNOFF]\)$' + let comd = 'QUIT' + elseif comd =~# '^\%(CH\%[AT]\)$' + let comd = 'QUERY' + elseif comd =~# '^DA\%[TE]$' + let comd = 'TIME' + elseif comd =~# '^\%(LE\%[AVE]\)$' + let comd = 'PART' + elseif comd =~# '^\%(ME\)$' + let comd = 'ACTION' + elseif comd =~# '^\%(M\%[SG]\)$' + let comd = 'PRIVMSG' + elseif comd =~# '^\%(NICKS\)$' + let comd = 'NAMES' + elseif comd =~# '^\%(\%(RE\)\=CO\%[NNECT]\)$' + let comd = 'SERVER' + else + " Handle abbreviations + if comd =~# '^AC\%[TIO]$' + let comd = 'ACTION' + elseif comd =~# '^AL\%[IA]$' + let comd = 'ALIAS' + elseif comd =~# '^AWA\=$' + let comd = 'AWAY' + elseif comd =~# '^CTC\=$' + let comd = 'CTCP' + elseif comd =~# '^DC$' + let comd = 'DCC' + elseif comd =~# '^DEO$' + let comd = 'DEOP' + elseif comd =~# '^DES\%[CRIB]$' + let comd = 'DESCRIBE' + elseif comd =~# '^DEV\%[OIC]$' + let comd = 'DEVOICE' + elseif comd =~# '^GA\%[WA]$' + let comd = 'GAWAY' + elseif comd =~# '^GQ\%[UI]$' + let comd = 'GQUIT' + elseif comd =~# '^H\%[EL]$' + let comd = 'HELP' + elseif comd =~# '^INF\=$' + let comd = 'INFO' + elseif comd =~# '^J\%[OI]$' + let comd = 'JOIN' + elseif comd =~# '^L\%[IS]$' + let comd = 'LIST' + elseif comd =~# '^MOD\=$' + let comd = 'MODE' + elseif comd =~# '^MOT$' + let comd = 'MOTD' + elseif comd =~# '^NA\%[ME]$' + let comd = 'NAMES' + elseif comd =~# '^NIC\=$' + let comd = 'NICK' + elseif comd =~# '^NO\%[TIC]$' + let comd = 'NOTICE' + elseif comd =~# '^O$' + let comd = 'OP' + elseif comd =~# '^PAR\=$' + let comd = 'PART' + elseif comd =~# '^PIN\=$' + let comd = 'PING' + elseif comd =~# '^PR\%[IVMS]$' + let comd = 'PRIVMSG' + elseif comd =~# '^Q\%[UER]$' + let comd = 'QUERY' + elseif comd =~# '^QUI$' + let comd = 'QUIT' + elseif comd =~# '^S\%[ERVE]$' + let comd = 'SERVER' + "elseif comd =~# '^SET$' + elseif comd =~# '^T\%[IM]$' + let comd = 'TIME' + elseif comd =~# '^TO\%[PI]$' + let comd = 'TOPIC' + elseif comd =~# '^UNA\%[LIA]$' + let comd = 'UNALIAS' + elseif comd =~# '^UNSE\=$' + let comd = 'UNSET' + elseif comd =~# '^V\%[OIC]$' + let comd = 'VOICE' + elseif comd =~# '^WH$' + let comd = 'WHO' + elseif comd =~# '^WHOAM\=$' + let comd = 'WHOAMI' + elseif comd =~# '^WHOI$' + let comd = 'WHOIS' + endif + endif + return comd +endfunction + +" Expand '%' to the current channel, etc. +" TODO: How should we behave to syntax errors? +function! s:ExpandArgs(comd, args) + "let errmsg = 'SYNTAX ERROR ' + let args = a:args + + if a:comd =~# '^\%(MODE\)$' + if a:args =~ '^\%([-+].*\)\=$' + let args = (s:IsChannel(s:channel) ? s:channel + \ : s:GetCurNick()).' '.a:args + endif + elseif a:comd =~# '^\%(NAMES\)$' + " Syntax: CHANNEL [ CHANNEL]* + " User might delimit targets with spaces, which is wrong + let args = s:ExpandChannel(substitute(a:args, '\s\+', ',', 'g')) + elseif a:comd =~# '^WHOIS$' + " Syntax: [SERVER] USER [ USER]* + let server= s:ExpandChannel(s:StrDivide(a:args, 1)) + let nicks = s:ExpandChannel(substitute(s:StrDivide(a:args, 2), + \ '\s\+', ',', 'g')) + let args = s:StrTrim(server.(s:IsNick(server) ? ',' : ' ').nicks) + elseif a:comd =~# '^WHOWAS$' + " Syntax: USER [COUNT [SERVER]] + " NOTE: Some servers accept multiple users, delimited with commas, whereas + " RFC1459 specifies only one user. I adopt the latter. + if 1 || a:args =~ '^\S\+\%(\s\+-\=\d\+\%(\s\+\S\+\)\=\)\=$' + let args = s:ExpandChannel(a:args, ' ') + endif + elseif a:comd =~# '^\%(ISON\|USERHOST\)$' + " Syntax: USER [ USER]* + " User might delimit targets with commas, which is wrong + let args = s:ExpandChannel(substitute(a:args, ',', ' ', 'g'), ' ') + elseif a:comd =~# '^\%(INVITE\)$' + " Syntax: USER CHANNEL + let rx = '^\(\S\+\)\%(\s\+\(\S\+\)\)'.(s:IsChannel(s:channel) + \ ? '\=' : '').'$' + if a:args =~ rx + let nick = s:StrMatch(a:args, rx, '\1') + let channel = s:StrMatch(a:args, rx, '\2') + let args = nick.' '.{s:IsChannel(channel) ? 'l' : 's'}:channel + endif + elseif !s:IsCmdUnary(a:comd) + if a:comd =~# '^\%(ACTION\)$' + " NOTE: "/ME" command MUST come with NO targets specified + if strlen(s:channel) + let args = s:channel.' '.a:args + endif + elseif ( a:comd =~# '^\%(DESCRIBE\|NOTICE\|PART\|PRIVMSG\|TOPIC\)$' + \|| a:comd =~# '^\%(KICK\)$') + " Syntax: TARGET [MESSAGE] + " CHANNEL USER [MESSAGE] + " Divide arguments into two parts, to see if the former is righteously + " a channel or not + let rx = '^\(\S\+\)\%(\s\+\(.\+\)\)\=$' + let channel = s:ExpandChannel(s:StrMatch(a:args, rx, '\1')) + let message = s:StrMatch(a:args, rx, '\2') + + if !s:IsChannel(channel) && a:comd =~# '^\%(KICK\|PART\|TOPIC\)$' + " If user ommitted channel(s), supply the current one + let channel = s:channel + let message = a:args + endif + " Restore it, potentially squeezing the spaces in-between + if strlen(channel) + let args = channel.' '.message + endif + endif + endif + + return args +endfunction + +function! s:ExpandChannel(channel, ...) + let delimit = (a:0 && strlen(a:1)) ? a:1 : ',' + let channel = substitute(a:channel, '%', s:channel, '') + let channel = substitute(channel, '%', '', 'g') + let channel = s:StrCompress(channel, delimit) + return channel +endfunction + +" Syntax: COMMAND [MESSAGE] +function! s:IsCmdUnary(comd) + return (a:comd =~# '^\%(G\=\%(AWAY\|QUIT\)\|WALLOPS\)$') +endfunction + +" Syntax: COMMAND TARGET [MESSAGE] +function! s:IsCmdBinary(comd) + return (a:comd =~# +\'^\%(ACTION\|DESCRIBE\|KILL\|NOTICE\|PART\|PRIVMSG\|TOPIC\|SQU\%(ERY\|IT\)\)$') +endfunction + +" Syntax: COMMAND CHANNEL USER [MESSAGE] +function! s:IsCmdTernary(comd) + return (a:comd =~# '^\%(KICK\)$') +endfunction + +function! s:GetCmdRx(comd) + let rx = '' + if !strlen(a:comd) + " Pattern of the command line + let rx = '^/\(\csp\%[lit]\s\+/\=\)\=\(\S\+\)\%(\s\+\(.\+\)\)\=$' + elseif s:IsCmdUnary(a:comd) + let rx = '^\(\):\=\(.\+\)$' + elseif s:IsCmdBinary(a:comd) + let rx = '^\(\S\+\)\s\+:\=\(.\+\)$' + elseif s:IsCmdTernary(a:comd) + let rx = '^\(\S\+\s\+\S\+\)\s\+:\=\(.\+\)$' + endif + return rx +endfunction + +" +" Wrapper functions for commencing communication +" + +function! s:ReconnServer(force) + let server = s:GetVimVar('b:server') + if !s:IsServer(server) + let server = s:server + endif + if !s:IsConnected(server) && (a:force + \ || s:Confirm_YN('Reconnect to server '.server)) + call s:Cmd_SERVER(server) + call s:MainLoop() + endif +endfunction + +function! s:StartServer(...) + let manual = a:0 + let list = manual ? a:1 : s:server + let retval = (strlen(list) + \ && (!manual || s:Confirm_YN('Open '.list.' with VimIRC'))) + + if retval + " irc.server.com/#channel + let rx = '^\%(irc://\)\=\(..\{-\}\)\%([/:]\(\S\+\)\)\=$' + " NOTE: The above style of channel specification is not allowed for + " `g:vimirc_server'. Use `g:vimirc_autojoin' instead. + + while strlen(list) + let server = s:StrDivide(list, 1) + if strlen(server) + let channel= manual ? s:StrMatch(server, rx, '\2') : '' + let server = s:StrMatch(server, rx, '\1') + + if manual + call s:HiliteURL(server) + if strlen(channel) + " I encountered this notation, unfortunately (missing #): + " irc://irc.efnet.net/hydrairc + let channel = (s:IsChannel(channel) ? '' : '#').channel + if s:StartChannel(channel, server) > 0 " already logged-in + return retval + endif + endif + endif + call s:Cmd_SERVER(server) + endif + let list = s:StrDivide(list, 2) + endwhile + endif + return retval +endfunction + +function! s:StartChannel(channel, ...) + let server = a:0 ? a:1 : s:GetVimVar('b:server') + let retval = (s:IsServer(server) && s:Confirm_YN('Join channel '.a:channel)) + if retval + if !s:IsBufType_List() + call s:HiliteURL(a:channel) + endif + if s:IsConnected(server) + call s:SetCurServer(server) + call s:Send_JOIN('JOIN', a:channel) + else + " Open specified channel after logging in the server + " FIXME: This can be overridden if called before the previous login + " attempt completes + let s:autojoin = a:channel + let retval = -1 + endif + endif + return retval +endfunction + +" TODO: Keep track of the state of the nick you are chatting with (ison, nick +" change) +function! s:StartChat(nick, ...) + " Open up a separate window as with DCC CHAT + " TODO: Accept multiple nicks + if s:IsNick(a:nick) + call s:OpenBuf_Chat(a:nick, (a:0 ? a:1 : s:server)) + call s:OpenBuf_Command(1) + throw 'IMGONNAPOST' + elseif strlen(a:nick) + call s:EchoError('Not a proper nick: '.a:nick) + endif +endfunction + +function! s:StartWeb(url) + let comd = (s:IsServer(a:url) + \ && s:Confirm_YN('Open '.a:url.' with web browser')) + \ ? s:GetUserBrowser() : '' + let retval = strlen(comd) + + if retval + " "irc..." server not opened with VimIRC + let url = (a:url !~ '^\a\+://' ? 'http://' : '').a:url + call s:HiliteURL(url) + + let url = s:StrQuote(s:EscapeFName(url)) + if comd =~# '%URL%' + " Avoid special chars to be replaced with the matched pattern + let comd = substitute(comd, '%URL%', escape(url, '&\'), 'g') + else + let comd = comd.' '.url + endif + call s:ExecuteShell(comd) + endif + return retval +endfunction + +function! s:Cmd_QUERY(args) + if !strlen(a:args) + call s:QuitChat(s:channel) + else + call s:StartChat(a:args) + endif +endfunction + +function! s:UpdateList() + if s:IsBufType_List() && s:IsConnected(b:server) + " Prevent excessive updating + if (localtime() - b:updated) > 60 + call s:SetCurServer(b:server) + call s:UnloadList() + call s:Send_LIST('LIST', '') + else + call s:EchoWarn('You are too eager to update. Wait another minute.') + endif + call s:MainLoop() + endif +endfunction + +function! s:DoAutoJoin() + let autojoin = s:GetServerOpt(s:GetVimVar('g:vimirc_autojoin'), s:server) + while strlen(autojoin) + let channel = s:StrDivide(autojoin, 1) + if channel ==? 'list' + if s:GetBufNum_List() < 0 + call s:Send_LIST('LIST', '') + endif + else + call s:Send_JOIN('JOIN', substitute(channel, ':', ' ', '')) + endif + let autojoin = s:StrDivide(autojoin, 2) + endwhile + + if exists('s:autojoin') + call s:Send_JOIN('JOIN', s:autojoin) + unlet s:autojoin + endif +endfunction + +" +" Opening URLs with web browser +" (heavily borrowed from Chalice) +" + +function! s:ExtractChannel(str) + return matchstr(a:str, '\%(^\|\W\)\@<=[&#+!][^,:[:space:]]\+') +endfunction + +function! s:ExtractURL(str) + " FIXME: Filenames would likely be regarded as URLs + " Catch raw IPs? + let url = matchstr(a:str, + \ '\%(\a\+://\)\=\%(\w[-.[:alnum:]]\+\.\)\+\a\{2,\}\%(/\S*\)\=') + if strlen(url) + let url = s:ValidateURL(url) + endif + return url +endfunction + +function! s:ExtractLink() + let link = '' + let line = getline('.') + if line =~ '^\S\+ \*:' + return link + endif + + let word = expand('') + let link = s:ExtractChannel(word) + if !strlen(link) + " Get URL under/after/before cursor + let link = s:ExtractURL(word) + if !strlen(link) + let link = s:ExtractURL(strpart(line, col('.'))) + if !strlen(link) + let link = s:ExtractURL(line) + if !strlen(link) + let link = s:ExtractChannel(line) + endif + endif + endif + endif + return link +endfunction + +function! s:OpenLink(split, ...) + let link = s:ExtractLink() + let open = strlen(link) + + if open + let s:split = a:split + let open = s:IsChannel(link) ? s:StartChannel(link) + \ : (s:IsServerIRC(link) && s:StartServer(link) + \ || s:StartWeb(link)) + let s:split = 0 + endif + if !(open || s:IsBufType_List()) + " Advance cursor if user selected nothing: this is called via key + call s:DoNormal((a:0 ? a:1 : v:count1)."\") + endif +endfunction + +" +" Logging +" + +function! s:LogBuffer(bufnum) + if s:log && s:MakeDir(s:logdir) && bufloaded(a:bufnum) + \ && (s:BufVisit(a:bufnum) || s:OpenBufNum('split', a:bufnum)) + \ && !(s:IsBufEmpty() || (s:GetVimVar('b:lastsave') >= line('$'))) + let save_cpoptions = &cpoptions + set cpoptions-=A + + let range = ((exists('b:lastsave') ? b:lastsave : 0) + 1).',$' + let logfile = s:logdir.'/'.s:GenFName_Log() + + " If the file is loaded in another buffer, unload it + call s:BufUnload(logfile) + + execute 'redir >>' logfile + silent echo '(Logged at' s:GetTime(0).")\n" + redir END + call s:ExecuteShell(range.'write! >> '.logfile) + + let b:lastsave = line('$') + let &cpoptions = save_cpoptions + endif +endfunction + +function! s:GenFName_Log() + " I'm prepending your nick to avoid corrupted data in case you're running + " several instances. No locking. + return s:EscapeFName(s:GetCurNick().'@'.b:server.(exists('b:channel') + \ ? '.'.b:channel + \ : '')) +endfunction + +" +" Caching +" + +function! s:GenFName_List() + return s:logdir.'/'.b:server.'.list' +endfunction + +function! s:CacheList(bufnum) + if (getbufvar(a:bufnum, 'updated') + 0 > 0) && s:MakeDir(s:logdir) + \ && (s:BufVisit(a:bufnum) || s:OpenBufNum('split', a:bufnum)) + call s:Write(s:GenFName_List(), 0) + let b:updated = 0 + endif +endfunction + +function! s:LoadList() + call s:OpenBuf_List() + + if s:IsBufEmpty() + let list = s:GenFName_List() + if filereadable(list) && (localtime() - getftime(list)) < s:listexpire + call s:Read(list) + endif + endif + return !s:IsBufEmpty() +endfunction + +function! s:UnloadList() + let b:updated = localtime() + let b:updating= 1 + call s:ModifyBuf(1) + call s:BufClear() + call s:ModifyBuf(0) + call delete(s:GenFName_List()) +endfunction + +" +" Notifications +" + +function! s:NotifyNewEntry(force) + " If the bottom line is already visible, or just forced to do so, + if a:force || s:IsBottomVisible(1) + " Scroll down + call s:HiliteLine('$') + else + " And if not, do not scroll. User might want to stay there to read old + " messages + call s:Beep(1) + endif +endfunction + +function! s:NotifyOffline() + call s:HiliteClear() + + if s:IsBufType_IRC() + echo s:IsSockOpen() ? s:IsBufType_Command() + \ ? 'Hitting will send out the current line' + \ : 'Hit to get online' + \ : 'Do /SERVER to get connected' + endif + if s:in_loop + if s:debug + call s:EchoHL('You have to consider seriously why you are seeing this ' + \'message', 'WarningMsg') + endif + let s:in_loop = 0 + endif + + try + call s:DoTimer() + catch /^Vim:Interrupt$/ + endtry +endfunction + +function! s:GetBufTitle() + return exists('b:title') ? b:title : bufname('%') +endfunction + +function! s:SetTitleBar(time) + let &titlestring = s:client." [".strftime('%H:%M', a:time).']: '. + \(s:IsBufType_IRC() + \ ? b:server.' '.(s:IsBufType_Channel() + \ ? b:channel.': '.b:topic : '') + \ : fnamemodify(expand('%'), ':p')) + " NOTE: Redrawing is necessary to update the title + redraw +endfunction + +function! s:SetUserMode(umode) + let bufnum = s:GetBufNum_Server() + if bufnum >= 0 + let umode = (a:umode == '+' ? '' : a:umode) + call setbufvar(bufnum, 'umode', umode) + call setbufvar(bufnum, 'title', ' '.s:GetCurNick(). + \(strlen(umode) ? " [".umode.']' : '').' @ '.s:server) + endif +endfunction + +function! s:SetChannelTopic(channel, topic) + let bufnum = s:GetBufNum_Channel(a:channel) + if bufnum >= 0 + call setbufvar(bufnum, 'topic', a:topic) + endif +endfunction + +function! s:SetChannelMode(channel, cmode) + let bufnum = s:GetBufNum_Channel(a:channel) + if bufnum >= 0 + call setbufvar(bufnum, 'cmode', a:cmode) + call setbufvar(bufnum, 'title', ' '.a:channel." [".a:cmode.'] @ '.s:server) + endif +endfunction + +" +" Help +" + +function! s:Cmd_HELP(...) + if a:0 && strlen(a:1) + if a:1 ==? 'DCC' + call s:Cmd_DCCHELP() + elseif a:1 =~? '^\%(SERVER\|REMOTE\)\>' + call s:Cmd_REMOTEHELP(s:StrDivide(a:1, 2)) + endif + return + endif + + try + let save_more = &more + set more + + let hlname = 'Title' + call s:EchoHL(' VimIRC Help', hlname) + echo "\n" + call s:EchoHL(' Available commands:', hlname) + call s:EchoHL(' (letters in [] are optional)', 'Comment') + echo "\n" + + call s:EchoHL('/s[erver] [host[:port] [password]]', hlname) + echo "\tTry to connect with a new server. Or reconnect the current server" + echo "\twhen no argument is given." + echo "\tSynonym: co[nnect]" + call s:EchoHL('/qui[t] [reason]', hlname) + echo "\tDisconnect with the current server." + echo "\tSynonyms: b[ye], e[xit], si[gnoff]." + call s:EchoHL('/gq[uit] [reason]', hlname) + echo "\tDisconnect with all the servers." + echo "\n" + call s:EchoHL('/l[ist]', hlname) + echo "\tShow the list of active channels on the server." + echo "\n" + call s:EchoHL('/j[oin] channel(s) [key(s)]', hlname) + echo "\tJoin specified channels. Use commas to separate channels." + call s:EchoHL('/pa[rt] [channel(s)] [message]', hlname) + echo "\tExit from the specified channels. If channels are ommitted, exit" + echo "\tfrom the current channel." + echo "\tSynonym: le[ave]." + echo "\n" + call s:EchoHL('/to[pic] [channel] [topic]', hlname) + echo "\tSet or show the current topic for channel." + echo "\n" + call s:EchoHL('/q[uery] [nick]', hlname) + echo "\tWith nick, start a query session with the user. Without nick," + echo "\tclose the current query session." + echo "\tSynonym: ch[at]" + echo "\n" + call s:EchoHL('message', hlname) + echo "\tSend a message to the current channel, or the nick currently" + echo "\tchatting with." + echo "\n" + call s:EchoHL('/m[sg] target message', hlname) + echo "\tSend a message to the nick/channel." + call s:EchoHL('', hlname) + echo "\n" + call s:EchoHL('/me message', hlname) + echo "\tSend a message to the current channel/query target, playing some" + echo "\trole." + call s:EchoHL('/des[cribe] target message', hlname) + echo "\tSend a message to the nick/channel, playing some role." + echo "\n" + call s:EchoHL('/t[ime]', hlname) + echo "\tShow what time it is on the server now." + echo "\tSynonym: da[te]" + echo "\n" + call s:EchoHL('/aw[ay] [reason]', hlname) + echo "\tWith reason, notify the server that you are away. Without reason," + echo "\tnotify her that you are back." + call s:EchoHL('/ga[way] [reason]', hlname) + echo "\tDo the same thing with all servers." + echo "\n" + call s:EchoHL('/whoa[mi]', hlname) + echo "\tShow the current nickname of you." + call s:EchoHL('/whoi[s] [server] nick(s)', hlname) + echo "\tShow information on nicks, separated by commas." + echo "\n" + call s:EchoHL('/o[p] [channel] nick(s)', hlname) + call s:EchoHL('/v[oice] [channel] nick(s)', hlname) + echo "\tGive operator or voice privileges to the selected nick(s)." + echo "\tUse spaces or commas to separate them." + call s:EchoHL('/deo[p] [channel] nick(s)', hlname) + call s:EchoHL('/dev[oice] [channel] nick(s)', hlname) + echo "\tDeprive operator or voice privileges of the selected nick(s)." + echo "\n" + call s:EchoHL('/set [option [value]]', hlname) + echo "\tSet or show internal option values. List of settable options will" + echo "\tbe displayed if option is ommited." + call s:EchoHL('/uns[et] option', hlname) + echo "\tClear the value of the option" + echo "\n" + call s:EchoHL('/al[ias] /new-command /blah blah', hlname) + echo "\tAdd a new command \"new-command\" which expands into \"blah blah\"." + call s:EchoHL('/una[lias] /new-command', hlname) + echo "\tRemove it." + echo "\n" + call s:EchoHL('/sp[lit]', hlname) + echo "\tThis is a prefix to commands such as /join, /list, /query, and" + echo "\t/server, to execute those commands in separate windows." + echo "\n" + call s:EchoHL('/h[elp]', hlname) + echo "\tDisplay this message." + call s:EchoHL('/dc[c] help', hlname) + echo "\tDisplay a help message for DCC commands." + echo "\n" + call s:PromptEnter(0) + catch /^Vim:Interrupt$/ + finally + let &more = save_more + endtry +endfunction + +function! s:Cmd_DCCHELP(...) + try + let save_more = &more + set more + + let hlname = 'Title' + call s:EchoHL(' Available DCC commands:', hlname) + echo "\n" + call s:EchoHL('/dc[c] send [nick [file]]', hlname) + echo "\tOffer DCC SEND to nick" + call s:EchoHL('/dc[c] chat [nick]', hlname) + echo "\tOffer DCC CHAT to, or accept pending offer from, nick" + call s:EchoHL('/dc[c] get [nick [file]]', hlname) + echo "\tAccept pending SEND offer from nick" + call s:EchoHL('/dc[c] close [type [nick]]', hlname) + echo "\tClose SEND/CHAT/GET connection with nick" + call s:EchoHL('/dc[c] list', hlname) + echo "\tList all active/pending DCC connections" + echo "\n" + call s:PromptEnter(0) + catch /^Vim:Interrupt$/ + finally + let &more = save_more + endtry +endfunction + +function! s:Cmd_REMOTEHELP(args) + " XXX: Are there any servers who accept arguments? + call s:DoSend('HELP', a:args) +endfunction + +function! s:Cmd_WHOAMI(...) + call s:EchoNormal('You are '.s:GetCurNick()) +endfunction + +" +" Misc. utility functions +" + +" Generic ones + +function! s:Beep(times) + if a:times <= 0 || s:lastbeep >= localtime() - 1 + return + endif + + try + let errorbells = &errorbells + let visualbell = &visualbell + set errorbells + set novisualbell + + let i = 1 + while i <= a:times + call s:DoNormal("\") + call s:DoWait(a:times, i) + let i = i + 1 + endwhile + finally + let &errorbells = errorbells + let &visualbell = visualbell + let s:lastbeep = localtime() + endtry +endfunction + +function! s:ClearCommand(newline) + echon "\r" (a:newline ? "\n" : '') +endfunction + +function! s:EchoHL(mesg, hlname) + try + execute 'echohl' a:hlname + echo a:mesg + catch /^Vim:Interrupt$/ + finally + echohl None + endtry +endfunction + +function! s:EchoError(mesg) + call s:Beep(1) + call s:HandlePromptKey(1, a:mesg, 'ErrorMsg') +endfunction + +function! s:EchoNormal(mesg) + call s:HandlePromptKey(1, a:mesg, 'Normal') +endfunction + +function! s:EchoWarn(mesg) + call s:HandlePromptKey(1, a:mesg, 'WarningMsg') +endfunction + +function! s:Execute(comd, ...) + let retval = 0 + if strlen(a:comd) + let silent = (a:0 && a:1) + try + let retval = s:Execute{silent ? 'Silent' : 'Loud'}(a:comd) + catch /^Vim:Interrupt$/ + endtry + endif + return retval +endfunction + +function! s:ExecuteLoud(comd) + let loud = 0 + call s:UntoggleCursor(1) + try + let save_more = &more + let save_reg = @" + + let @" = @_ + set more + + redir @" + execute a:comd + redir END + + " XXX: Vim sometimes echoes just "\r\n" + let loud = (strlen(@") && @" !~ '^[\r\n]\+$') + if !loud && strlen(@") + call s:RedrawScreen(0) + endif + finally + let &more = save_more + let @" = save_reg + call s:UntoggleCursor(0) + endtry + return loud +endfunction + +function! s:ExecuteSafe(prefix, comd) + execute (exists(':'.a:prefix) == 2 ? a:prefix.' ' : '').a:comd +endfunction + +function! s:ExecuteShell(comd) + let comd = a:comd + if &shellxquote == '"' && s:IsWin3264() && !match(a:comd, '^start ') + " Remove unecessary escapes + let comd = substitute(comd, '\\"\@=', '', 'g') + endif + call s:ExecuteSilent('!'.comd) +endfunction + +function! s:ExecuteSilent(comd) + let v:errmsg = '' + silent! execute a:comd + return !strlen(v:errmsg) +endfunction + +function! s:DoInsert(append) + execute 'startinsert'.(a:append ? '!' : '') +endfunction + +function! s:DoIterate(ex, comd, times) + let loud = 0 + if strlen(a:comd) + let i = 1 + while i <= a:times + if a:ex + let loud = s:ExecuteLoud(a:comd) + else + call s:DoNormal(a:comd, 1) + endif + let i = i + 1 + endwhile + endif + return loud +endfunction + +function! s:DoNormal(comd, ...) + if strlen(a:comd) + if !(a:0 && a:1) + execute 'normal!' a:comd + else + call s:DoNormalSafe(a:comd) + endif + endif +endfunction + +" Executes normal mode commands. Do not try to make changes with this. +" NOTE: This can be slow if used repetitively +function! s:DoNormalSafe(comd) + try + let orgbuf = bufnr('%') + let modifiable = &l:modifiable + " Temporarily forbid user to tamper with the buffer. + call setbufvar(orgbuf, '&modifiable', 0) + execute 'normal!' a:comd + finally + call setbufvar(orgbuf, '&modifiable', modifiable) + endtry +endfunction + +function! s:DoWincmd(comd, ...) + call s:ExecuteSilent((a:0 ? a:1 : '').'wincmd '.a:comd) + return winnr() +endfunction + +function! s:GetEnv(var) + let var = expand(a:var) + if var ==# a:var + let var = '' + endif + return var +endfunction + +function! s:GetTime(short, ...) + return strftime((a:short ? '%H:%M' : '%Y/%m/%d %H:%M:%S'), + \ (a:0 && a:1 ? a:1 : localtime())) +endfunction + +function! s:GetVimVar(varname) + return exists('{a:varname}') ? {a:varname} : '' +endfunction + +function! s:Read(file) + let save_cpoptions = &cpoptions + set cpoptions-=a + if !&l:modifiable + setlocal modifiable + endif + + call s:ExecuteSilent('read '.a:file) + 1call s:DelLine() + + let &cpoptions = save_cpoptions + return !s:IsBufEmpty() +endfunction + +function! s:RedrawScreen(all) + execute 'redraw'.(a:all ? '!' : '') +endfunction + +function! s:RedrawStatus(...) + call s:ExecuteSilent('redrawstatus'.(a:0 && a:1 ? '!' : '')) +endfunction + +" Show user that she is in pending-mode +function! s:ShowCmd(comd) + " Right-aligning the message + echon s:StrMultiply(' ', (&columns - 20)) + \ strpart(a:comd, (strlen(a:comd) > 8 ? strlen(a:comd) - 8 : 0)) "\r" +endfunction + +function! s:Write(file, append) + " TODO: Range support + if s:IsBufEmpty() + return + endif + + let save_cpoptions = &cpoptions + set cpoptions-=A + " Cannot write if it is loaded elsewhere + call s:BufUnload(a:file) + call s:ExecuteSilent('write! '.(a:append ? '>> ' : '').a:file) + let &cpoptions = save_cpoptions +endfunction + +" Interactive functions + +function! s:Confirm(mesg, list) + let choice = 0 + call s:UntoggleCursor(1) + + try + let choice = confirm(a:mesg, a:list) + catch /^Vim:Interrupt$/ + finally + call s:RedrawScreen(0) + call s:UntoggleCursor(0) + endtry + return choice +endfunction + +function! s:Confirm_YN(mesg) + return (s:PromptChar(20, ' '.a:mesg.'? (y/[n]): ', 'Question') ==? 'y') +endfunction + +function! s:Input(mesg, ...) + return s:DoInput(0, a:mesg.': ', (a:0 ? a:1 : '')) +endfunction + +function! s:InputS(mesg) + return s:DoInput(1, a:mesg.': ', '') +endfunction + +function! s:DoInput(secret, mesg, text) + let input = '' + call s:UntoggleCursor(1) + + try + let input = s:StrCompress(input{a:secret ? 'secret' : ''}(a:mesg, a:text)) + catch /^Vim:Interrupt$/ + finally + call s:UntoggleCursor(0) + endtry + return input +endfunction + +function! s:Key2Char(key) + " Special keys ( etc.) are char values already (f_getchar() in eval.c) + return type(a:key) ? a:key : nr2char(a:key) +endfunction + +function! s:GetChar(cursor) + return s:Key2Char(s:GetKey(a:cursor)) +endfunction + +function! s:GetKey(cursor) + let key = 0 + if a:cursor + call s:UntoggleCursor(1) + endif + + try + let key = getchar() + catch /^Vim:Interrupt$/ + finally + call s:ClearCommand(0) + if a:cursor + call s:UntoggleCursor(0) + endif + endtry + return key +endfunction + +function! s:PromptChar(timeout, mesg, ...) + return s:Key2Char(s:PromptKey(a:timeout, a:mesg, (a:0 ? a:1 : 'MoreMsg'))) +endfunction + +function! s:PromptEnter(timeout) + call s:ClearCommand(1) + call s:HandlePromptKey(a:timeout, 'Hit ENTER or type command to continue') +endfunction + +function! s:PromptKey(timeout, mesg, ...) + " TODO: Make timeout-length configurable + let key = 0 + let hlname = a:0 ? a:1 : 'MoreMsg' + + call s:ClearCommand(0) + if a:timeout > 0 + let key = s:PromptKeyTick(a:timeout, a:mesg, hlname) + else + call s:EchoHL(a:mesg, hlname) + let key = s:GetKey(1) + endif + + call s:ClearCommand(0) + return key +endfunction + +" PromptKey with timeout feature +function! s:PromptKeyTick(timeout, mesg, hlname) + let key = 0 + + try + let startT = localtime() + while (a:timeout - (s:DoTick(a:mesg, '\|/-', a:hlname) - startT)) > 0 + if getchar(1) + let key = getchar(0) + break + endif + call s:DoWait(1, 0) + endwhile + catch /^Vim:Interrupt$/ + endtry + return key +endfunction + +function! s:DoTick(mesg, ticker, ...) + let nowT = localtime() + try + execute 'echohl' (a:0 ? a:1 : 'Normal') + echon a:mesg + execute 'echohl' s:hl_cursor + echon a:ticker[nowT % strlen(a:ticker)] "\r" + finally + echohl None + endtry + return nowT +endfunction + +" Sleep except for the final round +function! s:DoWait(final, round) + if (a:final - a:round) + sleep 250 m + endif +endfunction + +function! s:RequestFile(mesg) + let fname = '' + call s:UntoggleCursor(1) + + try + let fname = has('browse') ? browse(0, 'Select file '.a:mesg, './', '') + \ : input('Enter filename '.a:mesg.': ') + catch /^Vim:Interrupt$/ + finally + call s:UntoggleCursor(0) + endtry + return fname +endfunction + +function! s:SearchWord(comd) + let word = s:DoInput(0, a:comd, '') + if strlen(word) + let @/ = word + call s:DoNormal((a:comd == '/' ? 'n' : 'N')) + endif +endfunction + +function! s:ConsumeKey() + let key = getchar(0) + while !s:IsZero(getchar(1)) + if !key + call s:UnstickKey(1) + endif + call getchar(0) + endwhile + return key +endfunction + +" KLUGE: Without this, special keys hit while in normal mode keep generating +" key codes by themselves, resulting in nearly 100% CPU-time consumption, or +" user-interaction impossibility (vim bug?) +function! s:UnstickKey(force) + if a:force || !s:IsZero(getchar(1)) + silent! normal! lh + endif +endfunction + +" String manipulation + +function! s:StrMatch(str, pat, sub) + " A wrapper function to substitute(). First extract an interesting part + " upon which we perform matching, so that only necessary string (sub) will + " be obtained. An empty string will be returned on failure. + " I took this clever trick from Chalice. + return substitute(matchstr(a:str, a:pat), a:pat, a:sub, '') +endfunction + +function! s:StrQuote(str) + let quote = (&shellxquote == '"' ? '\' : '').'"' + return quote.a:str.quote +endfunction + +" Remove unnecessary spaces in a string +function! s:StrTrim(str, ...) + let space = (!a:0 || a:1 =~ '^ \=$') + let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)' + + let str = substitute(a:str, '[[:cntrl:]]\+', '', 'g') " just in case + let str = substitute(str, '\%(^'.patrn.'\+\|'.patrn.'\+$\)', '', 'g') + return str +endfunction + +" Ditto +function! s:StrCompress(str, ...) + let space = (!a:0 || a:1 =~ '^ \=$') + let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)' + let subst = space ? ' ' : a:1 + + let str = s:StrTrim(a:str, subst) + return substitute(str, patrn.'\+', subst, 'g') +endfunction + +" Severer version of the above +function! s:StrSquash(str, ...) + let space = (!a:0 || a:1 =~ '^ \=$') + let patrn = space ? '\s' : '\%(\s*\V'.a:1.'\m\s*\)' + + return substitute(a:str, patrn.'\+', '', 'g') +endfunction + +function! s:StrMultiply(from, times) + " Super cool logic entirely copied from Chalice + let to = '' + let from = a:from + let times = a:times + while times + if times % 2 + " This is binary addition in actuality, performed with strings + let to = to.from + endif + let times = times / 2 + let from = from.from + endwhile + return to +endfunction + +" Divide string at space or comma +function! s:StrDivide(str, group) + return s:StrMatch(a:str, + \ '^\([^[:space:],]\+\)\=\%([[:space:],]\+\(.*\)\)\=$', + \ '\'.a:group) +endfunction + +function! s:EscapeFName(str) + return escape(a:str, '%#') +endfunction + +function! s:EscapeMagic(str) + return escape(a:str, '$*.\^~') +endfunction + +function! s:EscapeQuote(str) + return escape(a:str, '"\') +endfunction + +" Confirmation functions + +function! s:Is2KeyCmd(char) + return (a:char =~# "^[zgftFTm`'@Z]$") +endfunction + +function! s:IsCtrl_W(key) + " Can accept both key code and char + return (s:Key2Char(a:key) == "\") +endfunction + +function! s:IsWin3264() + return (has('win32') || has('win32unix') || has('win64')) +endfunction + +function! s:IsZero(num) + " Evaluate it as string, since "a:num == 0" unexpectedly (?) evaluates to + " true if a:num is a string value + return (''.a:num == '0') +endfunction + +" Validation functions + +function! s:ValidatePath(path) + let path = a:path + if strlen(path) + let path = fnamemodify(path, ':p') + let path = substitute(path, '\', '/', 'g') + let path = substitute(path, '/\{2,\}', '/', 'g') + let path = substitute(path, '/$', '', '') + endif + return path +endfunction + +function! s:ValidateURL(url) + let url = a:url + if strlen(a:url) + " Cut the unnecessary tail, as in "http://www.foo.com/)." + let url = substitute(a:url, '[),.:;>\]]\+$', '', '') + if !s:IsServerIRC(url) + " "www" stuff + if url !~ '^\a\+://' + let url = 'http://'.url + endif + " domain only, without the final slash + if url =~ '^\a\+://[^/]\+$' + let url = url.'/' + endif + endif + endif + return url +endfunction + +" Line functions + +function! s:SearchLine(line) + return strlen(a:line) ? search('\m^'.s:EscapeMagic(a:line).'$', 'w') : 0 +endfunction + +function! s:DelLine() range + call s:ExecuteSilent(a:firstline.','.a:lastline.'delete _') +endfunction + +function! s:OpenNewLine() + let lnum = '$' + if strlen(getline(lnum)) + call append(lnum, '') + endif + call s:WinBottom(lnum) +endfunction + +" Buffer functions + +function! s:ExistsBufVar(bufnum, bufvar) + return strlen(getbufvar(a:bufnum, a:bufvar)) +endfunction + +function! s:IsBufCurrent(bufnum) + return (bufnr('%') == a:bufnum) +endfunction + +function! s:IsBufEmpty() + return !(line('$') > 1 || strlen(getline(1))) +endfunction + +function! s:BufClear() + if !s:IsBufEmpty() + %call s:DelLine() + endif +endfunction + +function! s:BufClose(bufnum) + let retval = 1 + if bufwinnr(a:bufnum) >= 0 + let &equalalways = 0 + + while s:BufVisit(a:bufnum) + if !s:ExecuteSilent('close!') + let retval = 0 + break + endif + endwhile + + let &equalalways = 1 + endif + return retval +endfunction + +function! s:BufReverse() + perl $curbuf->Set(1, reverse($curbuf->Get(1 .. $curbuf->Count()))) +endfunction + +function! s:BufTrim() + while search('^\s*$', 'w') && line('$') > 1 + call s:DelLine() + endwhile +endfunction + +function! s:BufUnload(bufname) + " If it is loaded in another buffer, unload it + if bufloaded(a:bufname) && !s:IsBufCurrent(bufnr(a:bufname)) + call s:ExecuteSilent('bunload! '.a:bufname) + endif +endfunction + +function! s:BufVisit(bufnum) + return (a:bufnum >= 0 && ( s:IsBufCurrent(a:bufnum) + \ || s:WinVisit(bufwinnr(a:bufnum)))) +endfunction + +" Window functions + +function! s:WinClose(...) + let retval = 1 + let winnum = a:0 ? a:1 : winnr() + + if s:WinVisit(winnum) + let &equalalways = 0 + let retval = s:ExecuteSilent('close!') + let &equalalways = 1 + endif + return retval +endfunction + +function! s:WinVisit(winnum) + let curwin = winnr() + if a:winnum >= 0 && curwin != a:winnum + let curwin = s:DoWincmd('w', a:winnum) + endif + return (curwin == a:winnum) +endfunction + +function! s:IsBottomVisible(offset) + return (winheight(0) - winline() + a:offset >= line('$') - line('.')) +endfunction + +if 1 +function! s:WinBottom(...) + call s:DoNormal('0zb') + return a:0 ? s:WinLine(a:1) : line('.') +endfunction +else +function! s:WinBottom(...) + if a:0 + call s:WinLine(a:1) + endif + call s:DoNormal('0zb') + return line('.') +endfunction +endif + +function! s:WinLine(lnum) + " For safety, convert it to number if it isn't + let lnum = a:lnum ? a:lnum : line(a:lnum) + if lnum > 0 && line('.') != lnum + execute lnum + endif + return line('.') +endfunction + +function! s:WinResize(size, vertical) + if a:size > 0 + execute (a:vertical ? 'vertical ' : '').'resize' a:size + endif +endfunction + +function! s:WinScroll(cnt) + if a:cnt + let org = line('.') + let upw = (a:cnt < 0) + let cnt = a:cnt * (upw ? -1 : 1) + call s:DoNormal(cnt.nr2char(5 * (upw ? 5 : 1))) " count / + call s:WinLine(org) + call s:RedrawScreen(0) + endif +endfunction + +function! s:WinScroll2(lnum) + " First, scroll to the specified line + call s:WinBottom((type(a:lnum) || a:lnum > 0) ? a:lnum : '$') + " Then make the bottom line visible, if possible + while !s:IsBottomVisible(0) && winline() > 2 + cal s:DoNormal("\") + endwhile +endfunction + +" Highlighting-related (no syntax-highlighting) stuffs + +function! s:GetHilite(name, mode) + return synIDattr(synIDtrans(hlID(a:name)), a:mode) +endfunction + +function! s:SetHilite(name, fg, bg, ...) + let mode= has('gui') ? 'gui' : 'cterm' + let fg = strlen(a:fg) ? a:fg : 'NONE' + let bg = strlen(a:bg) ? a:bg : 'NONE' + let etc = (a:0 && strlen(a:1)) ? a:1 : 'NONE' + + execute 'highlight' (a:name) (mode.'='.etc) (mode.'fg='.fg) (mode.'bg='.bg) +endfunction + +function! s:SetHlCursor() + " TODO: Deal with colorscheme changes + let hlname = 'Cursor' + let s:hl_cursor = 'VimIRCCursor' + + let s:cursor_fg = s:GetHilite(hlname, 'fg') + let s:cursor_bg = s:GetHilite(hlname, 'bg') + if !(strlen(s:cursor_fg) || strlen(s:cursor_bg)) + let hlname = 'Visual' + let s:cursor_fg = s:GetHilite(hlname, 'fg') + let s:cursor_bg = s:GetHilite(hlname, 'bg') + endif + + let s:cursor_etc = '' + let etclist = 'bold,italic,reverse,underline' + while strlen(etclist) + let etc = s:StrDivide(etclist, 1) + if s:GetHilite(hlname, etc) + let s:cursor_etc = (strlen(s:cursor_etc) ? s:cursor_etc.',' : '').etc + endif + let etclist = s:StrDivide(etclist, 2) + endwhile + + call s:SetHilite(s:hl_cursor, s:cursor_fg, s:cursor_bg, s:cursor_etc) +endfunction + +function! s:ToggleCursor(hide) + if hlexists('Cursor') + call s:SetHilite('Cursor', (a:hide ? '' : s:cursor_fg), + \ (a:hide ? '' : s:cursor_bg), + \ (a:hide ? '' : s:cursor_etc)) + endif +endfunction + +" Temporarily restores cursor for prompting +function! s:UntoggleCursor(show) + if s:in_loop + call s:ToggleCursor(!a:show) + endif +endfunction + +function! s:HiliteBuffer() + call s:HiliteColumn(s:IsBufType_Info() ? 'DiffText' : s:hl_cursor) +endfunction + +function! s:HiliteClear() + match none + " Clear command line incidentally + if s:in_loop + call s:ClearCommand(0) + endif +endfunction + +" Function calls should be as few as possible for better performance +function! s:HiliteColumn(...) + silent! execute 'match' (a:0 ? a:1 : s:hl_cursor) '/\%#\s*\S*/' + redraw +endfunction + +function! s:HiliteLine(lnum, ...) + silent! execute 'match' (a:0 ? a:1 : s:hl_cursor) '/\%'.s:WinLine(a:lnum).'l/' + redraw +endfunction + +function! s:HiliteURL(url) + let patrn = substitute(a:url, '\%(^\a\+://\|/$\)', '', 'g') + " Do you want to retain old highlights? + if 1 + call s:ExecuteSilent('syntax clear VimIRCURL') + endif + call s:ExecuteSilent('syntax match VimIRCURL "\V'.patrn.'"') + highlight link VimIRCURL DiffChange +endfunction + +" +" And the Perl part +" + +if has('perl') +function! s:MakeDir(dir) + if isdirectory(a:dir) + return 1 + endif + + perl <Get(1 .. $curbuf->Count()); + + if ($cmp eq 'channel') + { + @lns = map { $_->[0] } + sort { if ($dir) { lc($b->[1]) cmp lc($a->[1]) } + else { lc($a->[1]) cmp lc($b->[1]) } } + map { [ $_, /^(\S+).*$/ ] } @lns; + } + elsif ($cmp eq 'member') + { + @lns = map { $_->[0] } + sort { if ($dir) { $b->[1] <=> $a->[1] } + else { $a->[1] <=> $b->[1] } } + map { [ $_, /^\S+\s+(\d+).*$/ ] } @lns; + } + else + { + @lns = map { $_->[0] } + sort { if ($dir) { $b->[1] cmp $a->[1] } + else { $a->[1] cmp $b->[1] } } + map { [ $_, /^\S+\s+\d+\s*(.*)$/ ] } @lns; + } + $curbuf->Set(1, @lns); +} +EOP +endfunction + +function! s:PostLoadList(updated) + let bufnum = s:GetBufNum_List() + if a:updated + call setbufvar(bufnum, 'updated', localtime()) + endif + + perl <{'list'}->{'bufnum'} = VIM::Eval('l:bufnum'); + set_info($Current_Server, $INFO_UPDATE); + } +} +EOP + call setbufvar(bufnum, 'updating', 0) +endfunction + +function! s:DoTimer() + let lasttime = s:RecvData() + if lasttime + if lasttime <= s:lasttime + " If lasttime is zero, i.e., the connection was lost, do NOT exit here: + " we should update the infobar to mark the server as dead. + return lasttime + endif + let s:lasttime = lasttime + endif + + if s:current_changed + call s:SetCurChanServ() + endif + + perl <') + 0 + call s:DelBufNum(abuf) + if !s:IsBufType_ChanServ(abuf) + return + endif + let channel= getbufvar(abuf, 'channel') + let server = getbufvar(abuf, 'server') + + perl <{'bufnum'} >= 0) + { + $cref->{'bufnum'} = -1; + set_info($sref, $INFO_UPDATE); + } + } +} +EOP +endfunction + +function! s:SetCurChanServ() + let bufnum = bufnr('%') + if !s:IsBufType_ChanServ(bufnum) + return + endif + + perl <{'sock'}); + + if (has_info($cref, $INFO_HASNEW)) + { + set_info($sref, $INFO_UPDATE); + unset_info($cref, $INFO_HASNEW); + } + + if (is_chan($chan) && (has_info($cref, $INFO_UPDATE) || $donick)) + { + draw_nickwin($chan); + unset_info($cref, $INFO_UPDATE); + } + } +} +EOP + let s:current_changed = 1 +endfunction + +" NOTE: This is a tricky function written solely for ease of typing: you +" (developer) can omit `server' argument for some functions even if you should +" provide one. So, don't forget to call this when necessary so that vimirc +" will not get confused about which server should be addressed to. +function! s:SetCurServer(server, ...) + if a:0 + call s:SetCurChannel(a:1) + endif + + perl <{'sock'}); + } +} +EOP +endfunction + +function! s:SetCurChannel(channel) + let s:channel = s:IsList(a:channel) ? '' : a:channel +endfunction + +function! s:GetBufNum_Next(forward, cnt) + let bufnum = 0 + + perl <{'bufnum'} >= 0) + { + unshift(@chanserv, $sref->{'bufnum'}); + } + + if (my $lref = find_list($sref)) + { + unshift(@chanserv, $lref->{'bufnum'}); + } + + foreach my $cref (@{$sref->{'chans'}}) + { + if ($cref->{'bufnum'} >= 0) + { + unshift(@chanserv, $cref->{'bufnum'}); + } + } + foreach my $cref (@{$sref->{'chats'}}) + { + if ($cref->{'bufnum'} >= 0) + { + unshift(@chanserv, $cref->{'bufnum'}); + } + } + } + + if ($#chanserv >= 0) + { + my $first = $chanserv[0]; + my $next = $first; + my $count = VIM::Eval('a:cnt') % scalar(@chanserv); + + if ($#chanserv > 0 && $count) + { + + if (VIM::Eval('a:forward')) + { + @chanserv = reverse(@chanserv); + $first = $chanserv[0]; + } + + # Find the position of $orgbuf + if ($first != $orgbuf) + { + while ($chanserv[0] != $orgbuf) + { + push(@chanserv, shift(@chanserv)); + # $orgbuf wasn't in the list + if ($chanserv[0] == $first) + { + last; + } + } + } + $next = $chanserv[$count]; + } + + VIM::DoCommand("let bufnum = $next"); + } +} +EOP + return bufnum +endfunction + +function! s:GetCurNick(...) + perl <{'nick'}) && $sref->{'nick'}) + { + $nick = $sref->{'nick'}; + } + else + { + $nick = vim_getvar('s:nick'); + } + + VIM::DoCommand('return "'.do_escape($nick).'"'); +} +EOP +endfunction + +function! s:IsConnected(...) + let retval = 0 + let server = a:0 ? a:1 : s:server + if !s:IsServer(server) + return retval + endif + + perl <count()); + } +} +EOP + return 0 +endfunction + +function! s:RecvData() + let retval = localtime() + + perl <select($RS, $WS, undef, 0.2); + my $sock; + + foreach $sock (@{$w}) + { + dcc_check($sock, 1); + } + + foreach $sock (@{$r}) + { + unless (dcc_check($sock)) + { + unless (irc_recv($sock)) + { + unless ($RS->count()) + { + VIM::DoCommand('let retval = 0'); + last; + } + } + } + } +} +EOP + return retval +endfunction + +function! s:Cmd_SERVER(server) + let server = strlen(a:server) ? a:server : s:GetVimVar('b:server') + let port = 0 + let pass = '' + + if !strlen(server) + let server = s:Input('Enter server name') + endif + + " TODO: Validity check of server? + let rx = '^\(..\{-\}\)\%(:\(\d\+\)\)\=\%(\s\+\(\S\+\)\)\=$' + if server !~ rx + return + endif + + let s:server = tolower(s:StrMatch(server, rx, '\1')) + let port = s:StrMatch(server, rx, '\2') + 0 + let pass = s:StrMatch(server, rx, '\3') + + if !port + let port = getbufvar(s:GetBufNum_Server(), 'port') + 0 + if port <= 0 + let port = 6667 + endif + endif + + call s:OpenBuf_Server(port) + call s:CloseWin_DeadServer() + + perl <{'away'} = $mesg; + irc_send("%s :%s", $comd, $mesg); + } + elsif ($Current_Server->{'away'}) + { + $Current_Server->{'away'} = undef; + irc_send("%s", $comd); + } +} +EOP +endfunction + +function! s:Send_CTCP(comd, args) + perl <{'list'}, $INFO_LINE1); + irc_send("%s%s", $comd, ($args ? " $args" : "")); +} +EOP +endfunction + +function! s:Send_NAMES(comd, args) + perl <{'chans'}}) + { + vim_close_chan($cref->{'name'}); + } + + foreach my $cref (@{$Current_Server->{'chats'}}) + { + vim_close_chat($cref->{'nick'}, $Current_Server->{'server'}); + } + + while (my $dcc = find_dccclient()) + { + dcc_close($dcc); + } + + $Current_Server->{'conn'} |= $CS_QUIT; +} +EOP + call s:CloseBuf_Server() +endfunction + +function! s:Send_NOTICE(comd, args) + call s:SendMSG(a:comd, a:args) +endfunction + +function! s:Send_PRIVMSG(comd, args) + call s:SendMSG(a:comd, a:args) +endfunction + +function! s:SendGlobally(comd, args) + perl <{'conn'} & $CS_LOGIN) + { + set_curserver($sref->{'sock'}); + VIM::DoCommand('call s:Send_{a:comd}(a:comd, a:args)'); + } + } + set_curserver($save_cur->{'sock'}); +} +EOP +endfunction + +function! s:SendMSG(comd, args) + if !strlen(a:args) + return + endif + + perl <{'nick'}; + + unless ($comd) + { + $comd = 'PRIVMSG'; + } + $priv = ($comd eq 'PRIVMSG'); + + irc_send("%s %s :%s", $comd, $chans, $mesg); + + foreach my $chan (split(/,/, $chans)) + { + if (is_chan($chan)) + { + irc_chan_line($chan, "%s%s%s%s: %s", + ($priv ? '<' : '['), + find_nickprefix($nick, $chan), + $nick, + ($priv ? '>' : ']'), + $mesg); + } + else + { + irc_chat_line($chan, "%s%s%s: %s", + ($priv ? '<' : '['), + $nick, + ($priv ? '>' : ']'), + $mesg); + } + } + } + } +} +EOP +endfunction + +function! s:DoSend(comd, args) + perl < undef, + port => 0, + local => undef, + sock => undef, + conn => 0, + info => 0, + bufnum => -1, + nick => undef, + pass => undef, + umode => undef, + away => undef, + motd => 0, + chans => [], + chats => [], + timers => [], + lastping => time(), + lastbuf => undef, + lines => [] + }; + + add_list($sref); + push(@Servers, $sref); + return $sref; +} + +sub find_server +{ + my $server = shift; + + foreach my $sref (@Servers) + { + if ($server eq $sref->{'server'}) + { + return $sref; + } + } + + return undef; +} + +sub set_connected +{ + if ($_[0]) + { + # Leave CS_RECON flag untouched. It'll be used to supress motd message + $Current_Server->{'conn'} &= ~$CS_QUIT; + $Current_Server->{'conn'} |= $CS_LOGIN; + } + else + { + close_server($Current_Server); + } + set_info($Current_Server, $INFO_UPDATE); +} + +sub close_server +{ + if (my $sref = shift) + { + $sref->{'conn'} &= ~$CS_LOGIN; + $sref->{'motd'} = 0; + $sref->{'lastbuf'}= undef; + + if ($sref->{'conn'} & $CS_QUIT) + { + @{$sref->{'timers'}} = (); + } + + conn_close($sref); + } +} + +sub open_server +{ + my $sref = shift; + + if (conn_open($sref)) + { + conn_watchin($sref); # add it to IO::Select + #$sref->{'local'} = $sref->{'sock'}->sockhost(); + login_server($sref); + return 1; + } + + irc_chan_line('', "!: Could not establish connection: %s", $!); + return 0; +} + +sub cmd_server +{ + my ($server, $port, $nick, $pass) = @_; + + if ($port <= 0) + { + $port = 6667; + } + + unless ($pass) + { + $pass = VIM::Eval('s:GetServerPASS()'); + } + + if ($Current_Server = find_server($server)) + { + if (($Current_Server->{'conn'} & $CS_LOGIN) + && $Current_Server->{'port'} == $port) + { + return; + } + close_server($Current_Server); + } + else + { + if ($Current_Server = add_server()) + { + $Current_Server->{'server'} = $server; + $Current_Server->{'nick'} = $nick; + } + } + + if ($Current_Server) + { + $Current_Server->{'port'} = $port; + unless ($Current_Server->{'umode'}) + { + $Current_Server->{'umode'} = vim_get_serverumode(); + } + if ($pass) + { + $Current_Server->{'pass'} = $pass; + } + open_server($Current_Server); + } +} + +sub login_server +{ + my $sref = shift; + + if ($Current_Server != $sref) + { + $Current_Server = $sref; + } + + if ($sref->{'pass'}) + { + irc_send("PASS %s", $sref->{'pass'}); + } + irc_send("NICK %s", $sref->{'nick'}); + irc_send("USER %s %s %s :%s", + vim_getvar('s:user'), + vim_get_serverumode(), + vim_getvar('s:user'), + vim_getvar('s:realname')); +} + +sub post_login_server +{ + my $sref = shift; + + $sref->{'motd'} = 1; + $sref->{'conn'} &= ~$CS_RECON; # clear the "reconnecting" flag + + irc_send("USERHOST %s", $sref->{'nick'}); + + if ($sref->{'umode'}) + { + irc_send("MODE %s %s", $sref->{'nick'}, $sref->{'umode'}); + } + + if ($sref->{'away'}) + { + irc_send("AWAY :%s", $sref->{'away'}); + } + + if (@{$sref->{'chans'}}) + { + # Re-join channels + foreach my $cref (@{$sref->{'chans'}}) + { + my $args = [ "JOIN %s%s", $cref->{'name'}, $cref->{'key'} + ? " $cref->{'key'}" + : "" ]; + add_timer(2, \&irc_send, $args); + } + } + else + { + # Wait a few seconds so that you join channels (hopefully) after + # auto-identifying the nick + add_timer(2, \&do_auto_join); + } +} + +sub conn_close +{ + my $sref = shift; + + if ($sref->{'sock'}) + { + $RS->remove($sref->{'sock'}); + + if (defined($WS) && $WS->exists($sref->{'sock'})) + { + $WS->remove($sref->{'sock'}); + } + + #$sref->{'sock'}->shutdown(2); + $sref->{'sock'}->close(); + } +} + +sub conn_open +{ + use IO::Socket; + + my $sref = shift; + my $sock; + + if ($sref->{'server'} && $sref->{'port'}) + { + $sock = IO::Socket::INET->new(PeerAddr => $sref->{'server'}, + PeerPort => $sref->{'port'}, + Proto => 'tcp', + Timeout => 10); + if ($sock) + { + $sref->{'sock'} = $sock; + } + } + + return defined($sock); +} + +sub conn_watchin +{ + my $sref = shift; + + if (defined($sref->{'sock'})) + { + unless (defined($RS)) + { + use IO::Select; + $RS = IO::Select->new(); + } + $RS->add($sref->{'sock'}); + } +} + +sub conn_watchout +{ + my $sref = shift; + + if (defined($sref->{'sock'})) + { + unless (defined($WS)) + { + $WS = IO::Select->new(); + } + $WS->add($sref->{'sock'}); + } +} + +sub set_curserver +{ + my $sock = shift; + + foreach my $sref (@Servers) + { + if ($sock == $sref->{'sock'}) + { + $Current_Server = $sref; + VIM::DoCommand("let s:server = \"$sref->{'server'}\""); + last; + } + } +} + +sub has_lines +{ + my $cref = shift; + + return (exists($cref->{'lines'}) && @{$cref->{'lines'}}); +} + +sub put_all_lines +{ + my $sref = shift; + + if (has_lines($sref)) + { + put_serv_lines($sref); + } + + if (has_lines($sref->{'list'})) + { + put_list_lines($sref->{'list'}); + } + + foreach my $cref (@{$sref->{'chans'}}) + { + if (has_lines($cref)) + { + put_chan_lines($cref); + } + } + + foreach my $cref (@{$sref->{'chats'}}) + { + if (has_lines($cref)) + { + put_chat_lines($cref, $sref); + } + } +} + +sub put_serv_lines +{ + my $sref = shift; + my ($orgw, $peek) = pre_put_lines('serv'); + do_put_lines($sref, $peek); + post_put_lines($sref, $orgw, $peek); +} + +sub put_list_lines +{ + my $lref = shift; + my ($orgw, $peek) = pre_put_lines('list'); + do_put_lines($lref, $peek); + + unless (has_info($lref, $INFO_LINE1)) + { + $curbuf->Delete(1); + set_info($lref, $INFO_LINE1); + } + post_put_lines($lref, $orgw, $peek); +} + +sub put_chan_lines +{ + my $cref = shift; + my ($orgw, $peek) = pre_put_lines('chan', [ $cref->{'name'} ]); + do_put_lines($cref, $peek); + post_put_lines($cref, $orgw, $peek); +} + +sub put_chat_lines +{ + my ($cref, $sref) = @_; + my ($orgw, $peek) = pre_put_lines('chat', [ $cref->{'nick'}, + $sref->{'server'} ]); + do_put_lines($cref, $peek); + post_put_lines($cref, $orgw, $peek); +} + +sub pre_put_lines +{ + my ($type, $args) = @_; + my $orgw = vim_winnr(); + my $peek = !&{'vim_visit_'.$type}(@{$args}); + + if ($peek) + { + vim_peekbuf(0); + &{'vim_open_'.$type}(@{$args}); + } + return ($orgw, $peek); +} + +sub do_put_lines +{ + my ($cref, $peek) = @_; + # Shouldn't scroll down nor beep while you're away + my $notify = !($peek || $Current_Server->{'away'} || is_list($cref)); + + vim_modifybuf(1); + + if ($notify) + { + foreach my $line (@{$cref->{'lines'}}) + { + $curbuf->Append($curbuf->Count(), $line); + VIM::DoCommand("call s:NotifyNewEntry(0)"); + } + } + else + { + $curbuf->Append($curbuf->Count(), @{$cref->{'lines'}}); + } + vim_modifybuf(0); +} + +sub post_put_lines +{ + my ($cref, $orgw, $peek) = @_; + + if ($cref->{'bufnum'} < 0) + { + # I sometimes see this fail + $cref->{'bufnum'} = vim_bufnr(); + set_info($Current_Server, $INFO_UPDATE); + } + + if ($peek) + { + unless (has_info($cref, $INFO_HASNEW)) + { + set_info($cref, $INFO_HASNEW); + set_info($Current_Server, $INFO_UPDATE); + } + vim_peekbuf($orgw); + } + else + { + vim_winvisit($orgw); + } + + @{$cref->{'lines'}} = (); +} + +sub irc_push_line +{ + my $cref = shift; + my $args = shift; + my $form = shift(@{$args}); + + return push(@{$cref->{'lines'}}, sprintf("%s $form", vim_gettime(1), + @{$args})); +} + +sub irc_chan_line +{ + my $chan = shift; + my $cref = is_chan($chan) ? find_chan($chan) : undef; + + unless ($cref) + { + $cref = $Current_Server; + } + + if (irc_push_line($cref, \@_) >= 10) + { + &{'put_'.($cref == $Current_Server ? 'serv' : 'chan').'_lines'}($cref); + } +} + +sub irc_chat_line +{ + my $nick = shift; + my $cref = find_chat($nick, $Current_Server); + + unless ($cref) + { + $cref = add_chat($nick, $Current_Server); + } + + if (irc_push_line($cref, \@_) >= 10) + { + put_chat_lines($cref, $Current_Server); + } +} + +sub irc_list_line +{ + my $lref = $Current_Server->{'list'}; + + if (push(@{$lref->{'lines'}}, sprintf("%-22s %5d %s", @_)) >= 30) + { + put_list_lines($lref); + } +} + +sub irc_recv +{ + my $sock = shift; + my ($buffer, @lines); + + set_curserver($sock); + + unless (sysread($sock, $buffer, 2048)) + { + set_connected(0); + + unless ($Current_Server->{'conn'} & $CS_QUIT) + { + $Current_Server->{'conn'} |= $CS_RECON; + irc_chan_line('', "!: Connection with %s lost", + $Current_Server->{'server'}); + irc_chan_line('', "*: Reconnecting..."); + + if (open_server($Current_Server)) + { + return 1; + } + } + return 0; + } + + if ($Current_Server->{'lastbuf'}) + { + $buffer = $Current_Server->{'lastbuf'}.$buffer; + $Current_Server->{'lastbuf'} = undef; + } + + if ($ENC_VIM && $ENC_IRC) + { + Encode::from_to($buffer, $ENC_IRC, $ENC_VIM); + $buffer =~ s/\\x([[:xdigit:]]{2})/pack('H2', $1)/eg; + $buffer =~ s/\\x\{([[:xdigit:]]{4})\}/pack('H4', $1)/eg; + } + + @lines = split(/\x0D?\x0A/, $buffer); + if (substr($buffer, -1) ne "\x0A") + { + # Data obtained partially. Save the last line for later use + $Current_Server->{'lastbuf'} = pop(@lines); + } + + foreach my $line (@lines) + { + if (0) + { + vim_printf($line); + } + parse_line(\$line); + } + + return 1; +} + +sub irc_send +{ + my $form = shift; + my @args = @_; + + if ($ENC_VIM && $ENC_IRC) + { + foreach my $arg (@args) + { + Encode::from_to($arg, $ENC_VIM, $ENC_IRC); + } + } + syswrite($Current_Server->{'sock'}, sprintf("$form\x0D\x0A", @args)); +} + +# +# Infobar +# + +our $INFO_UPDATE = 0x01; +our $INFO_HASNEW = 0x02; +our $INFO_LINE1 = 0x04; + +sub update_info +{ + my $orgw = vim_winnr(); + # TODO: Preserve previous window (?) + + if (my $opened = vim_open_info()) + { + my ($orgln) = $curwin->Cursor(); + + vim_modifybuf(1); + $curbuf->Delete(1, $curbuf->Count()); + + foreach my $sref (@Servers) + { + my $dead = !($sref->{'conn'} & $CS_LOGIN); + + $curbuf->Append($curbuf->Count(), + sprintf("%s[%d]%s", + is_current($sref) + ? '*' + : has_info($sref, $INFO_HASNEW) + ? '+' + : $dead ? '-' : ' ', + $sref->{'bufnum'}, + $sref->{'server'})); + + if (my $lref = find_list($sref)) + { + $curbuf->Append($curbuf->Count(), + sprintf(" %s[%d]*list\t\t\t%s", + is_current($lref) + ? '*' + : has_info($lref, $INFO_HASNEW) + ? '+' + : $dead ? '-' : ' ', + $lref->{'bufnum'}, + $sref->{'server'})); + } + + foreach my $cref (@{$sref->{'chans'}}) + { + $curbuf->Append($curbuf->Count(), + sprintf(" %s[%d]%s\t\t\t%s", + is_current($cref) + ? '*' + : has_info($cref, $INFO_HASNEW) + ? '+' + : $dead ? '-' : ' ', + $cref->{'bufnum'}, + $cref->{'name'}, + $sref->{'server'})); + } + + foreach my $cref (@{$sref->{'chats'}}) + { + $curbuf->Append($curbuf->Count(), + sprintf(" %s[%d]%s\t\t\t%s", + is_current($cref) + ? '*' + : has_info($cref, $INFO_HASNEW) + ? '+' + : $dead + && index($cref->{'nick'}, '=') + ? '-' : ' ', + $cref->{'bufnum'}, + $cref->{'nick'}, + $sref->{'server'})); + } + + unset_info($sref, $INFO_UPDATE); + } + + $curbuf->Delete(1); + if ($orgln) + { + $curwin->Cursor($orgln, 0); + } + vim_modifybuf(0); + vim_winvisit($orgw + ($opened < 0)); + } +} + +sub has_info +{ + my ($sref, $info) = @_; + + return (exists($sref->{'info'}) && ($sref->{'info'} & $info)); +} + +sub set_info +{ + my ($sref, $info) = @_; + + if (exists($sref->{'info'}) && !($sref->{'info'} & $info)) + { + $sref->{'info'} |= $info; + } +} + +sub unset_info +{ + my ($sref, $info) = @_; + + if (exists($sref->{'info'}) && ($sref->{'info'} & $info)) + { + $sref->{'info'} &= ~$info; + } +} + +# Make it noticeable that a hidden channel has got a new message +#sub refresh_chans +#{ +# my ($chans, $cref) = @_; +# +# while ($chans->[0] != $cref) +# { +# push(@{$chans}, shift(@{$chans})); +# } +#} + +# +# Timer +# + +sub add_timer +{ + my ($secs, $func, $args) = @_; + my $timer; + + if (my $timers = $Current_Server->{'timers'}) + { + $timer = { time => time() + $secs, + func => $func, + args => $args, + done => 0 + }; + push(@{$timers}, $timer); + } + + return $timer; +} + +sub del_timer +{ + foreach my $sref (@Servers) + { + for (my $i = 0; $i <= $#{$sref->{'timers'}}; $i++) + { + if ($sref->{'timers'}->[$i]->{'done'}) + { + splice(@{$sref->{'timers'}}, $i--, 1); + } + } + } +} + +sub do_timer +{ + my $time = shift; + my $did = 0; + my $info = 0; + + my $save_cur = $Current_Server; + + foreach my $sref (@Servers) + { + set_curserver($sref->{'sock'}); + put_all_lines($sref); + + if ($sref->{'conn'} & $CS_LOGIN) + { + do_auto_ping($sref, $time); + do_auto_away($sref, $time); + + foreach my $timer (@{$sref->{'timers'}}) + { + if ($timer->{'time'} <= $time) + { + $timer->{'func'}(@{$timer->{'args'}}); + $timer->{'done'} = 1; + $did++; + } + } + } + + unless ($info) + { + $info = has_info($sref, $INFO_UPDATE); + } + } + + if ($did) + { + del_timer(); + } + if ($info) + { + update_info(); + } + set_curserver($save_cur->{'sock'}); +} + +sub do_auto_away +{ + my ($sref, $time) = @_; + + if (!$sref->{'away'} && vim_getvar('s:autoaway')) + { + my $lastactive = vim_getvar('s:lastactive'); + + if ($time >= $lastactive + vim_getvar('s:autoawaytime')) + { + irc_chan_line('', "*: Auto-awaying..."); + $sref->{'away'} = sprintf("I think I'm gone (been idle since %s).", + vim_gettime(0, $lastactive)); + irc_send("AWAY :%s", $sref->{'away'}); + } + } +} + +# Play ping-pong with servers to keep connected +sub do_auto_ping +{ + my ($sref, $time) = @_; + + if ($time >= $sref->{'lastping'} + 90) + { + irc_send("PING %d", $time); + $sref->{'lastping'} = $time; + } +} + +sub do_auto_join +{ + VIM::DoCommand('call s:DoAutoJoin()'); +} + +# +# CTCP +# + +# Heavily based on ircii and x-chat, regarding the DCC part. + +our @DCC_TYPES = qw(. GET SEND CHAT CHAT); + +our $DCC_FILERECV = 0x01; +our $DCC_FILESEND = 0x02; +our $DCC_CHATRECV = 0x03; +our $DCC_CHATSEND = 0x04; +our $DCC_TYPE = 0x0f; + +our $DCC_RESUME = 0x10; +our $DCC_QUEUED = 0x20; +our $DCC_ACTIVE = 0x40; +our $DCC_FAILED = 0x80; + +our $DCC_BLOCK_SIZE = 2048; + +sub ctcp_send +{ + # TODO: Follow the rules described in: + # "REVISED AND UPDATED CTCP SPECIFICATION" + # Dated Fri, 12 Aug 94 00:21:54 edt + # By ben@gnu.ai.mit.edu et al. + my $query = shift; + my $target= shift; + + irc_send("%s %s :\x01%s\x01", ($query ? 'PRIVMSG' : 'NOTICE'), $target, + sprintf(shift(@_), @_)); +} + +sub ctcp_query_action +{ + my ($from, $pref, $chan, $mesg) = @_; + + if (is_chan($chan)) + { + irc_chan_line($chan, "*%s%s*: %s", $pref, $from, ${$mesg}); + } + elsif (is_me($chan)) + { + irc_chat_line($from, "*%s*: %s", $from, ${$mesg}); + vim_beep(1); + } +} + +sub ctcp_query_clientinfo +{ + my ($from, $comd, $args) = @_; + our %info; + + unless (defined(%info)) + { + %info = ( + CLIENTINFO => 'gives information about available CTCP commands', + ECHO => 'returns arguments you send', + PING => 'returns arguments you send', + TIME => 'tells you the time on my side', + VERSION => 'shows client type and version' + ); + } + + unless ($args) + { + ctcp_send(0, $from, "%s %s. Use %s to get more specific + \ information", $comd, join(' ', keys(%info)), $comd); + } + else + { + $args = uc($args); + + if (exists($info{$args})) + { + ctcp_send(0, $from, "%s %s", $comd, $info{$args}); + } + else + { + ctcp_send(0, $from, "%s No such function implemented: %s", + $comd, $args); + } + } +} + +sub ctcp_query_echo +{ + ctcp_send(0, $_[0], "%s %s", $_[1], $_[2]); +} + +sub ctcp_query_ping +{ + ctcp_send(0, $_[0], "%s %s", $_[1], $_[2]); +} + +sub ctcp_query_time +{ + ctcp_send(0, $_[0], "%s %s", $_[1], vim_gettime(0)); +} + +sub ctcp_query_version +{ + ctcp_send(0, $_[0], "%s %s", $_[1], vim_getvar('s:client')); +} + +sub process_ctcp_query +{ + my ($from, $pref, $chan, $mesg) = @_; + + while (${$mesg} =~ s/\x01(.*?)\x01//) + { + # TODO: flood-protection codes here + if (my ($comd, $args) = ($1 =~ /^(\S+)(?:\s+(.+))?$/)) + { + if ($comd eq 'DCC') + { + if (is_me($chan)) # is this check necessary? + { + process_dcc_query($from, $args); + } + } + elsif ($comd eq 'ACTION') + { + ctcp_query_action($from, $pref, $chan, \$args); + } + else + { + my $func = 'ctcp_query_'.lc($comd); + + irc_chan_line($chan, "?%s%s(CTCP)?: %s %s", $pref, $from, $comd, + $args); + + if (defined(&{$func})) + { + &{$func}($from, $comd, $args); + } + else + { + if (is_me($chan)) + { + ctcp_send(0, $from, "ERRMSG %s: unknown query", $comd); + } + irc_chan_line('', "!: CTCP query from %s unprocessed: %s", + $from, $comd); + } + } + } + } + + return length(${$mesg}); +} + +sub process_ctcp_reply +{ + my ($from, $chan, $mesg) = @_; + + if (${$mesg} =~ s/\x01(.*?)\x01//) + { + if (my ($comd, $args) = ($1 =~ /^(\S+)(?:\s+(.+))?$/)) + { + if ($comd eq 'DCC') + { + if (is_me($chan)) + { + process_dcc_reply($from, $args); + } + } + elsif ($comd eq 'PING') + { + my $diff = time() - $args; + irc_chan_line('', "[%s(%s)]: %d second%s delay", + $from, $comd, $diff, ($diff != 1 ? 's' : '')); + } + else + { + irc_chan_line('', "[%s(%s)]: %s", $from, $comd, $args); + } + } + } + return length(${$mesg}); +} + +sub process_dcc_query +{ + my ($from, $args) = @_; + + if (1) + { + irc_chan_line('', "?%s(DCC)?: %s", $from, $args); + } + + # Hope no one includes spaces in filenames + if (my ($type, $desc, $server, $port, $size) = + ($args =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(\d+)(?:\s+(\d+))?$/)) + { + my $dcc; + + if ($type eq 'RESUME') + { + # NOTE: resumes don't have a `server' part + dcc_they_resume($from, $desc, $server, $port); + return; + } + elsif ($type eq 'ACCEPT') + { + dcc_i_resume($from, $desc, $server, $port); + return; + } + + # TODO: IPv6 + if (index($server, '.') <= 0) # XXX: could it be in dotted-quad form? + { + $server = inet_ntoa(pack('N', $server)); + } + + if ($type eq 'CHAT') + { + $dcc = find_dccclient($from, $DCC_CHATRECV); + if ($dcc) + { + dcc_close($dcc); + } + $dcc = find_dccclient($from, $DCC_CHATSEND); + if ($dcc) + { + dcc_close($dcc); + } + + $dcc = add_dccclient(); + if ($dcc) + { + $dcc->{'nick'} = $from; + $dcc->{'server'}= $server; + $dcc->{'port'} = $port; + $dcc->{'state'} = $DCC_CHATRECV; + $dcc->{'desc'} = $desc; + } + } + elsif ($type eq 'SEND') + { + $dcc = find_dccclient($from, $DCC_FILERECV, $desc); + if ($dcc) + { + return; + } + + $dcc = add_dccclient(); + if ($dcc) + { + my $dccdir = vim_getvar('s:dccdir'); + my ($fname)= ($desc =~ m#^(?:.*/)?(.+)$#); + + $dcc->{'nick'} = $from; + $dcc->{'server'}= $server; + $dcc->{'port'} = $port; + $dcc->{'state'} = $DCC_FILERECV; + $dcc->{'desc'} = $desc; + $dcc->{'fname'} = sprintf("%s/%s", $dccdir, do_urldecode($fname)); + $dcc->{'fsize'} = $size; + + # FIXME: Currently turning off the RESUME: append seems to + # corrupt data + if (0 && -e $dcc->{'fname'}) + { + if ((my $fsize = (-s $dcc->{'fname'})) < $size) + { + # Need confirmation? + $dcc->{'state'} |= ($DCC_RESUME | $DCC_QUEUED); + $dcc->{'bytesread'} = $fsize; + ctcp_send(1, $dcc->{'nick'}, "DCC RESUME %s %d %d", + $desc, $port, $fsize); + return; + } + } + } + } + else + { + irc_chan_line('', "DCC: Query from %s unprocessed: %s", $from, $args); + ctcp_send(0, $from, "DCC REJECT %s %s", $type, $desc); + } + + if ($dcc) + { + $dcc->{'state'} |= $DCC_QUEUED; + dcc_open($dcc); + } + } +} + +sub process_dcc_reply +{ + my ($from, $args) = @_; + + if (my ($type, $desc) = ($args =~ /^(\S+)\s+(.+)$/)) + { + dcc_was_rejected($from, uc($type), $desc); + } + else + { + irc_chan_line('', "DCC: Reply from %s: %s", $from, $args); + } +} + +sub is_dcc_type +{ + my ($dcc, $type) = @_; + + return (exists($dcc->{'state'}) && ($dcc->{'state'} & $DCC_TYPE) == $type); +} + +sub dcc_ask_accept_offer +{ + my $dcc = shift; + my $mesg = sprintf("%s is offering DCC %s to you. Accept it", + $dcc->{'nick'}, + (is_dcc_type($dcc, $DCC_FILERECV) + ? 'SEND' + : $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE])); + + vim_beep(2); + return vim_confirm($mesg); +} + +sub dcc_show_list +{ + irc_chan_line('', "*: Listing DCC sessions..."); + + foreach my $sref (@Servers) + { + if (exists($Clients{$sref->{'server'}}) + && @{$Clients{$sref->{'server'}}}) + { + irc_chan_line('', "DCC(LIST): * Via %s:", $sref->{'server'}); + foreach my $dcc (@{$Clients{$sref->{'server'}}}) + { + irc_chan_line('', "DCC(LIST): %-5s %-10.10s (%s) %7u/%-7u %s", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'nick'}, + (($dcc->{'state'} & $DCC_ACTIVE) + ? 'active' + : ($dcc->{'state'} & $DCC_QUEUED) + ? 'queued' + : 'closed'), + ($dcc->{'bytessent'} + ? $dcc->{'bytessent'} + : $dcc->{'bytesread'}), + $dcc->{'fsize'}, + $dcc->{'fname'}); + } + } + } + irc_chan_line('', "End of /DCC LIST"); +} + +sub dcc_was_rejected +{ + my ($nick, $type, $desc) = @_; + + for (my $i = 1; $i < $#DCC_TYPES; $i++) + { + if ($type eq $DCC_TYPES[$i]) + { + $type = $i; + } + } + + unless ($type & $DCC_TYPE) + { + return; + } + + if (my $dcc = find_dccclient($nick, $type, $desc)) + { + irc_chan_line('', "DCC(%s): Offering %s to %s was rejected", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $desc, + $nick); + dcc_close($dcc); + } +} + +sub dcc_change_nicks +{ + my ($old, $new) = @_; + + # XXX: Why while? I don't remember + while (my $dcc = find_dccclient($old)) + { + $dcc->{'nick'} = $new; + } +} + +sub dcc_parse_line +{ + my ($cref, $dcc, $itsme) = @_; + + foreach my $line (split(/\x0D?\x0A/, $dcc->{'linebuf'})) + { + my $action = ($line =~ s/^\x01ACTION (.*?)\x01/$1/); + + if ($ENC_VIM && $ENC_IRC) + { + Encode::from_to($line, $ENC_IRC, $ENC_VIM); + } + if (irc_push_line($cref, [ "%s%s%s: %s", + ($action ? '*' : '='), + ($itsme + ? $dcc->{'iserver'}->{'nick'} + : $dcc->{'nick'}), + ($action ? '*' : '='), + $line ]) >= 10) + { + put_chat_lines($cref, $dcc->{'iserver'}); + } + } +} + +sub dcc_chat_line +{ + my ($dcc, $itsme) = @_; + my $cref = find_chat("=$dcc->{'nick'}", $dcc->{'iserver'}); + + dcc_parse_line($cref, $dcc, $itsme); + + unless ($itsme) + { + vim_beep(1); + } +} + +sub add_dccclient +{ + # To avoid nick collisions, we hold dcc clients on server basis + my $server = $Current_Server->{'server'}; + my $dcc = { nick => undef, + iserver => $Current_Server, + server => undef, + port => 0, + sock => undef, + state => 0, + desc => undef, + fh => undef, + fname => undef, + fsize => 0, + linebuf => undef, + lastbuf => undef, + bytesread => 0, + bytessent => 0, + starttime => 0, + lasttime => 0 + }; + + unless (exists($Clients{$server})) + { + $Clients{$server} = []; + } + + unshift(@{$Clients{$server}}, $dcc); + return $dcc; +} + +sub find_dccclient_fd +{ + my $sock = shift; + + foreach my $sref (@Servers) + { + unless (exists($Clients{$sref->{'server'}})) + { + next; + } + + foreach my $dcc (@{$Clients{$sref->{'server'}}}) + { + if ($sock == $dcc->{'sock'}) + { + return $dcc; + } + } + } + + return undef; +} + +sub find_dccclient +{ + # An empty argument will be used as a wildcard (i.e., matches anything) + # NOTE: Only clients on the current IRC server will be searched, since this + # will only be called upon queries via that server + my ($nick, $type, $desc) = @_; + + foreach my $dcc (@{$Clients{$Current_Server->{'server'}}}) + { + if ($dcc->{'state'} & $DCC_FAILED) # do not return closed/rejected one + { + next; + } + if ($nick && $dcc->{'nick'} ne $nick) + { + next; + } + if ($type && !is_dcc_type($dcc, $type)) + { + next; + } + if ($desc && $dcc->{'desc'} ne $desc) + { + next; + } + return $dcc; + } + + return undef; +} + +sub del_dccclient +{ + my $dcc = shift; + + foreach my $sref (@Servers) + { + if (exists($Clients{$sref->{'server'}})) + { + my $clients = $Clients{$sref->{'server'}}; + + for (my $i = 0; $i <= $#{$clients}; $i++) + { + if ($dcc == $clients->[$i]) + { + splice(@{$clients}, $i, 1); + return; + } + } + } + } +} + +sub dcc_init_listen +{ + my $dcc = shift; + # local address in unsigned long integer + my $local= unpack('N', inet_aton($Current_Server->{'local'})); + # port to watch at (normally 0) + my $port = vim_getvar('s:dccport') + 0; + my $ctcp; + + # Open up a listening socket + $dcc->{'sock'} = IO::Socket::INET->new( LocalAddr => undef, + LocalPort => $port, + Proto => 'tcp', + Listen => 1, + ReuseAddr => 1 ); + unless ($dcc->{'sock'}) + { + del_dccclient($dcc); + irc_chan_line('', "DCC(%s): Failed in opening socket: %s", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], $!); + return; + } + + $dcc->{'port'} = $port ? $port : $dcc->{'sock'}->sockport(); + + if (is_dcc_type($dcc, $DCC_FILESEND)) + { + ctcp_send(1, $dcc->{'nick'}, "DCC %s %s %lu %u %ld", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'desc'}, + $local, + $dcc->{'port'}, + $dcc->{'fsize'}); + } + else + { + ctcp_send(1, $dcc->{'nick'}, "DCC %s chat %lu %u", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $local, + $port); + } + conn_watchin($dcc); +} + +sub dcc_i_accept +{ + my $dcc = shift; + my $sock = $dcc->{'sock'}->accept(); + + unless ($sock) + { + irc_chan_line('', "DCC(%s): Could not accept incoming connect from %s: %s", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'nick'}, + $!); + dcc_close($dcc); + return; + } + + conn_close($dcc); + #$sock->sockopt(SO_SNDLOWAT, $DCC_BLOCK_SIZE); + $dcc->{'sock'} = $sock; + $dcc->{'server'} = $sock->peerhost(); + conn_watchin($dcc); + $dcc->{'state'} &= ~$DCC_QUEUED; + $dcc->{'state'} |= $DCC_ACTIVE; + + irc_chan_line('', "DCC(%s): Connection with %s established", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'nick'}); + + if (is_dcc_type($dcc, $DCC_FILESEND)) + { + # FIXME: it blocks + #conn_watchout($dcc); + dcc_send_file($dcc); + } + elsif (is_dcc_type($dcc, $DCC_CHATSEND)) + { + # Drop the `send' flag off + $dcc->{'state'} = ($DCC_CHATRECV | $DCC_ACTIVE); + } + + if (is_dcc_type($dcc, $DCC_CHATRECV)) + { + vim_open_chat("=$dcc->{'nick'}", $dcc->{'iserver'}->{'server'}); + } +} + +sub dcc_they_accept +{ + my $dcc = shift; + + if (conn_open($dcc)) + { + $dcc->{'state'} &= ~$DCC_QUEUED; + $dcc->{'state'} |= $DCC_ACTIVE; + + if (is_dcc_type($dcc, $DCC_CHATRECV)) + { + add_chat("=$dcc->{'nick'}", $dcc->{'iserver'}); + } + + conn_watchin($dcc); + } + else + { + irc_chan_line('', "DCC(%s): Could not initialize connection with %s: %s", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'nick'}, $!); + del_dccclient($dcc); + } +} + +sub dcc_open +{ + my $dcc = shift; + + unless ($dcc->{'state'}) + { + # Huh? + del_dccclient($dcc); + return; + } + + if (is_dcc_type($dcc, $DCC_FILESEND) || is_dcc_type($dcc, $DCC_CHATSEND)) + { + dcc_init_listen($dcc); + } + else + { + if (($dcc->{'state'} & $DCC_RESUME) || dcc_ask_accept_offer($dcc)) + { + dcc_they_accept($dcc); + } + } +} + +sub dcc_close +{ + my ($dcc, $destroy) = @_; + + conn_close($dcc); + + if ($dcc->{'fh'}) + { + close($dcc->{'fh'}); + } + + # XXX: Delete it right now? Or should we wait a few seconds? + if (!defined($destroy) || $destroy) + { + if (is_dcc_type($dcc, $DCC_CHATRECV) || is_dcc_type($dcc, $DCC_CHATSEND)) + { + del_chat("=$dcc->{'nick'}", $dcc->{'iserver'}); + } + del_dccclient($dcc); + } + else + { + $dcc->{'state'} &= ~$DCC_ACTIVE; + } +} + +sub dcc_i_resume +{ + my ($nick, $desc, $port, $resume) = @_; + my $dcc = find_dccclient($nick, $DCC_FILERECV, $desc); + + if ($dcc && ($dcc->{'state'} & $DCC_QUEUED)) + { + dcc_open($dcc); + } +} + +sub dcc_they_resume +{ + my ($nick, $desc, $port, $resume) = @_; + my $dcc = find_dccclient($nick, $DCC_FILESEND, $desc); + + if ($dcc && ($dcc->{'port'} == $port) && ($resume < $dcc->{'fsize'})) + { + $dcc->{'bytessent'} = $resume; + ctcp_send(1, $dcc->{'nick'}, "DCC ACCEPT %s %d %d", $desc, $port, + $resume); + } +} + +sub dcc_recv_file +{ + my $dcc = shift; + my ($buffer, $bytesread); + + unless ($dcc->{'fh'}) + { + my $dccdir = vim_getvar('s:dccdir'); + + unless (my_mkdir($dccdir)) + { + irc_chan_line('', "DCC(GET): Could not create download directory: %s", + $!); + dcc_close($dcc); + return; + } + + if ($dcc->{'state'} & $DCC_RESUME) + { + sysopen($dcc->{'fh'}, $dcc->{'fname'}, O_BINARY|O_WRONLY|O_APPEND); + } + else + { + if (-e $dcc->{'fname'}) + { + my $n = 0; + my $newname = $dcc->{'fname'}; + while (-e $newname) + { + $newname = sprintf("%s.%d", $dcc->{'fname'}, ++$n); + } + $dcc->{'fname'} = $newname; + irc_chan_line('', "DCC(GET): File already exists. + \ Saving it as %s", $newname); + } + sysopen($dcc->{'fh'}, $dcc->{'fname'}, + O_BINARY|O_WRONLY|O_EXCL|O_CREAT); + } + + unless ($dcc->{'fh'}) + { + irc_chan_line('', "DCC(GET): Could not open file %s for writing: + \ %s", $dcc->{'fname'}, $!); + dcc_close($dcc); + return; + } + } + + unless ($bytesread = sysread($dcc->{'sock'}, $buffer, $DCC_BLOCK_SIZE)) + { + if ($dcc->{'bytesread'} < $dcc->{'fsize'}) + { + irc_chan_line('', "DCC(GET): Connection with %s lost", $dcc->{'nick'}); + } + dcc_close_filetransfer($dcc); + } + else + { + unless (syswrite($dcc->{'fh'}, $buffer, $bytesread)) # hdd full? + { + irc_chan_line('', "DCC(GET): Failed in writing to file: %s", $!); + dcc_close($dcc); + } + else + { + # Notify the progress to the peer + $dcc->{'bytesread'} += $bytesread; + unless (syswrite($dcc->{'sock'}, pack('N', $dcc->{'bytesread'}), 4)) + { + irc_chan_line('', "DCC(GET): Connection with %s lost", + $dcc->{'nick'}); + dcc_close($dcc); + } + } + } +} + +sub dcc_recv_ack +{ + my $dcc = shift; + my $ack; + + if (sysread($dcc->{'sock'}, $ack, 4) < 4) + { + irc_chan_line('', "DCC(SEND): Connection with %s lost", $dcc->{'nick'}); + dcc_close($dcc); + } + else + { + if ($dcc->{'bytessent'} >= $dcc->{'fsize'}) + { + if (unpack('N', $ack) >= $dcc->{'fsize'}) + { + dcc_close_filetransfer($dcc); + } + } + else + { + dcc_send_file($dcc); + } + } +} + +sub dcc_send_file +{ + my $dcc = shift; + my ($buffer, $bytesread); + + unless ($dcc->{'fh'}) + { + if (sysopen($dcc->{'fh'}, $dcc->{'fname'}, O_BINARY|O_RDONLY)) + { + if ($dcc->{'bytessent'}) + { + use Fcntl qw(SEEK_SET); + sysseek($dcc->{'fh'}, $dcc->{'bytessent'}, SEEK_SET); + } + } + else + { + irc_chan_line('', "DCC(SEND): Could not open file for reading: %s", + $!); + dcc_close($dcc); + return; + } + } + + if ($bytesread = sysread($dcc->{'fh'}, $buffer, $DCC_BLOCK_SIZE)) + { + # FIXME: It blocks if called via $WS + if (syswrite($dcc->{'sock'}, $buffer, $bytesread) < $bytesread) + { + irc_chan_line('', "DCC(SEND): Connection with %s lost", + $dcc->{'nick'}); + dcc_close($dcc); + } + else + { + $dcc->{'bytessent'} += $bytesread; + } + } +} + +sub dcc_close_filetransfer +{ + my $dcc = shift; + + irc_chan_line('', "DCC(%s): Transfer of %s with %s completed (%d/%d)", + $DCC_TYPES[$dcc->{'state'} & $DCC_TYPE], + $dcc->{'desc'}, + $dcc->{'nick'}, + ($dcc->{'bytessent'} + ? $dcc->{'bytessent'} + : $dcc->{'bytesread'}), + $dcc->{'fsize'}); + dcc_close($dcc); +} + +sub dcc_recv_chat +{ + my $dcc = shift; + + if (my $bytesread = sysread($dcc->{'sock'}, $dcc->{'linebuf'}, 1024)) + { + $dcc->{'bytesread'} += $bytesread; + + if ($dcc->{'lastbuf'}) + { + $dcc->{'linebuf'} = $dcc->{'lastbuf'}.$dcc->{'linebuf'}; + $dcc->{'lastbuf'} = undef; + } + + if (substr($dcc->{'linebuf'}, -1) ne "\x0A") + { + # irssi sends "\n" separately after sending a message line. Why? + $dcc->{'lastbuf'} = $dcc->{'linebuf'}; + } + else + { + dcc_chat_line($dcc); + } + } + else + { + irc_chan_line('', "DCC(CHAT): Connection with %s lost", $dcc->{'nick'}); + dcc_close($dcc); + } +} + +sub dcc_send_chat +{ + my ($nick, $mesg) = @_; + my $dcc = find_dccclient($nick, $DCC_CHATRECV); + + if ($dcc->{'state'} & $DCC_ACTIVE) + { + if ($ENC_VIM && $ENC_IRC) + { + Encode::from_to($mesg, $ENC_VIM, $ENC_IRC); + } + + $dcc->{'linebuf'} = sprintf("%s\x0D\x0A", $mesg); + if (my $bytessent = syswrite($dcc->{'sock'}, $dcc->{'linebuf'})) + { + $dcc->{'bytessent'} += $bytessent; + dcc_chat_line($dcc, 1); + } + } +} + +sub dcc_check +{ + my ($sock, $write) = @_; + my $dcc = find_dccclient_fd($sock); + + unless ($dcc) + { + return 0; + } + + if ($write) + { + if (is_dcc_type($dcc, $DCC_FILESEND)) + { + # We don't take much care about user acknowledgement + dcc_send_file($dcc); + } + } + else + { + if (($dcc->{'state'} & $DCC_QUEUED) && (is_dcc_type($dcc, $DCC_FILESEND) + || is_dcc_type($dcc, $DCC_CHATSEND))) + { + dcc_i_accept($dcc); + } + else + { + if (is_dcc_type($dcc, $DCC_FILERECV)) + { + dcc_recv_file($dcc); + } + elsif (is_dcc_type($dcc, $DCC_FILESEND)) + { + # Pity we have to wait for a user ack before sending out + dcc_recv_ack($dcc); + } + elsif (is_dcc_type($dcc, $DCC_CHATRECV)) + { + # MEMO: `send' flag must have already been dropped off + dcc_recv_chat($dcc); + } + } + } + return 1; +} + +sub cmd_dcc +{ + my ($type, $nick, $desc) = @_; + my $dcc; + + if ($type eq 'CLOSE') + { + # /dcc close type [nick] + $type = uc($nick); + $nick = $desc; + # Hmm, can't specify files + + if ($type) + { + for (my $i = 1; $i < $#DCC_TYPES; $i++) + { + if ($type eq $DCC_TYPES[$i]) + { + $type = $i; + last; + } + } + unless ($type & $DCC_TYPE) + { + $type = 0; + } + } + + while ($dcc = find_dccclient($nick, $type, $desc)) + { + dcc_close($dcc); + } + return; + } + elsif ($type eq 'LIST') + { + dcc_show_list(); + return; + } + elsif ($type eq 'GET') + { + $dcc = find_dccclient($nick, $DCC_FILERECV, $desc); + if ($dcc && ($dcc->{'state'} & $DCC_ACTIVE)) + { + return; + } + } + elsif ($type eq 'SEND') + { + if ($dcc = find_dccclient($nick, $DCC_FILESEND, $desc)) + { + # already active or in queue + return; + } + else + { + if ($dcc = add_dccclient()) + { + my ($fname) = ($desc =~ m#^(?:.*/)?(.+)$#); + + $dcc->{'nick'} = $nick; + $dcc->{'state'} = $DCC_FILESEND; + $dcc->{'desc'} = do_urlencode($fname); + $dcc->{'fname'} = $desc; + $dcc->{'fsize'} = (-s $desc); + } + } + } + elsif ($type eq 'CHAT') + { + # Reuse already connected one, if found + if ($dcc = find_dccclient($nick, $DCC_CHATSEND)) + { + # it must already be in queue on the peer side + return; + } + else + { + unless ($dcc = find_dccclient($nick, $DCC_CHATRECV)) + { + if ($dcc = add_dccclient()) + { + $dcc->{'nick'} = $nick; + $dcc->{'state'}= $DCC_CHATSEND; + $dcc->{'desc'} = lc($type); + + add_chat("=$dcc->{'nick'}", $dcc->{'iserver'}); + } + } + } + } + + if ($dcc) + { + if ($dcc->{'state'} & $DCC_QUEUED) + { + dcc_they_accept($dcc); + } + elsif (!($dcc->{'state'} & $DCC_ACTIVE)) + { + $dcc->{'state'} |= $DCC_QUEUED; + dcc_open($dcc); + } + } +} + +# +# Channel management +# + +our $UMODE_VOICE = 0x01; +our $UMODE_CHOP = 0x02; + +sub is_current +{ + my $cref = shift; + + return (exists($cref->{'bufnum'}) && $cref->{'bufnum'} == $Current_ChanServ); +} + +sub is_chan +{ + return ($_[0] =~ /^[&#+!]/); +} + +sub is_list +{ + return ($_[0] == $Current_Server->{'list'} || $_[0] eq '*list'); +} + +sub process_umode +{ + my $modes = shift; + + while ($modes =~ s/^\s*([+-])(\S+)//) + { + my $add = ($1 eq '+'); + + foreach my $mode (split(//, $2)) + { + if ($add) + { + unless ($Current_Server->{'umode'} =~ /$mode/) + { + $Current_Server->{'umode'} .= $mode; + } + } + else + { + $Current_Server->{'umode'} =~ s/$mode//; + } + } + } + + irc_chan_line('', "*: Your user modes: %s", $Current_Server->{'umode'}); + vim_setumode($Current_Server->{'umode'}); +} + +sub process_cmode +{ + my ($chan, $modes) = @_; + + if (0) + { + vim_printf("modes=%s", $modes); + } + + if (my $cref = find_chan($chan)) + { + if (my ($modes, $args) = ($modes =~ /^(\S+)(?:\s+(.+))?/)) + { + my ($add, $mode); + my @args = split(/\s+/, $args); + + while ($mode = substr($modes, 0, 1)) + { + if ($mode =~ /[-+]/) + { + $add = ($mode eq '+'); + } + elsif ($mode =~ /[beIO]/) # I just ignore these + { + shift(@args); + } + elsif ($mode =~ /[fJ]/) # dunno what these are for + { + shift(@args); + } + elsif ($mode eq 'k') + { + if ($add) + { + $cref->{'key'} = shift(@args); + } + else + { + $cref->{'key'} = undef; + } + } + elsif ($mode eq 'l') + { + if ($add) + { + $cref->{'limit'} = shift(@args); + } + else + { + $cref->{'limit'} = 0; + } + } + elsif ($mode =~ /[ov]/) + { + my $nick = shift(@args); + + if (my $nref = find_nick($nick, $chan)) + { + my $val = $mode eq 'o' ? $UMODE_CHOP : $UMODE_VOICE; + if ($add) + { + $nref->{'umode'} |= $val; + if (is_me($nick)) + { + $cref->{'umode'} |= $val; + } + } + else + { + $nref->{'umode'} &= ~$val; + if (is_me($nick)) + { + $cref->{'umode'} &= ~$val; + } + } + } + draw_nickwin($chan); + } + else + { + if ($add) + { + if (index($cref->{'cmode'}, $mode) < 0) + { + $cref->{'cmode'} .= $mode; + } + } + else + { + $cref->{'cmode'} =~ s/$mode//; + } + } + $modes = substr($modes, 1); + } + } + + { + my $chan = do_escape($chan); + my $modes = $cref->{'cmode'}; + + if ($cref->{'key'} && $cref->{'limit'}) + { + $modes .= "kl $cref->{'key'} $cref->{'limit'}"; + } + elsif ($cref->{'key'}) + { + $modes .= "k $cref->{'key'}"; + } + elsif ($cref->{'limit'}) + { + $modes .= "l $cref->{'limit'}"; + } + VIM::DoCommand("call s:SetChannelMode(\"$chan\", \"$modes\")"); + } + } +} + +sub add_chan +{ + my ($chan, $key) = @_; + my $cref; + # We should not change cases here. It may cause troubles with non-ascii + # characters + + if (my $chans = $Current_Server->{'chans'}) + { + unless (find_chan($chan)) + { + $cref = { name => $chan, + key => $key, + limit => 0, + info => 0, + bufnum => -1, + umode => 0, + cmode => undef, + nicks => [], + splits => [], + lines => [] + }; + push(@{$chans}, $cref); + set_info($Current_Server, $INFO_UPDATE); + } + } + + return $cref; +} + +sub find_chan +{ + my ($chan, $sref) = @_; + + if ($chan) + { + unless ($sref) + { + $sref = $Current_Server; + } + + # XXX: Here and there I'm assuming `foreach' is faster than `for'. + # Correct me if it is wrong. + foreach my $cref (@{$sref->{'chans'}}) + { + if (lc($cref->{'name'}) eq lc($chan)) + { + return $cref; + } + } + } + + return undef; +} + +sub del_chan +{ + my $chan = shift; + + if (my $chans = $Current_Server->{'chans'}) + { + for (my $i = 0; $i <= $#{$chans}; $i++) + { + if (lc($chans->[$i]->{'name'}) eq lc($chan)) + { + splice(@{$chans}, $i, 1); + set_info($Current_Server, $INFO_UPDATE); + last; + } + } + } + # NOTE: Do not lower case here + vim_close_chan($chan); +} + +sub add_list +{ + my $sref = shift; + + $sref->{'list'} = { bufnum => -1, info => 0, lines => [] }; +} + +sub find_list +{ + my $sref = shift; + + unless ($sref) + { + $sref = $Current_Server; + } + + return ($sref->{'list'}->{'bufnum'} >= 0) ? $sref->{'list'} : undef; +} + +# +# User management +# + +sub is_me +{ + return (lc($_[0]) eq lc($Current_Server->{'nick'})); +} + +sub get_nicks +{ + if (my $cref = find_chan($_[0])) + { + return $cref->{'nicks'}; + } + return undef; +} + +sub init_nicks +{ + if (my $nicks = get_nicks($_[0])) + { + @{$nicks} = (); + } +} + +sub sort_nicks +{ + if (my $nicks = get_nicks($_[0])) + { + @{$nicks} = sort { $b->{'umode'} <=> $a->{'umode'} } + sort { lc($a->{'nick'}) cmp lc($b->{'nick'}) } + @{$nicks}; + } +} + +# Move designated nick on top of a list, making it easier to get prefix ([@+]) +# for active speakers +sub refresh_nicks +{ + my ($nick, $chan) = @_; + + if (my $nicks = get_nicks($chan)) + { + for (my $i = 0; $i <= $#{$nicks}; $i++) + { + if ($nicks->[$i]->{'nick'} eq $nick) + { + my $nref = $nicks->[$i]; + + if ($i) + { + splice(@{$nicks}, $i, 1); + unshift(@{$nicks}, $nref); + return 1; + } + last; + } + } + } + return 0; +} + +sub add_nick +{ + my ($nick, $mode, $chan, $force) = @_; + + if (my $cref = find_chan($chan)) + { + if (is_me($nick)) + { + $cref->{'umode'} = $mode; + } + + if ($force || !find_nick($nick, $chan)) + { + unshift(@{$cref->{'nicks'}}, { nick => $nick, umode => $mode }); + } + } +} + +sub find_nick +{ + my ($nick, $chan) = @_; + + if (my $nicks = get_nicks($chan)) + { + foreach my $nref (@{$nicks}) + { + if ($nref->{'nick'} eq $nick) + { + return $nref; + } + } + } + return undef; +} + +sub del_nick +{ + my ($nick, $chan) = @_; + + if (my $nicks = get_nicks($chan)) + { + for (my $i = 0; $i <= $#{$nicks}; $i++) + { + if ($nicks->[$i]->{'nick'} eq $nick) + { + splice(@{$nicks}, $i, 1); + return 1; + } + } + } + return 0; +} + +sub change_nicks +{ + my ($old, $new, $chan) = @_; + + if (my $nref = find_nick($old, $chan)) + { + $nref->{'nick'} = $new; + return 1; + } + return 0; +} + +sub find_nickprefix +{ + my ($nick, $chan) = @_; + + if (is_me($nick)) + { + if (my $cref = find_chan($chan)) + { + return get_nickprefix($cref->{'umode'}); + } + } + else + { + if (my $nref = find_nick($nick, $chan)) + { + return get_nickprefix($nref->{'umode'}); + } + } + return undef; +} + +sub get_nickprefix +{ + if (my $mode = $_[0]) + { + my $pref = ''; + + if ($mode & $UMODE_CHOP) + { + $pref .= '@'; + } + if ($mode & $UMODE_VOICE) + { + $pref .= '+'; + } + return $pref; + } + return undef; +} + +sub draw_nickline +{ + my ($nick, $pref, $chan, $add) = @_; + my $orgw = vim_winnr(); + + if (vim_visit_nicks($chan)) + { + my $orgln = ($orgw == vim_winnr()) ? VIM::Eval("getline('.')") + : undef; + my $todel = vim_search("$pref$nick"); + + unless ($add && $todel == 1) + { + vim_modifybuf(1); + if ($todel) + { + $curbuf->Delete($todel); + } + if ($add) + { + $curbuf->Append(0, sprintf("%s%s", $pref, $nick)); + } + vim_modifybuf(0); + } + if ($orgln) # Restore the cursor position + { + vim_search($orgln); + } + else + { + # Keep top lines visible. Since I dislike modifying the jumplist, + # I just set the cursor position here. + $curwin->Cursor(1, 0); # but this doesn't update the view itself + VIM::DoCommand('normal! 0'); # so this is necessary + } + VIM::DoCommand("$orgw wincmd w"); + } + else + { + draw_nickwin($chan); + } +} + +# TODO: Preserve the previous window (?) + +sub draw_nickwin +{ + my $chan = shift; + # Use bufnum since window number can change + my $orgb = vim_bufnr(); + my $orgw = vim_winnr(); + + if (vim_open_nicks($chan)) + { + my $nicks = get_nicks($chan); + my ($orgln) = ($orgw == vim_winnr()) ? $curwin->Cursor() : (0); + + vim_modifybuf(1); + $curbuf->Delete(1, $curbuf->Count()); + + foreach my $nref (@{$nicks}) + { + $curbuf->Append($curbuf->Count(), sprintf("%s%s", + get_nickprefix($nref->{'umode'}), + $nref->{'nick'})); + } + $curbuf->Delete(1); + vim_modifybuf(0); + + if ($orgln) + { + $curwin->Cursor($orgln, 0); + } + else + { + VIM::DoCommand('normal! 0'); + } + vim_bufvisit($orgb); + } + else + { + # Redraw later + if (my $cref = find_chan($chan)) + { + set_info($cref, $INFO_UPDATE); + } + } +} + +sub identify_nick +{ + # There might be a generalized way... + my ($from, $mesg) = @_; + my $nick = $Current_Server->{'nick'}; + our %NickServ; + + unless (defined(%NickServ)) + { + $NickServ{'irc.freenode.net'} = + { nickserv => 'NickServ', + regex => qr(type /msg NickServ \x02IDENTIFY\x02), + keyword => 'IDENTIFY', + }; + } + + if (my $nickserv = $NickServ{$Current_Server->{'server'}}) + { + if ($from eq $nickserv->{'nickserv'} && ${$mesg} =~ $nickserv->{'regex'}) + { + unless (my $pass = $nickserv->{$nick}) + { + my ($nickpass) = (vim_getvar('g:vimirc_nickpass') + =~ /([^,]+)\@$Current_Server->{'server'}/); + ($pass) = ($nickpass =~ /(?:^|\|)$nick:([^|]+)/); + + unless ($pass) + { + $pass = $nickpass; + unless ($pass) + { + $pass = VIM::Eval("s:InputS('Enter the nick password')"); + } + } + if ($pass) + { + $nickserv->{$nick} = $pass; + } + } + if ($nickserv->{$nick}) + { + irc_chan_line('', "*: Identifying yourself..."); + irc_send("PRIVMSG %s :%s %s", + $nickserv->{'nickserv'}, + $nickserv->{'keyword'}, + $nickserv->{$nick}); + return 1; + } + } + } + return 0; +} + +sub add_chat +{ + my ($nick, $sref) = @_; + my $cref; + + if (my $chats = $sref->{'chats'}) + { + $cref = { nick => $nick, + info => 0, + bufnum=> -1, + lines => [] + }; + push(@{$chats}, $cref); + set_info($sref, $INFO_UPDATE); + } + + return $cref; +} + +sub find_chat +{ + my ($nick, $sref) = @_; + + unless ($sref) + { + $sref = $Current_Server; + } + + foreach my $cref (@{$sref->{'chats'}}) + { + if (lc($cref->{'nick'}) eq lc($nick)) + { + return $cref; + } + } + + return undef; +} + +sub del_chat +{ + my ($nick, $sref) = @_; + + unless ($sref) + { + $sref = $Current_Server; + } + + if (my $chats = $sref->{'chats'}) + { + for (my $i = 0; $i <= $#{$chats}; $i++) + { + if (lc($chats->[$i]->{'nick'}) eq lc($nick)) + { + splice(@{$chats}, $i, 1); + set_info($sref, $INFO_UPDATE); + last; + } + } + } + vim_close_chat($nick, $sref->{'server'}); +} + +sub find_chanserv +{ + my ($chan, $serv) = @_; + my ($cref, $sref); + + if ($sref = find_server($serv)) + { + $cref = $chan ? is_list($chan) + ? $sref->{'list'} + : &{'find_cha'.(is_chan($chan) + ? 'n' : 't')}($chan, $sref) + : $sref; + } + + return ($cref, $sref); +} + +# +# Netsplit +# + +sub add_netsplit +{ + my ($chan, $split) = @_; + my $ns; + + if (my $cref = find_chan($chan)) + { + $ns = { split => $split, + joins => 0, + nicks => {} }; + push(@{$cref->{'splits'}}, $ns); + } + + return $ns; +} + +sub find_netsplit +{ + my ($nick, $chan, $split) = @_; + + foreach my $cref (@{$Current_Server->{'chans'}}) + { + unless ($chan && $cref->{'name'} ne $chan) + { + foreach my $ns (@{$cref->{'splits'}}) + { + if ($split && $ns->{'split'} ne $split) + { + next; + } + if ($nick && !exists($ns->{'nicks'}->{$nick})) + { + next; + } + return $ns; + } + if ($chan) + { + last; + } + } + } + + return undef; +} + +sub del_netsplit +{ + my ($chan, $split) = @_; + + if (my $cref = find_chan($chan)) + { + my $splits = $cref->{'splits'}; + + for (my $i = 0; $i <= $#{$splits}; $i++) + { + if ($splits->[$i]->{'split'} eq $split) + { + splice(@{$splits}, $i, 1); + draw_nickwin($chan); + last; + } + } + } +} + +sub add_splitnick +{ + my ($nick, $chan, $split) = @_; + my $ns = find_netsplit(undef, $chan, $split); + + unless ($ns) + { + if ($ns = add_netsplit($chan, $split)) + { + # Forget about the leftovers? + add_timer(600, \&del_netsplit, [ $chan, $split ]); + + unless (find_netsplit($nick, undef, $split)) + { + irc_chan_line('', "!: Netsplit detected: %s", $split); + } + } + } + + if ($ns) + { + $ns->{'nicks'}->{$nick} = 1; + } +} + +sub del_splitnick +{ + my ($nick, $chan) = @_; + + if (my $ns = find_netsplit($nick, $chan)) + { + my $split = $ns->{'split'}; + + unless ($ns->{'joins'}++) + { + my $tojoin = scalar(keys(%{$ns->{'nicks'}})); + irc_chan_line('', "*: Netjoin detected: %s, %d %s to join", + $split, $tojoin, ($tojoin > 1 ? 'are' : 'is')); + } + + delete($ns->{'nicks'}->{$nick}); + unless (%{$ns->{'nicks'}}) + { + del_netsplit($chan, $split); + irc_chan_line('', "*: Netsplit is over: %s", $split); + } + return 1; + } + return 0; +} + +# +# Parsing IRC messages +# + +sub parse_number +{ + my ($from, $comd, $args) = @_; + my ($to, $mesg) = (${$args} =~ /^(\S+)\s+:?(.*)$/); + + if (0) + { + vim_printf("from=%s comd=%s args=\"%s\"", $from, $comd, ${$args}); + } + + if ($comd == 001) # RPL_WELCOME + { + set_connected(1); + irc_chan_line('', $mesg); + } + elsif ($comd == 002) # RPL_YOURHOST + { + irc_chan_line('', $mesg); + } + elsif ($comd == 003) # RPL_CREATED + { + irc_chan_line('', $mesg); + } + elsif ($comd == 004) # RPL_MYINFO + { + irc_chan_line('', $mesg); + } + elsif ($comd == 005) # RPL_BOUNCE + { + # Most servers do not seem to use this code as what RFC suggests: + # instead, they use it to indicate what options they have set, e.g., the + # maximum length of nick + # TODO: Make use of those options? + irc_chan_line('', $mesg); + } + elsif ($comd == 221) # RPL_UMODEIS + { + if ($mesg =~ /^(\+\S*)/) + { + irc_chan_line('', "*: Your user modes: %s", $1); + vim_setumode($1); + } + } + elsif ($comd >= 250 && $comd <= 259) # RPL_LUSERCLIENT etc. + { + irc_chan_line('', $mesg); + } + elsif ($comd == 265 || $comd == 266) + { + irc_chan_line('', $mesg); + } + elsif ($comd == 301) # RPL_AWAY + { + if (my ($nick, $mesg) = ($mesg =~ /^(\S+)\s+:(.*)$/)) + { + irc_chan_line('', "%s is away: %s", $nick, $mesg); + } + } + elsif ($comd == 302) # RPL_USERHOST + { + # Get the first one. This should be called upon logon, to obtain user's + # hostname. Necessary for dcc things. + if (my ($nick, $host) = ($mesg =~ /^([^=]+)\S+@(\S+)/)) + { + if (is_me($nick)) + { + $Current_Server->{'local'} = $host; + irc_chan_line('', "*: Your local host: %s", $host); + } + else + { + irc_chan_line('', "USERHOST: %s", $mesg); + } + } + } + elsif ($comd == 303) # RPL_ISON + { + irc_chan_line('', "ISON: %s", $mesg); + } + elsif ($comd == 305 || $comd == 306) # RPL_UNAWAY/RPL_NOWAWAY + { + irc_chan_line('', $mesg); + } + elsif ($comd == 311 || $comd == 314) # RPL_WHOISUSER + { + if (my ($nick, $user, $host, $info) = + ($mesg =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.+)$/)) + { + irc_chan_line('', "%s %ss %s@%s %s", + $nick, + ($comd == 311 ? "i" : "wa"), + $user, + $host, + $info); + } + } + elsif ($comd == 312) # RPL_WHOISSERVER + { + if (my ($nick, $server) = ($mesg =~ /^(\S+)\s+(.+)$/)) + { + irc_chan_line('', "%s using %s", $nick, $server); + } + } + elsif ($comd == 315) # RPL_ENDOFWHO + { + irc_chan_line('', $mesg); + } + elsif ($comd == 317) # RPL_WHOISIDLE + { + if (my ($nick, $idle, $signon) = ($mesg =~ /^(\S+)\s+(\d+)(?:\s+(\d+))?/)) + { + $idle = sprintf("%02d:%02d:%02d", + ($idle / 3600), + (($idle % 3600) / 60), + ($idle % 60)); + irc_chan_line('', "%s has been idle for %s%s", + $nick, + $idle, + ($signon + ? ', signed on '.vim_gettime(0, $signon) + : '')); + } + } + elsif ($comd == 318 || $comd == 369) # RPL_ENDOFWHOIS + { + if (my ($nick, $mesg) = ($mesg =~ /^(\S+)\s+:?(.+)$/)) + { + irc_chan_line('', "%s %s", $nick, $mesg); + } + } + elsif ($comd == 319) # RPL_WHOISCHANNELS + { + if (my ($nick, $chan) = ($mesg =~ /^(\S+)\s+:?(.+)$/)) + { + irc_chan_line('', "%s on %s", $nick, $chan); + } + } + elsif ($comd == 320) # I see "is an identified user" on dancer-ircd + { + if (my ($nick, $mesg) = ($mesg =~ /^(\S+)\s+:?(.+)$/)) + { + irc_chan_line('', "%s %s", $nick, $mesg); + } + } + elsif ($comd == 321) # RPL_LISTSTART + { + irc_chan_line('', '*: Listing channels...'); + } + elsif ($comd == 323) # RPL_LISTEND + { + VIM::DoCommand('call s:PostLoadList(1)'); + irc_chan_line('', $mesg); + } + elsif ($comd == 324) # RPL_CHANNELMODEIS + { + if (my ($chan, $modes) = ($mesg =~ /^(\S+)\s+:?(.+)$/)) + { + process_cmode($chan, $modes); + } + } + elsif ($comd == 329) + { + if (my ($chan, $time) = ($mesg =~ /^(\S+)\s+(\d+)$/)) + { + irc_chan_line($chan, "*: %s came into existence on %s", $chan, + vim_gettime(0, $time)); + } + } + elsif ($comd == 331) # RPL_NOTOPIC + { + if (my ($chan, $mesg) = ($mesg =~ /^(\S+)\s+:?(.*)$/)) + { + irc_chan_line($chan, "*: %s", $mesg); + } + } + elsif ($comd == 332) # RPL_TOPIC + { + if (my ($chan, $topic) = ($mesg =~ /^(\S+)\s+:(.*)$/)) + { + irc_chan_line($chan, "*: Topic for %s:", $chan); + irc_chan_line($chan, "+: %s", $topic); + + if (find_chan($chan)) + { + $chan = do_escape($chan); + $topic= do_escape($topic); + VIM::DoCommand("call s:SetChannelTopic(\"$chan\", \"$topic\")"); + } + } + } + elsif ($comd == 333) + { + if (my ($chan, $nick, $time) = ($mesg =~ /^(\S+)\s+(\S+)\s+(\d+)$/)) + { + irc_chan_line($chan, "*: Topic set by %s at %s", $nick, + vim_gettime(0, $time)); + } + } + elsif ($comd == 341) # RPL_INVITING + { + if (my ($nick, $chan) = ($mesg =~ /^(\S+)\s+(\S+)$/)) + { + irc_chan_line('', "*: %s has been invited to %s", $nick, $chan); + } + } + elsif ($comd == 352) # RPL_WHOREPLY + { + # Discarding the hopcount + my ($chan, $user, $host, $server, $nick, $flag, undef, $real) = + ($mesg =~ /^(\S+) (\S+) (\S+) (\S+) (\S+) (\S+) :(\d+) (.+)$/); + irc_chan_line('', "%-16s %-12s %-3s %s@%s (%s)", + $chan, $nick, $flag, $user, $host, $real); + } + elsif ($comd == 353) # RPL_NAMREPLY + { + if (my ($type, $chan, $nicks) = ($mesg =~ /^(.)\s+(\S+)\s+:(.+)$/)) + { + if (find_chan($chan)) + { + foreach my $nick (split(/\s+/, $nicks)) + { + my $mode = 0; + if ($nick =~ s/^@//) + { + $mode |= $UMODE_CHOP; + } + if ($nick =~ s/^\+//) + { + $mode |= $UMODE_VOICE; + } + add_nick($nick, $mode, $chan, 1); + } + } + else + { + irc_chan_line('', "*: Names for %s: %s", $chan, $nicks); + } + } + } + elsif ($comd == 366) # RPL_ENDOFNAMES + { + if (my ($chan) = ($mesg =~ /^(\S+)/)) + { + if (find_chan($chan)) + { + sort_nicks($chan); + draw_nickwin($chan); + } + else + { + irc_chan_line('', "*: End of names"); + } + } + } + elsif ($comd == 371 || $comd == 374) + { + irc_chan_line('', "INFO: %s", $mesg); + } + elsif ($comd == 372 || $comd == 375) # RPL_MOTD/RPL_MOTDSTART + { + unless ($Current_Server->{'conn'} & $CS_RECON) + { + irc_chan_line('', $mesg); + } + } + elsif ($comd == 376) # RPL_ENDOFMOTD + { + unless ($Current_Server->{'conn'} & $CS_RECON) + { + irc_chan_line('', $mesg); + } + unless ($Current_Server->{'motd'}) + { + # Nick may have been trimmed by server + unless (is_me($to)) + { + $Current_Server->{'nick'} = $to; + vim_setumode(); + } + post_login_server($Current_Server); + } + } + elsif ($comd == 391) # RPL_TIME + { + irc_chan_line('', $mesg); + } + elsif ($comd >= 431 && $comd <= 433) # ERR_NICKNAMEINUSE etc. + { + irc_chan_line('', $mesg); + unless ($Current_Server->{'conn'} & $CS_LOGIN) + { + if ($comd == 433) + { + $Current_Server->{'nick'} .= '_'; + } + else + { + vim_beep(1); + $Current_Server->{'nick'} = VIM::Eval("s:Input('Enter new nick')"); + } + vim_setumode(); + irc_send("NICK %s", $Current_Server->{'nick'}); + } + } + elsif ($comd == 471 || $comd == 475) # ERR_BADCHANNELKEY + { + if (my ($chan) = ($mesg =~ /^(\S+)/)) + { + del_chan($chan); + irc_chan_line('', "!: %s", $mesg); + } + } + else + { + irc_chan_line('', "%s: %s", $comd, $mesg); + } +} + +sub p_322 +{ + my (undef, $args) = @_; + + if (my ($chan, $num, $topic) = (${$args} + =~ /^\S+\s+(\S+)\s+(\d+)\s+:(.*)$/)) + { + irc_list_line($chan, $num, $topic); + } +} + +sub p_invite +{ + my ($from, $args) = @_; + + if (my ($chan) = (${$args} =~ /^\S+\s+:?(\S+)$/)) + { + irc_chan_line('', "!: %s invites you to channel %s", $from, $chan); + # Ask user to join + vim_beep(2); + + if (vim_confirm("$from invites you to join $chan. Accept it")) + { + VIM::DoCommand('call s:Send_JOIN("JOIN", "'.do_escape($chan).'")'); + } + } +} + +sub p_join +{ + my ($from, $args) = @_; + + if (my ($chan) = (${$args} =~ /^:?(\S+)/)) + { + add_nick($from, 0, $chan); + + unless (del_splitnick($from, $chan)) + { + # If it is you who is entering, get the channel mode. + if (is_me($from)) + { + # Keep your name from appearing twice in the nicks window, + # assuming NAMES are sent just after the JOIN line. I.e., + # assuming the internal nicks list contains your name only at + # this point in time. I'm doing this because I don't want to + # check duplicates in add_nick(), for speed issue. + init_nicks($chan); + irc_send("MODE %s", $chan); + } + else + { + draw_nickline($from, undef, $chan, 1); + } + irc_chan_line($chan, "->: Enter %s [%s]", $from, $From_Server); + } + } +} + +sub p_kick +{ + my ($from, $args) = @_; + + if (my ($chan, $nick, $mesg) = (${$args} =~ /^(\S+)\s+(\S+)\s+:(.*)$/)) + { + my $pref = find_nickprefix($nick, $chan); + + del_nick($nick, $chan); + + if (is_me($nick)) + { + irc_chan_line('', "!: You have been kicked off channel %s by %s (%s)", + $chan, $from, $mesg); + del_chan($chan); + } + else + { + draw_nickline($nick, $pref, $chan, 0); + irc_chan_line($chan, "!: %s kicks off %s (%s)", $from, $nick, $mesg); + } + } +} + +sub p_mode +{ + my ($from, $args) = @_; + + if (my ($chan, $modes) = (${$args} =~ /^(\S+)\s+:?(.+)$/)) + { + if (is_chan($chan)) + { + process_cmode($chan, $modes); + irc_chan_line($chan, "*: %s sets new mode: %s", $from, $modes); + } + elsif (is_me($chan)) + { + process_umode($modes); + } + } +} + +sub p_nick +{ + my ($from, $args) = @_; + + if (my ($nick) = (${$args} =~ /^:(.+)$/)) + { + if (is_me($from)) + { + $Current_Server->{'nick'} = $nick; + irc_chan_line('', "*: New nick %s approved", $nick); + irc_send("MODE %s", $nick); + } + + foreach my $cref (@{$Current_Server->{'chans'}}) + { + my $chan = $cref->{'name'}; + if (change_nicks($from, $nick, $chan)) + { + draw_nickwin($chan); + irc_chan_line($chan, "*: %s is now known as %s", $from, $nick); + } + } + if (0) + { + dcc_change_nicks($from, $nick); + } + } +} + +sub p_notice +{ + my ($from, $args) = @_; + + if (my ($chan, $mesg) = (${$args} =~ /^(\S+)\s+:?(.*)$/)) + { + unless (identify_nick($from, \$mesg)) # if not from nickserv + { + if (process_ctcp_reply($from, $chan, \$mesg)) + { + if (is_chan($chan)) + { + irc_chan_line($chan, "[%s%s]: %s", + find_nickprefix($from, $chan), + $from, $mesg); + } + else + { + &{'irc_cha'.(find_chat($from) ? 't' : 'n').'_line'}( + $from, "[%s]: %s", $from, $mesg); + } + vim_beep(1); + } + } + } +} + +sub p_part +{ + my ($from, $args) = @_; + my ($chan, $mesg) = (${$args} =~ /^(\S+)\s+:(.*)$/); + my $pref = find_nickprefix($from, $chan); + + if (del_nick($from, $chan)) + { + if (is_me($from)) + { + del_chan($chan); + } + else + { + draw_nickline($from, $pref, $chan, 0); + irc_chan_line($chan, "<-: Exit %s%s [%s] (%s)", $pref, $from, + $From_Server, $mesg); + } + } +} + +sub p_ping +{ + my $args = shift; + + $Current_Server->{'lastping'} = time(); + irc_send("PONG :%s", ${$args}); + + if (0) + { + irc_chan_line('', "Ping? Pong!"); + } +} + +sub p_pong +{ + my ($from, $args) = @_; + + if (0 && (my ($time) = (${$args} =~ /(\d+)$/))) + { + my $diff = time() - $time; + irc_chan_line('', "*: Pong from %s (%d second%s)", $from, $diff, + ($diff != 1 ? 's' : '')); + } +} + +sub p_privmsg +{ + my ($from, $args) = @_; + + if (my ($chan, $mesg) = (${$args} =~ /^(\S+)\s+:(.*)$/)) + { + my $pref; + + if (is_chan($chan)) + { + $pref = find_nickprefix($from, $chan); + + if (refresh_nicks($from, $chan)) + { + draw_nickline($from, $pref, $chan, 1); + } + } + + # Handle CTCP messages first + if (process_ctcp_query($from, $pref, $chan, \$mesg)) + { + if (is_chan($chan)) + { + irc_chan_line($chan, "<%s%s>: %s", $pref, $from, $mesg); + } + else + { + &{'irc_cha'.(is_me($chan) ? 't' : 'n').'_line'}( + $from, "<%s>: %s", $from, $mesg); + vim_beep(1); + } + } + } +} + +sub p_quit +{ + my ($from, $args) = @_; + my ($mesg)= (${$args} =~ /^:(.+)$/); + my $regex = qr([[:alnum:]]+(?:\.[-[:alnum:]]+)+); + my $split = ($mesg =~ /^$regex $regex$/); + + foreach my $cref (@{$Current_Server->{'chans'}}) + { + my $chan = $cref->{'name'}; + my $pref = find_nickprefix($from, $chan); + + if (del_nick($from, $chan)) + { + # Handle netsplits: just hide QUIT messages if one occurs. Also, + # hold the nicks, who are to be resurrected, to hide the expected + # JOIN messages. + if ($split) + { + add_splitnick($from, $chan, $mesg); + } + else + { + draw_nickline($from, $pref, $chan, 0); + irc_chan_line($chan, "<=: Exit %s%s [%s] (%s)", $pref, $from, + $From_Server, $mesg); + } + } + } +} + +sub p_topic +{ + my ($from, $args) = @_; + + if (my ($chan, $topic) = (${$args} =~ /^(\S+)\s+:(.*)$/)) + { + irc_chan_line($chan, "*: %s sets new topic: %s", $from, $topic); + + { + my $chan = do_escape($chan); + my $topic= do_escape($topic); + VIM::DoCommand("call s:SetChannelTopic(\"$chan\", \"$topic\")"); + } + } +} + +sub p_wallops +{ + my ($from, $args) = @_; + + if (my ($mesg) = (${$args} =~ /^:(.+)$/)) + { + irc_chan_line('', "!%s!: %s", $from, $mesg); + vim_beep(2); + } +} + +sub parse_line +{ + my $line = shift; + + if (my ($from, $comd, $args) = (${$line} =~ /^:(\S+)\s+(\S+)\s+(.*)$/)) + { + ($from, $From_Server) = ($from =~ /^([^!]+)(?:!(\S+))?$/); + + if (0) + { + vim_printf("from=%s server=%s comd=%s args=%s", $from, $From_Server, + $comd, $args); + } + + $comd = lc($comd); + + if (defined(&{'p_'.$comd})) + { + &{'p_'.$comd}($from, \$args); + } + elsif ($comd + 0) + { + parse_number($from, $comd, \$args); + } + else + { + irc_chan_line('', "%s", ${$line}); + } + } + else + { + if (($comd, $args) = (${$line} =~ /^(\S+)\s+:?(.*)$/)) + { + $comd = lc($comd); + if (0) + { + vim_printf("comd=%s args=%s", $comd, $args); + } + + if ($comd eq 'notice') + { + irc_chan_line('', "%s", $args); + } + elsif ($comd && defined(&{'p_'.$comd})) + { + &{'p_'.$comd}(\$args); + } + else + { + irc_chan_line('', "%s", ${$line}); + } + } + } +} + +# +# Misc. utility functions +# + +# When passing string to vim, '"' and '\' must be escaped +sub do_escape +{ + my $str = shift; + + $str =~ s/(["\\])/\\$1/g; + return $str; +} + +sub do_urldecode +{ + my $str = shift; + + $str =~ s/%([[:xdigit:]]{2})/pack('C', hex($1))/eg; + return $str; +} + +sub do_urlencode +{ + my $str = shift; + + $str =~ s/([`'!@#\$%^&*(){}<>~|\\\";? ,\/])/'%'.unpack('H2', $1)/eg; + return $str; +} + +# Implements `mkdir -p' +sub my_mkdir +{ + my $dir = shift; + # Dunno why perl doesn't have `pwd'. Does it? + my $cwd = VIM::Eval('getcwd()'); + + unless (-d $dir) + { + my $tmp = $dir; + while ($tmp =~ s#^(.+?/)##) + { + unless (chdir($1) || (mkdir($1) && chdir($1))) + { + last; + } + } + unless (-d $dir) # final check + { + mkdir($dir); + } + } + + VIM::DoCommand("cd $cwd"); + return (-d $dir); +} + +sub vim_bufnr +{ + return VIM::Eval("bufnr('%')"); +} + +sub vim_winnr +{ + return VIM::Eval('winnr()'); +} + +sub vim_getvar +{ + return VIM::Eval("exists('$_[0]')") ? scalar(VIM::Eval("$_[0]")) : undef; +} + +sub vim_printf +{ + VIM::Msg(sprintf(shift(@_), @_)); +} + +sub vim_confirm +{ + return scalar(VIM::Eval('s:Confirm_YN("'.do_escape($_[0]).'")')); +} + +sub vim_beep +{ + unless ($Current_Server->{'away'}) + { + # XXX: Force it vomit lines, to notify user what is going wrong + if (has_lines($Current_Server)) + { + put_serv_lines($Current_Server); + } + VIM::DoCommand("call s:Beep($_[0])"); + } +} + +sub vim_search +{ + return scalar(VIM::Eval('s:SearchLine("'.do_escape($_[0]).'")')); +} + +sub vim_gettime +{ + # I think Vim's strftime is much faster than perl's equivalent + return scalar(VIM::Eval("s:GetTime($_[0], $_[1])")); +} + +sub vim_get_serverumode +{ + return scalar(VIM::Eval('s:GetServerUMODE()')); +} + +sub vim_setumode +{ + VIM::DoCommand("call s:SetUserMode(\"$_[0]\")"); +} + +sub vim_open_serv +{ + VIM::DoCommand('call s:OpenBuf_Server()'); +} + +sub vim_visit_serv +{ + return scalar(VIM::Eval('s:VisitBuf_Server()')); +} + +sub vim_open_list +{ + VIM::DoCommand('call s:OpenBuf_List()'); +} + +sub vim_visit_list +{ + return scalar(VIM::Eval('s:VisitBuf_List()')); +} + +sub vim_open_chan +{ + VIM::DoCommand('call s:OpenBuf_Channel("'.do_escape($_[0]).'")'); +} + +sub vim_visit_chan +{ + return scalar(VIM::Eval('s:VisitBuf_Channel("'.do_escape($_[0]).'")')); +} + +sub vim_close_chan +{ + VIM::DoCommand('call s:CloseBuf_Channel("'.do_escape($_[0]).'")'); +} + +sub vim_open_nicks +{ + return scalar(VIM::Eval('s:OpenBuf_Nicks("'.do_escape($_[0]).'")')); +} + +sub vim_visit_nicks +{ + return scalar(VIM::Eval('s:VisitBuf_Nicks("'.do_escape($_[0]).'")')); +} + +sub vim_open_chat +{ + VIM::DoCommand('call s:OpenBuf_Chat("'.do_escape($_[0]).'", "'.$_[1].'")'); +} + +sub vim_visit_chat +{ + return scalar(VIM::Eval('s:VisitBuf_Chat("'.do_escape($_[0]).'", "'.$_[1].'")')); +} + +sub vim_close_chat +{ + VIM::DoCommand('call s:CloseBuf_Chat("'.do_escape($_[0]).'", "'.$_[1].'")'); +} + +sub vim_open_info +{ + return scalar(VIM::Eval('s:OpenBuf_Info()')); +} + +sub vim_bufvisit +{ + return scalar(VIM::Eval("s:BufVisit($_[0])")); +} + +sub vim_winvisit +{ + VIM::DoCommand("call s:WinVisit($_[0])"); +} +sub vim_modifybuf +{ + VIM::DoCommand("call s:ModifyBuf($_[0])"); +} + +sub vim_peekbuf +{ + VIM::DoCommand("call s:PeekBuf($_[0])"); +} + +EOP +endfunction +if exists('s:sid') && s:debug + call s:PerlIRC() +endif +endif + +let &cpoptions = s:save_cpoptions +unlet s:save_cpoptions + +" vim:ts=8:sts=2:sw=2:fdm=indent: