-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[==[ Standard library with core utilities for sleeping, random ID generation, environment access, and system information. Aggregates submodules for file system, process control, text, table, and conversion operations. Some of the module's functions make use of `math.random`, but it's user's responsibility to properly initialize random generator with something like `math.randomseed(os.time())`. _Lilush shell_ & all _lilush_ apps do that on start. ]==] local core = require("std.core") local buffer = require("string.buffer") local utf = require("std.utf") local ps = require("std.ps") local fs = require("std.fs") local tbl = require("std.tbl") local txt = require("std.txt") local conv = require("std.conv") local mime = require("std.mime") local logger = require("std.logger") ---! Sleep for the given number of seconds ---@ sleep(`seconds`{.num}) local function sleep(seconds) core.sleep(seconds) end ---! Sleep for the given number of milliseconds ---@ sleep_ms(`milliseconds`{.num}) local function sleep_ms(milliseconds) core.sleep_ms(milliseconds) end ---! Parse /etc/passwd and return a table of system users keyed by UID ---@ system_users() -> `users`{.tbl} --[===[ Returns a table mapping numeric UIDs to tables with `login` and `gid` fields. Unknown UIDs return `{login="unknown", gid=-1}` via metatable. ]===] local system_users = function() local passwd_raw, err = fs.read_file("/etc/passwd") local users = {} if passwd_raw then for line in passwd_raw:gmatch("(.-)\n") do local user, uid, gid = line:match("^([^:]+):x:([^:]+):([^:]+)") if user and uid and gid then users[tonumber(uid)] = { login = user, gid = tonumber(gid) } end end end setmetatable(users, { __index = function(tbl, key) return { login = "unknown", gid = -1 } end, }) return users end ---! Read a file and substitute environment variables in its content ---@ envsubst(`filename`{.file}) -> `content`{.str .or_nil}, `err`{.str .err} --[===[ Reads `filename` and replaces every `${VAR}` placeholder with the corresponding value from the process environment at the time of the call. Variables set via `ps.setenv` are visible. Returns nil and an error string if the file cannot be read. ]===] local function envsubst(filename) local content, err = fs.read_file(filename) if content then -- txt.template uses std.environ() as the substitute table -- and `${[^}]+}` as the pattern by default return txt.template(content) end return nil, err end ---! Escape Lua pattern magic characters in a string ---@ escape_magic_chars(`str`{.str}) -> `escaped`{.str} --[===[ Prepends `%` before each of the eleven Lua pattern magic characters: `+ * % . $ [ ] ? ( ) -`. The result is safe to use as a literal pattern inside `string.find`, `string.match`, etc. ]===] local escape_magic_chars = function(str) str = str or "" -- escape all possible magic characters that are used in Lua string patterns return str:gsub("[+*%%%.%$[%]%?%(%)-]", "%%%1") end ---! Generate a random binary salt string ---@ salt(`length`{.num .opt}) -> `salt`{.str} --[===[ Returns a string of `length` random bytes (default 16). The output is raw binary, NOT hex-encoded. Byte value 0x00 is excluded. Suitable for use as HMAC key material or a password-hashing salt. Requires `math.randomseed` to have been called before use. ]===] local function salt(length) length = length or 16 local salt = buffer.new() for i = 1, length do -- we intentionally exclude 0 here salt:put(string.char(math.random(255))) end return salt:get() end ---! Generate a random UUID v4 string ---@ uuid() -> `id`{.str} --[===[ Returns a randomly generated UUID in the canonical form `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`, where `4` fixes the version and `y` is constrained to `8`–`b` per RFC 4122. Uses `math.random`; call `math.randomseed` before use. ]===] local function uuid() local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" return string.gsub(template, "[xy]", function(c) local v = (c == "x") and math.random(0, 0xf) or math.random(8, 0xb) return string.format("%x", v) end) end -- port of https://github.com/ai/nanoid, seems a nice thing to have, -- UUIDs are ugly fucks to look at, I'll give ya that... ---! Generate a compact random ID (nanoid) ---@ nanoid(`length`{.num .opt}) -> `id`{.str} --[===[ Generates a URL-safe random ID using alphanumeric characters plus `-` and `_`. Default length is 21 characters. Port of https://github.com/ai/nanoid. ]===] local function nanoid(length) local charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_" length = length or 21 local id = "" for i = 1, length do local rand = math.random(64) local char = charset:sub(rand, rand) id = id .. char end return id end ---! Check if a Lua module is available for require ---@ module_available(`name`{.str}) -> `available`{.bool} --[===[ Returns true if the module `name` can be loaded, without actually loading it. If `name` is not already in `package.loaded`, each `package.searcher` is tried in order. When a searcher returns a loader function, it is registered in `package.preload` so the next `require(name)` call is instant. ]===] local function module_available(name) if package.loaded[name] then return true else for _, searcher in ipairs(package.searchers) do local loader = searcher(name) if type(loader) == "function" then package.preload[name] = loader return true end end return false end end ---! Get all environment variables as a table ---@ environ() -> `env`{.tbl} --[===[ Returns a table mapping each environment variable name to its value string. For the raw `"NAME=VALUE"` array used internally by the process, see `ps.environ()`. ]===] local function environ() local env = {} local strings = core.environ() for i, s in ipairs(strings) do local name = s:match("^([^=]+)") local value = s:match("^[^=]+=(.*)") if name then env[name] = value end end return env end ---! Get the system hostname ---@ hostname() -> `name`{.str} local hostname = function() local name = fs.read_file("/etc/hostname") or "" name = name:gsub("\n", "") return name end ---! Get the current user's home directory ---@ home() -> `path`{.str .or_nil} --[===[ Resolves the home directory using the HOME environment variable first, then falls back to `getpwuid(getuid())`. Returns nil if neither method succeeds. Result is cached on first call. ]===] local __home = nil local home = function() if __home ~= nil then return __home end local h = os.getenv("HOME") if h then __home = h return __home end local uid = core.getuid() local pw = core.getpwuid(uid) if pw then __home = pw return __home end return nil end ---! Replace `~` in the line with current user's home directory ---@ replace_tilde_with_home(`line`{.str}) -> `line`{.str .or_nil} --[===[ Replaces tilde occurrences in the line with user's home directory. Tilde is replaced only in those position where it makes sense to replace it with home, thus it won't be replaced in a middle of a word. Returns `nil` if user's home can't be determined or if `line` is nil. ]===] local replace_tilde_with_home = function(line) if not line then return line end local h = home() if not h then return line end -- Tilde at start must be followed by / or end of word line = line:gsub("^~(/)", h .. "%1") line = line:gsub("^~$", h) -- Tilde after whitespace or shell separators line = line:gsub("([%s|;<>()])~(/)", "%1" .. h .. "%2") line = line:gsub("([%s|;<>()])~$", "%1" .. h) return line end local PROGRESS_ICONS = { "⣿", "⣗", "⡯", "⣦", "⣢", "⣲", "⣶", "⣮", "⣦", "⢿", "⡟", "⣤" } ---! Start an animated progress indicator in the terminal ---@ progress_icon() -> `handle`{.tbl} --[===[ Returns a handle with a `handle.stop() method that kills the animation and cleans up. The indicator renders braille characters at ~5 fps. ]===] local progress_icon = function() local pid = ps.launch({ name = "progress_bar", func = function(cmd, args) while true do for _, icon in ipairs(PROGRESS_ICONS) do io.write(icon) io.flush() sleep_ms(200) io.write("\b \b") io.flush() end end end, }, nil, nil, nil) return { stop = function() ps.kill(pid, 9) ps.wait(pid) io.write("\b \b") io.flush() end, } end ---! Start an animated progress indicator rendering `text` as a braille-masked cycling string ---@ progress_text(`text`{.str}, `r`{.num .opt}, `g`{.num .opt}) -> `handle`{.tbl} --[===[ Returns a handle with a `handle.stop()` method that kills the animation and cleans up. Each frame re-renders `text` via `std.txt.mask_with_braille`, then erases it with backspaces before the next frame. The optional `r` and `g` values anchor the hue so the animation stays in one color family; omit them for per-frame-randomized colors. ]===] local progress_text = function(text, r, g) text = tostring(text or "") local n = utf.len(text) local erase = string.rep("\b \b", n) local pid = ps.launch({ name = "progress_text", func = function(cmd, args) while true do io.write(txt.mask_with_braille(text, nil, r, g)) io.flush() sleep_ms(200) io.write(erase) io.flush() end end, }, nil, nil, nil) return { stop = function() ps.kill(pid, 9) ps.wait(pid) io.write(erase) io.flush() end, } end local __last_sec = 0 local __counter = 0 ---! Returns unique monotonic IDs based on timestamp, pseudo mircoseconds ---@ now() -> `id`{.num} local now = function() local sec = os.time() if sec == __last_sec then counter = counter + 1 else __last_sec = sec counter = 0 end return sec * 1000000 + counter end ---! Get the current CPU time in seconds ---@ clockticks() -> `ticks`{.num} local function clockticks() return core.clockticks() end ---! Create a POSIX shared memory object ---@ create_shm(`name`{.str}, `data`{.str}) -> `ret`{.num}, `err`{.str .err} --[===[ Creates a POSIX shared memory segment named `name` and initializes it with the contents of `data`. Returns 0 on success, or nil and an error string on failure. ]===] local function create_shm(name, data) return core.create_shm(name, data) end ---! Pack three numbers into a 12-byte little-endian binary string ---@ pack3d(`x`{.num}, `y`{.num}, `z`{.num}) -> `packed`{.str} --[===[ Packs three 32-bit float values into a 12-byte binary string using little-endian byte order. Useful for compact serialization of 3D coordinates. ]===] local function pack3d(x, y, z) return core.pack3d(x, y, z) end ---! Unpack a 12-byte binary string into three numbers ---@ unpack3d(`packed`{.str}) -> `x`{.num}, `y`{.num}, `z`{.num} --[===[ Unpacks a 12-byte binary string (as produced by `pack3d`) back into three 32-bit float values. Raises an error if the input is not exactly 12 bytes. ]===] local function unpack3d(packed) return core.unpack3d(packed) end ---! Format a JSON string as "pretty" JSON ---@ prettify_json(`json`{.str}) -> `pretty_json`{.str} local function prettify_json(json) json = json or "" local indent = " " local out = buffer.new() local level = 0 local in_string = false local escaped = false local i = 1 local len = #json local function newline() out:put("\n", indent:rep(level)) end while i <= len do local c = json:sub(i, i) if in_string then out:put(c) if escaped then escaped = false elseif c == "\\" then escaped = true elseif c == '"' then in_string = false end else if c == '"' then in_string = true out:put(c) elseif c == "{" or c == "[" then -- Peek for an immediately empty container. local j = i + 1 while j <= len and json:sub(j, j):match("%s") do j = j + 1 end local next_char = json:sub(j, j) if (c == "{" and next_char == "}") or (c == "[" and next_char == "]") then out:put(c, next_char) i = j else level = level + 1 out:put(c) newline() end elseif c == "}" or c == "]" then level = level - 1 newline() out:put(c) elseif c == "," then out:put(c) newline() elseif c == ":" then out:put(": ") elseif c:match("%s") then -- Skip whitespace outside of strings. else out:put(c) end end i = i + 1 end return out:get() end local std = { utf = utf, tbl = tbl, fs = fs, ps = ps, conv = conv, mime = mime, logger = logger, txt = txt, environ = environ, clockticks = clockticks, create_shm = create_shm, sleep = sleep, sleep_ms = sleep_ms, now = now, module_available = module_available, envsubst = envsubst, escape_magic_chars = escape_magic_chars, uuid = uuid, nanoid = nanoid, salt = salt, system_users = system_users, hostname = hostname, home = home, replace_tilde_with_home = replace_tilde_with_home, progress_icon = progress_icon, progress_text = progress_text, pack3d = pack3d, unpack3d = unpack3d, prettify_json = prettify_json, } return std