Skip to content

Instantly share code, notes, and snippets.

@pmarreck
Last active July 26, 2025 16:28
Show Gist options
  • Save pmarreck/80ad97decadf42bfda26707b92d34c24 to your computer and use it in GitHub Desktop.
Save pmarreck/80ad97decadf42bfda26707b92d34c24 to your computer and use it in GitHub Desktop.
Visual Terminal Timer, a timer written in LuaJIT that slowly fills your terminal with blocks (and responds to terminal resize events)
local ffi = require("ffi")
local bit = require("bit")
ffi.cdef([[ struct winsize {
unsigned short ws_row;
unsigned short ws_col;
unsigned short ws_xpixel;
unsigned short ws_ypixel;
};
int ioctl(int fd, unsigned long request, ...);
typedef void (*sighandler_t)(int);
int signal(int signum, sighandler_t handler);
int usleep(unsigned int usec);
unsigned int sleep(unsigned int seconds);
]])
local TIOCGWINSZ = 0x5413
local TIOCGWINSZ_BSD = 0x40087468
local SIGWINCH = 28
local SIGINT = 2
local STDOUT_FILENO = 1
local STDERR_FILENO = 2
local terminal_resized = false
local interrupted = false
local Timer
do
local _class_0
local _base_0 = {
start = function(self)
self.is_running = true
self.start_time = os.time()
end,
stop = function(self)
self.is_running = false
end,
advance_to = function(self, elapsed_seconds)
self.elapsed_seconds = math.min(elapsed_seconds, self.total_seconds)
self.remaining_seconds = math.max(0, self.total_seconds - self.elapsed_seconds)
self.start_time = os.time() - self.elapsed_seconds
end,
update = function(self)
if self.is_running then
local current_time = os.time()
self.elapsed_seconds = current_time - self.start_time
self.remaining_seconds = math.max(0, self.total_seconds - self.elapsed_seconds)
if self.remaining_seconds == 0 then
self.is_running = false
return true
end
end
return false
end,
get_progress = function(self)
if self.total_seconds == 0 then
return 1.0
end
return self.elapsed_seconds / self.total_seconds
end,
is_finished = function(self)
return self.remaining_seconds <= 0
end
}
if _base_0.__index == nil then
_base_0.__index = _base_0
end
_class_0 = setmetatable({
__init = function(self, seconds)
self.total_seconds = seconds
self.remaining_seconds = seconds
self.elapsed_seconds = 0
self.start_time = os.time()
self.is_running = false
end,
__base = _base_0,
__name = "Timer"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({ }, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
Timer = _class_0
end
local Terminal
do
local _class_0
local _base_0 = {
try_ioctl_detection = function(self)
local winsize = ffi.new("struct winsize")
local ioctls = {
TIOCGWINSZ,
TIOCGWINSZ_BSD
}
local fds = {
STDOUT_FILENO,
STDERR_FILENO,
0
}
for _index_0 = 1, #ioctls do
local ioctl_val = ioctls[_index_0]
for _index_1 = 1, #fds do
local fd = fds[_index_1]
local result = ffi.C.ioctl(fd, ioctl_val, winsize)
if result == 0 and winsize.ws_row > 0 and winsize.ws_col > 0 then
return winsize.ws_row, winsize.ws_col, "ioctl(" .. tostring(fd) .. ", " .. tostring(string.format('0x%x', ioctl_val)) .. ")"
end
end
end
return nil, nil, "ioctl failed"
end,
try_tput_detection = function(self)
local success, result = pcall(function()
local rows_cmd = io.popen("tput lines 2>/dev/null")
if rows_cmd then
local rows_str = rows_cmd:read("*l")
rows_cmd:close()
local cols_cmd = io.popen("tput cols 2>/dev/null")
if cols_cmd then
local cols_str = cols_cmd:read("*l")
cols_cmd:close()
local rows = tonumber(rows_str)
local cols = tonumber(cols_str)
if rows and cols and rows > 0 and cols > 0 then
return rows, cols, "tput command"
end
end
end
end)
if success and result then
return result
end
return nil, nil, "tput command failed"
end,
get_dimensions = function(self)
if self.override_rows and self.override_cols then
return self.override_rows, self.override_cols
end
local rows, cols, method = self:try_ioctl_detection()
if rows and cols then
self.last_rows = rows
self.last_cols = cols
self.detection_method = method
return rows, cols
end
rows, cols, method = self:try_tput_detection()
if rows and cols then
self.last_rows = rows
self.last_cols = cols
self.detection_method = method
return rows, cols
end
rows = tonumber(os.getenv("LINES"))
cols = tonumber(os.getenv("COLUMNS"))
if rows and cols and rows > 0 and cols > 0 then
self.last_rows = rows
self.last_cols = cols
self.detection_method = "environment variables"
return rows, cols
end
rows = self.last_rows or 24
cols = self.last_cols or 80
if rows <= 0 then
rows = 24
end
if cols <= 0 then
cols = 80
end
self.last_rows = rows
self.last_cols = cols
self.detection_method = "defaults"
return rows, cols
end,
update_dimensions = function(self)
self.rows, self.cols = self:get_dimensions()
end,
clear_screen = function(self)
return io.write("\027[2J\027[H")
end,
move_cursor = function(self, row, col)
return io.write(string.format("\027[%d;%dH", row, col))
end,
hide_cursor = function(self)
return io.write("\027[?25l")
end,
show_cursor = function(self)
return io.write("\027[?25h")
end
}
if _base_0.__index == nil then
_base_0.__index = _base_0
end
_class_0 = setmetatable({
__init = function(self, override_rows, override_cols)
if override_rows == nil then
override_rows = nil
end
if override_cols == nil then
override_cols = nil
end
self.last_rows = 0
self.last_cols = 0
self.override_rows = override_rows
self.override_cols = override_cols
return self:update_dimensions()
end,
__base = _base_0,
__name = "Terminal"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({ }, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
Terminal = _class_0
end
local VisualDisplay
do
local _class_0
local _base_0 = {
update_dimensions = function(self)
self.terminal:update_dimensions()
self.display_rows = self.terminal.rows - 2
self.rows = self.display_rows
self.cols = self.terminal.cols
self.total_blocks = self.display_rows * self.cols
end,
blocks_for_progress = function(self, progress)
return math.floor(self.total_blocks * math.min(1.0, progress))
end,
render = function(self, progress, remaining_time, total_time)
local blocks_filled = self:blocks_for_progress(progress)
self.terminal:clear_screen()
self.terminal:hide_cursor()
local block_count = 0
for row = 1, self.display_rows do
self.terminal:move_cursor(row, 1)
for col = 1, self.cols do
block_count = block_count + 1
if block_count <= blocks_filled then
io.write(self.fill_char)
else
io.write(self.empty_char)
end
end
end
self.terminal:move_cursor(self.display_rows + 1, 1)
local progress_percent = math.floor(progress * 100)
local time_str = self:format_time(remaining_time)
local total_str = self:format_time(total_time)
local info_line = string.format("Progress: %d%% | Time: %s / %s | Press Ctrl+C to exit", progress_percent, time_str, total_str)
io.write(info_line)
return io.flush()
end,
format_time = function(self, seconds)
local hours = math.floor(seconds / 3600)
local minutes = math.floor((seconds % 3600) / 60)
local secs = seconds % 60
if hours > 0 then
return string.format("%d:%02d:%02d", hours, minutes, secs)
else
return string.format("%d:%02d", minutes, secs)
end
end
}
if _base_0.__index == nil then
_base_0.__index = _base_0
end
_class_0 = setmetatable({
__init = function(self, terminal)
self.terminal = terminal
self.fill_char = "█"
self.empty_char = "░"
return self:update_dimensions()
end,
__base = _base_0,
__name = "VisualDisplay"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({ }, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
VisualDisplay = _class_0
end
local SignalHandler
do
local _class_0
local _base_0 = {
setup_signal_handlers = function(self)
local winch_handler = ffi.cast("sighandler_t", function(signum)
terminal_resized = true
end)
local int_handler = ffi.cast("sighandler_t", function(signum)
interrupted = true
end)
local winch_result = ffi.C.signal(SIGWINCH, winch_handler)
local int_result = ffi.C.signal(SIGINT, int_handler)
return (winch_result ~= ffi.cast("sighandler_t", -1)) and (int_result ~= ffi.cast("sighandler_t", -1))
end
}
if _base_0.__index == nil then
_base_0.__index = _base_0
end
_class_0 = setmetatable({
__init = function(self)
return self:setup_signal_handlers()
end,
__base = _base_0,
__name = "SignalHandler"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({ }, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
SignalHandler = _class_0
end
local parse_time_preset
parse_time_preset = function(preset)
preset = preset:lower():gsub("%s+", "")
if preset:match("^%d+s$") then
local seconds = tonumber(preset:match("^(%d+)s$"))
return seconds
elseif preset:match("^%d+m$") then
local minutes = tonumber(preset:match("^(%d+)m$"))
return minutes * 60
elseif preset:match("^%d+h$") then
local hours = tonumber(preset:match("^(%d+)h$"))
return hours * 3600
elseif preset:match("^%d+$") then
return tonumber(preset) * 60
else
return error("Invalid time format: " .. tostring(preset) .. ". Use formats like '30s', '5m', '1h', etc.")
end
end
local VisualTimer
do
local _class_0
local _base_0 = {
play_completion_beeps = function(self)
if not self.options.silence then
for i = 1, 3 do
os.execute("tput bel 2>/dev/null")
if i < 3 then
ffi.C.usleep(1000000)
end
end
end
end,
setup_signal_handling = function(self) end,
run = function(self, time_preset)
local seconds = parse_time_preset(time_preset)
self.timer = Timer(seconds)
if not (self.options.single_frame or self.options.advance_time) then
print("Visual Timer starting for " .. tostring(self.display:format_time(seconds)))
print("Terminal: " .. tostring(self.terminal.rows) .. "x" .. tostring(self.terminal.cols) .. " (" .. tostring(self.display.display_rows) .. "x" .. tostring(self.display.cols) .. " display area)")
print("Detection method: " .. tostring(self.terminal.detection_method or 'unknown'))
print("Total blocks: " .. tostring(self.display.total_blocks))
print("Starting timer... (Press Ctrl+C to exit)")
end
self.timer:start()
if self.options.advance_time then
self.timer:advance_to(self.options.advance_time)
end
if not self.options.single_frame then
for i = 1, self.terminal.rows do
print("")
end
end
if self.options.single_frame then
local progress = self.timer:get_progress()
self.display:render(progress, self.timer.remaining_seconds, self.timer.total_seconds)
self.terminal:show_cursor()
return
end
while not self.timer:is_finished() do
if interrupted then
self.terminal:show_cursor()
self.terminal:move_cursor(self.terminal.rows, 1)
print("\n\nTimer interrupted. Goodbye!")
return
end
local completed = self.timer:update()
if terminal_resized then
terminal_resized = false
self.display:update_dimensions()
end
local progress = self.timer:get_progress()
self.display:render(progress, self.timer.remaining_seconds, self.timer.total_seconds)
if completed then
break
end
ffi.C.usleep(100000)
end
self.display:render(1.0, 0, self.timer.total_seconds)
self:play_completion_beeps()
self.terminal:move_cursor(self.terminal.rows, 1)
print("\n\n🎉 Timer completed! Press Enter to exit.")
self.terminal:show_cursor()
return io.read()
end
}
if _base_0.__index == nil then
_base_0.__index = _base_0
end
_class_0 = setmetatable({
__init = function(self, options)
if options == nil then
options = { }
end
self.options = options or { }
self.terminal = Terminal(self.options.rows, self.options.cols)
self.display = VisualDisplay(self.terminal)
self.signal_handler = SignalHandler()
self.timer = nil
return self:setup_signal_handling()
end,
__base = _base_0,
__name = "VisualTimer"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({ }, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
VisualTimer = _class_0
end
local parse_args
parse_args = function(args)
local options = { }
local time_preset = nil
local i = 1
while i <= #args do
local arg_val = args[i]
if arg_val == "--dimensions" or arg_val == "-d" then
if i + 1 <= #args then
local dims = args[i + 1]:match("^(%d+)x(%d+)$")
if dims then
local rows, cols = args[i + 1]:match("^(%d+)x(%d+)$")
options.rows = tonumber(rows)
options.cols = tonumber(cols)
i = i + 2
else
error("Invalid dimensions format. Use: 24x80")
end
else
error("--dimensions requires a value (e.g., 24x80)")
end
elseif arg_val == "--advance-time" or arg_val == "-a" then
if i + 1 <= #args then
options.advance_time = tonumber(args[i + 1])
i = i + 2
else
error("--advance-time requires a value (seconds)")
end
elseif arg_val == "--single-frame" then
options.single_frame = true
i = i + 1
elseif arg_val == "--silence" or arg_val == "-s" then
options.silence = true
i = i + 1
elseif arg_val == "--help" or arg_val == "-h" then
print("Visual Terminal Timer")
print("")
print("A visual countdown timer that fills your terminal with blocks as time progresses.")
print("Perfect for pomodoro sessions, cooking timers, or any timed activity.")
print("The display automatically adapts to your terminal size and redraws properly")
print("when the window is resized. Plays 3 beeps when the timer completes.")
print("")
print("Usage: vtt <time> [options]")
print("")
print("Time formats:")
print(" 30s # 30 seconds")
print(" 5m # 5 minutes")
print(" 55m # 55 minutes")
print(" 1h # 1 hour")
print(" 2h # 2 hours")
print("")
print("Options:")
print(" --silence, -s Disable completion beeps")
print("")
print("Testing options:")
print(" --dimensions, -d ROWSxCOLS Override terminal dimensions (e.g., 24x80)")
print(" --advance-time, -a SECONDS Advance timer to specific time position")
print(" --single-frame Output single frame and exit (no loop)")
print(" --help, -h Show this help message")
print("")
print("Examples:")
print(" vtt 5m # 5-minute timer with beeps")
print(" vtt 30m --silence # 30-minute silent timer")
print(" vtt 25m # Pomodoro timer")
print("")
print("The timer shows:")
print(" • Filled blocks (█) for elapsed time")
print(" • Empty blocks (░) for remaining time")
print(" • Progress percentage and remaining time")
print(" • Terminal dimensions detected automatically")
os.exit(0)
elseif not time_preset then
if arg_val:match("^%-") then
error("Unknown option: " .. tostring(arg_val) .. ". Use --help for available options.")
else
time_preset = arg_val
i = i + 1
end
else
error("Unknown argument: " .. tostring(arg_val))
end
end
return time_preset, options
end
local main
main = function()
if #arg == 0 then
print("Visual Terminal Timer")
print("Usage: vtt <time> [options]")
print("Use --help for more information")
os.exit(2)
end
local success, err = pcall(function()
local time_preset, options = parse_args(arg)
if not time_preset then
error("Time preset is required")
end
local app = VisualTimer(options)
return app:run(time_preset)
end)
if not success then
print("Error: " .. tostring(err))
if err:match("Unknown option:") or err:match("Invalid time format:") or err:match("Time preset is required") or err:match("requires a value") then
return os.exit(2)
else
return os.exit(1)
end
end
end
if arg and arg[0] and arg[0]:match("vtt") then
return main()
end
@pmarreck
Copy link
Author

If the syntax seems a little off, it's because it was generated to Lua via YueScript. I should have a separate repo up for that soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment