├── LICENSE ├── README.md └── cpwrap.c /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpwrap: like rlwrap, but for copilot suggestions 2 | `cpwrap [arg ...]` 3 | wraps the execution of whatever command you pass it. It sends the session transcript to [GitHub Copilot](https://github.com/features/copilot) (you must be subscribed) and shows suggestions you can accept by pressing tab, at which point it's just like you had typed that yourself into the command. There is no need for the wrapped program to know anything about copilot; `cpwrap` creates a new pseudo-terminal, so the wrapped program can't (and more importantly doesn't need to) tell the difference between what you typed and what you autocompleted. 4 | 5 | Tab=accept, ESC=reject, F1=toggle on/off 6 | 7 | https://user-images.githubusercontent.com/6289391/213608944-41bc2ae1-5c50-4c49-b82b-c474c2ee1f55.mp4 8 | 9 | # Requirements 10 | * [`node`](https://nodejs.org/) 11 | * copilot `dist` folder somewhere on your system (like what comes with [copilot.vim](https://github.com/github/copilot.vim/tree/release/copilot/dist)) 12 | 13 | `cpwrap` will look for the dist folder in: 14 | * `CPWRAPPATHDIST` environment variable 15 | * `~/.config/cpwrap/pathdist` file 16 | * `pathcopilotdist` global variable in `cpwrap.c` 17 | * extension locations for vim and neovim 18 | 19 | Otherwise, it will prompt for the location and then save that. 20 | 21 | # Is this allowed? 22 | I'm not sure; there are some other unofficial plugins. `cpwrap` tries to be a good citizen (identifying itself, giving notifications for show/accept/reject). 23 | 24 | # How does it work? 25 | Copilot thinks we are editing a file called `/tmp/shell_session.txt` whose contents are `$ arg ...\n` followed by the transcript of the session. 26 | 27 | # Fun fact 28 | My first pass at this used the [Codex API](https://openai.com/blog/openai-codex/), but it ended up being way too expensive. One interesting thing was that when you started a shell session, like `cpwrap sh`, the first suggestion _every time_ was `cat /etc/passwd`. I notice that when you use copilot in that situation, it doesn't say anything. 29 | -------------------------------------------------------------------------------- /cpwrap.c: -------------------------------------------------------------------------------- 1 | /* 2 | have vim work 3 | restore underline 4 | make utf8 aware 5 | line overflow? 6 | */ 7 | #define _XOPEN_SOURCE 700 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | const char *pathcopilotdist = NULL; // hard-coded path to try 28 | const char verbose = 0; 29 | const char verbosecopilot = 0; // 1 for messages only, 2 +parsing/waiting 30 | const char verbosetranscript = 0; 31 | 32 | // Terminal settings to restore 33 | struct termios oldtermios; 34 | char gottermios = 0; 35 | int fdterm = -1; 36 | 37 | // Copilot process 38 | int fdscp[4]; 39 | pid_t pidcp = -1; 40 | enum errcopilot {ECPNONE,ECPUNKNOWN,ECPAUTHSTR,ECPAUTH,ECPAUTHGETCODEURI,ECPREADEOF} errcopilot = 0; 41 | char *serrcopilot = NULL; 42 | atomic_char mainexited = 0; 43 | 44 | // Transcript 45 | pthread_mutex_t muttranscript = PTHREAD_MUTEX_INITIALIZER; 46 | size_t sizetranscript=0, ntranscript=0; 47 | char *transcript = NULL; 48 | atomic_ulong vtranscript = 0; 49 | pthread_cond_t condtranscript = PTHREAD_COND_INITIALIZER; 50 | size_t itranscriptcur = 0; 51 | 52 | // Completion 53 | pthread_mutex_t mutcompletion = PTHREAD_MUTEX_INITIALIZER; 54 | size_t sizecompletion=0, ncompletion=0; 55 | char *completion = NULL; 56 | unsigned vtranscriptcompletion = -1; 57 | int fdscompletion[2]; 58 | unsigned vtranscriptcompletionshown = -1; 59 | char acceptedcompletion = 0; 60 | atomic_char ison = 1; 61 | 62 | /* sprintf into a statically allocated buffer */ 63 | char* 64 | sprintfs(const char *fmt, ...) 65 | { 66 | static _Thread_local size_t sizeret=0; static _Thread_local char *ret=NULL; 67 | va_list ap; 68 | va_start(ap, fmt); 69 | int n = vsnprintf(NULL, 0, fmt, ap); if (n<0) { perror("cpwrap: vsnprintf"); exit(1); } 70 | va_end(ap); 71 | if (sizeret='0' && *json<='9') cp |= *json-'0'; 195 | else if (*json>='a' && *json<='f') cp |= *json-'a'+10; 196 | else if (*json>='A' && *json<='F') cp |= *json-'A'+10; 197 | } 198 | if (!cp) return NULL; 199 | if (cp < 0x80) { if (*pk++ != cp) goto eatstringgetkjson; } 200 | else if (cp < 0x800) { if (*pk++!=0xc0+(cp>>6) || *pk++!=0x80+(cp&0x3f)) goto eatstringgetkjson; } 201 | else if (cp < 0x10000) { if (*pk++!=0xe0+(cp>>12) || *pk++!=0x80+((cp>>6)&0x3f) || *pk++!=0x80+(cp&0x3f)) goto eatstringgetkjson; } 202 | else if (cp <= 0x10ffff) { if (*pk++!=0xf0+(cp>>18) || *pk++!=0x80+((cp>>12)&0x3f) || *pk++!=0x80+((cp>>6)&0x3f) || *pk++!=0x80+(cp&0x3f)) goto eatstringgetkjson; } 203 | else return NULL; 204 | } else if (*json == '"' || *json == '\\' || *json == '/') { 205 | if (*pk++ != *json++) goto eatstringgetkjson; 206 | } else { 207 | const char *escs = "bfnrt"; 208 | char *pesc = strchr(escs, *json++); 209 | if (!pesc) return NULL; 210 | if (*pk++ != "\b\f\n\r\t"[pesc-escs]) goto eatstringgetkjson; 211 | } 212 | } else if (*json=='"' || !*json || *pk++!=*json++) goto eatstringgetkjson; 213 | } 214 | if (*json++ != '"') goto eatstringgetkjson; 215 | while (*json && strchr(" \n\r\t",*json)) json++; 216 | if (*json++ != ':') return NULL; 217 | while (*json && strchr(" \n\r\t",*json)) json++; 218 | return json; 219 | 220 | // Move to next key 221 | eatstringgetkjson:; 222 | for (; *json && *json!='"'; json++) if (*json == '\\') json++; 223 | if (*json++ != '"') return NULL; 224 | while (*json && strchr(" \n\r\t",*json)) json++; 225 | if (*json++ != ':') return NULL; 226 | while (*json && strchr(" \n\r\t",*json)) json++; 227 | if (!(json=eatvaluejson(json))) return NULL; 228 | while (*json && strchr(" \n\r\t",*json)) json++; 229 | if (*json++ != ',') return NULL; 230 | while (*json && strchr(" \n\r\t",*json)) json++; 231 | } 232 | return NULL; 233 | } 234 | 235 | char* 236 | getksjson(char *json, char *k) 237 | { 238 | 239 | // Get string value 240 | json = getkjson(json, k); 241 | if (!json) return NULL; 242 | while (*json && strchr(" \n\r\t",*json)) json++; 243 | if (*json++ != '"') return NULL; 244 | 245 | // Initialize s 246 | static size_t sizes=0; static char *s=NULL; 247 | if (!sizes && !(s=malloc(sizes=256))) { perror("cpwrap: getksjson: malloc"); exit(1); } 248 | size_t ns = 0; 249 | 250 | // Decode string 251 | while (*json != '"') { 252 | if (!*json) return NULL; 253 | if (sizes='0' && *json<='9') cp |= *json-'0'; 261 | else if (*json>='a' && *json<='f') cp |= *json-'a'+10; 262 | else if (*json>='A' && *json<='F') cp |= *json-'A'+10; 263 | } 264 | if (!cp) return NULL; 265 | if (cp < 0x80) s[ns++]=cp; 266 | else if (cp < 0x800) s[ns++]=0xc0+(cp>>6), s[ns++]=0x80+(cp&0x3f); 267 | else if (cp < 0x10000) s[ns++]=0xe0+(cp>>12), s[ns++]=0x80+((cp>>6)&0x3f), s[ns++]=0x80+(cp&0x3f); 268 | else if (cp <= 0x10ffff) s[ns++]=0xf0+(cp>>18), s[ns++]=0x80+((cp>>12)&0x3f), s[ns++]=0x80+((cp>>6)&0x3f), s[ns++]=0x80+(cp&0x3f); 269 | else return NULL; 270 | } else if (*json == '"' || *json == '\\' || *json == '/') { 271 | s[ns++] = *json++; 272 | } else { 273 | const char *escs = "bfnrt"; 274 | char *pesc = strchr(escs, *json++); 275 | if (!pesc) return NULL; 276 | s[ns++] = "\b\f\n\r\t"[pesc-escs]; 277 | } 278 | } else s[ns++] = *json++; 279 | } 280 | s[ns] = 0; 281 | 282 | return s; 283 | } 284 | 285 | long 286 | getknjson(char *json, char *k) 287 | { 288 | json = getkjson(json, k); 289 | if (!json) return LONG_MAX; 290 | while (*json && strchr(" \n\r\t",*json)) json++; 291 | if (*json<'0' || *json>'9') return LONG_MAX; 292 | long n=0; while (*json>='0' &&*json<='9') n=n*10+*json++-'0'; 293 | return n; 294 | } 295 | 296 | char* 297 | getijson(char *json, unsigned iget) 298 | { 299 | if (!json) return NULL; 300 | while (*json && strchr(" \n\r\t",*json)) json++; 301 | if (*json++ != '[') return NULL; 302 | while (*json && strchr(" \n\r\t",*json)) json++; 303 | if (*json == ']') return NULL; 304 | for (unsigned iarr=0;; iarr++) { 305 | if (iarr == iget) return json; 306 | if (!(json=eatvaluejson(json))) return NULL; 307 | while (*json && strchr(" \n\r\t",*json)) json++; 308 | if (*json++ != ',') return NULL; 309 | while (*json && strchr(" \n\r\t",*json)) json++; 310 | } 311 | return NULL; 312 | } 313 | 314 | char 315 | willblockread(int fd) 316 | { 317 | while (1) { 318 | int res = poll(&(struct pollfd){.fd=fd,.events=POLLIN}, 1, 0); if (res==-1 && errno!=EINTR && errno!=EAGAIN) { fprintf(stderr,"cpwrap: poll fd %d: %s\n",fd,strerror(errno)); exit(1); } 319 | if (res == -1) continue; 320 | return res == 0; 321 | } 322 | } 323 | 324 | char 325 | willblockwrite(int fd) 326 | { 327 | while (1) { 328 | int res = poll(&(struct pollfd){.fd=fd,.events=POLLOUT}, 1, 0); if (res==-1 && errno!=EINTR && errno!=EAGAIN) { fprintf(stderr,"cpwrap: poll fd %d: %s\n",fd,strerror(errno)); exit(1); } 329 | if (res == -1) continue; 330 | return res == 0; 331 | } 332 | } 333 | 334 | void 335 | writeall(int fd, size_t nbuf, const char *buf) 336 | { 337 | for (size_t ibuf=0; ibufpmut))) { fprintf(stderr,"cpwrap: pthread_mutex_lock writecond: %s\n",strerror(res)); exit(1); } 372 | while (1) { 373 | 374 | // Wait until it is wanted 375 | while (1) { 376 | if (wc->want) break; 377 | if ((res=pthread_cond_wait(wc->pcond,wc->pmut))) { fprintf(stderr,"cpwrap: pthread_cond_wait writecond: %s\n",strerror(res)); exit(1); } 378 | } 379 | 380 | // Wait for fn to return true 381 | while (1) { 382 | if (wc->fn(wc->arg)) break; 383 | if ((res=pthread_cond_wait(wc->pcond,wc->pmut))) { fprintf(stderr,"cpwrap: pthread_cond_wait writecond2: %s\n",strerror(res)); exit(1); } 384 | } 385 | 386 | // Notify 387 | wc->want = 0; 388 | while (1) { 389 | ssize_t w = write(wc->fd, "", 1); if (w==-1 && errno!=EINTR) { perror("cpwrap: dowritecond write"); exit(1); } 390 | if (w != -1) break; 391 | } 392 | } 393 | return NULL; 394 | } 395 | 396 | int 397 | isvtranscriptdifferent(void *arg) 398 | { 399 | return atomic_load(&vtranscript) != *(unsigned long*)arg; 400 | } 401 | 402 | void 403 | msgcopilot(char *msg) 404 | { 405 | if (errcopilot) return; 406 | int nmsg = strlen(msg); 407 | char len[64]; int nlen=snprintf(len, sizeof(len), "Content-Length: %d\r\n\r\n", nmsg); 408 | if (verbose || verbosecopilot) fprintf(stderr,"\n\33[33mwriting to copilot:\n%s\33[0m\n",msg); 409 | writeall(fdscp[1], nlen, len); 410 | writeall(fdscp[1], nmsg, msg); 411 | } 412 | 413 | /* 0=EAGAIN or done, 1=err */ 414 | int 415 | getmsgcopilotideagain(long id, char **pmsg) 416 | { 417 | if (verbose || verbosecopilot>=2) fprintf(stderr,"\33[33mtrying to get a message from copilot with id %ld\33[0m\n",id); 418 | *pmsg = NULL; 419 | if (errcopilot) return 1; 420 | static size_t sizebuf=0,nbuf=0; static char *buf=NULL; 421 | 422 | // Loop forever getting messages 423 | while (1) { 424 | 425 | // Macro to fill to a certain point 426 | #define FILL(nexpr) { \ 427 | size_t n = nexpr; \ 428 | if (sizebuf=2) && nread==-1 && errno==EAGAIN) fprintf(stderr,"\33[33mread copilot: EAGAIN\33[0m\n"); \ 432 | if (nread==-1 && errno==EAGAIN) return 0; \ 433 | if (!nread) { errcopilot=ECPREADEOF; return 1; } \ 434 | nbuf += nread; \ 435 | } \ 436 | } 437 | 438 | // Get content length header 439 | size_t ibuf = 0; 440 | char *want="Content-Length: "; int nwant=strlen(want); 441 | if (verbose || verbosecopilot>=2) fprintf(stderr,"\33[33mfilling for content-length\33[0m\n"); 442 | FILL(ibuf+nwant) 443 | if (memcmp(buf+ibuf,want,nwant)) { fprintf(stderr,"cpwrap: copilot: bad header\n"); errcopilot=1; return 1; } 444 | ibuf += nwant; 445 | size_t nnum=0; while (1) { 446 | buf[nbuf] = 0; 447 | for (; ibuf+nnum=2) fprintf(stderr,"\33[33mfilling for content-length #\33[0m\n"); 449 | FILL(ibuf+nnum+4) 450 | } gotcontentlen:; 451 | if (!nnum) { fprintf(stderr,"cpwrap: copilot: bad header content-length #\n"); errcopilot=1; return 1; } 452 | unsigned len=0; for (size_t i=0; i=2) fprintf(stderr,"\33[33mcontent-length: %u\nfilling for \\r\\n\\r\\n\33[0m\n",len); 454 | FILL(ibuf+4) 455 | if (memcmp(buf+ibuf,"\r\n\r\n",4)) { fprintf(stderr,"cpwrap: copilot: bad header no CRNLCRNL\n"); errcopilot=1; return 1; } 456 | ibuf += 4; 457 | 458 | // Get content 459 | if (verbose || verbosecopilot>=2) fprintf(stderr,"\33[33mfilling for message (%zu/%u)\33[0m\n",nbuf-ibuf,len); 460 | FILL(ibuf+len) 461 | #undef FILL 462 | if (verbose || verbosecopilot) fprintf(stderr,"\n\33[33mmsg from copilot:\n%.*s\33[0m\n", (int)len, buf+ibuf); 463 | 464 | // Get id 465 | char cend0 = buf[ibuf+len]; 466 | buf[ibuf+len] = 0; 467 | long idmsg = getknjson(buf+ibuf, "id"); 468 | 469 | // Make a copy if it's our message 470 | static size_t sizeret=0; static char *ret=NULL; 471 | if (idmsg == id) { 472 | if (sizeretid) { fprintf(stderr,"cpwrap: copilot: missed id\n"); errcopilot=1; return 1; } 484 | } 485 | return 1; 486 | } 487 | 488 | char* 489 | getmsgcopilotid(long id) 490 | { 491 | char *ret; getmsgcopilotideagain(id, &ret); 492 | return ret; 493 | } 494 | 495 | int 496 | getmsgcopilotidnonblock(long id, char **pmsg) 497 | { 498 | int flags0 = fcntl(fdscp[2], F_GETFL, 0); if (flags0 == -1) { perror("cpwrap: fcntl get flags"); exit(1); } 499 | if (fcntl(fdscp[2],F_SETFL,flags0|O_NONBLOCK) == -1) { perror("cpwrap: fcntl copilot"); exit(1); } 500 | int ret = getmsgcopilotideagain(id, pmsg); 501 | if (fcntl(fdscp[2],F_SETFL,flags0) == -1) { perror("cpwrap: fcntl copilot"); exit(1); } 502 | return ret; 503 | } 504 | 505 | void* 506 | docopilot(void *arg) 507 | { 508 | //return NULL; 509 | int res; 510 | 511 | // Send initial messages 512 | msgcopilot("{\"jsonrpc\":\"2.0\",\"params\":{\"capabilities\":{}},\"method\":\"initialize\",\"id\":1}"); 513 | msgcopilot("{\"params\":{\"editorInfo\":{\"name\":\"cpwrap\",\"version\":\"1\"},\"editorPluginInfo\":{\"name\":\"cpwrap\",\"version\":\"1\"}},\"id\":2,\"method\":\"setEditorInfo\",\"jsonrpc\":\"2.0\"}"); 514 | 515 | // Create dummy file 516 | char *tmpdir = getenv("TMPDIR"); if (!tmpdir) tmpdir = "/tmp"; 517 | char *pathdummy = sprintfs("%s/shell_session.txt", tmpdir); 518 | int fddummy = open(pathdummy, O_RDWR|O_CREAT|O_EXCL, 0644); 519 | if (fddummy != -1) close(fddummy); 520 | int nesc=0; for (char *p=pathdummy; *p; p++) if (*p == '/') nesc++; 521 | char *pathdummyesc = malloc(strlen(pathdummy)+nesc+1); if (!pathdummyesc) { perror("cpwrap: malloc"); exit(1); } 522 | char *p=pathdummyesc; for (char *q=pathdummy; *q; q++) { if (*q == '/') *p++='\\'; *p++=*q; } *p=0; 523 | 524 | // Make sure auth'd 525 | unsigned idmsgcopilotnext = 3; 526 | msgcopilot(sprintfs("{\"params\":{},\"method\":\"checkStatus\",\"jsonrpc\":\"2.0\",\"id\":%ld}",idmsgcopilotnext++)); 527 | while (1) { 528 | 529 | // Break if auth'd 530 | char *msg = getmsgcopilotid(idmsgcopilotnext-1); if (!msg) goto checkerrcopilot; 531 | char *result = getkjson(msg, "result"); 532 | char *status = getkjson(result, "status"); 533 | if (!strncmp(status,"\"OK\"",4)) break; 534 | if (strncmp(status,"\"NotSignedIn\"",13)) { 535 | char *message = getksjson(getkjson(msg,"error"), "message"); 536 | if (message) { 537 | errcopilot=ECPAUTHSTR; if (!(serrcopilot=strdup(message))) { perror("cpwrap: copilot: strdup"); exit(1); } 538 | goto checkerrcopilot; 539 | } else { 540 | errcopilot=ECPAUTH; 541 | goto checkerrcopilot; 542 | } 543 | } 544 | 545 | // Do auth flow 546 | msgcopilot(sprintfs("{\"jsonrpc\":\"2.0\",\"params\":{},\"method\":\"signInInitiate\",\"id\":%d}",idmsgcopilotnext++)); 547 | if (!(msg=getmsgcopilotid(idmsgcopilotnext-1))) goto checkerrcopilot; 548 | result = getkjson(msg, "result"); 549 | char *usercode = getksjson(result, "userCode"); if (usercode && !(usercode=strdup(usercode))) { perror("cpwrap: strdup"); exit(1); } 550 | char *verificationuri = getksjson(result, "verificationUri"); 551 | if (!usercode || !verificationuri) { free(usercode),errcopilot=ECPAUTHGETCODEURI; goto checkerrcopilot; } 552 | if ((res=pthread_mutex_lock(&muttranscript))) { fprintf(stderr,"cpwrap: pthread_mutex_lock transcript: %s\n",strerror(res)); exit(1); } 553 | writes(fdterm, sprintfs("%scpwrap: To authorize, enter this code at this uri:\n%s\n%s\n",ntranscript&&transcript[ntranscript-1]!='\n'?"\n":"",usercode,verificationuri)); 554 | if ((res=pthread_mutex_unlock(&muttranscript))) { fprintf(stderr,"cpwrap: pthread_mutex_unlock transcript: %s\n",strerror(res)); exit(1); } 555 | msgcopilot(sprintfs("{\"jsonrpc\":\"2.0\",\"params\":{\"userCode\":\"%s\"},\"method\":\"signInConfirm\",\"id\":%d}",usercode,idmsgcopilotnext++)); 556 | free(usercode); 557 | } 558 | 559 | // Start thread to write when transcript changes 560 | static unsigned long vtranscriptrequested = 0; 561 | int fdspolltranscript[2]; if (pipe(fdspolltranscript)) { perror("cpwrap: pipe"); exit(1); } 562 | struct writecond waittranscript = {fdspolltranscript[1], &muttranscript, &condtranscript, isvtranscriptdifferent, (void*)&vtranscriptrequested}; 563 | pthread_t twaittranscript; if ((res=pthread_create(&twaittranscript,NULL,dowritecond,&waittranscript))) { fprintf(stderr,"cpwrap: pthread_create waitpoll: %s\n",strerror(res)); exit(1); } 564 | 565 | // Loop forever getting completions 566 | while (1) { 567 | startloopdocopilot:; 568 | 569 | // Wait for the transcript to change 570 | if ((res=pthread_mutex_lock(&muttranscript))) { fprintf(stderr,"cpwrap: pthread_mutex_lock transcript copilot0: %s\n",strerror(res)); exit(1); } 571 | while (1) { 572 | if (vtranscriptcompletion!=atomic_load(&vtranscript) && itranscriptcur==ntranscript && atomic_load(&ison)) break; 573 | if (verbose) fprintf(stderr,"\033[33mwaiting for transcript to change\033[0m\n"); 574 | if ((res=pthread_cond_wait(&condtranscript, &muttranscript))) { fprintf(stderr,"cpwrap: pthread_cond_wait transcript copilot: %s\n",strerror(res)); exit(1); } 575 | } 576 | if (verbose || verbosetranscript) fprintf(stderr,"\033[33mtranscript (%zu):\n%.*s\033[0m\n", ntranscript, (int)ntranscript, transcript); 577 | 578 | // Get copy of transcript 579 | static size_t sizetranscriptcopy=0; size_t ntranscriptcopy=0; static char *transcriptcopy=NULL; 580 | if (sizetranscriptcopy>4)&0xf], req[nreq++]=hex[*p&0xf]; 615 | else req[nreq++] = *p; 616 | //TODO utf8 617 | } 618 | npart = strlen(part=sprintfs("\"},\"position\":{\"character\":%u,\"line\":%u}},\"id\":%u,\"method\":\"getCompletions\",\"jsonrpc\":\"2.0\"}",icharline,iline,idmsgcopilotnext++)); 619 | if (sizereq=2) fprintf(stderr,"\33[33mwaiting for transcript/copilot\33[0m\n"); 634 | if ((res=poll(pfds,sizeof(pfds)/sizeof(*pfds),-1)) == -1) { perror("cpwrap: poll"); exit(1); } 635 | char c; while (!willblockread(fdspolltranscript[0])) if (read(fdspolltranscript[0],&c,1)==-1 && errno!=EINTR) { perror("cpwrap: read polltranscript"); exit(1); } 636 | } 637 | 638 | // Parse completion response 639 | char *jcompletion = getijson(getkjson(getkjson(msg,"result"),"completions"), 0); 640 | size_t ncompletionnew=0; char *completionnew=NULL; 641 | free(uuid); if ((uuid=getksjson(jcompletion,"uuid"))) { 642 | if (!(uuid=strdup(uuid))) { perror("cpwrap: strdup uuid"); exit(1); } 643 | char *completionreq = getksjson(jcompletion, "displayText"); 644 | if (completionreq) { 645 | if (!(completionnew=strdup(completionreq))) { fprintf(stderr,"cpwrap: strdup completionnew: %s\n",strerror(errno)); exit(1); } 646 | while (!memchr("\r\n",completionnew[ncompletionnew],3)) ncompletionnew++; 647 | } 648 | } 649 | 650 | // Set completion 651 | if ((res=pthread_mutex_lock(&mutcompletion))) { fprintf(stderr, "cpwrap: pthread_mutex_lock mutcompletion failed: %s\n", strerror(res)); exit(1); } 652 | free(completion), completion=completionnew; 653 | ncompletion = ncompletionnew; 654 | vtranscriptcompletion = vtranscriptcopy; 655 | acceptedcompletion = 0; 656 | writeall(fdscompletion[1], 1, ""); 657 | if ((res=pthread_mutex_unlock(&mutcompletion))) { fprintf(stderr, "cpwrap: pthread_mutex_unlock mutcompletion failed: %s\n", strerror(res)); exit(1); } 658 | 659 | // Send shown message 660 | if (uuid) msgcopilot(sprintfs("{\"params\":{\"uuid\":\"%s\"},\"id\":%u,\"method\":\"notifyShown\",\"jsonrpc\":\"2.0\"}",uuid,idmsgcopilotnext++)); 661 | 662 | // Check copilot errors 663 | checkerrcopilot:; 664 | if (atomic_load(&mainexited)) return NULL; 665 | if (errcopilot) { 666 | fprintf(stderr, "cpwrap: copilot error: "); 667 | if (errcopilot == ECPUNKNOWN) fprintf(stderr, "unknown error"); 668 | else if (errcopilot == ECPAUTHSTR) fprintf(stderr, "error while authing: %s",serrcopilot), free(serrcopilot); 669 | else if (errcopilot == ECPAUTH) fprintf(stderr, "error while authing"); 670 | else if (errcopilot == ECPAUTHGETCODEURI) fprintf(stderr, "error while getting the authentication userCode and verificationUri"); 671 | else if (errcopilot == ECPREADEOF) fprintf(stderr, "EOF while reading"); 672 | fprintf(stderr, "\n"); 673 | errcopilot = 0; 674 | // TODO: restart and retry? meh 675 | atomic_store(&ison, 0); 676 | return NULL; 677 | } 678 | } 679 | return NULL; 680 | } 681 | 682 | int 683 | main(int argc, char *argv[]) 684 | { 685 | int res; 686 | 687 | // Find copilot dist directory 688 | char *copilotdir=NULL, *sourcecpdir=NULL; 689 | if ((copilotdir=getenv("CPWRAPPATHDIST"))) { sourcecpdir="$CPWRAPPATHDIST"; goto gotdistdir; } 690 | char *xdgconfighomevar = getenv("XDG_CONFIG_HOME"); 691 | char *home = getenv("HOME"); 692 | char *xdgconfighome=xdgconfighomevar; if (!xdgconfighome && home && !(xdgconfighome=strdup(sprintfs("%s/.config",home)))) { perror("cpwrap: strdup xdgconfighome"); exit(1); } 693 | if (xdgconfighome && (copilotdir=readstr(sprintfs("%s/cpwrap/pathdist",xdgconfighome)))) { sourcecpdir="${XDG_CONFIG_HOME:$HOME/.config}/cpwrap/pathdist"; goto gotdistdir; } 694 | if ((copilotdir=(char*)pathcopilotdist)) { sourcecpdir="pathcopilotdist"; goto gotdistdir; } 695 | if (home) { 696 | char *s; 697 | if (!access(s=sprintfs("%s/nvim/pack/github/start/copilot.vim/copilot/dist",xdgconfighome),R_OK)) { if(!(copilotdir=strdup(s))){perror("cpwrap: strdup");exit(1);} sourcecpdir="${XDG_CONFIG_HOME:$HOME/.config}/nvim/pack/github/start/copilot.vim/copilot/dist"; goto gotdistdir; } 698 | if (!access(s=sprintfs("%s/.vim/pack/github/start/copilot.vim/copilot/dist",home),R_OK)) { if(!(copilotdir=strdup(s))){perror("cpwrap: strdup");exit(1);} sourcecpdir="~/.vim/pack/github/start/copilot.vim/copilot/dist"; goto gotdistdir; } 699 | } 700 | //TODO: windows? 701 | printf("Please enter the absolute path to the copilot extension 'dist' directory. If you have installed the copilot vim/nvim extension, it will be wherever that extension is stored. Or you can get the folder from here:\nhttps://github.com/github/copilot.vim/tree/release/copilot/dist\nand enter its path on your system.\nPath: "); fflush(stdout); 702 | while (1) { 703 | size_t sizel=0; if (getline(&copilotdir,&sizel,stdin) == -1) { perror("cpwrap: getline"); exit(1); } 704 | if (!*copilotdir) return 0; 705 | char *pnl=strchr(copilotdir,'\n'); if (pnl)*pnl='\0'; 706 | if (*copilotdir != '/') printf("Please enter an absolute path (starting with '/')"); 707 | else if (!access(copilotdir,F_OK) && !access(sprintfs("%s/agent.js",copilotdir),R_OK)) break; 708 | else printf("access '%s/agent.js': %s",copilotdir,strerror(errno)); 709 | printf("\nPath: "); fflush(stdout); 710 | } 711 | sourcecpdir = "user input"; 712 | gotdistdir:; 713 | if (access(copilotdir,F_OK)) { fprintf(stderr, "cpwrap: couldn't access copilot dist directory '%s' gotten from %s: %s\n", copilotdir, sourcecpdir, strerror(errno)); exit(1); } 714 | if (access(sprintfs("%s/agent.js",copilotdir),R_OK)) { fprintf(stderr, "cpwrap: couldn't access agent.js file in dist directory '%s' gotten from %s (are you sure that is the dist directory?): %s\n", copilotdir, sourcecpdir, strerror(errno)); exit(1); } 715 | if (home && !strcmp(sourcecpdir,"user input")) mkdir(sprintfs("%s/cpwrap",xdgconfighome),0755), writestr(sprintfs("%s/cpwrap/pathdist",xdgconfighome),copilotdir), printf("Saved to %s/cpwrap/pathdist\n",xdgconfighome); 716 | 717 | // Start copilot 718 | if (pipe(fdscp) || pipe(fdscp+2)) { perror("cpwrap: pipe fdscp"); exit(1); } 719 | if (fcntl(fdscp[0],F_SETFL,FD_CLOEXEC)==-1 || fcntl(fdscp[1],F_SETFL,FD_CLOEXEC)==-1 || fcntl(fdscp[2],F_SETFL,FD_CLOEXEC)==-1 || fcntl(fdscp[3],F_SETFL,FD_CLOEXEC)==-1) { perror("cpwrap: fcntl fdscp"); exit(1); } 720 | if ((pidcp=fork()) == -1) { perror("cpwrap: fork"); exit(1); } 721 | if (!pidcp) { 722 | if (dup2(fdscp[0],0)==-1 || dup2(fdscp[3],1)==-1) { perror("cpwrap: dup2 fdscp"); exit(1); } 723 | if (!verbose && !verbosecopilot) close(2); 724 | if (execlp("node","node",sprintfs("%s/agent.js",copilotdir),NULL) == -1) { perror("cpwrap: execlp node (make sure node.js is installed)"); exit(1); } 725 | } 726 | close(fdscp[0]), close(fdscp[3]); 727 | 728 | // Fork args as pseudoterminal 729 | int fdmaster = posix_openpt(O_RDWR); if (fdmaster == -1) { perror("cpwrap: posix_openpt"); exit(1); } 730 | if (grantpt(fdmaster) == -1) { perror("cpwrap: grantpt"); exit(1); } 731 | if (unlockpt(fdmaster) == -1) { perror("cpwrap: unlockpt"); exit(1); } 732 | char *pathslave = ptsname(fdmaster); if (!pathslave) { perror("cpwrap: ptsname"); exit(1); } 733 | int fdslave = open(pathslave, O_RDWR); if (fdslave == -1) { perror("cpwrap: open"); exit(1); } 734 | if (fcntl(fdmaster,F_SETFL,FD_CLOEXEC)==-1 || fcntl(fdslave,F_SETFL,FD_CLOEXEC)==-1) { perror("cpwrap: fcntl fdmaster fdslave"); exit(1); } 735 | pid_t pid = fork(); if (pid == -1) { perror("cpwrap: fork"); exit(1); } 736 | if (!pid) { 737 | if (setsid() == -1) { perror("cpwrap: setsid"); exit(1); } 738 | if (ioctl(fdslave,TIOCSCTTY,0) == -1) { perror("cpwrap: ioctl"); exit(1); } 739 | if (dup2(fdslave,0)==-1 || dup2(fdslave,1)==-1 || dup2(fdslave,2)==-1) { perror("cpwrap: dup2"); exit(1); } 740 | if (execvp(argv[1], argv+1)) { perror("cpwrap: execvp"); exit(1); } 741 | } 742 | close(fdslave); 743 | 744 | // Open terminal for writing 745 | fdterm = open("/dev/tty", O_WRONLY|O_CLOEXEC); if (fdterm == -1) { perror("cpwrap: open /dev/tty"); exit(1); } 746 | 747 | // Clean up in exit 748 | if (atexit(cleanup)) { perror("cpwrap: atexit"); exit(1); } 749 | 750 | // Set terminal to raw mode 751 | if (tcgetattr(fdterm,&oldtermios) == -1) { perror("cpwrap: tcgetattr"); exit(1); } 752 | gottermios = 1; 753 | struct termios newtermios = oldtermios; 754 | newtermios.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 755 | if (tcsetattr(fdterm,TCSANOW,&newtermios)) { perror("cpwrap: tcsetattr"); exit(1); } 756 | 757 | // Buffers for in-|>process-|>out 758 | size_t sizefromstdin=0, nfromstdin=0; char *fromstdin=NULL; 759 | size_t sizefromprocess=0, nfromprocess=0; char *fromprocess=NULL; 760 | if (!(fromstdin=malloc(sizefromstdin=2048)) || !(fromprocess=malloc(sizefromprocess=2048))) { perror("cpwrap: malloc"); exit(1); } 761 | 762 | // Create completion pipe 763 | if (pipe(fdscompletion)) { perror("cpwrap: pipe fdscompletion"); exit(1); } 764 | if (fcntl(fdscompletion[0],F_SETFL,FD_CLOEXEC)==-1 || fcntl(fdscompletion[1],F_SETFL,FD_CLOEXEC)==-1) { perror("cpwrap: fcntl fdscompletion"); exit(1); } 765 | 766 | // Start transcript off with args 767 | int nsargs=3; for (int i=1; i 1) transcript[ntranscript++]=' '; 771 | strcpy(transcript+ntranscript,argv[i]), ntranscript+=strlen(argv[i]); 772 | } 773 | 774 | // Start copilot thread 775 | pthread_t tcopilot; if (pthread_create(&tcopilot,NULL,docopilot,NULL)) { perror("cpwrap: pthread_create docopilot"); exit(1); } 776 | 777 | // Say help 778 | writes(fdterm, "cpwrap: TAB=accept, ESC=reject, F1=toggle\n"); 779 | 780 | // Loop forever piping between 781 | char closedprocess=0, visiblecompletion=0, visiblestatus=0; 782 | while (!closedprocess || nfromprocess) { 783 | 784 | // Wait for anything to be actionable 785 | struct pollfd pfds[] = { 786 | {0, POLLIN}, 787 | {closedprocess?-1:fdmaster, POLLIN|(nfromstdin?POLLOUT:0)}, 788 | {1, nfromprocess?POLLOUT:0}, 789 | {fdscompletion[0], POLLIN} 790 | }; 791 | if (verbose) fprintf(stderr, "waiting on poll (%zu %zu)...\n", nfromstdin, nfromprocess); 792 | int mspoll = atomic_load(&ison)&&vtranscriptcompletion!=atomic_load(&vtranscript) ? 100 : -1; 793 | if (poll(pfds,sizeof(pfds)/sizeof(*pfds),mspoll)==-1 && errno!=EAGAIN && errno!=EINTR) { perror("cpwrap: poll"); exit(1); } 794 | if (verbose) fprintf(stderr, "poll returned: %d %d %d %d (POLLIN=%d, POLLOUT=%d, POLLERR=%d, POLLHUP=%d)\n", pfds[0].revents, pfds[1].revents, pfds[2].revents, pfds[3].revents, POLLIN, POLLOUT, POLLERR, POLLHUP); 795 | if ((res=pthread_mutex_lock(&muttranscript))) { fprintf(stderr, "cpwrap: pthread_mutex_lock transcript main: %d\n", res); exit(1); } 796 | unsigned vtranscript0 = atomic_load(&vtranscript); 797 | 798 | // Read all of stdin 799 | size_t nfromstdin0 = nfromstdin; 800 | while (!willblockread(0)) { 801 | if (sizefromstdin>4)&0xf], hex[fromstdin[nfromstdin+i]&0xf]); fputc('\n', stderr); 805 | if (!n) exit(0); 806 | if (n > 0) nfromstdin += n; 807 | } 808 | //if (nfromstdin > nfromstdin0) { const char*hex="0123456789abcdef"; fprintf(stderr,"\n\033[32mread from stdin (%zu):\n", nfromstdin-nfromstdin0); for (size_t i=nfromstdin0; i>4)); writeall(2,1,hex+(c&15)); } else writeall(2,1,&c); } writes(2,"\033[0m\n"); } 809 | 810 | // Accept completion 811 | if (vtranscriptcompletionshown==atomic_load(&vtranscript) && visiblecompletion && nfromstdin-nfromstdin0==1 && fromstdin[nfromstdin0]=='\t') { 812 | writes(fdterm, "\033[K"); 813 | visiblecompletion=0, acceptedcompletion=1; 814 | if (sizefromstdin 0) memmove(fromstdin,fromstdin+n,nfromstdin-=n); 837 | } 838 | 839 | // Read from process 840 | size_t nfromprocess0 = nfromprocess; 841 | while (!willblockread(fdmaster)) { 842 | if (sizefromprocess0?nfromprocess+n:nfromprocess); 845 | if (n==-1 && errno==EIO) { closedprocess=1; break; } 846 | if (!n) { closedprocess=1; break; } 847 | if (n > 0) nfromprocess += n; 848 | } 849 | if (sizefromprocess nfromprocess0) { const char*hex="0123456789abcdef"; fprintf(stderr,"\n\033[32mread from process (%zu):\n", nfromprocess-nfromprocess0); for (size_t i=nfromprocess0; i>4)); writeall(2,1,hex+(c&15)); } else writeall(2,1,&c); } writes(2,"\033[0m\n"); } 852 | 853 | // Append to output portion of transcript, processing terminal escape codes 854 | size_t nadd = nfromprocess - nfromprocess0; 855 | if (sizetranscript=sizeof(s)-1 && !memcmp(fromprocess+i,s,sizeof(s)-1) && (i+=sizeof(s)-1,1)) 861 | ELIF("\33[C") { if (itranscriptcur++ == ntranscript) transcript[ntranscript]=' '; } 862 | ELIF("\33[K") ntranscript=itranscriptcur; 863 | ELIF("\33[") { 864 | char *pend; unsigned long n = strtoul(fromprocess+i, &pend, 10); 865 | if (pend == fromprocess+i) n=-1; 866 | i = pend - fromprocess + (pend ntranscript) ntranscript=itranscriptcur; 886 | } 887 | if (itranscriptcur < ntranscript) { 888 | for (size_t i=itranscriptcur; i>4)); writeall(2,1,hex+(c&15)); } else writeall(2,1,&c); } writes(2,"\033[0m\n"); 893 | 894 | // Release transcript 895 | if (atomic_load(&vtranscript)!=vtranscript0 && ((res=pthread_cond_broadcast(&condtranscript)))) { fprintf(stderr, "cpwrap: pthread_cond_broadcast transcript main: %d\n", res); exit(1); } 896 | if ((res=pthread_mutex_unlock(&muttranscript))) { fprintf(stderr, "cpwrap: pthread_mutex_unlock transcript main: %d\n", res); exit(1); } 897 | 898 | // Write all to stdout 899 | while (nfromprocess && !willblockwrite(1)) { 900 | if (visiblecompletion || visiblestatus) writes(fdterm,"\33[K"), visiblecompletion=visiblestatus=0; 901 | ssize_t n = write(1, fromprocess, nfromprocess); if (n==-1 && errno!=EAGAIN && errno!=EINTR) { perror("cpwrap: write stdout"); exit(1); } 902 | if (!n) exit(0); 903 | if (n > 0) memmove(fromprocess,fromprocess+n,nfromprocess-=n); 904 | } 905 | 906 | // Draw completion status 907 | if ((res=pthread_mutex_lock(&mutcompletion))) { fprintf(stderr, "cpwrap: pthread_mutex_lock completion main: %s\n",strerror(res)); exit(1); } 908 | char tmp; while (!willblockread(fdscompletion[0])) { ssize_t r=read(fdscompletion[0],&tmp,sizeof(tmp)); if (r==-1 && errno!=EAGAIN && errno!=EINTR) { perror("cpwrap: read completion"); exit(1); } if (!r) { fputs("cpwrap: unexpected EOF on completion pipe\n",stderr); exit(1); } } 909 | unsigned msnow = getms(); 910 | if (atomic_load(&ison) && itranscriptcur==ntranscript) { 911 | if (atomic_load(&vtranscript)==vtranscriptcompletion && vtranscriptcompletionshown!=vtranscriptcompletion) { 912 | if (visiblecompletion || visiblestatus) writes(fdterm,"\33[K"), visiblecompletion=visiblestatus=0; 913 | if (ncompletion) { 914 | writes(fdterm, "\0337\033[4m"); 915 | writeall(fdterm, ncompletion, completion); 916 | writes(fdterm, "\033[24m\033[K\0338"); 917 | visiblecompletion = 1; 918 | } 919 | vtranscriptcompletionshown = vtranscriptcompletion; 920 | } else if (atomic_load(&vtranscript) != vtranscriptcompletion) { 921 | if (visiblecompletion || visiblestatus) writes(fdterm,"\33[K"), visiblecompletion=visiblestatus=0; 922 | writes(fdterm, "\0337\33[4m"); 923 | writeall(fdterm, 1, ". "+(msnow/100)%2); 924 | writes(fdterm, "\33[24m\0338"); 925 | visiblestatus = 1; 926 | } 927 | } 928 | if ((res=pthread_mutex_unlock(&mutcompletion))) { fprintf(stderr, "cpwrap: pthread_mutex_unlock completion main: %s\n",strerror(res)); exit(1); } 929 | } 930 | if (visiblestatus || visiblecompletion) writes(fdterm,"\33[K"); 931 | atomic_store(&mainexited, 1); 932 | } 933 | --------------------------------------------------------------------------------