code: plan9front

Download patch

ref: 299d450bc303ccdbd6483c120a565bed85212d45
parent: 2719003b36ef8fdd4723ccb5a8e12e4b10fe41ed
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;
+		}
+	}
+}