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