Skip to content

Instantly share code, notes, and snippets.

@talyguryn
Last active March 4, 2026 08:49
Show Gist options
  • Select an option

  • Save talyguryn/25fdfb255e2caae91e5a19f1e558bce3 to your computer and use it in GitHub Desktop.

Select an option

Save talyguryn/25fdfb255e2caae91e5a19f1e558bce3 to your computer and use it in GitHub Desktop.
Hammerspoon config for Klipper menubar monitor
--------------------------------------------------
-- Klipper menubar monitor
--------------------------------------------------
local SERVER_IP = "192.168.31.76"
local MOONRAKER_PORT = 7125
local FLUIDD_PORT = 8080
local MOONRAKER_URL = "http://" .. SERVER_IP .. ":" .. MOONRAKER_PORT .. "/printer/objects/query?virtual_sdcard&print_stats"
local FLUIDD_URL = "http://" .. SERVER_IP .. ":" .. FLUIDD_PORT .. "/"
local REFRESH_SECONDS = 5
local REQUEST_TIMEOUT = 4
local STALE_AFTER = 15
local menubar = hs.menubar.new()
menubar:setTitle("…")
--------------------------------------------------
-- State
--------------------------------------------------
local requestInFlight = false
local lastSuccessTime = 0
local watchdogTimer = nil
--------------------------------------------------
-- Utils
--------------------------------------------------
local function secondsToHhMm(seconds)
local h = math.floor(seconds / 3600)
local m = math.floor((seconds % 3600) / 60)
return string.format("%dh %dm", h, m)
end
local function setDisconnected()
menubar:setTitle("Disconnected")
end
--------------------------------------------------
-- Core update function
--------------------------------------------------
local function updateKlipperStatus()
if requestInFlight then return end
requestInFlight = true
local requestFinished = false
-- Watchdog for hung requests
if watchdogTimer then watchdogTimer:stop() end
watchdogTimer = hs.timer.doAfter(REQUEST_TIMEOUT, function()
if not requestFinished then
requestInFlight = false
setDisconnected()
end
end)
hs.http.asyncGet(MOONRAKER_URL, nil, function(status, body)
requestFinished = true
requestInFlight = false
if watchdogTimer then watchdogTimer:stop() end
if status ~= 200 or not body then
setDisconnected()
return
end
local data = hs.json.decode(body)
if not data or not data.result or not data.result.status then
setDisconnected()
return
end
local ps = data.result.status.print_stats
local vsd = data.result.status.virtual_sdcard
lastSuccessTime = os.time()
if not ps or ps.state ~= "printing" or not vsd or not vsd.progress then
menubar:setTitle("Printer is idle")
return
end
local elapsed = ps.print_duration or 0
local progress = vsd.progress
if progress <= 0 then
menubar:setTitle("Printer is idle")
return
end
local total = elapsed / progress
local remaining = math.max(0, total - elapsed)
local progressInPercent = math.floor(progress * 100)
menubar:setTitle("Printing " .. progressInPercent .. "%" ..", " .. secondsToHhMm(remaining) .. " left" )
end)
end
--------------------------------------------------
-- Stale connection monitor
--------------------------------------------------
local function staleCheck()
if lastSuccessTime == 0 then return end
if os.difftime(os.time(), lastSuccessTime) > STALE_AFTER then
setDisconnected()
end
end
--------------------------------------------------
-- Menu
--------------------------------------------------
menubar:setMenu({
{ title = "Open dashboard", fn = function()
hs.urlevent.openURL(FLUIDD_URL)
end },
{ title = "Refresh", fn = updateKlipperStatus }
})
--------------------------------------------------
-- Timers (strong references)
--------------------------------------------------
klipperPollTimer = hs.timer.doEvery(REFRESH_SECONDS, updateKlipperStatus)
klipperStaleTimer = hs.timer.doEvery(REFRESH_SECONDS, staleCheck)
--------------------------------------------------
-- Initial run
--------------------------------------------------
updateKlipperStatus()
-- -----------------------------------------------------------------------
-- -- Arduino Window Control via Serial (UNO) using hs.task and auto-detect
-- -----------------------------------------------------------------------
-- local fixedPort = "/dev/cu.usbserial-1110"
-- local function findSerialPort()
-- return fixedPort
-- end
-- -----------------------------------------------------------------------
-- -- Команды macOS
-- -----------------------------------------------------------------------
-- local function handleCommand(cmd)
-- cmd = cmd:gsub("%s+", "")
-- if cmd == "CLOSE" then
-- hs.eventtap.keyStroke({"cmd"}, "w")
-- elseif cmd == "MINIMIZE" then
-- hs.eventtap.keyStroke({"cmd"}, "m")
-- elseif cmd == "FULLSCREEN" then
-- hs.eventtap.keyStroke({"ctrl", "cmd"}, "f")
-- end
-- end
-- -----------------------------------------------------------------------
-- -- Serial via hs.task (cat)
-- -----------------------------------------------------------------------
-- local serialTask = nil
-- local reconnectTimer = nil
-- local function startSerial(port)
-- if serialTask then
-- serialTask:terminate()
-- serialTask = nil
-- end
-- hs.printf("Opening serial: %s", port)
-- serialTask = hs.task.new(
-- "/bin/cat",
-- function(exitCode)
-- hs.printf("Serial disconnected. exit=%d", exitCode)
-- reconnectTimer:start()
-- end,
-- function(task, data)
-- handleCommand(data)
-- return true
-- end,
-- {port}
-- )
-- serialTask:start()
-- end
-- -----------------------------------------------------------------------
-- -- Автопоиск и автоподключение
-- -----------------------------------------------------------------------
-- reconnectTimer = hs.timer.new(2, function()
-- local port = findSerialPort()
-- if port then
-- hs.printf("Reconnected to %s", port)
-- reconnectTimer:stop()
-- startSerial(port)
-- else
-- hs.printf("Waiting for Arduino…")
-- end
-- end)
-- local function init()
-- hs.printf("Arduino window controller starting...")
-- local port = findSerialPort()
-- if port then
-- startSerial(port)
-- else
-- hs.printf("No device found. Waiting...")
-- reconnectTimer:start()
-- end
-- end
-- init()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment