Something went wrong. Try again.
a Bluesky client for PICO-8 picosky.vinnymac.dev
pico-8 simulation bluesky games
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214-- footer menu: second cursor zone; ←/→ pick, 🅾 activate, x/select return.in_menu=falsemenu_sel=1menu_labels=split"reply,repost,quote,like,post,credits"-- footer icons parallel to menu_labels; "•" separates engagement (1-4) from global (5-6).menu_icons={8,9,14,11,12,15}
function mobile() return (rpin(126)&1)>0 end-- footer label for the 🅾 button: the 🅾 glyph (\142) on mobile, the "z" key on-- desktop (where 🅾 is bound to z, so showing the glyph would mislead).function obtn() return mobile() and "\142" or "z" end
-- "like"/"repost" are contextual: show the undo verb once already done.function menu_label(i) local l=menu_labels[i] local p=posts[sel] if l=="like" then return (p and p.liked) and "unlike" or "like" end if l=="repost" then return (p and p.reposted) and "unrepost" or "repost" end return lend
-- bump a plain-integer count field by d (formatted "k" counts are left alone).function bump_count(p,k,d) local n=tonum(p[k]) if n and n==flr(n) then p[k]=tostr(flr(n+d)) endend
-- toggle like on a post/comment: optimistic local flip + tell js (op_like).function toggle_like(p) if not p then return end p.liked=not p.liked bump_count(p,"likes",p.liked and 1 or -1) if ONLINE then enqueue(op_like,p.refid,p.liked and "1" or "0") end sfx(p.liked and 3 or 1)end
-- toggle repost: same shape as like, against an app.bsky.feed.repost record.function toggle_repost(p) if not p then return end p.reposted=not p.reposted bump_count(p,"reposts",p.reposted and 1 or -1) if ONLINE then enqueue(op_repost,p.refid,p.reposted and "1" or "0") end sfx(p.reposted and 3 or 1)end
function toggle_menu() in_menu=not in_menu if in_menu then menu_sel=1 end -- always open on the first item (reply) sfx(0)end
function menu_activate() local m=menu_labels[menu_sel] local p=posts[sel] in_menu=false if m=="reply" then if p then open_compose(p.refid) sfx(3) end elseif m=="like" then toggle_like(p) elseif m=="repost" then toggle_repost(p) elseif m=="quote" then if p then open_compose(0,p.refid,p.name) sfx(3) end elseif m=="post" then open_compose(0) sfx(3) elseif m=="credits" then enter_demo() endend
-- 1 when the next 🅾 press opens compose (feed, or menu on post/quote)function compose_intent() if scr==scr_timeline or scr==scr_thread then if in_menu then local m=menu_labels[menu_sel] return (m=="post" or m=="quote" or m=="reply") and 1 or 0 end return 0 end return 0end
function update_nav() local is_nav=scr==scr_timeline or scr==scr_thread -- handle keyboard: menu keys take priority over global shortcuts while stat(30) do local c=stat(31) if is_nav and in_menu then if c=="\13" then menu_activate() return elseif c=="\27" then toggle_menu() return end elseif is_nav and c=="\27" and scr==scr_thread then open_timeline(false) sfx(1) return elseif is_nav and (c=="l" or c=="L") then toggle_like(posts[sel]) elseif is_nav and (c=="s" or c=="S") then toggle_repost(posts[sel]) elseif is_nav and (c=="q" or c=="Q") then local p=posts[sel] if p then open_compose(0,p.refid,p.name) sfx(3) end elseif c=="t" or c=="T" then theme=1-theme sfx(0) elseif c=="r" or c=="R" then open_timeline(true) elseif c=="d" or c=="D" then enter_demo() elseif c=="n" or c=="N" then open_compose(0) sfx(3) end end if is_nav then -- cursor in the footer menu: ←/→ pick, 🅾/z/enter activate, x/esc exit if in_menu then if btnp(0) then menu_sel=max(1,menu_sel-1) sfx(0) end if btnp(1) then menu_sel=min(#menu_labels,menu_sel+1) sfx(0) end if btnp(4) then menu_activate() end if btnp(5) then in_menu=false sfx(1) end return end local p=posts[sel] local expanded=p and p.expanded if expanded then -- up/down scroll within the expanded post; navigate at boundaries if btnp(2) then if (p.scroll or 0)>0 then p.scroll-=1 sfx(0) else sel=max(1,sel-1) sfx(0) end end if btnp(3) then local total=p.lines and #p.lines or 0 local cap=p.vis_lines or 3 if (p.scroll or 0)<max(0,total-cap) then p.scroll+=1 sfx(0) else sel=min(max(1,#posts),sel+1) sfx(0) end end else -- normal post navigation if btnp(2) then sel=max(1,sel-1) sfx(0) end if btnp(3) then sel=min(max(1,#posts),sel+1) sfx(0) end end -- right: expand selected post (only when there is hidden content) if btnp(1) then if p then if not p.lines then p.lines=wrap(p.text,flr((120-2-p.depth*4)/4)) end local has_more=#p.lines>3 if has_more and not p.expanded then p.expanded=true p.scroll=0 scroll=sel-1 -- pin expanded post to top of viewport sfx(0) end end end -- left: collapse selected post if btnp(0) then if p and p.expanded then p.expanded=false p.scroll=0 sfx(0) end end if scr==scr_timeline then if btnp(4) then toggle_menu() end if btnp(5) and posts[sel] then local cp=posts[sel] if cp.depth>0 then -- self-reply: find nearest depth-0 parent above, open its thread focused here local par=nil for i=sel-1,1,-1 do if posts[i].depth<cp.depth then par=posts[i] break end end if par then open_thread(par.refid,cp.refid) sfx(2) elseif cp.replies!="0" then open_thread(cp.refid) sfx(2) end elseif cp.replies!="0" then open_thread(cp.refid) sfx(2) end end elseif scr==scr_thread then if btnp(4) then toggle_menu() end if btnp(5) then open_timeline(false) sfx(1) end end -- auto-load: guard scr so open_thread's nav can't race in-flight REQ_MORE if scr==scr_timeline and sel>=max(1,#posts-2) then load_more() end elseif scr==scr_auth then -- waits for js to recover the session end -- skip scroll-clamp on first frame back: heights not cached yet, would stomp restored scroll. if just_switched then just_switched=false else local p=posts[sel] if p and p.expanded then scroll=sel-1 elseif p then local cb=status_showing() and 111 or 118 local top,ok=12,sel>scroll for i=scroll+1,sel-1 do local h=posts[i] and posts[i].h if not h then ok=false break end top+=h end local h=p.h if (not ok) or sel<=scroll or top<11 or not h or top+h-2>cb then scroll=sel-1 end end endend
function cancel_compose() reset_draft() sfx(1) -- back/cancel open_timeline(false)end
function update_compose() poke(0x5f30,1) -- suppress pause this frame if btnp(4) and btn(5) or btn(4) and btnp(5) then poke(0x5f2d,1) end -- o+x: re-open soft keyboard -- cancel is keyboard-only (esc/backspace): 🅾/❎ keys double as typed chars here. while stat(30) do local c=stat(31) if c=="\13" then -- enter = send send_draft() return elseif c=="\27" then -- esc = cancel cancel_compose() return elseif c=="\8" then -- backspace (on empty = cancel) if #draft>0 then draft=sub(draft,1,#draft-1) sfx(5) else cancel_compose() return end elseif c>=" " and #draft<maxchars then draft=draft..c sfx(4) -- keystroke tick end endend