SHOW:
|
|
- or go back to the newest paste.
| 1 | -- LemonOS: Pocket Shell for Lemonphone | |
| 2 | -- by ChatGPT (GPT-5) | |
| 3 | -- Features: Persistent settings, live clock, distance-based signal bars, auto-netchat install | |
| 4 | ||
| 5 | -- ===== CONFIG PATHS ===== | |
| 6 | local CONFIG_PATH = "/lemonos.cfg" | |
| 7 | local NETCHAT_PATH = "/netchat" | |
| 8 | local STARTUP_PATH = "/startup.lua" | |
| 9 | ||
| 10 | -- ===== FIRMWARE INFO ===== | |
| 11 | local firmware = {
| |
| 12 | device = "Lemonphone", | |
| 13 | version = "LemonOS 2.0" | |
| 14 | } | |
| 15 | ||
| 16 | -- ===== SETTINGS ===== | |
| 17 | local config = {
| |
| 18 | username = "User", | |
| 19 | accentColor = colors.yellow, | |
| 20 | bgColor = colors.black, | |
| 21 | rednetSide = "back" | |
| 22 | } | |
| 23 | ||
| 24 | -- ===== CONFIG LOAD/SAVE ===== | |
| 25 | local function saveConfig() | |
| 26 | local file = fs.open(CONFIG_PATH, "w") | |
| 27 | if file then | |
| 28 | file.write(textutils.serialize(config)) | |
| 29 | file.close() | |
| 30 | end | |
| 31 | end | |
| 32 | ||
| 33 | local function loadConfig() | |
| 34 | if fs.exists(CONFIG_PATH) then | |
| 35 | local file = fs.open(CONFIG_PATH, "r") | |
| 36 | if file then | |
| 37 | local data = file.readAll() | |
| 38 | file.close() | |
| 39 | local ok, tbl = pcall(textutils.unserialize, data) | |
| 40 | if ok and type(tbl) == "table" then | |
| 41 | for k, v in pairs(tbl) do config[k] = v end | |
| 42 | end | |
| 43 | end | |
| 44 | end | |
| 45 | end | |
| 46 | loadConfig() | |
| 47 | ||
| 48 | -- ===== AUTO-DOWNLOAD NETCHAT ===== | |
| 49 | if not fs.exists(NETCHAT_PATH) then | |
| 50 | print("Downloading netchat...")
| |
| 51 | if http then | |
| 52 | local ok, resp = pcall(http.get, "https://pastebin.com/raw/KeRK2UWE") | |
| 53 | if ok and resp then | |
| 54 | local content = resp.readAll() | |
| 55 | resp.close() | |
| 56 | local file = fs.open(NETCHAT_PATH, "w") | |
| 57 | file.write(content) | |
| 58 | file.close() | |
| 59 | print("netchat installed!")
| |
| 60 | else | |
| 61 | print("Failed to download netchat.")
| |
| 62 | end | |
| 63 | else | |
| 64 | print("HTTP API not enabled.")
| |
| 65 | end | |
| 66 | -- set startup to run firm.lpsw | |
| 67 | local file = fs.open(STARTUP_PATH, "w") | |
| 68 | file.write('shell.run("firm.lpsw")')
| |
| 69 | file.close() | |
| 70 | end | |
| 71 | ||
| 72 | -- ===== INITIALIZE REDNET ===== | |
| 73 | if peripheral.find("modem") then
| |
| 74 | rednet.open(config.rednetSide) | |
| 75 | else | |
| 76 | rednet.close(config.rednetSide) | |
| 77 | end | |
| 78 | ||
| 79 | -- ===== GLOBAL SIGNAL VARIABLE ===== | |
| 80 | local signalStrength = 0 -- 1–3 bars | |
| 81 | ||
| 82 | -- ===== UTILITIES ===== | |
| 83 | local function centerText(y, text, color) | |
| 84 | local w = term.getSize() | |
| 85 | local x = math.floor((w - #text) / 2) | |
| 86 | term.setCursorPos(x, y) | |
| 87 | if color then term.setTextColor(color) end | |
| 88 | term.write(text) | |
| 89 | term.setTextColor(colors.white) | |
| 90 | end | |
| 91 | ||
| 92 | -- Map distance to signal bars | |
| 93 | local function distanceToBars(distance) | |
| 94 | if distance <= 5 then | |
| 95 | return 3 | |
| 96 | elseif distance <= 20 then | |
| 97 | return 2 | |
| 98 | else | |
| 99 | return 1 | |
| 100 | end | |
| 101 | end | |
| 102 | ||
| 103 | -- ===== ASYNC SIGNAL UPDATER ===== | |
| 104 | local function updateSignal() | |
| 105 | while true do | |
| 106 | local strength = 0 | |
| 107 | if rednet.isOpen(config.rednetSide) then | |
| 108 | local peers = {}
| |
| 109 | local ids = rednet.lookup("lemonphone_ping") or {}
| |
| 110 | for _, id in ipairs(ids) do | |
| 111 | local ok = pcall(rednet.send, id, "ping", 0.1) | |
| 112 | if ok then | |
| 113 | local d = math.abs(os.getComputerID() - id) -- simple heuristic for distance | |
| 114 | table.insert(peers, d) | |
| 115 | end | |
| 116 | end | |
| 117 | if #peers > 0 then | |
| 118 | table.sort(peers) | |
| 119 | strength = distanceToBars(peers[1]) | |
| 120 | end | |
| 121 | end | |
| 122 | signalStrength = strength | |
| 123 | sleep(5) | |
| 124 | end | |
| 125 | end | |
| 126 | ||
| 127 | -- Draw signal bars based on latest value | |
| 128 | local function drawSignalBars() | |
| 129 | local barChar = "|" | |
| 130 | local maxBars = 3 | |
| 131 | for i = 1, maxBars do | |
| 132 | if i <= signalStrength then | |
| 133 | term.setTextColor(colors.lime) | |
| 134 | else | |
| 135 | term.setTextColor(colors.gray) | |
| 136 | end | |
| 137 | term.write(barChar) | |
| 138 | end | |
| 139 | term.setTextColor(colors.white) | |
| 140 | end | |
| 141 | ||
| 142 | -- ===== TOP BAR ===== | |
| 143 | local function drawTopBar() | |
| 144 | local w = term.getSize() | |
| 145 | term.setCursorPos(1,1) | |
| 146 | term.setBackgroundColor(config.accentColor) | |
| 147 | term.clearLine() | |
| 148 | ||
| 149 | -- Left: signal | |
| 150 | term.setCursorPos(2,1) | |
| 151 | if rednet.isOpen(config.rednetSide) then | |
| 152 | drawSignalBars() | |
| 153 | term.write(" Net")
| |
| 154 | else | |
| 155 | term.setTextColor(colors.red) | |
| 156 | term.write("X NoNet")
| |
| 157 | term.setTextColor(colors.black) | |
| 158 | end | |
| 159 | ||
| 160 | -- Right: time/date | |
| 161 | local timeStr = textutils.formatTime(os.time(), true) | |
| 162 | local dateStr = os.date("%b %d")
| |
| 163 | local clock = timeStr .. " | " .. dateStr | |
| 164 | term.setCursorPos(w - #clock - 1, 1) | |
| 165 | term.setTextColor(colors.black) | |
| 166 | term.write(clock) | |
| 167 | end | |
| 168 | ||
| 169 | -- ===== MENU ===== | |
| 170 | local function drawMenu(selected) | |
| 171 | local apps = {
| |
| 172 | {name="Messages", cmd="netchat"},
| |
| 173 | {name="Worm", cmd="worm"},
| |
| 174 | {name="Falling Blocks", cmd="falling"},
| |
| 175 | {name="Redirection", cmd="redirection"},
| |
| 176 | {name="Settings", cmd="settings"},
| |
| 177 | {name="About Lemonphone", cmd="about"},
| |
| 178 | {name="Shutdown", cmd="shutdown"}
| |
| 179 | } | |
| 180 | ||
| 181 | local startY = 4 | |
| 182 | term.setTextColor(colors.white) | |
| 183 | for i, app in ipairs(apps) do | |
| 184 | term.setCursorPos(4, startY + i - 1) | |
| 185 | if i == selected then | |
| 186 | term.setBackgroundColor(colors.gray) | |
| 187 | term.setTextColor(colors.black) | |
| 188 | else | |
| 189 | term.setBackgroundColor(config.bgColor) | |
| 190 | term.setTextColor(colors.white) | |
| 191 | end | |
| 192 | term.clearLine() | |
| 193 | term.setCursorPos(4, startY + i - 1) | |
| 194 | term.write(app.name) | |
| 195 | end | |
| 196 | ||
| 197 | term.setBackgroundColor(config.bgColor) | |
| 198 | return apps | |
| 199 | end | |
| 200 | ||
| 201 | -- ===== SETTINGS MENU ===== | |
| 202 | local function openSettings() | |
| 203 | while true do | |
| 204 | term.setBackgroundColor(config.bgColor) | |
| 205 | term.clear() | |
| 206 | centerText(3, "Settings", config.accentColor) | |
| 207 | term.setCursorPos(2,5) | |
| 208 | term.write("1. Change Username")
| |
| 209 | term.setCursorPos(2,6) | |
| 210 | term.write("2. Change Accent Color")
| |
| 211 | term.setCursorPos(2,7) | |
| 212 | term.write("3. Back")
| |
| 213 | ||
| 214 | local _, key = os.pullEvent("key")
| |
| 215 | if key == keys.one then | |
| 216 | term.setCursorPos(2,9) | |
| 217 | term.clearLine() | |
| 218 | term.write("Enter new username: ")
| |
| 219 | config.username = read() | |
| 220 | saveConfig() | |
| 221 | elseif key == keys.two then | |
| 222 | term.setCursorPos(2,9) | |
| 223 | term.clearLine() | |
| 224 | term.write("Color name (e.g. yellow): ")
| |
| 225 | local colorName = read() | |
| 226 | if colors[colorName] then | |
| 227 | config.accentColor = colors[colorName] | |
| 228 | saveConfig() | |
| 229 | else | |
| 230 | term.setCursorPos(2,10) | |
| 231 | term.write("Invalid color.")
| |
| 232 | sleep(1) | |
| 233 | end | |
| 234 | elseif key == keys.three then | |
| 235 | return | |
| 236 | end | |
| 237 | end | |
| 238 | end | |
| 239 | ||
| 240 | -- ===== ABOUT PAGE ===== | |
| 241 | local function aboutLemonphone() | |
| 242 | term.setBackgroundColor(config.bgColor) | |
| 243 | term.clear() | |
| 244 | centerText(3,"About Lemonphone", config.accentColor) | |
| 245 | term.setCursorPos(2,5) | |
| 246 | term.write("Device: " .. firmware.device)
| |
| 247 | term.setCursorPos(2,6) | |
| 248 | term.write("Firmware: " .. firmware.version)
| |
| 249 | term.setCursorPos(2,8) | |
| 250 | term.write("Press any key to return...")
| |
| 251 | os.pullEvent("key")
| |
| 252 | end | |
| 253 | ||
| 254 | -- ===== MAIN LOOP ===== | |
| 255 | local function mainLoop() | |
| 256 | local selected = 1 | |
| 257 | local refreshRate = 1.0 | |
| 258 | local timer = os.startTimer(refreshRate) | |
| 259 | ||
| 260 | while true do | |
| 261 | term.setBackgroundColor(config.bgColor) | |
| 262 | term.clear() | |
| 263 | drawTopBar() | |
| 264 | centerText(3, config.username .. "'s Lemonphone", config.accentColor) | |
| 265 | local apps = drawMenu(selected) | |
| 266 | ||
| 267 | local event, key = os.pullEvent() | |
| 268 | if event == "key" then | |
| 269 | if key == keys.up then selected = math.max(1, selected-1) | |
| 270 | elseif key == keys.down then selected = math.min(#apps, selected+1) | |
| 271 | elseif key == keys.enter then | |
| 272 | local chosen = apps[selected] | |
| 273 | if chosen.name == "Settings" then openSettings() | |
| 274 | elseif chosen.name == "About Lemonphone" then aboutLemonphone() | |
| 275 | else | |
| 276 | term.setBackgroundColor(config.bgColor) | |
| 277 | term.clear() | |
| 278 | term.setCursorPos(1,1) | |
| 279 | shell.run(chosen.cmd) | |
| 280 | end | |
| 281 | end | |
| 282 | elseif event == "timer" then | |
| 283 | timer = os.startTimer(refreshRate) | |
| 284 | drawTopBar() | |
| 285 | end | |
| 286 | end | |
| 287 | end | |
| 288 | ||
| 289 | -- ===== START SIGNAL UPDATER IN PARALLEL WITH MAIN LOOP ===== | |
| 290 | parallel.waitForAny(updateSignal, mainLoop) | |
| 291 |