#!/usr/bin/env lua local json = require "cjson" local arrr = require "arrr" local shapeshift = require "shapeshift" local lumber = require("lumber") local log = lumber.new { level = lumber.levels.WARN; format = require "lumber.format.term"; filter = function(message) if type(message) == "string" then return message else return require("inspect")(message) end end } local default_config = { { notify = "zenity", minutes = 60 }; { notify = "notify", minutes = 180 }; } local function zulu_offset() local current = os.date("*t") current.isdst = false local zulu = os.date("!*t") --- @cast zulu -string --- @cast current -string return os.difftime(os.time(current), os.time(zulu)) end local config_file do local xdg_home = os.getenv("XDG_CONFIG_HOME") local home = os.getenv("HOME") if xdg_home then config_file = xdg_home .. "/task/tasknotif.lua" elseif home then config_file = home .. "/.config/task/tasknotif.lua" else config_file = "tasknotif.lua" end end local params do local parse = arrr { { "Configuration to use (default: "..config_file..")", "--config", "-c" }; { "Sets the log level", "--log", nil, true }; { "Ignore anything that's already due soon at program start", "--pre-check" }; } local validate = shapeshift.table { __extra = "keep"; precheck = shapeshift.default(false, shapeshift.is.boolean); } params = select(2, validate(parse(arg))) end if params.log then log.level = lumber.levels[string.upper(params.log)] end log:debug("Params", params) log:info "Starting taskwarrior notifier" --- @type table local config if params.config then config = assert(loadfile(params.config))() else local chunk = loadfile(config_file) config = chunk and chunk() or default_config end local done = {} for severity in ipairs(config) do done[severity] = {} end --- @param run boolean Whether there should be any notification at all local function check(run) -- Save handled tasks in the current loop so the same task doesn't get several notififations at once local handled = {} if run == nil then run = true end for severity, condition in ipairs(config) do log:debug(condition) local data = json.decode(io.popen("task export"):read("*a")) for _, task in ipairs(data) do if task.status == "pending" and task.due then do local d = {} d.year, d.month, d.day, d.hour, d.min, d.sec = task.due:match("(%d%d%d%d)(%d%d)(%d%d)T(%d%d)(%d%d)(%d%d)Z") d.sec = d.sec + zulu_offset() task.due = os.time(d) end -- If anything gets postponed outside of the warning time, -- remove it from the done list if done[severity][task.uuid] then if os.difftime(task.due, os.time()) > 60 * condition.minutes then done[severity][task.uuid] = nil end end if os.difftime(task.due, os.time()) < 60 * condition.minutes then if handled[task.uuid] then log:info("Skipping task "..task.uuid..": already handled in this loop") end if not done[severity][task.uuid] then done[severity][task.uuid] = task if run and not handled[task.uuid] then log:info("Notifying:", task.uuid, task.description) if condition.notify == "zenity" then os.execute(string.format("zenity --warning --title '%s' --text '%s'", "Task due soon", task.description)) elseif condition.notify == "notify" then os.execute("notify-send 'Task due soon' '"..task.description:gsub([[']], [['"'"']]).."'") else error("Unknown notification type: " .. tostring(condition.notify)) end handled[task.uuid] = task end end end end end end end if params.precheck then log:info "Doing initial pre-scan" check(false) end log:info "Starting scan loop" while true do check(true) if not os.execute("sleep 30") then log:info "Exiting taskwarrior notifier" os.exit() end end