EWICS

mc code

Oct 20th, 2025
12
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.84 KB | None | 0 0
  1. -- todo.lua — Simple TODO viewer for ComputerCraft: Tweaked
  2. -- Works on a connected monitor (preferred) or falls back to the computer screen.
  3. -- Keys: ↑/↓ scroll • SPACE toggle • a add • d delete • q quit
  4. -- On an advanced monitor: touch an item to toggle it.
  5.  
  6. local SAVE_FILE = "todos.txt"
  7.  
  8. -- ---------- Utils ----------
  9. local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end
  10.  
  11. local function loadTodos()
  12. local t = {}
  13. if not fs.exists(SAVE_FILE) then return t end
  14. local f = fs.open(SAVE_FILE, "r")
  15. if not f then return t end
  16. while true do
  17. local line = f.readLine()
  18. if not line then break end
  19. local done, text = line:match("^([01])\t(.*)$")
  20. if done then
  21. table.insert(t, { done = (done == "1"), text = text })
  22. else
  23. -- backward-compat: lines without flag are treated as not done
  24. table.insert(t, { done = false, text = line })
  25. end
  26. end
  27. f.close()
  28. return t
  29. end
  30.  
  31. local function saveTodos(t)
  32. local f = fs.open(SAVE_FILE, "w")
  33. for _, item in ipairs(t) do
  34. f.writeLine((item.done and "1" or "0") .. "\t" .. item.text)
  35. end
  36. f.close()
  37. end
  38.  
  39. local function wordWrap(str, width)
  40. if width <= 4 then return { str } end
  41. local lines, line = {}, ""
  42. for word in str:gmatch("%S+") do
  43. if #line == 0 then
  44. line = word
  45. elseif #line + 1 + #word <= width then
  46. line = line .. " " .. word
  47. else
  48. table.insert(lines, line)
  49. line = word
  50. end
  51. end
  52. if #line > 0 then table.insert(lines, line) end
  53. return lines
  54. end
  55.  
  56. -- ---------- Peripherals ----------
  57. local mon = peripheral.find("monitor")
  58. local out = mon or term
  59.  
  60. local function setupSurface()
  61. if mon then
  62. -- Max out resolution for tiny monitors like 4x4 blocks
  63. pcall(function() mon.setTextScale(0.5) end)
  64. mon.setBackgroundColor(colors.black)
  65. mon.setTextColor(colors.white)
  66. mon.clear()
  67. mon.setCursorPos(1,1)
  68. else
  69. term.setBackgroundColor(colors.black)
  70. term.setTextColor(colors.white)
  71. term.clear()
  72. term.setCursorPos(1,1)
  73. end
  74. end
  75.  
  76. local function size()
  77. return out.getSize()
  78. end
  79.  
  80. local function clear() out.clear() end
  81. local function setCursor(x,y) out.setCursorPos(x,y) end
  82. local function writeAt(x,y,text)
  83. out.setCursorPos(x,y)
  84. out.write(text)
  85. end
  86.  
  87. -- ---------- State ----------
  88. local todos = loadTodos()
  89. local scroll = 0
  90. local selectedIdx = 1
  91.  
  92. -- ---------- Render ----------
  93. local function render()
  94. local w,h = size()
  95. clear()
  96. -- Header
  97. local title = "TODO"
  98. local pad = math.max(0, math.floor((w - #title)/2))
  99. writeAt(1,1,(" "):rep(w))
  100. writeAt(pad+1, 1, title)
  101.  
  102. -- Help (bottom line)
  103. local help = "↑/↓ scroll a add d del ␣ toggle q quit"
  104. if #help > w then help = help:sub(1, w) end
  105. writeAt(1, h, (" "):rep(w))
  106. writeAt(1, h, help)
  107.  
  108. -- Items area
  109. local y = 2
  110. local areaH = math.max(0, h - 2)
  111. if areaH <= 0 then return end
  112.  
  113. -- Build wrapped lines for each todo
  114. local lines = {}
  115. for i, it in ipairs(todos) do
  116. local prefix = (it.done and "[x] " or "[ ] ")
  117. local avail = math.max(1, w - #prefix)
  118. local wrapped = wordWrap(it.text, avail)
  119. if #wrapped == 0 then wrapped = { "" } end
  120. for j, ln in ipairs(wrapped) do
  121. table.insert(lines, { idx = i, first = (j==1), text = prefix .. ln })
  122. prefix = (" "):rep(4) -- indent continuation lines
  123. end
  124. end
  125.  
  126. -- Clamp scroll
  127. local maxScroll = math.max(0, #lines - areaH)
  128. if scroll > maxScroll then scroll = maxScroll end
  129. if scroll < 0 then scroll = 0 end
  130.  
  131. -- Draw visible window
  132. for i = 1, areaH do
  133. local row = i + scroll
  134. writeAt(1, y + i - 1, (" "):rep(w))
  135. local L = lines[row]
  136. if L then
  137. -- Highlight currently selected item’s first line
  138. if L.idx == selectedIdx and L.first then
  139. if out.setBackgroundColor then
  140. out.setBackgroundColor(colors.gray)
  141. out.setTextColor(colors.black)
  142. writeAt(1, y + i - 1, L.text .. (" "):rep(math.max(0, w - #L.text)))
  143. out.setBackgroundColor(colors.black)
  144. out.setTextColor(colors.white)
  145. else
  146. writeAt(1, y + i - 1, ">" .. L.text:sub(1, math.max(0, w-1)))
  147. end
  148. else
  149. writeAt(1, y + i - 1, L.text:sub(1, w))
  150. end
  151. end
  152. end
  153. end
  154.  
  155. -- ---------- Actions ----------
  156. local function moveSelection(delta)
  157. if #todos == 0 then return end
  158. selectedIdx = math.min(#todos, math.max(1, selectedIdx + delta))
  159. end
  160.  
  161. local function ensureSelectedVisible()
  162. -- Find the first line index of selected item in the wrapped list
  163. local w,h = size()
  164. local areaH = math.max(0, h - 2)
  165. local prefixW = 4
  166. local lines = {}
  167. local targetTop = nil
  168. local targetBottom = nil
  169. for i, it in ipairs(todos) do
  170. local prefix = (it.done and "[x] " or "[ ] ")
  171. local avail = math.max(1, w - #prefix)
  172. local wrapped = wordWrap(it.text, avail)
  173. if #wrapped == 0 then wrapped = { "" } end
  174. for j, ln in ipairs(wrapped) do
  175. table.insert(lines, { idx = i, first = (j==1) })
  176. if i == selectedIdx and j == 1 then
  177. targetTop = #lines
  178. targetBottom = #lines + #wrapped - 1
  179. end
  180. end
  181. end
  182. if not targetTop then return end
  183. if scroll > targetTop - 2 then
  184. scroll = math.max(0, targetTop - 2)
  185. end
  186. if scroll + areaH < targetBottom then
  187. scroll = math.max(0, targetBottom - areaH)
  188. end
  189. end
  190.  
  191. local function addItem()
  192. term.setCursorBlink(true)
  193. if mon then
  194. -- prompt on the computer terminal
  195. term.redirect(term.native())
  196. term.clear()
  197. term.setCursorPos(1,1)
  198. print("Add TODO (leave empty to cancel):")
  199. local t = trim(read())
  200. if #t > 0 then table.insert(todos, { done = false, text = t }); saveTodos(todos) end
  201. term.setCursorBlink(false)
  202. else
  203. term.setCursorPos(1,1); term.clear()
  204. print("Add TODO (leave empty to cancel):")
  205. local t = trim(read())
  206. if #t > 0 then table.insert(todos, { done = false, text = t }); saveTodos(todos) end
  207. term.setCursorBlink(false)
  208. end
  209. end
  210.  
  211. local function deleteItem()
  212. if #todos == 0 then return end
  213. table.remove(todos, selectedIdx)
  214. if selectedIdx > #todos then selectedIdx = #todos end
  215. saveTodos(todos)
  216. end
  217.  
  218. local function toggleItem(idx)
  219. if #todos == 0 then return end
  220. idx = idx or selectedIdx
  221. if not todos[idx] then return end
  222. todos[idx].done = not todos[idx].done
  223. saveTodos(todos)
  224. end
  225.  
  226. -- Map a monitor touch y to an item idx
  227. local function touchToIndex(tx, ty)
  228. local w,h = size()
  229. local areaTop = 2
  230. local areaH = math.max(0, h - 2)
  231. if ty < areaTop or ty >= areaTop + areaH then return nil end
  232. local lineRow = ty - areaTop + 1 + scroll
  233.  
  234. -- Rebuild wrapped lines and find which idx sits on that row
  235. local lines = {}
  236. for i, it in ipairs(todos) do
  237. local prefix = (it.done and "[x] " or "[ ] ")
  238. local avail = math.max(1, w - #prefix)
  239. local wrapped = wordWrap(it.text, avail)
  240. if #wrapped == 0 then wrapped = { "" } end
  241. for j, _ in ipairs(wrapped) do
  242. table.insert(lines, { idx = i, first = (j==1) })
  243. end
  244. end
  245. local L = lines[lineRow]
  246. return L and L.idx or nil
  247. end
  248.  
  249. -- ---------- Main ----------
  250. setupSurface()
  251. render()
  252.  
  253. -- If empty, create a starter note so the UI isn't blank
  254. if #todos == 0 then
  255. table.insert(todos, { done = false, text = "Press 'a' to add items" })
  256. end
  257.  
  258. ensureSelectedVisible()
  259. render()
  260.  
  261. while true do
  262. local e, p1, p2, p3 = os.pullEvent()
  263. if e == "key" then
  264. local key = p1
  265. if key == keys.up then
  266. moveSelection(-1); ensureSelectedVisible(); render()
  267. elseif key == keys.down then
  268. moveSelection(1); ensureSelectedVisible(); render()
  269. elseif key == keys.pageUp then
  270. scroll = math.max(0, scroll - 5); ensureSelectedVisible(); render()
  271. elseif key == keys.pageDown then
  272. scroll = scroll + 5; ensureSelectedVisible(); render()
  273. elseif key == keys.q then
  274. break
  275. elseif key == keys.space then
  276. toggleItem(); render()
  277. elseif key == keys.a then
  278. addItem(); ensureSelectedVisible(); render()
  279. elseif key == keys.d then
  280. deleteItem(); ensureSelectedVisible(); render()
  281. end
  282. elseif e == "char" then
  283. if p1 == "q" then break
  284. elseif p1 == "a" then addItem(); ensureSelectedVisible(); render()
  285. elseif p1 == "d" then deleteItem(); ensureSelectedVisible(); render()
  286. elseif p1 == " " then toggleItem(); render()
  287. end
  288. elseif e == "monitor_touch" and mon then
  289. -- p1 = side, p2 = x, p3 = y
  290. local idx = touchToIndex(p2, p3)
  291. if idx then
  292. selectedIdx = idx
  293. toggleItem(idx)
  294. ensureSelectedVisible()
  295. render()
  296. end
  297. elseif e == "term_resize" then
  298. setupSurface(); ensureSelectedVisible(); render()
  299. end
  300. end
  301.  
  302. -- Clean exit
  303. clear()
  304. setCursor(1,1)
  305. if mon then
  306. print("todo.lua closed. Display cleared.")
  307. else
  308. print("todo.lua closed.")
  309. end
  310.  
Advertisement
Add Comment
Please, Sign In to add comment