util  Check-in [bf5f4fd9ca]

Overview
Comment:add first parvan revision
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: bf5f4fd9caf55cc1b83781b7d889d2944e29ad42e83428dbd58242a09d2bc876
User & Date: lexi on 2022-04-25 21:01:22
Other Links: manifest | tags
Context
2022-04-26
02:02
add syn check-in: 0f6a5bda23 user: lexi tags: trunk
2022-04-25
21:01
add first parvan revision check-in: bf5f4fd9ca user: lexi tags: trunk
2021-09-11
22:18
add rd-key check-in: 4e2b17fce2 user: lexi tags: trunk
Changes

Modified newtab.c from [c8163da01b] to [ef6aaa6ad1].

     1      1   /* [ʞ] newtab.c
     2      2    *  ~ lexi hale <lexi@hale.su>
     3      3    *  $ cc -Ofast newtab.c -onewtab \
     4         - *      [-D_default_qutebrowser_location=/...]
            4  + *      [-D_default_qutebrowser_location=/...] \
            5  + *      [-D_enable_vblank]
     5      6    *  $ ./newtab [example.net]
     6      7    *  © AGPLv3
     7      8    *  ? may god have mercy on my soul.
     8      9    *    i wrote this because qutebrowser, being a python
     9     10    *    abomination, takes an absurdly fucking long time
    10     11    *    to load, even when there's already an instance
    11     12    *    running and i just want a new goddamn tab. it
................................................................................
    90     91   	} ; {
    91     92   		/* fuck this fuck this fuck this fuck this */
    92     93   		char* end = stpncpy(srv.sun_path, run, sizeof srv.sun_path);
    93     94   		end = stpncpy(end, ssz("/qutebrowser/"));
    94     95   		DIR* qb = opendir(srv.sun_path);
    95     96   		if (!qb) return start_instance;
    96     97   		struct dirent* ent;
    97         -		while (ent = readdir(qb)) {
           98  +		while ((ent = readdir(qb))) {
    98     99   			if (ent == NULL) return start_instance;
    99    100   			if (strncmp(ent -> d_name, "ipc-", 4) == 0) break;
   100    101   		}
   101    102   		if (ent == NULL) return start_instance;
   102    103   		end = stpncpy(end, ent->d_name,
   103    104   				(sizeof srv.sun_path) - (end - srv.sun_path));
   104    105   		closedir(qb);
................................................................................
   134    135   	if (argc > 2) {
   135    136   		printf("\x1b[1musage:\x1b[m %s [uri]\n", argv[0]);
   136    137   	} else {
   137    138   		const char* uri = argc < 2 ? NULL : argv[1];
   138    139   		enum status st = transmit(uri);
   139    140   		if (st == start_instance) {
   140    141   			if (!fork()) {
          142  +#				ifndef _enable_vblank
          143  +					setenv("vblank_mode","0",0);
          144  +#				endif
   141    145   				execl(dupl(_default_qutebrowser_location), uri, NULL);
   142    146   				execl(dupl("/usr/local/bin/qutebrowser"), uri, NULL);
   143    147   				execlp(dupl("qutebrowser"), uri, NULL);
   144    148   				st = fail_find;
   145    149   			} else {
   146    150   				return start_instance;
   147    151   			}
   148    152   		}
   149    153   		if (errors[st] != NULL) printf("\x1b[1;31m(error)\x1b[m %s\n", errors[st]);
   150    154   		return st;
   151    155   	}
   152    156   }

Added parvan.lua version [760cd798bc].

            1  +-- [ʞ] parvan.lua
            2  +--  ? tool for maintaining and searching dictionaries
            3  +-- [ CONTROL CLASS: GREEN ]
            4  +-- [ CODEWORDS: - GENERAL ACCESS - ]
            5  +-- [ CONTROLLING AUTHORITY: - INTERDIRECTORIAL -
            6  +--  < Commission for Defense Communication >
            7  +--    +WCO   Worlds Culture Overdirectorate
            8  +--      SSD  Social Sciences Directorate
            9  +--       ELS External Linguistics Subdirectorate
           10  +--    +WSO   Worlds Security Overdirectorate
           11  +--      EID  External Influence Directorate ]
           12  +
           13  +local function implies(a,b) return a==b or not(a) end
           14  +
           15  +local ansi = {
           16  +	levels = {
           17  +		plain = 0;
           18  +		ansi = 1;
           19  +		color = 2;
           20  +		color8b = 3;
           21  +		color24b = 4;
           22  +	};
           23  +}
           24  +
           25  +ansi.seqs = {
           26  +	br = {ansi.levels.ansi, "[1m", "[21m"};
           27  +	hl = {ansi.levels.ansi, "[7m", "[27m"};
           28  +	ul = {ansi.levels.ansi, "[4m", "[24m"};
           29  +	em = {ansi.levels.ansi, "[3m", "[23m"};
           30  +};
           31  +
           32  +function ansi.termclass(fd) -- awkwardly emulate isatty
           33  +	if fd:seek('cur',0) then
           34  +		return ansi.levels.plain
           35  +	end
           36  +
           37  +	if os.getenv('COLORTERM') == 'truecolor' then
           38  +		return ansi.levels.color24b
           39  +	end
           40  +
           41  +	local term = os.getenv('TERM')
           42  +	if term then
           43  +		if term:match '-256color' then
           44  +			return ansi.levels.color8b
           45  +		elseif term:match '-color' then
           46  +			return ansi.levels.color
           47  +		else
           48  +			return ansi.levels.ansi
           49  +		end
           50  +	end
           51  +
           52  +	return ansi.levels.plain
           53  +end
           54  +
           55  +function ansi.formatter(fd)
           56  +	local cl = ansi.termclass(fd)
           57  +	local id = function(...) return ... end
           58  +	local esc = '\27'
           59  +	local f = {}
           60  +	for k,v in pairs(ansi.seqs) do
           61  +		local lvl, on, off = table.unpack(v)
           62  +		if lvl <= cl then
           63  +			f[k] = function(s)
           64  +				return esc..on .. s .. esc..off
           65  +			end
           66  +		else f[k] = id end
           67  +	end
           68  +	local function ftoi(r,g,b)
           69  +		return math.ceil(r*0xff),
           70  +		       math.ceil(g*0xff),
           71  +		       math.ceil(b*0xff)
           72  +	end
           73  +	local reset = "\27[39m"
           74  +	function f.color(str, n, br)
           75  +		return string.format("\27[%s%cm",
           76  +			(bg and 4 or 3) +
           77  +			(br and 6 or 0), 0x30+n)
           78  +			.. str .. reset
           79  +	end
           80  +	function f.resetLine()
           81  +		return '\27[1K\13'
           82  +	end
           83  +	if cl == ansi.levels.color24b then
           84  +		function f.rgb(str, r,g,b, bg)
           85  +			return string.format("\27[%c8;2;%u;%u;%um", bg and 0x34 or 0x33,
           86  +				ftoi(r,g,b)) .. str .. reset
           87  +		end
           88  +	elseif cl == ansi.levels.color8b then
           89  +		function f.rgb(str, r,g,b, bg)
           90  +			local code = 16 + (r * 5)*36 + (g * 5)*6 + (b * 6)
           91  +			return string.format("\27[%c8;5;%um", bg and 0x34 or 0x33, code)
           92  +				.. str .. reset
           93  +		end
           94  +	elseif cl == ansi.levels.color then
           95  +		function f.rgb(str, r,g,b, bg)
           96  +			local code = 0x30 + 1 -- TODO
           97  +			return string.format("\27[%c%cm", bg and 0x34 or 0x33, code)
           98  +				.. str .. reset
           99  +		end
          100  +	else
          101  +		function f.rgb(s) return s end
          102  +		function f.color(s) return s end
          103  +		function f.resetLine() return '' end
          104  +	end
          105  +	return f
          106  +end
          107  +
          108  +
          109  +local function dump(v,pfx,cyc,ismeta)
          110  +	pfx = pfx or ''
          111  +	cyc = cyc or {}
          112  +	local np = pfx .. '  '
          113  +
          114  +	if type(v) == 'table' then
          115  +		if cyc[v] then return '<...>' else cyc[v] = true end
          116  +	end
          117  +
          118  +	if type(v) == 'string' then
          119  +		return string.format('%q', v)
          120  +	elseif type(v) == 'table' then
          121  +		local str = ''
          122  +		for k,v in pairs(v) do
          123  +			local tkey, tval = dump(k,np,cyc), dump(v,np,cyc)
          124  +			str = str .. string.format('%s[%s] = %s\n', np, tkey,tval)
          125  +		end
          126  +		local meta = ''
          127  +		if getmetatable(v) then
          128  +			meta = dump(getmetatable(v),pfx,cyc,true) .. '::'
          129  +		end
          130  +		if ismeta then
          131  +			return string.format('%s<|\n%s%s|>',meta,str,pfx)
          132  +		else
          133  +			return meta..'{\n' .. str .. pfx .. '}\n'
          134  +		end
          135  +	else
          136  +		return string.format('%s', v)
          137  +	end
          138  +end
          139  +
          140  +local struct = {
          141  +	__call = function(s,...) return s:mk(...) end;
          142  +}
          143  +function struct:mk(s)
          144  +	function s.is(o) return getmetatable(o) == s end
          145  +	return setmetatable(s, self)
          146  +end
          147  +setmetatable(struct, struct)
          148  +
          149  +local stream = struct {
          150  +	__index = {
          151  +		next = function(self, f)
          152  +			local flds = {string.unpack('<'..f, self.data, self.index)}
          153  +			self.index = flds[#flds]
          154  +			flds[#flds] = nil
          155  +			return table.unpack(flds)
          156  +		end;
          157  +	};
          158  +	mk = function(self, str)
          159  +		return setmetatable({
          160  +			data = str;
          161  +			index = 1;
          162  +		}, self)
          163  +	end;
          164  +}
          165  +
          166  +local fmt = {}
          167  +
          168  +local userError = struct {
          169  +	__tostring = function(self) return self.msg end;
          170  +	mk = function(self, s) return setmetatable({msg=s},self) end;
          171  +}
          172  +
          173  +local function id10t(...)
          174  +	error(userError(string.format(...)),0)
          175  +end
          176  +
          177  +local packer,unpacker =
          178  +	function(f) return function(...) return string.pack  ("<"..f, ...) end end,
          179  +	function(f) return function( s ) return s:next       (f) end end
          180  +local qpack = function(f) return {
          181  +	encode = packer(f);
          182  +	decode = unpacker(f);
          183  +} end
          184  +
          185  +local parse, marshal
          186  +fmt.string = qpack "s4"
          187  +fmt.u8 = qpack "I1"
          188  +fmt.u16 = qpack "I2"
          189  +fmt.u24 = qpack "I3"
          190  +fmt.u32 = qpack "I4"
          191  +fmt.list = function(t,ty) ty = ty or fmt.u32
          192  +	return {
          193  +		encode = function(a)
          194  +			local vals = {marshal(ty, #a)}
          195  +			for i=1,#a do
          196  +				table.insert(vals, marshal(t, a[i]))
          197  +			end
          198  +			return table.concat(vals)
          199  +		end;
          200  +		decode = function(s)
          201  +			local n = parse(ty, s)
          202  +			local vals = {}
          203  +			for i=1,n do
          204  +				table.insert(vals, parse(t, s))
          205  +			end
          206  +			return vals
          207  +		end;
          208  +	}
          209  +end
          210  +
          211  +fmt.map = function(from,to,ity)
          212  +	local ent = fmt.list({
          213  +		{'key', from},
          214  +		{'val', to}
          215  +	}, ity)
          216  +	return {
          217  +		encode = function(a)
          218  +			local m = {}
          219  +			for k,v in pairs(a) do
          220  +				table.insert(m, {key=k, val=v})
          221  +			end
          222  +			return ent.encode(m)
          223  +		end;
          224  +		decode = function(s)
          225  +			local lst = ent.decode(s)
          226  +			local m = {}
          227  +			for _,p in pairs(lst) do m[p.key] = p.val end
          228  +			return m
          229  +		end;
          230  +	}
          231  +end
          232  +
          233  +fmt.form = {
          234  +	{'form', fmt.u16};
          235  +	{'text', fmt.string};
          236  +}
          237  +
          238  +fmt.note = {
          239  +	{'kind', fmt.string};
          240  +	{'paras', fmt.list(fmt.string)};
          241  +}
          242  +
          243  +fmt.meaning = {
          244  +	{'lit', fmt.string};
          245  +	{'notes', fmt.list(fmt.note,fmt.u8)};
          246  +}
          247  +
          248  +fmt.def = {
          249  +	{'part', fmt.u8};
          250  +	{'branch', fmt.list(fmt.string,fmt.u8)};
          251  +	{'means', fmt.list(fmt.meaning,fmt.u8)};
          252  +	{'forms', fmt.list(fmt.form,fmt.u16)};
          253  +}
          254  +
          255  +fmt.word = {
          256  +	{'defs', fmt.list(fmt.def,fmt.u8)};
          257  +}
          258  +
          259  +fmt.dictHeader = {
          260  +	{'lang', fmt.string};
          261  +	{'meta', fmt.string};
          262  +	{'partsOfSpeech', fmt.list(fmt.string,fmt.u16)};
          263  +}
          264  +
          265  +fmt.dict = {
          266  +	{'header', fmt.dictHeader};
          267  +	{'words', fmt.map(fmt.string,fmt.word)};
          268  +}
          269  +
          270  +function marshal(ty, val)
          271  +	if ty.encode then
          272  +		return ty.encode(val)
          273  +	end
          274  +	local ac = {}
          275  +
          276  +	for idx,fld in ipairs(ty) do
          277  +		local name, fty = table.unpack(fld)
          278  +		table.insert(ac, marshal(fty, assert(val[name])))
          279  +	end
          280  +
          281  +	return table.concat(ac)
          282  +end
          283  +
          284  +function parse(ty, stream)
          285  +	if ty.decode then
          286  +		return ty.decode(stream)
          287  +	end
          288  +
          289  +	local obj = {}
          290  +	for idx,fld in ipairs(ty) do
          291  +		local name, fty = table.unpack(fld)
          292  +		obj[name] = parse(fty, stream)
          293  +	end
          294  +	return obj
          295  +end
          296  +
          297  +local function
          298  +atomizer()
          299  +	local map = {}
          300  +	local i = 1
          301  +	return function(v)
          302  +		if map[v] then return map[v] else
          303  +			map[v] = i
          304  +			i=i+1
          305  +			return i-1
          306  +		end
          307  +	end, map
          308  +end
          309  +
          310  +local function
          311  +writeDict(d)
          312  +	local atomizePoS, posMap = atomizer()
          313  +	for lit,w in pairs(d.words) do
          314  +		for j,def in ipairs(w.defs) do
          315  +			def.part = atomizePoS(def.part)
          316  +		end
          317  +	end
          318  +	d.header.partsOfSpeech = {}
          319  +	for v,i in pairs(posMap) do
          320  +		d.header.partsOfSpeech[i] = v
          321  +	end
          322  +	return marshal(fmt.dict, d)
          323  +end
          324  +
          325  +local function
          326  +readDict(file)
          327  +	local d = parse(fmt.dict, stream(file))
          328  +	-- handle atoms
          329  +	for lit,w in pairs(d.words) do
          330  +		for j,def in ipairs(w.defs) do
          331  +			def.part = d.header.partsOfSpeech[def.part]
          332  +		end
          333  +	end
          334  +	return d
          335  +end
          336  +
          337  +local function strwords(str) -- here be dragons
          338  +	local wds = {}
          339  +	local w = {}
          340  +	local state, d, quo, dquo = 0,0
          341  +	local function flush(n)
          342  +		if next(w) then
          343  +			table.insert(wds, utf8.char(table.unpack(w)))
          344  +			w = {}
          345  +		end
          346  +		state = n
          347  +		quo = nil
          348  +		dquo = nil
          349  +		d = 0
          350  +	end
          351  +	local function isws(c)
          352  +		return c == 0x20 or c == 0x09 or c == 0x0a
          353  +	end
          354  +	for p,cp in utf8.codes(str) do
          355  +		if state == 0 then -- begin
          356  +			if not(isws(cp)) then
          357  +				if cp == 0x22 or cp == 0x27 then
          358  +					quo = cp
          359  +				elseif cp == 0x5b then -- boxquote
          360  +					 quo = 0x5d
          361  +					dquo = 0x5b
          362  +				elseif cp == 0x7b then -- curlquote
          363  +					 quo = 0x7d
          364  +					dquo = 0x7b
          365  +				elseif cp == 0x201c then -- fancyquote
          366  +					 quo = 0x201d
          367  +					dquo = 0x201c
          368  +				end
          369  +				if quo then
          370  +					state = 2
          371  +					d = 1
          372  +				elseif cp == 0x5c then -- escape
          373  +					state = 11
          374  +				else
          375  +					state = 1
          376  +					table.insert(w, cp)
          377  +				end
          378  +			end
          379  +		elseif state == 1 then -- word
          380  +			if isws(cp) then flush(0)
          381  +			elseif cp == 0x5c then state = 11 else
          382  +				table.insert(w,cp)
          383  +			end
          384  +		elseif state == 2 then -- (nested?) quote
          385  +			if cp == 0x5c then state = 12
          386  +			elseif cp == quo then
          387  +				d = d - 1
          388  +				if d == 0 then
          389  +					flush(0)
          390  +				else
          391  +					table.insert(w,cp)
          392  +				end
          393  +			else
          394  +				if cp == dquo then d = d + 1 end
          395  +				table.insert(w,cp)
          396  +			end
          397  +		elseif state == 11 or state == 12 then -- escape
          398  +			-- 12 = quote escape, 11 = raw escape
          399  +			if cp == 0x63 then --n
          400  +				table.insert(w,0x0a)
          401  +			else
          402  +				table.insert(w,cp)
          403  +			end
          404  +			state = state - 10
          405  +		end
          406  +	end
          407  +	flush()
          408  +	return wds
          409  +end
          410  +
          411  +local predicates
          412  +local function parsefilter(str)
          413  +	local f = strwords(str)
          414  +	if #f == 1 then return function(e) return predicates.lit.fn(e,f[1]) end end
          415  +	if not predicates[f[1]] then
          416  +		id10t('no such predicate %s',f[1])
          417  +	else
          418  +		local p = predicates[f[1]].fn
          419  +		return function(e)
          420  +			return p(e, table.unpack(f,2))
          421  +		end
          422  +	end
          423  +end
          424  +
          425  +do
          426  +	local function p_all(e,pred,...)
          427  +		if pred == nil then return true end
          428  +		pred = parsefilter(pred)
          429  +		if not pred(e) then return false end
          430  +		return p_all(e,...)
          431  +	end;
          432  +	local function p_any(e,pred,...)
          433  +		if pred == nil then return false end
          434  +		pred = parsefilter(pred)
          435  +		if pred(e) then return true end
          436  +		return p_any(e,...)
          437  +	end;
          438  +	local function p_none(e,pred,...)
          439  +		if pred == nil then return true end
          440  +		pred = parsefilter(pred)
          441  +		if pred(e) then return false end
          442  +		return p_none(e,...)
          443  +	end;
          444  +	local function p_some(e,count,pred,...)
          445  +		if count == 0 then return true end
          446  +		if pred == nil then return false end
          447  +		pred = parsefilter(pred)
          448  +		if pred(e) then
          449  +			count = count-1
          450  +		end
          451  +		return p_some(e,count,...)
          452  +	end;
          453  +
          454  +	local function prepScan(...)
          455  +		local map = {}
          456  +		local tgt = select('#',...)
          457  +		for _,v in pairs{...} do map[v] = true end
          458  +		return map,tgt
          459  +	end
          460  +	predicates = {
          461  +		all = {
          462  +			fn = p_all;
          463  +			syntax = '<pred>…';
          464  +			help = 'every sub-<pred> matches'
          465  +		};
          466  +		any = {
          467  +			fn = p_any;
          468  +			syntax = '<pred>…';
          469  +			help = 'any sub-<pred> matches'
          470  +		};
          471  +		none = {
          472  +			fn = p_none;
          473  +			syntax = '<pred>…';
          474  +			help = 'no sub-<pred> matches'
          475  +		};
          476  +		some = {
          477  +			fn = p_some;
          478  +			syntax = '<count> <pred>…';
          479  +			help = '<count> or more sub-<pred>s match'
          480  +		};
          481  +		def = {
          482  +			help = 'word has at least one definition that contains all <keyword>s';
          483  +			syntax = '<keyword>…';
          484  +			fn = function(e,...)
          485  +				local kw = {...}
          486  +				for i,d in ipairs(e.word.defs) do
          487  +					for j,m in ipairs(d.means) do
          488  +						for k,n in ipairs(kw) do
          489  +							if not string.find(m.lit, n, 1, true) then
          490  +								goto notfound
          491  +							end
          492  +						end
          493  +						do return true end
          494  +						::notfound::
          495  +					end
          496  +				end
          497  +				return false
          498  +			end;
          499  +		};
          500  +		lit = {
          501  +			help = 'word is, begins with, or ends with <word>';
          502  +			syntax = '<word> [(pfx|sfx)]';
          503  +			fn = function(e,val,op)
          504  +				if not op then
          505  +					return e.lit == val
          506  +				elseif op == 'pfx' then
          507  +					return val == string.sub(e.lit,1,#val)
          508  +				elseif op == 'sfx' then
          509  +					return val == string.sub(e.lit,(#e.lit) - #val + 1)
          510  +				else
          511  +					id10t('[lit %s %s] is not a valid filter, “%s” should be either “pfx” or “sfx”',val,op,op)
          512  +				end
          513  +			end;
          514  +		};
          515  +		form = {
          516  +			help = 'match against word\'s inflected forms';
          517  +			syntax = '(<inflect> | <form> (set | is <inflect> | pfx <prefix> | sfx <suffix>))';
          518  +			fn = function(e, k, op, v)
          519  +			end;
          520  +		};
          521  +		part = {
          522  +			help = 'word has definitions for every <part> of speech';
          523  +			syntax = '<part>…';
          524  +			fn = function(e,...)
          525  +				local map, tgt = prepScan(...)
          526  +				local matches = 0
          527  +				for i,d in ipairs(e.word.defs) do
          528  +					if map[d.part] then matches = matches + 1 end
          529  +				end
          530  +				return matches == tgt
          531  +			end
          532  +		};
          533  +		root = {
          534  +			help = 'match a word that derives from every <word>';
          535  +			syntax = '<word>…';
          536  +			fn = function(e,...)
          537  +				local map, tgt = prepScan(...)
          538  +				for i,d in ipairs(e.word.defs) do
          539  +					local matches = 0
          540  +					for j,r in ipairs(d.branch) do
          541  +						if map[r] then matches = matches + 1 end
          542  +					end
          543  +					if matches == tgt then return true end
          544  +				end
          545  +			end
          546  +		};
          547  +	}
          548  +end
          549  +
          550  +local function
          551  +safeopen(file,...)
          552  +	if type(file) == 'string' then
          553  +		local fd = io.open(file,...)
          554  +		if not fd then error(userError("cannot open file " .. file),2) end
          555  +		return fd
          556  +	else
          557  +		return file
          558  +	end
          559  +end
          560  +
          561  +local function
          562  +safeNavWord(ctx, word, dn, mn, nn)
          563  +	local w = ctx.dict.words[word]
          564  +	if not w then id10t 'bad word' end
          565  +	if dn == nil then return w end
          566  +
          567  +	local d = w.defs[tonumber(dn)]
          568  +	if not d then id10t('no definition #%u',dn) end
          569  +	if mn == nil then return w,d end
          570  +
          571  +	local m = d.means[tonumber(mn)]
          572  +	if not m then id10t('no meaning #%u',mn) end
          573  +	if nn == nil then return w,d,m end
          574  +
          575  +	local n = m.notes[tonumber(nn)]
          576  +	if not n then id10t('no note #%u',nn) end
          577  +	return w,d,m,n
          578  +end
          579  +
          580  +local function copy(tab)
          581  +	local new = {}
          582  +	for k,v in pairs(tab) do new[k] = v end
          583  +	return new
          584  +end
          585  +
          586  +local function parsePath(p)
          587  +	local w,dn,mn,nn = p:match('^(.+)@([0-9]+)/([0-9]+):([0-9]+)$')
          588  +	if not w then w,dn,mn = p:match('^(.+)@([0-9]+)/([0-9]+)$') end
          589  +	if not w then w,dn = p:match('^(.+)@([0-9]+)$') end
          590  +	if not w then w=p:match('^(.-)%.?$') end
          591  +	return {w = w, dn = tonumber(dn), mn = tonumber(mn), nn = tonumber(nn)}
          592  +end
          593  +
          594  +local cmds = {
          595  +	create = {
          596  +		help = "initialize a new dictionary file";
          597  +		syntax = "<lang>";
          598  +		raw = true;
          599  +		exec = function(ctx, lang)
          600  +			if not lang then
          601  +				id10t 'for what language?'
          602  +			end
          603  +			local fd = safeopen(ctx.file,"wb")
          604  +			local new = {
          605  +				header = {
          606  +					lang = lang;
          607  +					meta = "";
          608  +					partsOfSpeech = {};
          609  +					branch = {};
          610  +				};
          611  +				words = {};
          612  +			}
          613  +			local o = writeDict(new);
          614  +			fd:write(o)
          615  +			fd:close()
          616  +		end;
          617  +	};
          618  +	coin = {
          619  +		help = "add a new word";
          620  +		syntax = "<word>";
          621  +		write = true;
          622  +		exec = function(ctx,word)
          623  +			if ctx.dict.words[word] then
          624  +				id10t "word already coined"
          625  +			end
          626  +			ctx.dict.words[word] = {defs={}}
          627  +		end;
          628  +	};
          629  +	def = {
          630  +		help = "define a word";
          631  +		syntax = "<word> <part-of-speech> [<meaning> [<root>…]]";
          632  +		write = true;
          633  +		exec = function(ctx,word,part,means,...)
          634  +			local etym = {...}
          635  +			if (not word) or not part then
          636  +				id10t 'bad definition'
          637  +			end
          638  +			if not ctx.dict.words[word] then
          639  +				ctx.dict.words[word] = {defs={}}
          640  +			end
          641  +			local n = #(ctx.dict.words[word].defs)+1
          642  +			ctx.dict.words[word].defs[n] = {
          643  +				part = part;
          644  +				branch = etym;
          645  +				means = {means and {lit=means,notes={}} or nil};
          646  +				forms = {};
          647  +			}
          648  +			ctx.log('info', string.format('added definition #%u to “%s”', n, word))
          649  +		end;
          650  +	};
          651  +	mean = {
          652  +		help = "add a meaning to a definition";
          653  +		syntax = "<word> <def#> <meaning>";
          654  +		write = true;
          655  +		exec = function(ctx,word,dn,m)
          656  +			local _,d = safeNavWord(ctx,word,dn)
          657  +			table.insert(d.means, {lit=m,notes={}})
          658  +		end;
          659  +	};
          660  +	mod = {
          661  +		help = "move, merge, split, or delete words or definitions";
          662  +		syntax = {
          663  +			"<path> (drop | [move|merge|clobber] <path> | out [<part> [<root>…]])";
          664  +			"path ::= <word>[(@<def#>[/<meaning#>[:<note#>]]|.)]";
          665  +		};
          666  +		write = true;
          667  +	};
          668  +	note = {
          669  +		help = "add a note to a definition or a paragraph to a note";
          670  +		syntax = {"(<m-path> (add|for) <kind> | <m-path>:<note#>) <para>…";
          671  +			"m-path ::= <word>@<def#>/<meaning#>"};
          672  +		write = true;
          673  +		exec = function(ctx,path,...)
          674  +			local paras, mng
          675  +			local dest = parsePath(path)
          676  +			local _,_,m = safeNavWord(ctx,dest.w,dest.dn,dest.mn)
          677  +			if dest.nn then
          678  +				paras = {...}
          679  +			else
          680  +				local op, kind = ...
          681  +				paras = { select(3, ...) }
          682  +				if op == 'add' then
          683  +					dest.nn = #(m.notes) + 1
          684  +					m.notes[dest.nn] = {kind=kind, paras=paras}
          685  +					return
          686  +				elseif op == 'for' then
          687  +					for i,nn in ipairs(m.notes) do
          688  +						if nn.kind == kind then
          689  +							dest.nn = i break
          690  +						end
          691  +					end
          692  +					if not dest.nn then
          693  +						id10t('no note of kind %s in %s',kind,path)
          694  +					end
          695  +				end
          696  +			end
          697  +			local dpa = m.notes[dest.nn].paras
          698  +			local top = #dpa
          699  +			for i,p in ipairs(paras) do
          700  +				dpa[top+i] = p
          701  +			end
          702  +		end
          703  +	};
          704  +	shell = {
          705  +		help = "open an interactive prompt";
          706  +		raw = true;
          707  +	};
          708  +	help = {
          709  +		help = "show help";
          710  +		nofile = true;
          711  +		syntax = "[<command>]";
          712  +	};
          713  +	predicates = {
          714  +		help = "show available filter predicates";
          715  +		nofile = true;
          716  +		syntax = "[<predicate>]";
          717  +	};
          718  +	dump = {
          719  +		exec = function(ctx) print(dump(ctx.dict)) end
          720  +	};
          721  +	ls = {
          722  +		help = "list all words that meet any given <filter>";
          723  +		syntax = {"[<filter>…]";
          724  +			"filter ::= (<word>|<pred> <arg>…)";
          725  +			"arg    ::= (<atom>|'['(<string>|<pred> <arg>…)']')"};
          726  +   }
          727  +}
          728  +
          729  +function cmds.predicates.exec(ctx, pred)
          730  +	local list = predicates
          731  +	if pred then list = {predicates[pred]} end
          732  +	local f = ctx.sty[io.stderr]
          733  +	for k,p in pairs(predicates) do
          734  +		if p.help then
          735  +			io.stderr:write(
          736  +				f.br('  - ' ..
          737  +					f.rgb('[',.8,.3,1) ..
          738  +					k .. ' ' ..
          739  +					(f.color(p.syntax,5) or '…') ..
          740  +					f.rgb(']',.8,.3,1)) .. ': ' ..
          741  +				f.color(p.help,4,true) .. '\n')
          742  +		end
          743  +	end
          744  +end
          745  +
          746  +function cmds.ls.exec(ctx,...)
          747  +	local filter = nil
          748  +	local out = {}
          749  +	for i,f in ipairs{...} do
          750  +		local fn = parsefilter(f)
          751  +		local of = filter or function() return false end
          752  +		filter = function(e)
          753  +			return fn(e) or of(e)
          754  +		end
          755  +	end
          756  +	for lit,w in pairs(ctx.dict.words) do
          757  +		local e = {lit=lit, word=w}
          758  +		if filter == nil or filter(e) then
          759  +			table.insert(out, e)
          760  +		end
          761  +	end
          762  +	table.sort(out, function(a,b) return a.lit < b.lit end)
          763  +	local fo = ctx.sty[io.stdout]
          764  +	local function meanings(d,md,n)
          765  +		local start = md and 2 or 1
          766  +		local part = string.format('(%s)', d.part)
          767  +		local pad = md and string.rep(' ', #part) or ''
          768  +		local function note(n,insert)
          769  +			if not next(n.paras) then return end
          770  +			local pad = string.rep(' ',#(n.kind) + 9)
          771  +			insert('      ' .. fo.hl(' ' .. n.kind .. ' ') .. ' ' .. n.paras[1])
          772  +			for i=2,#n.paras do
          773  +				insert(pad..n.paras[2])
          774  +			end
          775  +		end
          776  +		local m = { (function()
          777  +			if d.means[1] then
          778  +				if md then return
          779  +					string.format("  %s 1. %s", fo.em(part), d.means[1].lit)
          780  +				end
          781  +			else return
          782  +				fo.em(string.format('  %s [empty definition #%u]', part,n))
          783  +			end
          784  +		end)() }
          785  +		for i=start,#d.means do local v = d.means[i]
          786  +			table.insert(m, string.format('  %s %u. %s', pad, i, v.lit))
          787  +			for j,n in ipairs(v.notes) do
          788  +				note(n, function(v) table.insert(m, v) end)
          789  +			end
          790  +		end
          791  +		return table.concat(m,'\n')
          792  +	end
          793  +	for i, w in ipairs(out) do
          794  +		local d = fo.ul(w.lit)
          795  +		if #w.word.defs == 1 then
          796  +			d=d .. ' ' .. fo.em('('..(w.word.defs[1].part)..')') ..'\n'
          797  +				.. meanings(w.word.defs[1],false,1)
          798  +		else
          799  +			for j, def in ipairs(w.word.defs) do
          800  +				d=d .. '\n' .. meanings(def,true,j)
          801  +			end
          802  +		end
          803  +		io.stdout:write(d..'\n')
          804  +	end
          805  +end
          806  +
          807  +function cmds.mod.exec(ctx, orig, oper, dest, ...)
          808  +	if (not orig) or not oper then
          809  +		id10t '`mod` requires at least an origin and an operation'
          810  +	end
          811  +	local op, dp = parsePath(orig)
          812  +	local w,d,m,n = safeNavWord(ctx, op.w,op.dn,op.mn,op.nn)
          813  +	if oper == 'drop' then
          814  +		if not d then
          815  +			ctx.dict.words[op.w] = nil
          816  +		elseif not m then
          817  +			table.remove(w.defs, op.dn)
          818  +		elseif not n then
          819  +			table.remove(d.means, op.mn)
          820  +		else
          821  +			table.remove(m.notes, op.nn)
          822  +		end
          823  +	elseif oper == 'out' then
          824  +		if n or not m then
          825  +			id10t '`mod out` must target a meaning'
          826  +		end
          827  +		if not dest then id10t '`mod out` requires at least a part of speech' end
          828  +		local newdef = {
          829  +			part = dest;
          830  +			branch = {...};
          831  +			forms = {};
          832  +			means = {m};
          833  +		}
          834  +		table.insert(w.defs,op.dn+1, newdef)
          835  +		table.remove(d.means,op.mn)
          836  +	elseif oper == 'move' or oper == 'merge' or oper == 'clobber' then
          837  +		if dest
          838  +			then dp = parsePath(dest)
          839  +			else id10t('`mod %s` requires a target',oper)
          840  +		end
          841  +		if n then
          842  +			if not dp.mn then
          843  +				id10t '`mod` on a note requires a note or meaning destination'
          844  +			end
          845  +			local _,_,dm = safeNavWord(ctx, dp.w,dp.dn,dp.mn)
          846  +			if dp.nn then
          847  +				if oper == 'move' then
          848  +					table.insert(dm.notes, dp.nn, n)
          849  +				elseif oper == 'merge' then
          850  +					local top = #(dm.notes[dp.nn].paras)
          851  +					for i, v in ipairs(n.paras) do
          852  +						dm.notes[dp.nn].paras[i+top] = v
          853  +					end
          854  +				elseif oper == 'clobber' then
          855  +					dm.notes[dp.nn] = n
          856  +				end
          857  +			else
          858  +				if oper ~= 'move' then
          859  +					id10t('`mod note %s` requires a note target', oper)
          860  +				end
          861  +				table.insert(dm.notes, n)
          862  +			end
          863  +			if oper == 'move' and dp.nn and dm == m and op.nn > dp.nn then
          864  +				table.remove(m.notes,op.nn+1)
          865  +			else
          866  +				table.remove(m.notes,op.nn)
          867  +			end
          868  +		elseif m then
          869  +			if not dp.dn then
          870  +				local newdef = {
          871  +					part = d.part;
          872  +					branch = copy(d.branch);
          873  +					forms = copy(d.forms);
          874  +					means = {m};
          875  +				}
          876  +				if ctx.dict.words[dp.w] then
          877  +					table.insert(ctx.dict.words[dp.w].defs, newdef)
          878  +				else
          879  +					ctx.dict.words[dp.w] = {
          880  +						defs = {newdef};
          881  +					}
          882  +				end
          883  +				table.remove(d.means,dp.mn)
          884  +			else
          885  +				local dw, dd = safeNavWord(ctx, dp.w, dp.dn)
          886  +				if dp.mn then
          887  +					if dd.means[dp.mn] and (oper == 'merge' or oper=='clobber') then
          888  +						if oper == 'merge' then
          889  +							dd.means[dp.mn] = dd.means[dp.mn] .. '; ' .. m
          890  +						elseif oper == 'clobber' then
          891  +							dd.means[dp.mn] = m
          892  +						end
          893  +					else
          894  +						if oper == clobber then dd.means = {} end
          895  +						table.insert(dd.means, dp.mn, m)
          896  +					end
          897  +				else
          898  +					table.insert(dd.means, m)
          899  +				end
          900  +				if oper == 'move' and dp.mn and dd.means == d.means and op.mn > dp.mn then
          901  +					table.remove(d.means,op.mn+1)
          902  +				else
          903  +					table.remove(d.means,op.mn)
          904  +				end
          905  +			end
          906  +		elseif d then
          907  +			local ddefs = safeNavWord(ctx, dp.w).defs
          908  +			if dp.dn then
          909  +				if oper == 'merge' then
          910  +					local top = #(ddefs[dp.dn].means)
          911  +					for i,om in ipairs(d.means) do
          912  +						ddefs[dp.dn].means[top+i] = om
          913  +					end
          914  +					for k,p in pairs(d.forms) do
          915  +						ddefs[dp.dn].forms[k] = p -- clobbers!
          916  +					end
          917  +				else
          918  +					table.insert(ddefs, dp.dn, d)
          919  +				end
          920  +			else
          921  +				table.insert(ddefs, d)
          922  +			end
          923  +			if oper == 'move' and dp.mn and w.defs == ddefs and op.mn > dp.mn then
          924  +				table.remove(w.defs,op.dn+1)
          925  +			else
          926  +				table.remove(w.defs,op.dn)
          927  +			end
          928  +		else
          929  +			if ctx.dict.words[dp.w] then
          930  +				if oper ~= 'merge' then
          931  +					id10t('the word “%s” already exists; use `merge` if you want to merge the words together', dp.w)
          932  +				end
          933  +				for i,def in ipairs(w.defs) do
          934  +					if dp.dn then
          935  +						table.insert(ctx.dict.words[dp.w].defs, dp.dn+i-1, def)
          936  +					else
          937  +						table.insert(ctx.dict.words[dp.w].defs, def)
          938  +					end
          939  +				end
          940  +			else
          941  +				ctx.dict.words[dp.w] = w
          942  +			end
          943  +			ctx.dict.words[op.w] = nil
          944  +		end
          945  +	end
          946  +end
          947  +
          948  +local function fileLegible(file)
          949  +	-- check if we can access the file
          950  +	local fd = io.open(file,"rb")
          951  +	local ret = false
          952  +	if fd then ret = true end
          953  +	fd:close()
          954  +	return ret
          955  +end
          956  +
          957  +local function map(fn,lst)
          958  +	local new = {}
          959  +	for k,v in pairs(lst) do
          960  +		local nv, nk = fn(v,k)
          961  +		new[nk or k] = nv
          962  +	end
          963  +	return new
          964  +end
          965  +local function mapD(fn,lst) --destructive
          966  +	-- WARNING: this will not work if nk names an existing key!
          967  +	for k,v in pairs(lst) do
          968  +		local nv, nk = fn(v,k)
          969  +		if nk == nil or k == nk then
          970  +			lst[k] = nv
          971  +		else
          972  +			lst[k] = nil
          973  +			lst[nk] = nv
          974  +		end
          975  +	end
          976  +	return lst
          977  +end
          978  +
          979  +local function
          980  +prompt(p,multiline)
          981  +	-- returns string if successful, nil if EOF, false if ^C
          982  +	io.stderr:write(p)
          983  +	local ok, res = pcall(function()
          984  +	                      return io.stdin:read(multiline and 'a' or 'l')
          985  +	                     end)
          986  +	if ok then return res end
          987  +	return false
          988  +end
          989  +
          990  +function cmds.shell.exec(ctx)
          991  +	if not fileLegible(ctx.file) then
          992  +		-- avoid accidentally creating a file without the
          993  +		-- proper document structure and metadata
          994  +		id10t("file %s must already exist and be at least readable", ctx.file)
          995  +	end
          996  +
          997  +	local fd, rw = io.open(ctx.file,"r+b"), true
          998  +	if not fd then -- not writable
          999  +		ctx.log('warn',string.format('file %s is not writable', ctx.file))
         1000  +		fd, rw = io.open(ctx.file, "rb"), false
         1001  +	end
         1002  +	ctx.fd = fd
         1003  +	ctx.dict = readDict(fd:read 'a')
         1004  +	fd:close()
         1005  +
         1006  +	local written = false
         1007  +	local fo = ctx.sty[io.stdout]
         1008  +	local fe = ctx.sty[io.stderr]
         1009  +	repeat
         1010  +		local cmd = prompt(fe.br(string.format('(parvan %s) ', ctx.file)))
         1011  +		if cmd == false then
         1012  +			io.stderr:write(fe.resetLine())
         1013  +			if written then
         1014  +				ctx.log('warn', 'abandoning changes!')
         1015  +			end
         1016  +			return 0
         1017  +		end
         1018  +		if cmd and cmd ~= '' then
         1019  +			local words = strwords(cmd)
         1020  +			if next(words) then
         1021  +				if words[1] == 'bail'    or
         1022  +				   words[1] == 'abandon' or
         1023  +				   words[1] == 'q!'     then
         1024  +					if written then
         1025  +						ctx.log('warn', 'abandoning changes!')
         1026  +					end
         1027  +					return 0
         1028  +				end
         1029  +				local c = cmds[words[1]]
         1030  +				if c then
         1031  +					if c.raw then
         1032  +						ctx.log('fatal', words[1] .. ' cannot be run from `shell`')
         1033  +					elseif not implies(c.write, rw) then
         1034  +						ctx.log('fatal', ctx.file .. ' is not writable')
         1035  +					else
         1036  +						local ok = ctx.try(c.exec, ctx, table.unpack(words,2))
         1037  +						if ok then written = written or c.write end
         1038  +					end
         1039  +				elseif cmd == 'save' or cmd == 'wq' then
         1040  +					if not written then
         1041  +						ctx.log('info', 'no changes to save')
         1042  +					end
         1043  +					cmd = nil
         1044  +				elseif cmd == 'quit' or cmd == 'q' then
         1045  +					if not written then cmd = nil else
         1046  +						ctx.log('fatal', 'dictionary has unsaved changes')
         1047  +					end
         1048  +				else
         1049  +					ctx.log('fatal', words[1] .. ' is not a command')
         1050  +				end
         1051  +			end
         1052  +		end
         1053  +	until cmd == nil
         1054  +
         1055  +	if written then
         1056  +		ctx.log('info', 'saving file')
         1057  +		local out = writeDict(ctx.dict)
         1058  +		local fd = io.open(ctx.file,'w+b')
         1059  +		fd:write(out)
         1060  +		fd:close()
         1061  +	end
         1062  +end
         1063  +
         1064  +local function
         1065  +showHelp(ctx,k,v)
         1066  +	if not v then
         1067  +		id10t 'no such command'
         1068  +	end
         1069  +
         1070  +	if v.help then
         1071  +		local fe = ctx.sty[io.stderr]
         1072  +		local defs, synt = ''
         1073  +		if type(v.syntax) == 'table' then
         1074  +			synt = v.syntax[1]
         1075  +			local pad = string.rep(' ', #k+5)
         1076  +			for i=2,#v.syntax do
         1077  +				defs = defs .. pad .. fe.color(v.syntax[i],5) .. '\n'
         1078  +			end
         1079  +		else synt = v.syntax end
         1080  +
         1081  +		io.stderr:write(string.format(
         1082  +			"  > %s %s\n" .. defs ..
         1083  +			"    %s\n",
         1084  +			fe.br(k), synt and fe.br(fe.color(synt,5)) or '',
         1085  +			fe.em(fe.color(v.help,4,true))))
         1086  +	end
         1087  +end
         1088  +
         1089  +function cmds.help.exec(ctx,cmd)
         1090  +	if cmd then
         1091  +		showHelp(ctx, cmd, cmds[cmd])
         1092  +	else
         1093  +		for cmd,c in pairs(cmds) do
         1094  +			showHelp(ctx, cmd, c)
         1095  +		end
         1096  +	end
         1097  +end
         1098  +
         1099  +local function
         1100  +usage(me,ctx)
         1101  +	local ln = 0
         1102  +	local ct = {}
         1103  +	local fe = ctx.sty[io.stderr]
         1104  +	io.stderr:write(string.format(fe.br"usage:".." %s <file> [<command> [args…]]\n",me))
         1105  +	--[[
         1106  +	for k,v in pairs(cmds) do
         1107  +		local n = 1 + utf8.len(k) + utf8.len(v.syntax)
         1108  +		ct[k] = n
         1109  +		if n > ln then ln = n end
         1110  +	end
         1111  +	for k,v in pairs(cmds) do
         1112  +		local pad = string.rep(" ", ln - ct[k] + 3)
         1113  +		io.stderr:write(string.format("   "..fe.br'%s %s'.."%s%s\n",
         1114  +			k, v.syntax, pad, v.help))
         1115  +	end]]
         1116  +	for k,v in pairs(cmds) do
         1117  +		showHelp(ctx,k,v)
         1118  +	end
         1119  +	return 64
         1120  +end
         1121  +
         1122  +local function
         1123  +dispatch(argv, ctx)
         1124  +	local ferr = ctx.sty[io.stderr]
         1125  +	local file, cmd = table.unpack(argv)
         1126  +	if cmd and cmds[cmd] then
         1127  +		local c,fd,dict = cmds[cmd]
         1128  +		if (not c.raw) and not c.nofile then
         1129  +			fd = safeopen(file, "rb")
         1130  +			dict = readDict(fd:read 'a')
         1131  +			fd:close()
         1132  +			-- lua io has no truncate method, so we must
         1133  +			-- rely on the clobbering behavior of the open()
         1134  +			-- call instead :(
         1135  +		end
         1136  +
         1137  +		cmds[cmd].exec({
         1138  +			sty = ctx.sty;
         1139  +			try = ctx.try;
         1140  +			log = ctx.log;
         1141  +
         1142  +			file = file;
         1143  +			fd = fd;
         1144  +			dict = dict;
         1145  +		}, table.unpack(argv,3))
         1146  +
         1147  +		if (not c.raw) and c.write then
         1148  +			local output = writeDict(dict)
         1149  +			-- writeDict should always be given a chance to
         1150  +			-- bail before the previous file is destroyed!!
         1151  +			-- you don't want one bug to wipe out your entire
         1152  +			-- dictionary in one fell swoop
         1153  +			fd = safeopen(file,'w+b')
         1154  +			fd:write(output)
         1155  +			fd:close()
         1156  +		end
         1157  +
         1158  +		return 0
         1159  +	else
         1160  +		return usage(argv[0], ctx)
         1161  +	end
         1162  +end
         1163  +
         1164  +
         1165  +local argv if arg
         1166  +	then argv = arg
         1167  +	else argv = {[0] = 'parvan', ...}
         1168  +end
         1169  +
         1170  +local sty = {
         1171  +	[io.stdout] = ansi.formatter(io.stdout);
         1172  +	[io.stderr] = ansi.formatter(io.stderr);
         1173  +};
         1174  +
         1175  +local function log(lvl, msg)
         1176  +	local colors = {fatal=1,warn=3,info=4,debug=2}
         1177  +	local ferr = sty[io.stderr]
         1178  +	io.stderr:write(string.format(
         1179  +		ferr.color(ferr.br("(%s)"),colors[lvl]).." %s\n", lvl, msg))
         1180  +end
         1181  +local function try(...)
         1182  +	-- a wrapper around pcall that produces a standard error
         1183  +	-- message format when an error occurs
         1184  +	local res = { pcall(...) }
         1185  +	if not res[1] then
         1186  +		log('fatal', res[2])
         1187  +	end
         1188  +	return table.unpack(res)
         1189  +end
         1190  +
         1191  +local function stacktrace(err)
         1192  +	return debug.traceback(err,3)
         1193  +end
         1194  +local ok, res = xpcall(dispatch, stacktrace, argv, {
         1195  +	try = try, sty = sty, log = log
         1196  +})
         1197  +
         1198  +if not ok then
         1199  +	log('fatal', res)
         1200  +	os.exit(1)
         1201  +end
         1202  +
         1203  +os.exit(res)

Modified readme.md from [855194b85a] to [b037845c63].

     8      8   * **safekill.c**: utility to help keep from accidentally killing important windows; compile with `cc -Ofast safekill.c -lX11 -lc -osafekill`
     9      9   * **newtab.c**: a "open a new tab if there's already an instance running or launch a new instance otherwise" utility for qutebrowser
    10     10   * **fabulist.scm**: a work-in-progress communal fiction server
    11     11   * **bgrd.c**: it’s… a long story. just read the header.
    12     12   * **mkpw.c** an extremely fast mass random password generator
    13     13   * **kpw**: an extremely simple, lightweight, secure password manager for POSIX OSes written in C. depends on libsodium for crypto primitives. compile with `make kpw`.
    14     14   * **rosshil.ml**: tool to convert between the various calendars of the [Spirals](https://ʞ.cc/fic/spirals/) setting
           15  +* **parvan.lua**: a script for creating and querying dictionaries, intended as a conlanging tool. no dependencies, just run it with `lua`