shithub: grapple

Download patch

ref: 3cf63f0f168a405f958a3dfd50358a981c7b3585
author: phil9 <telephil9@gmail.com>
date: Sun Nov 21 08:30:16 EST 2021

initial import

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 phil9 <telephil9@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,27 @@
+grapple
+========
+grapple is a graphical menu of plumbable lines.
+When run, grapple captures all output from a pipe looking for plumbable text (pattern is: <path>(:<addr>)?). If the line matches the pattern it can then be sent to the plumber.
+When an item is selected, it is sent to the plumber `edit` port. If the `-q` option is passed, grapple will exit once a line is plumbed.
+
+Left-click an item to select it.  
+Right-click to activate the selected item.
+
+Keyboard shortcuts:
+- Arrow up / down change selection
+- Enter activate selection
+- Page up / down scroll by one screen page
+- Home go to first item in the list
+- End go to last item in the list
+- q or Del or Esc to exit grapple
+
+Usage:
+-------
+Install with usual ``mk install``  
+Run: ``... | grapple [-q]``
+
+The provided `gg` (grapple g) and `gm` (grapple mk) shows how to use grapple. 
+
+Bugs:
+------
+Indeed.
--- /dev/null
+++ b/gg
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+g $* |grapple
--- /dev/null
+++ b/gm
@@ -1,0 +1,3 @@
+#!/bin/rc
+
+mk >[2=1] |grapple
--- /dev/null
+++ b/grapple.c
@@ -1,0 +1,354 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <plumb.h>
+#include <regexp.h>
+
+typedef struct Line Line;
+
+struct Line
+{
+	char *text;
+	char ptext[255];
+};
+
+enum
+{
+	Emouse,
+	Eresize,
+	Ekeyboard,
+};
+
+enum
+{
+	Maxlines = 4096,
+	Padding = 4,
+	Scrollwidth = 12,
+};
+
+Mousectl *mctl;
+Keyboardctl *kctl;
+Image *selbg;
+Image *mfg;
+Rectangle ir;
+Rectangle sr;
+Rectangle lr;
+int lh;
+int lcount;
+int loff;
+int lsel;
+int scrollsize;
+int qmode;
+int plumbfd;
+Line lines[Maxlines];
+int nlines;
+char pwd[255] = {0};
+Reprog *re;
+
+void
+loadlines(void)
+{
+	Biobuf *bp;
+	char *s;
+	Resub sub;
+	char buf[255];
+	int n;
+
+	nlines = 0;
+	bp = Bfdopen(0, OREAD);
+	for(;;){
+		s = Brdstr(bp, '\n', 1);
+		if(s == nil)
+			break;
+		lines[nlines].text = s;
+		memset(&sub, 0, sizeof(sub));
+		if(regexec(re, s, &sub, 1) != 0)
+			snprint(lines[nlines].ptext, sizeof(lines[nlines].ptext), "%.*s", (int)(sub.ep - sub.sp), sub.sp);
+		else
+			lines[nlines].ptext[0] = 0;
+		nlines += 1;
+		if(nlines >= Maxlines)
+			break;
+	}
+	Bterm(bp);
+}
+
+void
+activate(void)
+{
+	Line l;
+
+	l = lines[lsel + loff];
+	if(l.ptext[0] == 0)
+		return;
+	plumbsendtext(plumbfd, argv0, nil, pwd, l.ptext);
+	if(qmode)
+		threadexitsall(nil);
+}
+
+int
+lineat(Point p)
+{
+	if(ptinrect(p, lr) == 0)
+		return -1;
+	return (p.y - lr.min.y) / lh;	
+}
+
+Rectangle
+linerect(int i)
+{
+	Rectangle r;
+
+	r.min.x = 0;
+	r.min.y = i * (font->height + Padding);
+	r.max.x = Dx(lr) - 2*Padding;
+	r.max.y = (i + 1) * (font->height + Padding);
+	r = rectaddpt(r, lr.min);
+	return r;
+}
+
+void
+drawline(int i, int sel)
+{
+	Point p;
+	char *t;
+
+	if(loff + i >= nlines)
+		return;
+	draw(screen, linerect(i), sel ? selbg : display->white, nil, ZP);
+	p = addpt(lr.min, Pt(0, i * lh));
+	t = lines[loff + i].text;
+	while(*t){
+		if(*t == '\t')
+			p = string(screen, p, display->black, ZP, font, "    ");
+		else
+			p = stringn(screen, p, display->black, ZP, font, t, 1);
+		t++;
+	}
+}
+
+void
+redraw(void)
+{
+	Rectangle scrposr;
+	int i, h, y, ye;
+
+	draw(screen, screen->r, display->white, nil, ZP);
+	draw(screen, sr, mfg, nil, ZP);
+	border(screen, sr, 0, display->black, ZP);
+	if(nlines > 0){
+		h = ((double)lcount / nlines) * Dy(sr);
+		y = ((double)loff / nlines) * Dy(sr);
+		ye = sr.min.y + y + h - 1;
+		if(ye >= sr.max.y)
+			ye = sr.max.y - 1;
+		scrposr = Rect(sr.min.x + 1, sr.min.y + y + 1, sr.max.x - 1, ye);
+	}else
+		scrposr = insetrect(sr, -1);
+	draw(screen, scrposr, display->white, nil, ZP);
+	for(i = 0; i < lcount; i++)
+		drawline(i, i == lsel);
+	flushimage(display, 1);
+}
+
+int
+scroll(int lines, int setsel)
+{
+	if(nlines <= lcount)
+		return 0;
+	if(lines < 0 && loff == 0)
+		return 0;
+	if(lines > 0 && loff + lcount >= nlines){
+		return 0;
+	}
+	loff += lines;
+	if(loff < 0)
+		loff = 0;
+	if(loff + nlines%lcount >= nlines)
+		loff = nlines - nlines%lcount;
+	if(setsel){
+		if(lines > 0)
+			lsel = 0;
+		else
+			lsel = lcount - 1;
+	}
+	redraw();
+	return 1;
+}
+
+void
+changesel(int from, int to)
+{
+	drawline(from, 0);
+	drawline(to, 1);
+	flushimage(display, 1);
+}
+
+void
+eresize(void)
+{
+	Rectangle r;
+
+	r = screen->r;
+	sr = Rect(r.min.x + Padding, r.min.y + Padding, r.min.x + Padding + Scrollwidth, r.max.y - Padding - 1);
+	lr = Rect(sr.max.x + Padding, r.min.y + Padding, r.max.x, r.max.y - Padding);
+	lh = font->height + Padding;
+	lcount = Dy(lr) / lh;
+	scrollsize = mousescrollsize(lcount);
+	redraw();
+}
+
+void
+emouse(Mouse *m)
+{
+	int n;
+
+	if(ptinrect(m->xy, lr)){
+		if(m->buttons == 1 || m->buttons == 4){
+			n = lineat(m->xy);
+			if(n != -1 && (loff + n) < nlines){
+				changesel(lsel, n);
+				lsel = n;
+			}
+			if(m->buttons == 4)
+				activate();
+		}else if(m->buttons == 8){
+			scroll(-scrollsize, 1);
+		}else if(m->buttons == 16){
+			scroll(scrollsize, 1);
+		}
+	}else if(ptinrect(m->xy, sr)){
+		if(m->buttons == 1){
+			n = (m->xy.y - sr.min.y) / lh;
+			scroll(-n, 1);
+		}else if(m->buttons == 2){
+			n = (m->xy.y - sr.min.y) * nlines / Dy(sr);
+			loff = n;
+			redraw();
+		}else if(m->buttons == 4){
+			n = (m->xy.y - sr.min.y) / lh;
+			scroll(n, 1);
+		}
+	}
+}
+
+void
+ekeyboard(Rune k)
+{
+	int osel;
+
+	switch(k){
+	case 'q':
+	case Kesc:
+	case Kdel:
+		threadexitsall(nil);
+		break;
+	case Kup:
+		if(lsel == 0 && loff > 0)
+			scroll(-lcount, 1);
+		else if(lsel > 0)
+			changesel(lsel, --lsel);
+		break;
+	case Kdown:
+		if(lsel < (nlines - 1)){
+			if(lsel == lcount - 1)
+				scroll(lcount, 1);
+			else if(loff + lsel < nlines - 1)
+				changesel(lsel, ++lsel);
+		}
+		break;
+	case Kpgup:
+		scroll(-lcount, 1);
+		break;
+	case Kpgdown:
+		scroll(lcount, 1);
+		break;
+	case Khome:
+		osel = lsel;
+		lsel = 0;
+		if(scroll(-nlines, 0) == 0)
+			changesel(osel, lsel);
+		break;
+	case Kend:
+		osel = lsel;
+		lsel = nlines%lcount - 1;
+		if(scroll(nlines, 0) == 0){
+			changesel(osel, lsel);
+		}
+		break;
+	case '\n':
+		if(lsel >= 0)
+			activate();
+		break;
+	}
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-q]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	Mouse m;
+	Rune k;
+	Alt a[] = {
+		{ nil, &m, CHANRCV },
+		{ nil, nil, CHANRCV },
+		{ nil, &k, CHANRCV },
+		{ nil, nil, CHANEND },
+	};
+
+	qmode = 0;
+	ARGBEGIN{
+	case 'q':
+		qmode = 1;
+		break;
+	default:
+		usage();
+	}ARGEND;
+	plumbfd = plumbopen("send", OWRITE|OCEXEC);
+	if(plumbfd < 0)
+		sysfatal("plumbopen: %r");
+	re = regcomp("([.a-zA-Z0-9_/+\\-]+(:[0-9]+)?)");
+	getwd(pwd, sizeof pwd);
+	loadlines();
+	if(initdraw(nil, nil, "fm") < 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;
+	selbg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0xEFEFEFFF);
+	mfg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, 0x999999FF);
+	loff = 0;
+	lsel = 0;
+	eresize();
+	for(;;){
+		switch(alt(a)){
+		case Emouse:
+			emouse(&m);
+			break;
+		case Eresize:
+			if(getwindow(display, Refnone) < 0)
+				sysfatal("getwindow: %r");
+			eresize();
+			break;
+		case Ekeyboard:
+			ekeyboard(k);
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,8 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=grapple
+OFILES=grapple.$O
+HFILES=a.h
+
+</sys/src/cmd/mkone