DorianH935

Untitled

Nov 16th, 2025
27
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 17.16 KB | None | 0 0
  1. -- LemonOS 3.0 -- Integrated client & UI with built-in DM system
  2. -- by ChatGPT (GPT-5)
  3. -- Features: integrated DMs (phone numbers s-xxxx-xxxx), registration, always-listening chat listener,
  4. -- beep on incoming messages, unread indicator, signal bars (async), persistent config.
  5.  
  6. -- ===== CONFIG PATHS & PROTOCOL =====
  7. local CONFIG_PATH = "/lemonos.cfg"
  8. local MESSAGES_PATH = "/lemonos_messages.db"
  9. local CHAT_PROTOCOL = "lemonos-net"
  10.  
  11. -- ===== FIRMWARE INFO =====
  12. local firmware = {
  13. device = "Lemonphone",
  14. version = "LemonOS 3.0"
  15. }
  16.  
  17. -- ===== DEFAULT SETTINGS =====
  18. local config = {
  19. username = "User",
  20. accentColor = colors.yellow,
  21. bgColor = colors.black,
  22. rednetSide = "back",
  23. serverNode = nil, -- preferred server node ID (can be nil; will auto-discover on register)
  24. phoneNumber = nil -- assigned phone number (persisted after register)
  25. }
  26.  
  27. -- ===== LOAD / SAVE CONFIG & MESSAGES =====
  28. local function saveConfig()
  29. local f = fs.open(CONFIG_PATH, "w")
  30. if not f then return false end
  31. f.write(textutils.serialize(config))
  32. f.close()
  33. return true
  34. end
  35.  
  36. local function loadConfig()
  37. if fs.exists(CONFIG_PATH) then
  38. local f = fs.open(CONFIG_PATH, "r")
  39. if f then
  40. local ok, t = pcall(textutils.unserialize, f.readAll())
  41. f.close()
  42. if ok and type(t) == "table" then
  43. for k,v in pairs(t) do config[k] = v end
  44. end
  45. end
  46. end
  47. end
  48. loadConfig()
  49.  
  50. -- optional persistent message store (very small)
  51. local function saveMessages(tbl)
  52. local f = fs.open(MESSAGES_PATH, "w")
  53. if not f then return false end
  54. f.write(textutils.serialize(tbl))
  55. f.close()
  56. return true
  57. end
  58.  
  59. local function loadMessages()
  60. if fs.exists(MESSAGES_PATH) then
  61. local f = fs.open(MESSAGES_PATH, "r")
  62. if f then
  63. local ok, t = pcall(textutils.unserialize, f.readAll())
  64. f.close()
  65. if ok and type(t) == "table" then return t end
  66. end
  67. end
  68. return {}
  69. end
  70.  
  71. -- ===== GLOBAL STATE =====
  72. local messages = loadMessages() -- { {from=phone, to=phone, text=..., ts=...}, ... }
  73. local unreadCount = 0
  74. local unreadIndicator = nil -- {from, text, expires}
  75. local signalStrength = 0 -- 0..3
  76. local myPhoneNumber = config.phoneNumber -- convenience local
  77. local clientMsgCounter = 0
  78.  
  79. -- ===== UTILITIES =====
  80. local function mkClientID()
  81. clientMsgCounter = clientMsgCounter + 1
  82. return tostring(os.getComputerID()) .. "-" .. tostring(os.time()) .. "-" .. tostring(clientMsgCounter)
  83. end
  84.  
  85. local function centerText(y, txt, clr)
  86. local w = term.getSize()
  87. local x = math.floor((w - #txt) / 2) + 1
  88. term.setCursorPos(x, y)
  89. if clr then term.setTextColor(clr) end
  90. term.write(txt)
  91. term.setTextColor(colors.white)
  92. end
  93.  
  94. local function beep()
  95. -- try speaker peripheral
  96. local ok, s = pcall(function() return peripheral.find("speaker") end)
  97. if ok and s and type(s.playSound) == "function" then
  98. pcall(function() s.playSound("note.harp", 1, 1) end)
  99. return
  100. end
  101. -- fallback: BEL character (may beep in some terminals)
  102. pcall(function() io.write("\7") end)
  103. end
  104.  
  105. -- ===== SIGNAL (ASYNC) =====
  106. local function distanceToBars(distance)
  107. if distance <= 5 then return 3
  108. elseif distance <= 20 then return 2
  109. else return 1 end
  110. end
  111.  
  112. local function updateSignal()
  113. while true do
  114. local strength = 0
  115. if rednet.isOpen(config.rednetSide) then
  116. local ids = rednet.lookup("lemonphone_ping") or {}
  117. local peers = {}
  118. for _, id in ipairs(ids) do
  119. -- try a non-fatal send to see if reachable quickly
  120. local ok = pcall(rednet.send, id, textutils.serialize({ ping=true }), CHAT_PROTOCOL)
  121. if ok then
  122. table.insert(peers, math.abs(os.getComputerID() - id))
  123. end
  124. end
  125. if #peers > 0 then
  126. table.sort(peers)
  127. strength = distanceToBars(peers[1])
  128. end
  129. end
  130. signalStrength = strength
  131. sleep(5)
  132. end
  133. end
  134.  
  135. local function drawSignalBarsInline()
  136. local barChar = "|"
  137. for i = 1, 3 do
  138. if i <= signalStrength then term.setTextColor(colors.lime) else term.setTextColor(colors.gray) end
  139. term.write(barChar)
  140. end
  141. term.setTextColor(colors.white)
  142. end
  143.  
  144. -- ===== TOP BAR DRAW =====
  145. local function drawTopBar()
  146. local w = term.getSize()
  147. -- draw full background of top line with accent color
  148. term.setCursorPos(1,1)
  149. term.setBackgroundColor(config.accentColor)
  150. term.setTextColor(colors.white)
  151. local blank = string.rep(" ", w)
  152. term.write(blank)
  153. -- left: signal + label
  154. term.setCursorPos(2,1)
  155. if rednet.isOpen(config.rednetSide) then
  156. drawSignalBarsInline()
  157. term.write(" Net")
  158. else
  159. term.setTextColor(colors.red)
  160. term.write("X NoNet")
  161. term.setTextColor(colors.white)
  162. end
  163. -- right: time/date
  164. local timeStr = textutils.formatTime(os.time(), true)
  165. local dateStr = os.date("%b %d")
  166. local clock = timeStr .. " | " .. dateStr
  167. term.setCursorPos(w - #clock - 1, 1)
  168. term.setTextColor(colors.white)
  169. term.write(clock)
  170. -- unread badge (top-right, just left of clock if any)
  171. if unreadCount > 0 then
  172. local badge = "[" .. tostring(unreadCount) .. "]"
  173. term.setCursorPos(w - #clock - #badge - 3, 1)
  174. term.setTextColor(colors.red)
  175. term.write(badge)
  176. term.setTextColor(colors.white)
  177. end
  178. term.setBackgroundColor(config.bgColor)
  179. term.setTextColor(colors.white)
  180. end
  181.  
  182. -- ===== CHAT: register, send, process incoming =====
  183. local function sendToNode(node, tbl)
  184. local ok, ser = pcall(textutils.serialize, tbl)
  185. if not ok then return false end
  186. rednet.send(node, ser, CHAT_PROTOCOL)
  187. return true
  188. end
  189.  
  190. -- Broadcast registration request and wait a short time for a server to respond
  191. local function registerWithAnyServer(serviceType)
  192. serviceType = tostring(serviceType or "2")
  193. local req = {
  194. msgType = "register_req",
  195. id = mkClientID(),
  196. serviceType = serviceType,
  197. clientInfo = { nodeID = os.getComputerID(), username = config.username },
  198. timestamp = os.time()
  199. }
  200. -- broadcast and wait
  201. local ok = pcall(rednet.broadcast, textutils.serialize(req), CHAT_PROTOCOL)
  202. if not ok then return false, "broadcast-failed" end
  203. local sender, raw = rednet.receive(CHAT_PROTOCOL, 5)
  204. if not sender or not raw then return false, "no-response" end
  205. local ok2, tbl = pcall(textutils.unserialize, raw)
  206. if not ok2 or type(tbl) ~= "table" then return false, "bad-response" end
  207. if tbl.msgType == "register_res" and tbl.inReplyTo == req.id and tbl.assigned then
  208. myPhoneNumber = tbl.assigned
  209. config.phoneNumber = tbl.assigned
  210. config.serverNode = sender
  211. saveConfig()
  212. return true
  213. end
  214. return false, "unexpected-response"
  215. end
  216.  
  217. -- direct register to configured server (if server node set)
  218. local function registerWithServerNode(node, serviceType)
  219. serviceType = tostring(serviceType or "2")
  220. local req = {
  221. msgType = "register_req",
  222. id = mkClientID(),
  223. serviceType = serviceType,
  224. clientInfo = { nodeID = os.getComputerID(), username = config.username },
  225. timestamp = os.time()
  226. }
  227. if not node then return false, "no-node" end
  228. local ok = sendToNode(tonumber(node), req)
  229. if not ok then return false, "send-failed" end
  230. local sender, raw = rednet.receive(CHAT_PROTOCOL, 5)
  231. if not sender or not raw then return false, "no-response" end
  232. local ok2, tbl = pcall(textutils.unserialize, raw)
  233. if ok2 and type(tbl) == "table" and tbl.msgType == "register_res" and tbl.inReplyTo == req.id then
  234. myPhoneNumber = tbl.assigned
  235. config.phoneNumber = tbl.assigned
  236. config.serverNode = node
  237. saveConfig()
  238. return true
  239. end
  240. return false, "bad-response"
  241. end
  242.  
  243. local function sendDM(toPhone, text)
  244. if not myPhoneNumber then return false, "not-registered" end
  245. local node = config.serverNode
  246. if not node then return false, "no-server" end
  247. local msg = {
  248. msgType = "dm",
  249. id = mkClientID(),
  250. fromPhone = myPhoneNumber,
  251. toPhone = toPhone,
  252. body = { text = tostring(text) },
  253. ttl = 8,
  254. path = {},
  255. timestamp = os.time()
  256. }
  257. -- send to server node
  258. return sendToNode(tonumber(node), msg)
  259. end
  260.  
  261. -- handle incoming chat messages (called from chatListener)
  262. local function handleIncomingChat(tbl, sender)
  263. if type(tbl) ~= "table" then return end
  264. if tbl.msgType == "dm" then
  265. if tbl.toPhone == myPhoneNumber then
  266. table.insert(messages, { from = tbl.fromPhone, to = tbl.toPhone, text = tbl.body and tbl.body.text or "", ts = tbl.timestamp or os.time() })
  267. saveMessages(messages)
  268. -- queue event for UI/main loop
  269. os.queueEvent("lemonos_new_message", tbl.fromPhone, tbl.body and tbl.body.text or "")
  270. end
  271. elseif tbl.msgType == "register_res" then
  272. -- could also be handled by register functions which wait for receive
  273. end
  274. end
  275.  
  276. -- chat listener (background)
  277. local function chatListener()
  278. while true do
  279. local sender, raw = rednet.receive(CHAT_PROTOCOL)
  280. if sender and raw then
  281. local ok, tbl = pcall(textutils.unserialize, raw)
  282. if ok and type(tbl) == "table" then
  283. handleIncomingChat(tbl, sender)
  284. end
  285. end
  286. end
  287. end
  288.  
  289. -- ===== UI: Menu, Settings, About & Chat UI =====
  290. local function drawMenu(selected)
  291. local apps = {
  292. {name="Messages", cmd="messages"},
  293. {name="Worm", cmd="worm"},
  294. {name="Falling Blocks", cmd="falling"},
  295. {name="Redirection", cmd="redirection"},
  296. {name="Settings", cmd="settings"},
  297. {name="About Lemonphone", cmd="about"},
  298. {name="Shutdown", cmd="shutdown"}
  299. }
  300. local startY = 4
  301. for i, app in ipairs(apps) do
  302. term.setCursorPos(4, startY + i - 1)
  303. if i == selected then
  304. term.setBackgroundColor(colors.gray)
  305. term.setTextColor(colors.black)
  306. else
  307. term.setBackgroundColor(config.bgColor)
  308. term.setTextColor(colors.white)
  309. end
  310. term.clearLine()
  311. term.setCursorPos(4, startY + i - 1)
  312. term.write(app.name)
  313. end
  314. term.setBackgroundColor(config.bgColor)
  315. term.setTextColor(colors.white)
  316. return apps
  317. end
  318.  
  319. local function openSettings()
  320. while true do
  321. term.setBackgroundColor(config.bgColor)
  322. term.clear()
  323. centerText(3, "Settings", config.accentColor)
  324. term.setCursorPos(2,5)
  325. term.write("1. Change Username")
  326. term.setCursorPos(2,6)
  327. term.write("2. Change Accent Color")
  328. term.setCursorPos(2,7)
  329. term.write("3. Set Server Node")
  330. term.setCursorPos(2,8)
  331. term.write("4. Register (auto-discover)")
  332. term.setCursorPos(2,9)
  333. term.write("5. Back")
  334.  
  335. local _, key = os.pullEvent("key")
  336. if key == keys.one then
  337. term.setCursorPos(2,11) term.clearLine() term.write("Enter new username: ")
  338. config.username = read()
  339. saveConfig()
  340. elseif key == keys.two then
  341. term.setCursorPos(2,11) term.clearLine() term.write("Color name: ")
  342. local c = read()
  343. if colors[c] then config.accentColor = colors[c] saveConfig() else term.setCursorPos(2,12) term.write("Invalid color") sleep(1) end
  344. elseif key == keys.three then
  345. term.setCursorPos(2,11) term.clearLine() term.write("Enter server node ID (or blank to clear): ")
  346. local v = read()
  347. if v == "" then config.serverNode = nil else config.serverNode = tonumber(v) end
  348. saveConfig()
  349. elseif key == keys.four then
  350. term.setCursorPos(2,11) term.clearLine() term.write("Registering (broadcast)...")
  351. local ok, err = registerWithAnyServer("2")
  352. term.setCursorPos(2,12)
  353. if ok then term.write("Registered as " .. tostring(myPhoneNumber)) else term.write("Failed: " .. tostring(err)) end
  354. sleep(1.5)
  355. elseif key == keys.five then
  356. return
  357. end
  358. end
  359. end
  360.  
  361. local function aboutPage()
  362. term.setBackgroundColor(config.bgColor)
  363. term.clear()
  364. centerText(3, "About Lemonphone", config.accentColor)
  365. term.setCursorPos(2,5) term.write("Device: " .. firmware.device)
  366. term.setCursorPos(2,6) term.write("Firmware: " .. firmware.version)
  367. term.setCursorPos(2,7) term.write("Phone: " .. tostring(myPhoneNumber or "<unregistered>"))
  368. term.setCursorPos(2,9) term.write("Press any key to return...")
  369. os.pullEvent("key")
  370. end
  371.  
  372. local function messagesUI()
  373. -- simple chat UI: list last 12 messages and an input to send DM
  374. while true do
  375. term.setBackgroundColor(config.bgColor)
  376. term.clear()
  377. centerText(2, "Messages", config.accentColor)
  378. local start = math.max(1, #messages - 11)
  379. local y = 4
  380. for i = start, #messages do
  381. local m = messages[i]
  382. term.setCursorPos(2, y)
  383. term.clearLine()
  384. term.setTextColor(colors.white)
  385. term.write(("[%s] %s: %s"):format(os.date("%H:%M", m.ts or os.time()), m.from, m.text))
  386. y = y + 1
  387. end
  388. term.setCursorPos(2, y+1)
  389. term.setTextColor(colors.gray)
  390. term.write("Send DM (phone s-xxxx-xxxx) or type 'back':")
  391. term.setTextColor(colors.white)
  392. term.setCursorPos(2, y+2)
  393. term.write("To: ")
  394. local to = read()
  395. if to == "back" then return end
  396. term.setCursorPos(2, y+3) term.clearLine() term.write("Message: ")
  397. local txt = read()
  398. if txt and #txt > 0 then
  399. local ok, err = sendDM(to, txt)
  400. term.setCursorPos(2, y+4)
  401. if ok then term.write("Sent!") else term.write("Failed: " .. tostring(err)) end
  402. sleep(1)
  403. end
  404. end
  405. end
  406.  
  407. -- ===== MAIN LOOP (handles UI and queued message events) =====
  408. local function mainLoop()
  409. local selected = 1
  410. local refreshRate = 1.0
  411. local timer = os.startTimer(refreshRate)
  412.  
  413. while true do
  414. -- draw screen
  415. term.setBackgroundColor(config.bgColor)
  416. term.clear()
  417. drawTopBar()
  418. centerText(3, (config.username or "User") .. "'s Lemonphone", config.accentColor)
  419. local apps = drawMenu(selected)
  420.  
  421. local event, a, b = os.pullEvent()
  422. if event == "timer" then
  423. if a == timer then
  424. timer = os.startTimer(refreshRate)
  425. -- refresh just top bar to update clock/unread
  426. drawTopBar()
  427. -- expire banner if needed
  428. if unreadIndicator and os.time() >= unreadIndicator.expires then unreadIndicator = nil unreadCount = math.max(0, unreadCount - 1) end
  429. end
  430. elseif event == "key" then
  431. local key = a
  432. if key == keys.up then selected = math.max(1, selected - 1)
  433. elseif key == keys.down then selected = math.min(7, selected + 1)
  434. elseif key == keys.enter then
  435. local appsList = apps
  436. local chosen = appsList[selected]
  437. if chosen.name == "Settings" then openSettings()
  438. elseif chosen.name == "About Lemonphone" then aboutPage()
  439. elseif chosen.name == "Messages" then
  440. -- if not registered, prompt to register
  441. if not myPhoneNumber then
  442. term.setCursorPos(2, 12) term.clearLine() term.write("You are not registered. Register now? (y/n): ")
  443. local c = read()
  444. if c == "y" then registerWithAnyServer("2") end
  445. end
  446. messagesUI()
  447. else
  448. -- run external apps by name (if present) else ignore
  449. term.setBackgroundColor(config.bgColor) term.clear()
  450. term.setCursorPos(1,1)
  451. -- we run shell.run on a filename: attempt to shell.run(chosen.cmd)
  452. pcall(shell.run, chosen.cmd)
  453. end
  454. end
  455. elseif event == "lemonos_new_message" then
  456. local fromPhone, text = a, b
  457. -- bump unread & banner, beep
  458. unreadCount = unreadCount + 1
  459. unreadIndicator = { from = fromPhone, text = text, expires = os.time() + 3 }
  460. beep()
  461. -- redraw top bar to show badge immediately
  462. drawTopBar()
  463. end
  464. end
  465. end
  466.  
  467. -- ===== START BACKGROUND TASKS AND UI =====
  468. -- ensure rednet open
  469. if peripheral.find("modem") then rednet.open(config.rednetSide) end
  470.  
  471. -- try to set myPhoneNumber from config
  472. myPhoneNumber = config.phoneNumber
  473.  
  474. -- run everything in parallel: updateSignal, chatListener, mainLoop
  475. parallel.waitForAny(updateSignal, chatListener, mainLoop)
  476.  
Advertisement
Add Comment
Please, Sign In to add comment