1# tabkey.jm: a full-featured tab key script for epic4
2#
3# written by nsx
4#
5# this script features:
6#
7#  * target history cycling for /msg and /notice
8#  * match possibility cycling (zsh-style completion)
9#  * reverse cycling (ctrl + r)
10#  * shell-like completion for /exec
11#  * script name completion for /load
12#  * the ability to complete words anywhere in the input line
13#  * proper handling of file names that contain spaces
14#  * multi-server support
15#  * completion of aliases, command names, channel names, nicknames,
16#    /set variables, /help topics and file names
17#
18#
19#  /set variables this script uses:
20#
21#    NICK_COMPLETION_CHAR -- the value of this set will be appended each time
22#                            nickname completion occurs as the first word of the
23#                            input line.  a typical value might be ':'.
24#
25#   TAB_HISTORY_CYCLE_SIZE -- how many nicknames to keep in the /msg history
26#
27#   ZSH_STYLE_COMPLETION -- turning this on allows you to cycle through completion
28#                           possibilities, when there is more than one completion
29#                           possibility, similar to how zsh behaves for tab
30#                           completion
31#
32#
33# note: this script uses the serial number 110 for its serial hooks
34#
35
36
37# *** config variables ***
38
39@nick_completion_char = []
40@tab_history_cycle_size = 10
41@zsh_style_completion = [off]
42
43
44# *** global variables ***
45
46@last_input_line = []
47@match_index = -1
48@match_cycle_list = []
49@target_index = 0
50@target_cycle_list = []
51
52
53bind ^I parse_command do_tabkey 1
54bind ^R parse_command do_tabkey -1
55
56xdebug +extractw
57
58alias add_target (msg_cmd, target) {
59	if (numwords($myservers()) > 1 && count(: $target) == 0 && left(1 $target) != [=]) {
60		@:target = [${servernum()}:${target}]
61	}
62
63	@target_cycle_list = remw("$msg_cmd $target" $target_cycle_list)
64
65	if (#target_cycle_list > ((tab_history_cycle_size * 2) -1)) {
66		@target_cycle_list = restw(2 $target_cycle_list)
67	}
68
69	@target_index = 0
70
71	@push(target_cycle_list $msg_cmd)
72	@push(target_cycle_list $target)
73}
74
75alias current_fragment_start {
76	@:i = 0
77	@:j = curpos() - 1
78	@:last_space = 0
79	@:last_quote = 0
80	@:quotes = 0
81
82	while (i < j) {
83		@:char = mid($i 1 $L)
84
85		if (char == [ ]) {
86			@:last_space = i
87		} elsif (char == ["]) {
88			@:last_quote = i
89			@:quotes++
90		}
91
92		@:i++
93	}
94
95	if (quotes % 2) {
96		if (mid($curpos() 1 $L) == ["] || last_quote > last_space) {
97			return ${last_quote + 1}
98		} elsif (mid(${curpos() - 1} 1 $L) == ["]) {
99			parsekey backward_character
100			return ${last_quote + 1}
101		} else {
102			return ${last_space + 1}
103		}
104	} else {
105		if (mid(${curpos() - 1} 1 $L) == [ ]) {
106			return $curpos()
107		} elsif (last_space) {
108			return ${last_space + 1}
109		} else {
110			return 0
111		}
112	}
113}
114
115alias do_match_cycle (direction) {
116	@:frag_start = current_fragment_start()
117
118	if (frag_start == -1) {
119		return
120	}
121
122	@:fraglen = curpos() - frag_start
123
124	if (mid($curpos() 1 $L) == ["]) {
125		parsekey forward_character
126		repeat ${fraglen + 1} parsekey backspace
127	} elsif (mid(${curpos() - 1} 1 $L) == ["]) {
128		repeat ${fraglen + 1} parsekey backspace
129	} else {
130		repeat $fraglen parsekey backspace
131	}
132
133	if (mid(${curpos() - 1} 1 $L) == ["]) {
134		parsekey backspace
135	}
136
137	@match_index += direction
138
139	if (match_index == #match_cycle_list) {
140		@match_index = 0
141	} elsif (match_index < 0) {
142		@match_index = #match_cycle_list - 1
143	}
144
145	@:new_match = word($match_index $match_cycle_list)
146
147	if (index("$chr(32)" $new_match) == -1) {
148		xtype -l $new_match
149	} else {
150		xtype -l "$new_match"
151		parsekey backward_character
152	}
153
154	@last_input_line = L
155}
156
157alias do_msg_cycle (direction) {
158	if (#target_cycle_list < 2) {
159		return
160	}
161
162	@target_index -= (direction * 2)
163
164	if (target_index == #target_cycle_list) {
165		@target_index = 0
166	} elsif (target_index < 0) {
167		@target_index = #target_cycle_list - 2
168	}
169
170	@:msg_cmd = word($target_index $target_cycle_list)
171	@:msg_targ = word(${target_index + 1} $target_cycle_list)
172
173	parsekey erase_line
174
175	xtype -l /${msg_cmd} ${msg_targ}${chr(32)}
176
177	@last_input_line = []
178}
179
180alias do_set (variable, value) {
181	if (value == []) {
182		^eval @:setval = $variable
183
184		if (setval == []) {
185			xecho -b No value for $variable has been set
186		} else {
187			xecho -b Current value of $variable is $setval
188		}
189
190		return
191	}
192
193	switch ($variable) {
194		(NICK_COMPLETION_CHAR) {
195			if (value == [<unset>]) {
196				@nick_completion_char = []
197
198				xecho -b Value of NICK_COMPLETION_CHAR set to <EMPTY>
199			} else {
200				@nick_completion_char = value
201
202				xecho -b Value of NICK_COMPLETION_CHAR set to $value
203			}
204		}
205
206		(TAB_HISTORY_CYCLE_SIZE) {
207			if (!isnumber($value)) {
208				xecho -b Value of TAB_HISTORY_CYCLE_SIZE must be numeric!
209			} elsif (value < 1) {
210				xecho -b Value of TAB_HISTORY_CYCLE_SIZE cannot be less than 1
211			} else {
212				@tab_history_cycle_size = value
213				@target_cycle_list = rightw(${value * 2} $target_cycle_list)
214				xecho -b Value of TAB_HISTORY_CYCLE_SIZE set to $value
215			}
216		}
217
218		(ZSH_STYLE_COMPLETION) {
219			if (value == [on]) {
220				@zsh_style_completion = [ON]
221				xecho -b Value of ZSH_STYLE_COMPLETION set to ON                        } elsif (value == [off]) {
222				@zsh_style_completion = [OFF]
223
224				xecho -b Value of ZSH_STYLE_COMPLETION set to OFF
225			} else {
226				xecho -b Value of ZSH_STYLE_COMPLETION must be ON or OFF!
227			}
228		}
229
230		(*) {
231			xecho -b I don't know how to handle "$variable"
232		}
233	}
234}
235
236alias do_tabkey (cycle_direction) {
237	@:input_line = left($curpos() $L)
238
239	if (L == []) {
240		@target_index = #target_cycle_list
241		@do_msg_cycle($cycle_direction)
242		return
243	} elsif (encode($L) == encode($last_input_line)) {
244		if (zsh_style_completion == [ON] && #match_cycle_list > 1) {
245			@do_match_cycle($cycle_direction)
246			return
247		} else {
248			return
249		}
250	} elsif (leftw(1 $L) == [/msg] || leftw(1 $L) == [/notice]) {
251		if (#L == 2 && mid(${@L - 1} 1 $L) == [ ]) {
252			@do_msg_cycle($cycle_direction)
253			return
254		}
255	}
256
257	@:frag_start = current_fragment_start()
258
259	if (frag_start == -1 || cycle_direction == -1) {
260		return
261	}
262
263	@:fragment = mid($frag_start ${curpos() - frag_start} $L)
264	@:fraglen = strlen($fragment)
265	@:padding = []
266
267	switch ($input_line) {
268		(/dcc %) {
269			@:matches = match_dcc($fragment)
270
271			if (#matches == 1) {
272				@:padding = [ ]
273			}
274		}
275
276		(/dcc send % *)
277
278		(/exec % *) {
279			@:matches = match_file($fragment)
280
281			if (#matches == 1 && !isdirectory($matches)) {
282				@:padding = [ ]
283			}
284		}
285
286		(/exec %) {
287			@:matches = match_exec($fragment)
288
289			if (#matches == 1 && !isdirectory($matches)) {
290				@:padding = [ ]
291			}
292		}
293
294		(/help %) {
295			@:matches = match_help($fragment)
296
297			if (#matches == 1) {
298				if (!isdirectory(${getset(HELP_PATH)}/${matches})) {
299					@:padding = [ ]
300				}
301			}
302		}
303
304		(/load *)
305
306		(/unload *) {
307			@:matches = match_load($fragment)
308
309			if (#matches == 1 && !isdirectory($matches)) {
310				@:padding = [ ]
311			}
312		}
313
314		(/notify -%) {
315			@:fragment = rest($fragment)
316			@:fraglen--
317			@:matches = match_notify($fragment)
318
319			if (#matches == 1) {
320				@:padding = [ ]
321			}
322		}
323
324		(/set -%) {
325			@:fragment = rest($fragment)
326			@:fraglen--
327			@:matches = match_set($fragment)
328
329			if (#matches == 1) {
330				@:padding = [ ]
331			}
332		}
333
334		(/set %) {
335			@:matches = match_set($fragment)
336
337			if (#matches == 1) {
338				@:padding = [ ]
339			}
340		}
341
342		(/%) {
343			@:fragment = rest($fragment)
344			@:fraglen--
345			@:matches = match_command($fragment)
346
347			if (#matches == 1) {
348				@:padding = [ ]
349			}
350		}
351
352		(*) {
353			@:matches = []
354
355			@push(matches $match_chan($fragment))
356			@push(matches $match_nick($fragment))
357
358			if (#matches == 1) {
359				if (#L == 1 && !ischannel($matches)) {
360					if (nick_completion_char != []) {
361						@:padding = [$nick_completion_char ]
362					}
363				} else {
364					@:padding = [ ]
365				}
366			}
367		}
368	}
369
370	@last_input_line = L
371	@match_index = -1
372	@:match_prefix = prefix($matches)
373
374	if (@match_prefix <= fraglen && #matches > 1) {
375		xecho -c Possible matches:
376		xecho -c -- $matches
377		xecho -c
378		return
379	}
380
381	if (match_prefix != []) {
382		@:new_fragment = left($fraglen $match_prefix)
383
384		if (encode($fragment) != encode($new_fragment)) {
385			repeat $fraglen parsekey backspace
386
387			xtype -l $new_fragment
388		}
389	}
390
391	@:completion = rest($fraglen $match_prefix)
392
393	if (completion == [] && padding == []) {
394		return
395	}
396
397	if (index("$chr(32)" $fragment) == -1) {
398		if (index("$chr(32)" $match_prefix) > -1) {
399			repeat $fraglen parsekey backspace
400
401			if (mid($curpos() 1 $L) == ["] && mid(${curpos() - 1} 1 $L) == ["]) {
402				xtype -l $match_prefix
403
404				if (padding != []) {
405					parsekey forward_character
406				}
407			} else {
408				xtype -l "$match_prefix"
409
410				if (padding == []) {
411					parsekey backward_character
412				}
413			}
414		} else {
415			xtype -l $completion
416		}
417	} else {
418		xtype -l $completion
419
420		if (padding == []) {
421			if (mid($curpos() 1 $L) != ["]) {
422				xtype -l "
423				parsekey backward_character
424			}
425		} else {
426			if (mid($curpos() 1 $L) == ["]) {
427				parsekey forward_character
428			} else {
429				xtype -l "
430			}
431		}
432	}
433
434	if (mid(${curpos()} 1 $L) != padding) {
435		xtype -l $padding
436	}
437
438	@last_input_line = []
439}
440
441alias isdirectory (pathname) {
442	@:statret = stat("$pathname")
443	@:file_type = left(1 $word(2 $statret))
444
445	if (file_type & 4) {
446		return 1
447	} else {
448		return 0
449	}
450}
451
452alias isexe (pathname) {
453	@:statret = stat("$pathname")
454	@:file_mode = word(2 $statret)
455	@:file_type = left(1 $file_mode)
456	@:permissions = right(3 $file_mode)
457	@:user_perm = left(1 $permissions)
458	@:group_perm = mid(1 1 $permissions)
459	@:other_perm = right(1 $permissions)
460
461	if (file_type == 1) {
462		if ((user_perm & 1) || (group_perm & 1) || (other_perm & 1)) {
463			return 1
464		} else {
465			return 0
466		}
467	} else {
468		return 0
469	}
470}
471
472alias match_chan (fragment) {
473	@:matches = pattern("${fragment}*" $mychannels())
474	@match_cycle_list = matches
475
476	return $matches
477}
478
479alias match_command (fragment) {
480	@:matches = []
481	@:command_matches = []
482
483	@push(matches $getcommands(${fragment}*))
484	@push(matches $aliasctl(alias pmatch ${fragment}*))
485
486	fe ($matches) match {
487		@push(command_matches /${match})
488	}
489
490	@match_cycle_list = uniq($command_matches)
491
492	return $uniq($matches)
493}
494
495alias match_dcc (fragment) {
496	@:dcc_cmds = [chat close closeall get list raw rename resume send]
497	@:matches = pattern(${fragment}% $dcc_cmds)
498	@match_cycle_list = matches
499
500	return $matches
501}
502
503alias match_exec (fragment) {
504	@:matches = []
505
506	if (index(/ $fragment) == -1) {
507		@:path_list = PATH
508
509		while (path_list != []) {
510			@:pathname = before(: $path_list)
511
512			if (pathname == []) {
513				@:pathname = path_list
514				@:path_list = []
515			} else {
516				@:path_list = after(: $path_list)
517			}
518
519			fe ($glob("${pathname}/${fragment}*")) filename {
520				if (isexe($filename)) {
521					@push(matches $after(-1 / $filename))
522				}
523			}
524		}
525
526		fe ($glob("${fragment}*")) filename {
527			if (isdirectory($filename)) {
528				@push(matches $filename)
529			}
530		}
531	} else {
532		fe ($glob("${fragment}*")) filename {
533			if (isexe($filename) || isdirectory($filename)) {
534				@push(matches $filename)
535			}
536		}
537	}
538
539	@match_cycle_list = matches = uniq($matches)
540
541	return $matches
542}
543
544alias match_file (fragment) {
545	@:matches = glob("${fragment}*")
546	@match_cycle_list = matches
547
548	return $matches
549}
550
551alias match_help (fragment) {
552	@:matches = []
553	@:help_path = getset(HELP_PATH)
554	@:help_matches = globi("${help_path}/${fragment}*")
555
556	fe ($help_matches) match {
557		@push(matches $rest(${@help_path + 1} $match))
558	}
559
560	@match_cycle_list = matches
561
562	return $matches
563}
564
565alias match_load (fragment) {
566	@:matches = []
567
568	if (index(/ $fragment) == -1) {
569		@:path_list = LOAD_PATH
570
571		while (path_list != []) {
572			@:pathname = before(: $path_list)
573
574			if (pathname == []) {
575				@:pathname = path_list
576				@:path_list = []
577			} else {
578				@:path_list = after(: $path_list)
579			}
580
581			fe ($glob("${pathname}/${fragment}*")) filename {
582				@push(matches $after(-1 / $filename))
583			}
584		}
585	}
586
587	@push(matches $glob("${fragment}*"))
588
589	@match_cycle_list = matches = uniq($matches)
590
591	return $matches
592}
593
594alias match_nick (fragment) {
595	@:nick_matches = pattern("${fragment}*" $onchannel())
596
597	if (nick_matches == []) {
598		@:nick_list = []
599
600		fe ($remw($C $mychannels())) chan {
601			@push(nick_list $onchannel($chan))
602		}
603
604		@push(nick_list $notify(on))
605
606		@:nick_matches = pattern(${fragment}* $nick_list)
607	}
608
609	if (nick_completion_char != [] && #matches > 1 && #L == 1) {
610		fe ($nick_matches) nickname {
611			@push(matches ${nickname}${nick_completion_char})
612		}
613	} else {
614		@:matches = nick_matches
615
616	}
617
618	@:matches = uniq($matches)
619	@match_cycle_list = matches
620
621	return $matches
622}
623
624alias match_notify (fragment) {
625	@:matches = pattern(${fragment}* $notify())
626	@match_cycle_list = matches
627
628	return $matches
629}
630
631alias match_set (fragment) {
632	@:matches = []
633	@:my_sets = [NICK_COMPLETION_CHAR TAB_HISTORY_CYCLE_SIZE ZSH_STYLE_COMPLETION]
634
635	@push(matches $getsets(${fragment}*))
636	@push(matches $pattern(${fragment}* $my_sets))
637
638	@match_cycle_list = matches
639
640	return $matches
641}
642
643alias match_theme (fragment) {
644	@:matches = []
645	@:theme_matches = globi("${theme_directory}/${fragment}*.theme")
646
647	fe ($theme_matches) match {
648		@push(matches $before(-1 . $rest(${@theme_directory + 1} $match)))
649	}
650
651	@match_cycle_list = matches
652
653	return $matches
654}
655
656alias tclear {
657	@target_cycle_list = []
658	@target_index = -2
659
660	xecho target history cleared
661}
662
663
664# *** hooks ***
665
666on #^action 110 "*" {
667	if (!rmatch($1 #* &* +*) ) {
668		@add_target(msg $0)
669	}
670}
671
672on #^dcc_chat 110 "*" {
673	@add_target(msg =$0)
674}
675
676on #^dcc_connect 110 "% CHAT *" {
677	@add_target(msg =$0)
678}
679
680on #^general_notice 110 "% *" {
681	@add_target(notice $1)
682}
683
684on #^msg 110 "*" {
685	@add_target(msg $0)
686}
687
688on #^send_action 110 "*" {
689	if (!rmatch($0 #* &* +*) ) {
690		@add_target(msg $0)
691	}
692}
693
694on #^send_dcc_chat 110 "*" {
695	@add_target(msg =$0)
696}
697
698on #^send_msg 110 "*" {
699	@add_target(msg $0)
700}
701
702on #^send_notice 110 "*" {
703	@add_target(notice $0)
704}
705
706on ?send_to_server "% % NOTICE %:% *" {
707	if (ischannel($3)) {
708		return 0
709	}
710
711	@:refnum = before(: $3)
712	@:target = after(: $3)
713
714	xquote -server $refnum NOTICE $target $4-
715
716	return 1
717}
718
719on ?send_to_server "% % PRIVMSG %:% *" {
720	if (ischannel($3)) {
721		return 0
722	}
723
724	@:refnum = before(: $3)
725	@:target = after(: $3)
726
727	xquote -server $refnum PRIVMSG $target $4-
728
729	return 1
730}
731
732on ^set "NICK_COMPLETION_CHAR *" {
733	@do_set(NICK_COMPLETION_CHAR $1)
734}
735
736on ^set "TAB_HISTORY_CYCLE_SIZE *" {
737	@do_set(TAB_HISTORY_CYCLE_SIZE $1)
738}
739
740on ^set "ZSH_STYLE_COMPLETION *" {
741	@do_set(ZSH_STYLE_COMPLETION $1)
742}
743