Skip to content

Instantly share code, notes, and snippets.

@talyguryn
Created January 13, 2026 18:16
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 = 80
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment