commit b19e7c94c42e1faa6a9fd3381e26accc41735927
parent b79b706d70fe6025eae2df981f106eb9aa6d4aae
Author: Tomas Nemec <nemi@skaut.cz>
Date: Sun, 8 Aug 2021 17:08:58 +0200
feat: Make it public
Diffstat:
8 files changed, 436 insertions(+), 99 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,15 +1,13 @@
-BINDIR = /usr/local/bin
-LUAPATH = /usr/lib/lua/5.4
+PREFIX = /usr/local
+BINDIR = $(PREFIX)/bin
all:
@echo "Nothing to do, try \"make install\" instead."
install:
@install -v -d "$(BINDIR)/" && install -m 0755 -v "./trun.lua" "$(BINDIR)/trun"
- @install -v -d "$(BINDIR)/" && install -m 0755 -v "./trun_status.lua" "$(BINDIR)/trun_status"
uninstall: trun.lua trun_status.lua
@rm -vrf "$(BINDIR)/trun"
- @rm -vrf "$(BINDIR)/trun_status"
.PHONY: all install uninstall
diff --git a/README b/README
@@ -0,0 +1,91 @@
+# Track run
+TrackRun parse stdin of command and cache status to file.
+Status is taken from handler module via `handle` function.
+
+! Lots of thing are explained inside scripts so check them out.
+
+# Dependencies
+
+It is written in lua and used/tested on linux. I have no plan to make it work
+for windows. But besides that nothing else is really needed.
+- If you want to trun to better handle process signals you will want to install
+ luaposix via loarocks (luarocks install luaposix). So trun can cleanup files on sig{term,kill,int,hup}
+
+And for some specific task you would need other tools:
+- For sending command to neovim instances you will need neovim-remote
+ (https://github.com/mhinz/neovim-remote).
+
+# Folders
+Inside project you will find number of different files.
+- examples/ - handler examples
+- handlers/ - real life handlers i use. (contact me to add more)
+ - TODO
+- tools/ - other tools that uses trun to make life easier
+ - print_status.lua: i use it for show semaphor of cmd (green,orange,red)
+ - trun_to_neovim_quickfix.lua: plugin to fill qf-list from cmd
+
+# Install
+You can install trun with:
+make install
+
+Also uninstall via `make uninstall`. But take a look at Makefile and see what
+it does... It is straightforward.
+
+
+# Usage
+
+<cmd> 1> >(trun <handler> <name>)
+
+_explanation_
+`1>` - copy output of cmd to stdout
+`>(...)` - copy output of cmd to stdin of subprocess
+So if you don't need output in terminal you can simply `|` (pipe) to trun
+
+
+# Handler
+Handler is module that implements specific functions to catch some key
+moment of tracking. Every handler must implement 'handle' function that
+returns status for that line. It takes two arguments:
+1. `userdata` is table for storing data between hooks
+2. `line` is current line to get status from.
+
+Returned status can be anything you want to store. Everything is just written
+to status_file
+
+Handlers must have `lua` ext. and be inside specific folder. viz->#Config
+
+## Handle function
+handle function can return either table or string. When table is returned,
+every item will end up on new line in status_file so it can be easily used
+by other scripts.
+
+## Hooks
+There are these hook functions you can implement:
+- on_start(userdata): before first line is passed to handle function
+- on_update(userdata, line, status): when status change
+- on_end(userdata): when script end
+
+## Userdata
+userdata has pre filled `name` key with value of trun name
+
+# Config
+There are 2 ENV variables you may want to set:
+- TRUN_HANDLERS_DIR: directory where handlers are stored (default to $XDG_CONFIG_HOME/trun)
+- TRUN_STATUS_DIR: directory to store files with statuses (default to $XDG_CACHE_HOME/trun)
+
+
+# Example
+1. angular handler
+inside $TRUN_HANDLERS_DIR/angular.lua you have module for angular serving
+output. Inside it you have simple handling just for status if build is
+running, succeeded or has error.
+viz -> examples/handler_simple.lua for way to run neovim command
+
+2. You can run angular serving cmd and pipe output to trun like `<cmd> | trun angular my_app`.
+
+3. angular status file
+Inside $TRUN_STATUS_DIR is created `my_app.angular` with current status in it.
+
+# Contact - Bugs, Suggestions, Questions...
+
+Feel free to contact me via mail on: <nemi@skaut.cz>
diff --git a/examples/handler_simple.lua b/examples/handler_simple.lua
@@ -0,0 +1,45 @@
+--- Trun simple handler for just status of command
+--
+-- interface
+-- - handle(userdata, line): return status
+-- - (optional) on_start(userdata): call once before start reading
+-- - (optional) on_update(userdata, line, status): calls every time when status changed
+-- - (optional) on_end(userdata): calls once after reading end
+--
+local status_map = {['running'] = 0, ['success'] = 1, ['error'] = -1}
+
+-- send nvim_cmd to all neovim instances
+local nvim_cmd = function(nvim_cmd)
+ local servers = io.popen('nvr --serverlist')
+ for socket in servers:lines() do
+ local cmd = string.format('nvr --servername "%s" -cc "%s" --nostart -s &', socket, nvim_cmd)
+ print(cmd)
+ os.execute(cmd)
+ end
+ servers:close()
+end
+
+return {
+ -- handle(userdata, line): return status
+ handle = function(_, line)
+ if line:find('SUCCESS') then
+ return status_map.success
+ elseif line:find('RUNNING') then
+ return status_map.running
+ else
+ return status_map.error
+ end
+ end,
+
+ -- on_update(userdata, line, status): calls every time when status changed
+ on_update = function(data, _, status)
+
+ -- you can send linux notifications
+ os.execute(string.format('notify-send "trun status changed for %s" "%s"', data.name, status))
+
+ -- or you can run neovim command to trigger neotification.
+ -- You need neovim-remote (https://github.com/mhinz/neovim-remote) to make it work
+ nvim_cmd(string.format('lua require(\'notify\')(\'%s\', \'%s\')', data.name, 'error'))
+ -- make sure to use single quotes so vim can properly handle command.
+ end,
+}
diff --git a/examples/handler_temp_output_file.lua b/examples/handler_temp_output_file.lua
@@ -0,0 +1,66 @@
+-- Trun advanced handler for *track status* and *cache* last output betwen status
+-- changes in tmp_file. It saves path to that file in status_file on second line.
+--
+-- So for exmple, on error you can load this output to quickfix list
+-- inside vim.
+-- viz->tools/trun_to_neovim_quickfix.lua and examples/handler_simple.lua for
+-- examples of neovim-remote (to notify inside neovim or you can run Trunqf to
+-- load quickfix list on fly)
+--
+-- interface
+-- - handle(userdata, line): return status
+-- - (optional) on_start(userdata): call once before start reading
+-- - (optional) on_update(userdata, line, status): calls every time when status changed
+-- - (optional) on_end(userdata): calls once after reading end
+--
+local status_map = {['running'] = 0, ['success'] = 1, ['error'] = -1}
+
+local notify =
+ function(status) os.execute('notify-send "trun status changed" "' .. status .. '"') end
+
+local function get_status(line)
+ if line:find('SUCCESS') then
+ return status_map.success
+ elseif line:find('RUNNING') then
+ return status_map.running
+ else
+ return status_map.error
+ end
+end
+
+return {
+
+ -- on_start(userdata): call once before start reading
+ on_start = function(data)
+ -- re-create tmp_file and save it to data table.
+ data.tmpfile = '/tmp/' .. data.name .. '.trun'
+ os.execute('rm ' .. data.tmpfile .. ' 2>/dev/null')
+ end,
+
+ -- handle(userdata, line): return status
+ handle = function(line, data)
+ -- write each line to tmp_file
+ local tmpfile = io.open(data.tmpfile, 'a')
+ tmpfile:write(line, '\n')
+ tmpfile:close()
+ return {get_status(line), data.tmpfile}
+ end,
+
+ -- on_update(userdata, line, status): calls every time when status changed
+ on_update = function(_, status, data)
+ -- on success, clear tmp_file.
+ if status[1] == status_map.success then
+ io.open(data.tmpfile, 'w'):close()
+ end
+ notify()
+
+ -- viz->examples/handler_simple.lua for other examples wit neovim-remote
+ end,
+
+ -- on_end(userdata): calls once after reading end
+ on_end = function(data)
+ notify()
+ -- remove tmp_file
+ os.execute('rm ' .. data.tmpfile)
+ end,
+}
diff --git a/tools/print_status.lua b/tools/print_status.lua
@@ -0,0 +1,91 @@
+#!/usr/bin/env lua
+
+-- Usage: trun_status.lua [-] [<name>]
+--
+-- Return formatted string for all truns to use inside status bar of your WM.
+-- It depends on status codes to be:
+-- - `0` - running
+-- - `1` - success
+-- - `-1` - error
+--
+-- You can change colors. viz->#Config
+--
+-- example output: [%{F#ff0000}NAME%{F-} %{F#00ff00}OTHERNAME%{F-}]
+--
+-- To ignore formatting add `-`(single dash) argument. Mostly for debugging.
+-- - It will just print output of status_file
+--
+-- To get only single trun, add `name` as argument.
+--
+-- It does not print any errors. (TODO)
+--
+-- Config
+local trun_status = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CONFIG_HOME') .. '/trun'
+-- map status to foreground colors.
+local status_map = {[0] = 'ffa500', [1] = '00ff00', [-1] = 'ff0000'}
+---
+
+-- silent fail if dir not exists
+local status_dir = trun_status
+if not io.open(status_dir) then
+ return ''
+end
+
+local trun_name = arg[1]
+local raw -- single dash argument means - no formatting, just print
+if arg[1] == '-' then
+ raw = true
+ trun_name = arg[2]
+end
+
+-- get all status files
+local status_files = {}
+local list = io.popen('ls ' .. status_dir)
+for f in list:lines() do
+ table.insert(status_files, f)
+end
+
+-- format status_file to string
+local format = function(file, name)
+ local output
+ if not file then
+ table.insert(output, '')
+ else
+ if raw then
+ local status = file:read('*a')
+ output = status
+ else
+ local status = file:read('*n')
+ -- Edit this to your liking output
+ output = '%{F#' .. status_map[tonumber(status)] .. '} ' .. name:upper() .. ' %{F-}'
+ file:close()
+ end
+ end
+ return output
+end
+
+local result = {}
+-- For every file fill out results
+for _, status_file_name in pairs(status_files) do
+ local name, _ = status_file_name:match('(.*)%.(.*)')
+ if name then
+ local status_file_path = status_dir .. '/' .. status_file_name
+ local status_file = io.open(status_file_path, 'r')
+ if trun_name then
+ if name == trun_name then
+ table.insert(result, format(status_file, name))
+ end
+ else
+ table.insert(result, format(status_file, name))
+ end
+ end
+end
+
+-- print out results
+if #result > 0 then
+ if raw then
+ print(table.concat(result, '\n'))
+ else
+ print('[' .. table.concat(result, ',') .. ']')
+ end
+end
diff --git a/tools/trun_to_neovim_quickfix.lua b/tools/trun_to_neovim_quickfix.lua
@@ -0,0 +1,64 @@
+-- Add this to your nvim folder to `plugin/trun.lua`.
+--
+-- use `:Trunqf <name>` to fill quickfix list with lines after last
+-- success status. It has autocompletion for running truns, press <tab> to list
+-- those.
+--
+-- !important
+-- Handler needs to store the output inside file and path to that file must be
+-- added as second line to status file. viz->examples/handler_tmp_output_file.lua
+--
+-- returns list of running truns
+local complete = function()
+ local dir = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CACHE_HOME') .. '/trun'
+ local ls = io.popen('ls ' .. dir)
+ local truns = {}
+ for trun in ls:lines() do
+ local name = trun:match('(.*)%..*')
+ table.insert(truns, name)
+ end
+ return truns
+end
+
+-- add trun to quickfix list
+-- it needs to have its tempfile for output
+local to_qf = function(name)
+ if not name then
+ return
+ end
+ vim.fn.setqflist({}, 'r')
+ local handle = io.popen('trun_status - ' .. name)
+ local o = {}
+ for line in handle:lines() do
+ table.insert(o, line)
+ end
+ if #o == 0 then
+ print('No running trun for "' .. name .. '"')
+ return
+ end
+ -- edit this if path to tmpfile(errorfile) is on another line
+ local errorfile = o[2]
+ if errorfile then
+ local errfile = io.open(errorfile, 'r')
+ local lines = {}
+ for line in errfile:lines() do
+ table.insert(lines, line)
+ end
+ vim.fn.setqflist({}, ' ', {lines = lines})
+ vim.cmd [[ copen ]]
+ vim.cmd [[ normal G ]]
+ else
+ print('Trun for "' .. name .. '" does not have tmp file')
+ return
+ end
+end
+
+-- Make functions globally accessible
+_G.Trun = {complete = complete, qf = to_qf}
+
+vim.cmd [[
+fun! TrunComplete(A,L,P)
+ return v:lua.Trun.complete()
+endfun
+]]
+vim.cmd [[command! -nargs=1 -complete=customlist,TrunComplete Trunqf v:lua.Trun.qf("<args>")]]
diff --git a/trun.lua b/trun.lua
@@ -1,59 +1,106 @@
#!/usr/bin/env lua
--- <run cmd> | run_track <handler> <name>
-local handlerName = arg[1]
-if not handlerName then
- error('No handler provided!')
+-- Version: 1.0.0 ( 08.08.2021 )
+--
+-- Usage
+-- <cmd> 1> >(trun <handler> <name>)
+--
+-- # Track run
+-- TrackRun parse stdin of command and cache status to file.
+-- Status is taken from handler module via `handle` function.
+--
+-- # Handler
+-- Handler is module that implements specific functions to catch some key
+-- moment of tracking. Every handler must implement 'handle' function that
+-- returns status for that line. It takes two arguments:
+-- 1. `userdata` is table for storing data between hooks
+-- 2. `line` is current line to get status from.
+--
+-- Handlers must have `lua` ext. and be inside specific folder. viz->#Config
+--
+-- ## Handle function
+-- handle function can return either table or string. When table is returned,
+-- every item will end up on new line in status_file so it can be easily used
+-- by other scripts.
+--
+-- ## Hooks
+-- There are these hook functions you can implement:
+-- - on_start(userdata): before first line is passed to handle function
+-- - on_update(userdata, line, status): when status change
+-- - on_end(userdata): when script end
+--
+-- ## Userdata
+-- userdata has pre filled `name` key with value of trun name
+--
+-- # Config
+-- There are 2 ENV VAR you may want to set:
+-- - TRUN_HANDLERS_DIR: directory where handlers are stored (default to XDG_CONFIG_HOME/trun)
+local handlers_dir = os.getenv('TRUN_HANDLERS_DIR') or os.getenv('XDG_CONFIG_HOME') .. '/trun'
+-- - TRUN_STATUS_DIR: directory to store files with statuses (default to XDG_CACHE_HOME/trun)
+local status_dir = os.getenv('TRUN_STATUS_DIR') or os.getenv('XDG_CACHE_HOME') .. '/trun'
+--
+
+local handler_name = arg[1]
+if not handler_name then
+ io.write('No handler provided!\n')
+ os.exit(1)
end
local name = arg[2] or 'trun'
-local handlerPath = os.getenv('CONFIG') .. '/trun/?.lua'
-package.path = package.path .. ';' .. handlerPath
-local handler = require(handlerName)
+-- load handler
+local handler_path = string.format('%s/?.lua', handlers_dir)
+package.path = handler_path .. ';' .. package.path
+local handler = require(handler_name)
+if not handler then
+ io.write('handler "' .. handler_name .. '" not found!\n')
+ os.exit(1)
+end
local userdata = {name = name}
-
-- status dir
-local status_dir = os.getenv('TRUN_CACHE') or os.getenv('HOME') .. '/.cache/trun'
if not io.open(status_dir) then
os.execute('mkdir ' .. status_dir)
end
-- status file
-local status_file = status_dir .. '/' .. name .. '.' .. handlerName
+local status_file = status_dir .. '/' .. name .. '.' .. handler_name
local file = io.open(status_file, 'w')
if not file then
os.execute('touch ' .. status_file)
- file = io.open(status_file, 'w')
end
------------------------------------------------------------------------------------------------------------------------
--- handling signals
------------------------------------------------------------------------------------------------------------------------
-local signal = require 'posix.signal'
-
+-- cleanup removes status file and notify handlers.
local function cleanup()
os.execute('rm ' .. status_file)
- if handler.onEnd then
- handler.onEnd(userdata)
+ if handler.on_end then
+ handler.on_end(userdata)
end
end
-local function onExit(signum)
+-----------------------------------------------------------------------------------------------------------------------
+-- handling signals
+-----------------------------------------------------------------------------------------------------------------------
+
+local function on_exit(signum)
cleanup()
os.exit(signum)
end
-signal.signal(signal.SIGINT, onExit)
-signal.signal(signal.SIGTERM, onExit)
-signal.signal(signal.SIGKILL, onExit)
-signal.signal(signal.SIGHUP, onExit)
+local signal = require 'posix.signal'
+if signal then
+ signal.signal(signal.SIGINT, on_exit)
+ signal.signal(signal.SIGTERM, on_exit)
+ signal.signal(signal.SIGKILL, on_exit)
+ signal.signal(signal.SIGHUP, on_exit)
+else
+ io.write('For signal handle install luaposix (`luarocks install luaposix`).\n')
+end
-----------------------------------------------------------------------------------------------------------------------
local status
-local function updateFile(output)
+local function update_file(output)
file = io.open(status_file, 'w')
if type(output) == 'table' then
file:write(table.concat(output, '\n'))
@@ -63,21 +110,20 @@ local function updateFile(output)
file:close()
end
-if handler.onStart then
- handler.onStart(userdata)
+if handler.on_start then
+ handler.on_start(userdata)
end
local lastStatus
for line in io.lines() do
lastStatus = status
- status = handler.handle(line, userdata)
+ status = handler.handle(userdata, line)
if status and status ~= lastStatus then
- updateFile(status)
- if handler.onUpdate then
- handler.onUpdate(line, status, userdata)
+ update_file(status)
+ if handler.on_update then
+ handler.on_update(userdata, line, status)
end
end
end
--- cleanup
cleanup()
diff --git a/trun_status.lua b/trun_status.lua
@@ -1,64 +0,0 @@
-#!/usr/bin/env lua
-
-local status_dir = os.getenv('HOME') .. '/.cache/trun'
-if not io.open(status_dir) then
- return ''
-end
-
-local name = arg[1]
-local raw
-if arg[1] == '-' then
- raw = true
- name = arg[2]
-end
-
-local ds = {[0] = 'ffa500', [1] = '00ff00', [-1] = 'ff0000'}
-
-local files = {}
-local list = io.popen('ls ' .. status_dir)
-for f in list:lines() do
- table.insert(files, f)
-end
-
-local function format(file, fname)
- local result
- if not file then
- table.insert(result, '')
- else
- if raw then
- local status = file:read('*a')
- result = status
- else
- local status = file:read('*n')
- result = '%{F#' .. ds[tonumber(status)] .. '} ' .. fname:upper() .. ' %{F-}'
- file:close()
- end
- end
- return result
-end
-
-local result = {}
-for _, f in pairs(files) do
- local fname, _ = f:match('(.*)%.(.*)')
- if fname then
- if name then
- if fname == name then
- local status_file = status_dir .. '/' .. f
- local file = io.open(status_file, 'r')
- table.insert(result, format(file, fname))
- end
- else
- local status_file = status_dir .. '/' .. f
- local file = io.open(status_file, 'r')
- table.insert(result, format(file, fname))
- end
- end
-end
-
-if #result > 0 then
- if raw then
- print(table.concat(result, '\n'))
- else
- print('[' .. table.concat(result, ',') .. ']')
- end
-end