Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- todo.lua — Simple TODO viewer for ComputerCraft: Tweaked
- -- Works on a connected monitor (preferred) or falls back to the computer screen.
- -- Keys: ↑/↓ scroll • SPACE toggle • a add • d delete • q quit
- -- On an advanced monitor: touch an item to toggle it.
- local SAVE_FILE = "todos.txt"
- -- ---------- Utils ----------
- local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end
- local function loadTodos()
- local t = {}
- if not fs.exists(SAVE_FILE) then return t end
- local f = fs.open(SAVE_FILE, "r")
- if not f then return t end
- while true do
- local line = f.readLine()
- if not line then break end
- local done, text = line:match("^([01])\t(.*)$")
- if done then
- table.insert(t, { done = (done == "1"), text = text })
- else
- -- backward-compat: lines without flag are treated as not done
- table.insert(t, { done = false, text = line })
- end
- end
- f.close()
- return t
- end
- local function saveTodos(t)
- local f = fs.open(SAVE_FILE, "w")
- for _, item in ipairs(t) do
- f.writeLine((item.done and "1" or "0") .. "\t" .. item.text)
- end
- f.close()
- end
- local function wordWrap(str, width)
- if width <= 4 then return { str } end
- local lines, line = {}, ""
- for word in str:gmatch("%S+") do
- if #line == 0 then
- line = word
- elseif #line + 1 + #word <= width then
- line = line .. " " .. word
- else
- table.insert(lines, line)
- line = word
- end
- end
- if #line > 0 then table.insert(lines, line) end
- return lines
- end
- -- ---------- Peripherals ----------
- local mon = peripheral.find("monitor")
- local out = mon or term
- local function setupSurface()
- if mon then
- -- Max out resolution for tiny monitors like 4x4 blocks
- pcall(function() mon.setTextScale(0.5) end)
- mon.setBackgroundColor(colors.black)
- mon.setTextColor(colors.white)
- mon.clear()
- mon.setCursorPos(1,1)
- else
- term.setBackgroundColor(colors.black)
- term.setTextColor(colors.white)
- term.clear()
- term.setCursorPos(1,1)
- end
- end
- local function size()
- return out.getSize()
- end
- local function clear() out.clear() end
- local function setCursor(x,y) out.setCursorPos(x,y) end
- local function writeAt(x,y,text)
- out.setCursorPos(x,y)
- out.write(text)
- end
- -- ---------- State ----------
- local todos = loadTodos()
- local scroll = 0
- local selectedIdx = 1
- -- ---------- Render ----------
- local function render()
- local w,h = size()
- clear()
- -- Header
- local title = "TODO"
- local pad = math.max(0, math.floor((w - #title)/2))
- writeAt(1,1,(" "):rep(w))
- writeAt(pad+1, 1, title)
- -- Help (bottom line)
- local help = "↑/↓ scroll a add d del ␣ toggle q quit"
- if #help > w then help = help:sub(1, w) end
- writeAt(1, h, (" "):rep(w))
- writeAt(1, h, help)
- -- Items area
- local y = 2
- local areaH = math.max(0, h - 2)
- if areaH <= 0 then return end
- -- Build wrapped lines for each todo
- local lines = {}
- for i, it in ipairs(todos) do
- local prefix = (it.done and "[x] " or "[ ] ")
- local avail = math.max(1, w - #prefix)
- local wrapped = wordWrap(it.text, avail)
- if #wrapped == 0 then wrapped = { "" } end
- for j, ln in ipairs(wrapped) do
- table.insert(lines, { idx = i, first = (j==1), text = prefix .. ln })
- prefix = (" "):rep(4) -- indent continuation lines
- end
- end
- -- Clamp scroll
- local maxScroll = math.max(0, #lines - areaH)
- if scroll > maxScroll then scroll = maxScroll end
- if scroll < 0 then scroll = 0 end
- -- Draw visible window
- for i = 1, areaH do
- local row = i + scroll
- writeAt(1, y + i - 1, (" "):rep(w))
- local L = lines[row]
- if L then
- -- Highlight currently selected item’s first line
- if L.idx == selectedIdx and L.first then
- if out.setBackgroundColor then
- out.setBackgroundColor(colors.gray)
- out.setTextColor(colors.black)
- writeAt(1, y + i - 1, L.text .. (" "):rep(math.max(0, w - #L.text)))
- out.setBackgroundColor(colors.black)
- out.setTextColor(colors.white)
- else
- writeAt(1, y + i - 1, ">" .. L.text:sub(1, math.max(0, w-1)))
- end
- else
- writeAt(1, y + i - 1, L.text:sub(1, w))
- end
- end
- end
- end
- -- ---------- Actions ----------
- local function moveSelection(delta)
- if #todos == 0 then return end
- selectedIdx = math.min(#todos, math.max(1, selectedIdx + delta))
- end
- local function ensureSelectedVisible()
- -- Find the first line index of selected item in the wrapped list
- local w,h = size()
- local areaH = math.max(0, h - 2)
- local prefixW = 4
- local lines = {}
- local targetTop = nil
- local targetBottom = nil
- for i, it in ipairs(todos) do
- local prefix = (it.done and "[x] " or "[ ] ")
- local avail = math.max(1, w - #prefix)
- local wrapped = wordWrap(it.text, avail)
- if #wrapped == 0 then wrapped = { "" } end
- for j, ln in ipairs(wrapped) do
- table.insert(lines, { idx = i, first = (j==1) })
- if i == selectedIdx and j == 1 then
- targetTop = #lines
- targetBottom = #lines + #wrapped - 1
- end
- end
- end
- if not targetTop then return end
- if scroll > targetTop - 2 then
- scroll = math.max(0, targetTop - 2)
- end
- if scroll + areaH < targetBottom then
- scroll = math.max(0, targetBottom - areaH)
- end
- end
- local function addItem()
- term.setCursorBlink(true)
- if mon then
- -- prompt on the computer terminal
- term.redirect(term.native())
- term.clear()
- term.setCursorPos(1,1)
- print("Add TODO (leave empty to cancel):")
- local t = trim(read())
- if #t > 0 then table.insert(todos, { done = false, text = t }); saveTodos(todos) end
- term.setCursorBlink(false)
- else
- term.setCursorPos(1,1); term.clear()
- print("Add TODO (leave empty to cancel):")
- local t = trim(read())
- if #t > 0 then table.insert(todos, { done = false, text = t }); saveTodos(todos) end
- term.setCursorBlink(false)
- end
- end
- local function deleteItem()
- if #todos == 0 then return end
- table.remove(todos, selectedIdx)
- if selectedIdx > #todos then selectedIdx = #todos end
- saveTodos(todos)
- end
- local function toggleItem(idx)
- if #todos == 0 then return end
- idx = idx or selectedIdx
- if not todos[idx] then return end
- todos[idx].done = not todos[idx].done
- saveTodos(todos)
- end
- -- Map a monitor touch y to an item idx
- local function touchToIndex(tx, ty)
- local w,h = size()
- local areaTop = 2
- local areaH = math.max(0, h - 2)
- if ty < areaTop or ty >= areaTop + areaH then return nil end
- local lineRow = ty - areaTop + 1 + scroll
- -- Rebuild wrapped lines and find which idx sits on that row
- local lines = {}
- for i, it in ipairs(todos) do
- local prefix = (it.done and "[x] " or "[ ] ")
- local avail = math.max(1, w - #prefix)
- local wrapped = wordWrap(it.text, avail)
- if #wrapped == 0 then wrapped = { "" } end
- for j, _ in ipairs(wrapped) do
- table.insert(lines, { idx = i, first = (j==1) })
- end
- end
- local L = lines[lineRow]
- return L and L.idx or nil
- end
- -- ---------- Main ----------
- setupSurface()
- render()
- -- If empty, create a starter note so the UI isn't blank
- if #todos == 0 then
- table.insert(todos, { done = false, text = "Press 'a' to add items" })
- end
- ensureSelectedVisible()
- render()
- while true do
- local e, p1, p2, p3 = os.pullEvent()
- if e == "key" then
- local key = p1
- if key == keys.up then
- moveSelection(-1); ensureSelectedVisible(); render()
- elseif key == keys.down then
- moveSelection(1); ensureSelectedVisible(); render()
- elseif key == keys.pageUp then
- scroll = math.max(0, scroll - 5); ensureSelectedVisible(); render()
- elseif key == keys.pageDown then
- scroll = scroll + 5; ensureSelectedVisible(); render()
- elseif key == keys.q then
- break
- elseif key == keys.space then
- toggleItem(); render()
- elseif key == keys.a then
- addItem(); ensureSelectedVisible(); render()
- elseif key == keys.d then
- deleteItem(); ensureSelectedVisible(); render()
- end
- elseif e == "char" then
- if p1 == "q" then break
- elseif p1 == "a" then addItem(); ensureSelectedVisible(); render()
- elseif p1 == "d" then deleteItem(); ensureSelectedVisible(); render()
- elseif p1 == " " then toggleItem(); render()
- end
- elseif e == "monitor_touch" and mon then
- -- p1 = side, p2 = x, p3 = y
- local idx = touchToIndex(p2, p3)
- if idx then
- selectedIdx = idx
- toggleItem(idx)
- ensureSelectedVisible()
- render()
- end
- elseif e == "term_resize" then
- setupSurface(); ensureSelectedVisible(); render()
- end
- end
- -- Clean exit
- clear()
- setCursor(1,1)
- if mon then
- print("todo.lua closed. Display cleared.")
- else
- print("todo.lua closed.")
- end
Advertisement
Add Comment
Please, Sign In to add comment