From 973e4bcedc5ae26b66bc099624e720ec7f2aae23 Mon Sep 17 00:00:00 2001 From: CaffeineFueled Date: Fri, 4 Sep 2026 12:30:30 +0200 Subject: [PATCH] feat:ADD ping cfunction via line number and FIX misallignmnent at the bottom of larger files between linenumber and textbox --- app.py | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index e479d1d..37441d5 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,11 @@ application = app # alias if you prefer "application" app.mount("/static", StaticFiles(directory=os.path.join(os.path.dirname(os.path.abspath(__file__)), "vendor")), name="vendor") +# Minimum gap between accepted pings, per connection. A ping is one inbound message +# fanned out to every peer, so this bounds that amplification. Low enough that rapid +# human clicking gets through. +PING_MIN_INTERVAL = 0.15 + # Languages offered in the editor dropdown. This is a security boundary as much as a # feature list: the value is interpolated into a module URL client-side, so it must # never be free-form. Keep in sync with vendor/fetch.sh. @@ -192,7 +197,11 @@ HTML = """ aukpad @@ -355,6 +400,7 @@ HTML = """ +
@@ -386,9 +432,11 @@ HTML = """
1
+ +

@@ -478,12 +526,85 @@ function updateGutter() { gutter.textContent = s; } ta.addEventListener("input", refresh); +// The textarea's horizontal scrollbar eats into its client height, so it can scroll +// further than the mirrors, which have overflow:hidden and no scrollbar. At the bottom +// they clamp and lag behind by the scrollbar's height. Give them matching bottom padding +// so their scrollable extents line up. Recomputed on input and resize, the only times a +// horizontal scrollbar can appear or vanish. +function syncExtent() { + const sb = ta.offsetHeight - ta.clientHeight; // 0 when there is no h-scrollbar + const pad = parseFloat(getComputedStyle(ta).paddingBottom) + sb; + gutter.style.paddingBottom = pad + "px"; + hl.style.paddingBottom = pad + "px"; + // Same idea horizontally, for anything anchored to the editor's right edge. + document.documentElement.style.setProperty("--sbw", (ta.offsetWidth - ta.clientWidth) + "px"); +} +window.addEventListener("resize", () => { syncExtent(); syncScroll(); }); + function syncScroll() { gutter.scrollTop = ta.scrollTop; // The overlay needs horizontal sync too: white-space:pre means long lines // scroll sideways, and the gutter never does. hl.scrollTop = ta.scrollTop; hl.scrollLeft = ta.scrollLeft; + // Pings sit at document coordinates; move the wrapper rather than every bar. + pingsIn.style.transform = "translateY(" + (-ta.scrollTop) + "px)"; + // Only drop the notice if the document shrank past the line it points at. + if (jumpLine && jumpLine > lineCount()) hideJump(); } + +// --- Line ping: click a line number to flash that line for every peer --- +// Read from computed style, never hardcoded: the touch media query switches the +// editor font from 14px to 16px, so a literal line height would be wrong on mobile. +const pingsIn = $("#pings-in"); +const lineH = () => parseFloat(getComputedStyle(ta).lineHeight); +const padTop = () => parseFloat(getComputedStyle(ta).paddingTop); +const lineCount = () => ta.value.split("\\n").length; + +gutter.addEventListener("click", (e) => { + if (!isAuthed || ws?.readyState !== 1) return; + const y = e.clientY - gutter.getBoundingClientRect().top + ta.scrollTop - padTop(); + const line = Math.floor(y / lineH()) + 1; + if (line < 1 || line > lineCount()) return; // e.g. clicked below the last line + ws.send(JSON.stringify({type: "ping", line})); +}); + +function showPing(line) { + // Re-pinging a visible line restarts it rather than stacking a second bar. + const prev = pingsIn.querySelector('[data-line="' + line + '"]'); + if (prev) prev.remove(); + const el = document.createElement("div"); + el.className = "ping"; + el.dataset.line = line; + el.style.top = (padTop() + (line - 1) * lineH()) + "px"; + el.style.height = lineH() + "px"; + el.addEventListener("animationend", () => el.remove()); + pingsIn.appendChild(el); +} + +// --- Ping notice --- +// Raised for every ping, for every peer, whether or not the line is on screen: on a long +// pad the bar alone is easy to miss. Click it to jump to the line and re-flash it. +const jump = $("#pingjump"); +let jumpLine = 0, jumpTimer = null; + +function showJump(line) { + jumpLine = line; + $("#pingjump-txt").textContent = "Ping on line " + line; + jump.classList.toggle("up", padTop() + (line - 1) * lineH() < ta.scrollTop); + jump.classList.add("show"); + clearTimeout(jumpTimer); + jumpTimer = setTimeout(hideJump, 10000); +} + +function hideJump() { jump.classList.remove("show"); jumpLine = 0; } + +jump.addEventListener("click", () => { + const line = jumpLine; + hideJump(); + ta.scrollTop = Math.max(0, padTop() + (line - 1) * lineH() - (ta.clientHeight - lineH()) / 2); + syncScroll(); + showPing(line); // re-flash: the original bar may have almost faded by now +}); ta.addEventListener("scroll", syncScroll); // Also sync on keydown for immediate response ta.addEventListener("keydown", () => { setTimeout(syncScroll, 0); }); @@ -560,6 +681,7 @@ function scheduleHl() { // Re-render gutter and highlighting together. function refresh() { updateGutter(); + syncExtent(); if (ta.value.length > HL_MAX) { wrap.classList.remove("hl"); langSel.disabled = true; @@ -710,6 +832,9 @@ function connect(){ ta.selectionEnd = adjustCursor(oldText, msg.text, e); } else if (msg.type === "peers_changed") { setPeers(msg.count); + } else if (msg.type === "ping") { + showPing(msg.line); + showJump(msg.line); } else if (msg.type === "lang_changed") { applyLang(msg.lang || ""); } else if (msg.type === "protected_changed") { @@ -921,6 +1046,8 @@ def get_system_info():

  • optional syntax highlighting (24 languages) from a per-pad dropdown, shared with everyone on the pad; highlight.js is vendored locally and loaded lazily, so a plain pad fetches nothing
  • line numbers; Tab inserts 4 spaces
  • +
  • click a line number to "ping" it — that line flashes for five seconds + for everyone on the pad
  • dark / light mode (auto-detects system preference, manual toggle)
  • copy-to-clipboard and "new pad" buttons, live peer count in the header
  • @@ -1235,6 +1362,7 @@ async def ws(doc_id: str, ws: WebSocket): await _broadcast(doc_id, {"type": "peers_changed", "count": len(room["peers"])}) # Per-connection auth state: already authed if pad has no password + last_ping = 0.0 # per-connection ping throttle; see the "ping" branch below authed = room["pw_hash"] is None if authed: room["authed_peers"].add(ws) @@ -1333,6 +1461,21 @@ async def ws(doc_id: str, ws: WebSocket): save_room_data_to_cache(doc_id, room) await _broadcast(doc_id, {"type": "lang_changed", "lang": room["lang"]}, authed_only=True) + elif data.get("type") == "ping": + # Flash a line for everyone. Stateless: nothing is stored, so there is + # nothing to drift, expire or clean up on disconnect. + now = time.monotonic() + if now - last_ping < PING_MIN_INTERVAL: + continue # one inbound message fans out to every peer - throttle it + last_ping = now + line = data.get("line") + # bool subclasses int, so True would otherwise pass as line 1 + if isinstance(line, bool) or not isinstance(line, int): + continue + if not 1 <= line <= room["text"].count("\n") + 1: + continue + await _broadcast(doc_id, {"type": "ping", "line": line}, authed_only=True) + except WebSocketDisconnect: pass finally: