ref: 4eb9df485b9daa6863ba67a6375563107285dbb6
parent: ce561537e72eff4801be4cd61cf460953237f343
author: phil9 <telephil9@gmail.com>
date: Tue Jan 30 04:22:25 EST 2024
vdiff: a visual diff viewer
--- /dev/null
+++ b/sys/man/1/vdiff
@@ -1,0 +1,33 @@
+.TH VDIFF 1
+.SH NAME
+vdiff \- a visual diff viewer
+.SH SYNOPSIS
+.B vdiff
+[
+.B -b
+]
+.SH DESCRIPTION
+.I vdiff
+reads unified diff output from standard input and displays a colored version. Right clicking on a line will send a
+.IR plumb (6)
+message to the
+.B edit
+port. Pressing
+.B q
+or
+.B del
+will exit
+.B vdiff
+.PP
+The
+.B \-b
+flag changes the color scheme to white text on a black background.
+.SH EXAMPLE
+% git/diff |vdiff
+.SH SOURCE
+/sys/src/cmd/vdiff.c
+.SH SEE ALSO
+.IR diff (1)
+.SH HISTORY
+vdiff first appeared in 9front (January, 2024).
+
--- /dev/null
+++ b/sys/src/cmd/vdiff.c
@@ -1,0 +1,478 @@
+#include <u.h>
+#include <libc.h>
+#include <plumb.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <bio.h>
+
+typedef struct Line Line;
+typedef struct Col Col;
+
+struct Line {+ int t;
+ char *s;
+ char *f;
+ int l;
+};
+
+struct Col {+ Image *bg;
+ Image *fg;
+};
+
+enum
+{+ Lfile = 0,
+ Lsep,
+ Ladd,
+ Ldel,
+ Lnone,
+ Ncols,
+};
+
+enum
+{+ Scrollwidth = 12,
+ Scrollgap = 2,
+ Margin = 8,
+ Hpadding = 4,
+ Vpadding = 2,
+};
+
+Mousectl *mctl;
+Keyboardctl *kctl;
+Rectangle sr;
+Rectangle scrollr;
+Rectangle scrposr;
+Rectangle listr;
+Rectangle textr;
+Col cols[Ncols];
+Col scrlcol;
+int scrollsize;
+int lineh;
+int nlines;
+int offset;
+int scrolling;
+int oldbuttons;
+Line **lines;
+int lsize;
+int lcount;
+int maxlength;
+int Δpan;
+const char ellipsis[] = "...";
+
+void
+plumb(char *f, int l)
+{+ int fd;
+ char wd[256], addr[300]={0};+
+ fd = plumbopen("send", OWRITE);+ if(fd<0)
+ return;
+ getwd(wd, sizeof wd);
+ snprint(addr, sizeof addr, "%s:%d", f, l);
+ plumbsendtext(fd, "vdiff", "edit", wd, addr);
+ close(fd);
+}
+
+void
+drawline(Rectangle r, Line *l)
+{+ Point p;
+ Rune rn;
+ char *s;
+ int off, tab, nc;
+
+ draw(screen, r, cols[l->t].bg, nil, ZP);
+ p = Pt(r.min.x + Hpadding, r.min.y + (Dy(r)-font->height)/2);
+ off = Δpan / stringwidth(font, " ");
+ for(s = l->s, nc = -1, tab = 0; *s; nc++, tab--, off--){+ if(tab <= 0 && *s == '\t'){+ tab = 4 - nc % 4;
+ s++;
+ }
+ if(tab > 0){+ if(off <= 0)
+ p = runestring(screen, p, cols[l->t].bg, ZP, font, L"█");
+ }else if((p.x+Hpadding+stringwidth(font, " ")+stringwidth(font, ellipsis)>=textr.max.x)){+ string(screen, p, cols[l->t].fg, ZP, font, ellipsis);
+ break;
+ }else{+ s += chartorune(&rn, s);
+ if(off <= 0)
+ p = runestringn(screen, p, cols[l->t].fg, ZP, font, &rn, 1);
+ }
+ }
+}
+
+void
+redraw(void)
+{+ Rectangle lr;
+ int i, h, y;
+
+ draw(screen, sr, cols[Lnone].bg, nil, ZP);
+ draw(screen, scrollr, scrlcol.bg, nil, ZP);
+ if(lcount>0){+ h = ((double)nlines/lcount)*Dy(scrollr);
+ y = ((double)offset/lcount)*Dy(scrollr);
+ scrposr = Rect(scrollr.min.x, scrollr.min.y+y, scrollr.max.x-1, scrollr.min.y+y+h);
+ }else
+ scrposr = Rect(scrollr.min.x, scrollr.min.y, scrollr.max.x-1, scrollr.max.y);
+ draw(screen, scrposr, scrlcol.fg, nil, ZP);
+ for(i=0; i<nlines && offset+i<lcount; i++){+ lr = Rect(textr.min.x, textr.min.y+i*lineh, textr.max.x, textr.min.y+(i+1)*lineh);
+ drawline(lr, lines[offset+i]);
+ }
+ flushimage(display, 1);
+}
+
+void
+pan(int off)
+{+ int max;
+
+ max = Hpadding + maxlength * stringwidth(font, " ") + 2 * stringwidth(font, ellipsis) - Dx(textr);
+ Δpan += off * stringwidth(font, " ");
+ if(Δpan < 0 || max <= 0)
+ Δpan = 0;
+ else if(Δpan > max)
+ Δpan = max;
+ redraw();
+}
+
+void
+clampoffset(void)
+{+ if(offset<0)
+ offset = 0;
+ if(offset+nlines>lcount)
+ offset = lcount-nlines+1;
+}
+
+void
+scroll(int off)
+{+ if(off<0 && offset<=0)
+ return;
+ if(off>0 && offset+nlines>lcount)
+ return;
+ offset += off;
+ clampoffset();
+ redraw();
+}
+
+int
+indexat(Point p)
+{+ int n;
+
+ if (!ptinrect(p, textr))
+ return -1;
+ n = (p.y - textr.min.y) / lineh;
+ if ((n+offset) >= lcount)
+ return -1;
+ return n;
+}
+
+void
+eresize(void)
+{+ if(getwindow(display, Refnone)<0)
+ sysfatal("cannot reattach: %r");+ sr = screen->r;
+ scrollr = sr;
+ scrollr.max.x = scrollr.min.x+Scrollwidth+Scrollgap;
+ listr = sr;
+ listr.min.x = scrollr.max.x;
+ textr = insetrect(listr, Margin);
+ lineh = Vpadding+font->height+Vpadding;
+ nlines = Dy(textr)/lineh;
+ scrollsize = mousescrollsize(nlines);
+ if(offset > 0 && offset+nlines>lcount)
+ offset = lcount-nlines+1;
+ redraw();
+}
+
+void
+ekeyboard(Rune k)
+{+ switch(k){+ case 'q':
+ case Kdel:
+ threadexitsall(nil);
+ break;
+ case Khome:
+ scroll(-1000000);
+ break;
+ case Kend:
+ scroll(1000000);
+ break;
+ case Kpgup:
+ scroll(-nlines);
+ break;
+ case Kpgdown:
+ scroll(nlines);
+ break;
+ case Kup:
+ scroll(-1);
+ break;
+ case Kdown:
+ scroll(1);
+ break;
+ case Kleft:
+ pan(-4);
+ break;
+ case Kright:
+ pan(4);
+ break;
+ }
+}
+
+void
+emouse(Mouse m)
+{+ int n;
+
+ if(oldbuttons == 0 && m.buttons != 0 && ptinrect(m.xy, scrollr))
+ scrolling = 1;
+ else if(m.buttons == 0)
+ scrolling = 0;
+
+ if(scrolling){+ if(m.buttons&1){+ n = (m.xy.y - scrollr.min.y) / lineh;
+ if(-n<lcount-offset){+ scroll(-n);
+ } else {+ scroll(-lcount+offset);
+ }
+ return;
+ }else if(m.buttons&2){+ n = (m.xy.y - scrollr.min.y) * lcount / Dy(scrollr);
+ offset = n;
+ clampoffset();
+ redraw();
+ }else if(m.buttons&4){+ n = (m.xy.y - scrollr.min.y) / lineh;
+ if(n<lcount-offset){+ scroll(n);
+ } else {+ scroll(lcount-offset);
+ }
+ return;
+ }
+ }else{+ if(m.buttons&4){+ n = indexat(m.xy);
+ if(n>=0 && lines[n+offset]->f != nil)
+ plumb(lines[n+offset]->f, lines[n+offset]->l);
+ }else if(m.buttons&8)
+ scroll(-scrollsize);
+ else if(m.buttons&16)
+ scroll(scrollsize);
+ }
+ oldbuttons = m.buttons;
+}
+
+void
+initcol(Col *c, ulong fg, ulong bg)
+{+ c->fg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, fg);
+ c->bg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, bg);
+}
+
+void
+initcols(int black)
+{+ if(black){+ initcol(&scrlcol, DBlack, 0x999999FF^(~0xFF));
+ initcol(&cols[Lfile], DWhite, 0x333333FF);
+ initcol(&cols[Lsep], DBlack, DPurpleblue);
+ initcol(&cols[Ladd], DWhite, 0x002800FF);
+ initcol(&cols[Ldel], DWhite, 0x3F0000FF);
+ initcol(&cols[Lnone], DWhite, DBlack);
+ }else{+ initcol(&scrlcol, DWhite, 0x999999FF);
+ initcol(&cols[Lfile], DBlack, 0xEFEFEFFF);
+ initcol(&cols[Lsep], DBlack, 0xEAFFFFFF);
+ initcol(&cols[Ladd], DBlack, 0xE6FFEDFF);
+ initcol(&cols[Ldel], DBlack, 0xFFEEF0FF);
+ initcol(&cols[Lnone], DBlack, DWhite);
+ }
+}
+
+int
+linetype(char *text)
+{+ int type;
+
+ type = Lnone;
+ if(strncmp(text, "+++", 3)==0)
+ type = Lfile;
+ else if(strncmp(text, "---", 3)==0){+ if(strlen(text) > 4)
+ type = Lfile;
+ }else if(strncmp(text, "@@", 2)==0)
+ type = Lsep;
+ else if(strncmp(text, "+", 1)==0)
+ type = Ladd;
+ else if(strncmp(text, "-", 1)==0)
+ type = Ldel;
+ return type;
+}
+
+Line*
+parseline(char *f, int n, char *s)
+{+ Line *l;
+ int len;
+
+ l = malloc(sizeof *l);
+ if(l==nil)
+ sysfatal("malloc: %r");+ l->t = linetype(s);
+ l->s = s;
+ l->l = n;
+ if(l->t != Lfile && l->t != Lsep)
+ l->f = f;
+ else
+ l->f = nil;
+ len = strlen(s);
+ if(len > maxlength)
+ maxlength = len;
+ return l;
+}
+
+int
+lineno(char *s)
+{+ char *p, *t[5];
+ int n, l;
+
+ p = strdup(s);
+ n = tokenize(p, t, 5);
+ if(n<=0)
+ return -1;
+ l = atoi(t[2]);
+ free(p);
+ return l;
+}
+
+void
+parse(int fd)
+{+ Biobuf *bp;
+ Line *l;
+ char *s, *f, *t;
+ int n, ab;
+
+ ab = 0;
+ n = 0;
+ f = nil;
+ lsize = 64;
+ lcount = 0;
+ lines = malloc(lsize * sizeof *lines);
+ if(lines==nil)
+ sysfatal("malloc: %r");+ bp = Bfdopen(fd, OREAD);
+ if(bp==nil)
+ sysfatal("Bfdopen: %r");+ for(;;){+ s = Brdstr(bp, '\n', 1);
+ if(s==nil)
+ break;
+ l = parseline(f, n, s);
+ if(l->t == Lfile && l->s[0] == '-' && strncmp(l->s+4, "a/", 2)==0)
+ ab = 1;
+ if(l->t == Lfile && l->s[0] == '+'){+ f = l->s+4;
+ if(ab && strncmp(f, "b/", 2)==0){+ f += 1;
+ if(access(f, AEXIST) < 0)
+ f += 1;
+ }
+ t = strchr(f, '\t');
+ if(t!=nil)
+ *t = 0;
+ }else if(l->t == Lsep)
+ n = lineno(l->s);
+ else if(l->t == Ladd || l->t == Lnone)
+ ++n;
+ lines[lcount++] = l;
+ if(lcount>=lsize){+ lsize *= 2;
+ lines = realloc(lines, lsize*sizeof *lines);
+ if(lines==nil)
+ sysfatal("realloc: %r");+ }
+ }
+}
+
+void
+usage(void)
+{+ fprint(2, "%s [-b]\n", argv0);
+ exits("usage");+}
+
+void
+threadmain(int argc, char *argv[])
+{+ enum { Emouse, Eresize, Ekeyboard, };+ Mouse m;
+ Rune k;
+ Alt a[] = {+ { nil, &m, CHANRCV },+ { nil, nil, CHANRCV },+ { nil, &k, CHANRCV },+ { nil, nil, CHANEND },+ };
+ int b;
+
+ scrolling = 0;
+ oldbuttons = 0;
+ b = 0;
+ ARGBEGIN{+ case 'b':
+ b = 1;
+ break;
+ default:
+ usage();
+ break;
+ }ARGEND;
+
+ parse(0);
+ if(lcount==0){+ fprint(2, "no diff\n");
+ exits(nil);
+ }
+ if(initdraw(nil, nil, "vdiff")<0)
+ sysfatal("initdraw: %r");+ display->locking = 0;
+ if((mctl = initmouse(nil, screen)) == nil)
+ sysfatal("initmouse: %r");+ if((kctl = initkeyboard(nil)) == nil)
+ sysfatal("initkeyboard: %r");+ a[Emouse].c = mctl->c;
+ a[Eresize].c = mctl->resizec;
+ a[Ekeyboard].c = kctl->c;
+ initcols(b);
+ eresize();
+ for(;;){+ switch(alt(a)){+ case Emouse:
+ emouse(m);
+ break;
+ case Eresize:
+ eresize();
+ break;
+ case Ekeyboard:
+ ekeyboard(k);
+ break;
+ }
+ }
+}
--
⑨