L
ling-wallet
↓ 0 downloadsLing Wallet: a post-quantum privacy-coin wallet, full node and HyperTorus miner for LNG
Keywords wallet, cryptocurrency, lng, node, miner, ling
lingfu install ling-walletCode / Files (116)
ling/ui/shell.ling
# shell.ling — cached backend reads, sidebar, status strip, section routing,
# keyboard shortcuts and auto-lock.
bind SECTIONS = ["WALLET", "SEND", "RECEIVE", "MINING", "NETWORK", "SECURITY", "ABOUT"]
bind SECTION_CAPS = ["Balance & activity", "Pay a verified address", "Your address & key bundle", "Background miner", "Peers, chain & supply audit", "Phrase, password, auto-lock", "Standards & limitations"]
fn section_color(i) {
return list_get([C_GREEN, C_ORANGE, C_BLUE, C_YELLOW, C_INDIGO, C_VIOLET, C_TEXT_DIM], i)
}
fn section() { return st_get("section", 0) }
fn set_section(i) {
if i != section() {
st_set("section", i)
focus_next_frame()
}
}
# Backend reads, refreshed four times a second (or when forced).
fn refresh(force) {
bind t = u_now()
bind state = be_state()
st_set("d_state", state)
if !force && t - st_get("d_last", 0 - 10) < 0.25 { return () }
st_set("d_last", t)
bind sm = match be_summary() {
ok(v) => v,
bad(e) => (),
}
st_set("d_summary", sm)
if state == "unlocked" {
st_set("d_history", be_history(50))
st_set("d_net", be_network())
st_set("d_audit", be_audit())
if st_get("d_bundle", "") == "" { st_set("d_bundle", be_export_bundle()) }
}
if section() != 3 || st_has("d_mining") < 1 { st_set("d_mining", be_mining()) }
}
fn d_summary() { return st_get("d_summary", ()) }
fn d_net() { return st_get("d_net", ()) }
# Lock now and drop anything sensitive the UI holds.
fn ui_lock() {
be_lock()
send_reset()
sec_wipe()
st_set("d_bundle", "")
tf_clear("unlock_pw")
st_set("d_state", be_state())
focus_next_frame()
}
fn autolock_secs() { return list_get([60, 300, 600, 1800, 0 - 1], st_get("autolock", 2)) }
# One frame of the whole app.
fn app_frame() {
bind t = u_now()
refresh(st_get("shot", 0) > 0)
bind state = st_get("d_state", "nowallet")
if state == "unlocked" && section() == 3 {
bind m = be_mining()
st_set("d_mining", m)
tz_feed("tz", m.trace, t)
}
if state != st_get("last_state", "") {
focus_next_frame()
st_set("last_state", state)
}
bind lim = autolock_secs()
if state == "unlocked" && !onboard_in_progress() && lim > 0 && st_get("shot", 0) < 1 {
if t - st_get("activity_t", t) > lim {
ui_lock()
toast("Locked after inactivity")
bind state = st_get("d_state", "locked")
}
}
bind w = get_width()
bind h = get_height()
fill(0, 0, w, h, C_BG)
if onboard_in_progress() || state == "nowallet" {
onboard_draw()
} else if state == "locked" {
unlock_draw()
} else {
shell_draw()
}
}
fn shell_draw() {
if u_ctrl() {
bind i = 0
while i < 7 {
if u_key(num_str(i + 1)) { set_section(i) }
bind i = i + 1
}
if u_key("l") {
ui_lock()
return ()
}
}
sidebar()
if status_strip() {
ui_lock()
return ()
}
bind w = get_width()
bind h = get_height()
bind ax = SIDE_W + PAD
bind ay = TOP_H + PAD + 8
bind aw = w - SIDE_W - 2 * PAD
bind ah = h - TOP_H - 2 * PAD - 8
clip_push(SIDE_W + 1, TOP_H + 1, w - SIDE_W - 1, h - TOP_H - 1)
bind s = section()
if s == 0 { wallet_draw(ax, ay, aw, ah) }
if s == 1 { send_draw(ax, ay, aw, ah) }
if s == 2 { receive_draw(ax, ay, aw, ah) }
if s == 3 { mining_draw(ax, ay, aw, ah) }
if s == 4 { network_draw(ax, ay, aw, ah) }
if s == 5 { security_draw(ax, ay, aw, ah) }
if s == 6 { about_draw(ax, ay, aw, ah) }
clip_pop()
}
# The brand mark: one solid "$" in the Daemon face.
fn logo(x, y, size, col) {
t_text(F_LOGO, size, x, y + round(size * list_get(FONT_CAP, F_LOGO)), "$", col)
return round(t_w(F_LOGO, size, "$"))
}
fn sidebar() {
bind h = get_height()
fill(0, 0, SIDE_W, h, C_SIDE)
vline(SIDE_W, 0, h, C_LINE_SOFT, 1)
cross(SIDE_W - 6, 0, 5, h, C_LINE_SOFT, 3, 0.5)
bind lw = logo(PAD, 20, 40, C_TEXT_STRONG)
bind bx = PAD + lw + 12
t_text_sp(F_HEAD, 13, bx, 35, "WALLET", C_TEXT_STRONG, 2)
label(bx, 52, "LNG", C_TEXT_DIM)
hline(PAD, 72, SIDE_W - 2 * PAD - 6, C_LINE_SOFT, 1)
hatch(PAD, 75, 40, 4, C_BLUE, true, 3, 0.6)
bind y = 92
bind i = 0
while i < len(SECTIONS) {
bind on = section() == i
bind hot = u_hover(0, y, SIDE_W - 6, 60)
bind sc = section_color(i)
if on {
fill(0, y, SIDE_W - 6, 60, C_ROW_ACTIVE)
fill(0, y, 3, 60, sc)
hatch(3, y, 10, 60, sc, true, 3, 0.35)
} else if hot {
fill(0, y, SIDE_W - 6, 60, C_ROW_HOVER)
}
bind col = sel(on || hot, sc, shade(sc, 0.35))
tile_word(PAD, y + 11, 17, list_get(SECTIONS, i), col, on)
t_text(F_UI, 15, PAD, y + 48, list_get(SECTION_CAPS, i), sel(on, C_TEXT, C_TEXT_FAINT))
if hot && u_pressed() { set_section(i) }
bind y = y + 60
bind i = i + 1
}
bind fy = h - 72
hline(PAD, fy, SIDE_W - 2 * PAD - 6, C_LINE_SOFT, 1)
bind s = d_summary()
bind height = "-"
if s != () {
if s.sync_kind != "connecting" { bind height = num_str(s.sync_height) }
}
label(PAD, fy + 24, "Block", C_TEXT_FAINT)
t_right(F_MONO, 20, SIDE_W - PAD - 6, fy + 25, height, C_TEXT)
label(PAD, fy + 48, "Protocol", C_TEXT_FAINT)
t_right(F_MONO, 20, SIDE_W - PAD - 6, fy + 49, "LW-1 v0.1", C_TEXT_DIM)
}
# Returns true when Lock was pressed.
fn status_strip() {
bind w = get_width()
fill(SIDE_W + 1, 0, w - SIDE_W - 1, TOP_H, C_BG)
hline(SIDE_W + 1, TOP_H, w - SIDE_W - 1, C_LINE_SOFT, 1)
bind s = d_summary()
bind n = d_net()
bind x = SIDE_W + 1 + PAD
bind cap_y = 22
bind val_y = 43
bind name = ""
if s != () { bind name = s.network_name }
label(x, cap_y, "Network", C_TEXT_FAINT)
bind tw = t_text(F_UI, 18, x, val_y, name, C_TEXT_STRONG)
bind x = x + round(max(tw, label_w("Network"))) + 20
vline(x, 12, 32, C_LINE_SOFT, 1)
bind x = x + 20
label(x, cap_y, "Sync", C_TEXT_FAINT)
bind kind = "connecting"
if s != () { bind kind = s.sync_kind }
if kind == "synced" {
disc(x + 4, val_y - 5, 3.5, C_GREEN, 1)
glow(x + 4, val_y - 5, 7, C_GREEN, 0.3)
bind a = round(t_text(F_UI, 18, x + 14, val_y, "Synced", C_TEXT_STRONG))
bind b = round(t_text(F_MONO, 20, x + 22 + a, val_y, "#" + num_str(s.sync_height), C_TEXT_DIM))
bind x = x + 22 + a + b
} else if kind == "syncing" {
bind p = 0
if s.sync_target > 0 { bind p = min(1, s.sync_height / s.sync_target) }
disc(x + 4, val_y - 5, 3.5, C_YELLOW, 1)
bind a = round(t_text(F_UI, 18, x + 14, val_y, "Syncing", C_TEXT_STRONG))
bind b = round(t_text(F_MONO, 20, x + 22 + a, val_y, num_str(s.sync_height) + "/" + num_str(s.sync_target), C_TEXT_DIM))
bind bw = 14 + a + 8 + b
fill(x, val_y + 6, bw, 2, C_LINE_SOFT)
fill(x, val_y + 6, floor(bw * p), 2, C_YELLOW)
bind x = x + bw
} else {
bind pulse = 0.4 + 0.6 * abs(sin(u_now() * 3))
disc(x + 4, val_y - 5, 3.5, C_ORANGE, pulse)
bind x = x + 14 + round(t_text(F_UI, 18, x + 14, val_y, "Connecting…", C_TEXT_STRONG))
}
bind x = x + 20
vline(x, 12, 32, C_LINE_SOFT, 1)
bind x = x + 20
bind peers = 0
if n != () { bind peers = len(n.peers) }
label(x, cap_y, "Peers", C_TEXT_FAINT)
bind b2 = 0
while b2 < 4 {
bind bh = 4 + b2 * 3
fill(x + b2 * 5, val_y - bh, 3, bh, sel(b2 < min(peers, 4), C_BLUE, C_LINE_SOFT))
bind b2 = b2 + 1
}
bind pw = round(t_text(F_MONO, 22, x + 26, val_y, num_str(peers), C_TEXT_STRONG))
bind x = x + 26 + max(pw, 10) + 20
vline(x, 12, 32, C_LINE_SOFT, 1)
bind x = x + 20
bind route = "-"
if n != () { bind route = n.route }
bind tor = str_starts_with(str_lower(route), "tor")
label(x, cap_y, "Route", C_TEXT_FAINT)
icon_draw("lock", x, val_y - 6, sel(tor, C_VIOLET, C_GREEN))
t_text(F_UI, 18, x + 24, val_y, list_get(str_split(route, " "), 0), C_TEXT_STRONG)
bind lx = w - PAD - 104
u_tip(lx, 10, 104, 36, "Lock the wallet now (Ctrl+L)")
return button(lx, 10, 104, 36, "Lock", C_VIOLET, "plain", true, false, "lock")
}
# Format atomic units as LNG with up to 8 decimals, trailing zeros dropped.
fn format_lng(atomic) {
bind a = round(atomic)
bind whole = floor(a / 100000000)
bind frac = a - whole * 100000000
if frac == 0 { return thousands_plain(whole) }
bind fs = num_str(frac + 100000000)
bind fs = substr(fs, 1, 8)
while str_ends_with(fs, "0") { bind fs = substr(fs, 0, str_len(fs) - 1) }
return thousands_plain(whole) + "." + fs
}
fn thousands_plain(v) { return num_str(v) }
# Parse "12.5" into atomic units; -1 when invalid.
fn parse_lng(s) {
if s == "" || s == "." { return 0 - 1 }
bind parts = str_split(s, ".")
if len(parts) > 2 { return 0 - 1 }
bind whole = list_get(parts, 0)
bind frac = ""
if len(parts) == 2 { bind frac = list_get(parts, 1) }
if str_len(frac) > 8 { return 0 - 1 }
bind w = 0
if whole != "" { bind w = to_number(whole) }
bind f = 0
if frac != "" { bind f = to_number(frac + str_repeat("0", 8 - str_len(frac))) }
return w * 100000000 + f
}
# Thousands separators for a decimal string or number.
fn thousands(v) {
bind s = to_str(v)
bind out = ""
bind n = str_len(s)
bind i = 0
while i < n {
if i > 0 && (n - i) % 3 == 0 { bind out = out + "," }
bind out = out + substr(s, i, 1)
bind i = i + 1
}
return out
}
fn ago(ts) {
bind d = max(0, floor(now_unix() - ts))
if d < 60 { return "just now" }
if d < 3600 { return num_str(floor(d / 60)) + "m ago" }
if d < 86400 { return num_str(floor(d / 3600)) + "h " + num_str(floor(d % 3600 / 60)) + "m ago" }
return num_str(floor(d / 86400)) + "d ago"
}
fn pad2(n) { return sel(n < 10, "0" + num_str(n), num_str(n)) }
fn duration(secs0) {
bind secs = floor(secs0)
if secs < 60 { return num_str(secs) + "s" }
if secs < 3600 { return num_str(floor(secs / 60)) + "m " + pad2(secs % 60) + "s" }
if secs < 86400 { return num_str(floor(secs / 3600)) + "h " + pad2(floor(secs % 3600 / 60)) + "m" }
return num_str(floor(secs / 86400)) + "d " + pad2(floor(secs % 86400 / 3600)) + "h"
}
# True while something on screen moves (keeps the frame rate up).
fn animating() {
bind st0 = st_get("d_state", "nowallet")
if st0 != "unlocked" || onboard_in_progress() { return true }
bind s = section()
if s == 3 { return true }
if s == 1 && st_get("send_phase", "compose") != "compose" { return true }
if s == 1 && st_get("send_lookup_kind", "") == "pending" { return true }
return false
}