pulse-rb 1.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +225 -0
  2. package/adapters/linoria.lua +233 -0
  3. package/adapters/windui.llms.txt +366 -0
  4. package/adapters/windui.lua +505 -0
  5. package/bin/rb.js +2 -0
  6. package/dist/index.js +3285 -0
  7. package/package.json +59 -0
  8. package/pulse/dev/debuggui.lua +1206 -0
  9. package/pulse/dev/devconfig.lua +81 -0
  10. package/pulse/dev/ui/9_DevPanel.lua +384 -0
  11. package/pulse/helpers/aim.lua +193 -0
  12. package/pulse/helpers/cache.lua +68 -0
  13. package/pulse/helpers/cleaner.lua +110 -0
  14. package/pulse/helpers/conn.lua +33 -0
  15. package/pulse/helpers/cooldown.lua +47 -0
  16. package/pulse/helpers/draw.lua +122 -0
  17. package/pulse/helpers/hitbox.lua +63 -0
  18. package/pulse/helpers/input.lua +24 -0
  19. package/pulse/helpers/log.lua +228 -0
  20. package/pulse/helpers/loop.lua +58 -0
  21. package/pulse/helpers/memory.lua +48 -0
  22. package/pulse/helpers/narrate.lua +160 -0
  23. package/pulse/helpers/notify.lua +51 -0
  24. package/pulse/helpers/perf.lua +48 -0
  25. package/pulse/helpers/remote.lua +128 -0
  26. package/pulse/helpers/restore.lua +59 -0
  27. package/pulse/helpers/store.lua +39 -0
  28. package/pulse/helpers/team.lua +83 -0
  29. package/pulse/helpers/testmode.lua +80 -0
  30. package/pulse/helpers/trace.lua +111 -0
  31. package/pulse/helpers/track.lua +85 -0
  32. package/pulse/helpers/vec.lua +52 -0
  33. package/pulse/helpers/world.lua +51 -0
  34. package/pulse/runtime.lua +343 -0
  35. package/pulse/ui/linoria_settings.lua +55 -0
  36. package/pulse/ui/windui_settings.lua +87 -0
  37. package/templates/AGENTS.md +177 -0
  38. package/templates/CLAUDE.md +424 -0
  39. package/templates/deploy_config.example +17 -0
  40. package/templates/example_esp.rblua +69 -0
  41. package/templates/example_fov.rblua +20 -0
  42. package/templates/example_speed.rblua +25 -0
  43. package/templates/gitignore +4 -0
  44. package/templates/globals.lua +66 -0
  45. package/templates/layout.rblua +28 -0
  46. package/templates/module.lua +7 -0
  47. package/templates/module.rblua +32 -0
  48. package/templates/page_home.rblua +9 -0
  49. package/templates/remote.lua +6 -0
  50. package/templates/remotes.lua +14 -0
  51. package/vscode/language-configuration.json +35 -0
  52. package/vscode/package.json +35 -0
  53. package/vscode/src/extension.js +397 -0
  54. package/vscode/syntaxes/rblua.tmLanguage.json +126 -0
@@ -0,0 +1,1206 @@
1
+ -- ── Pulse Dev Overlay ────────────────────────────────────────────────────────
2
+ -- Injected automatically in --dev builds. Available in every project that
3
+ -- uses the rb framework — no project-level files needed.
4
+ --
5
+ -- LOG tab : scrollable log with level toggles, search, copy, ERR↑ jump.
6
+ -- FEAT tab : lists every Linoria toggle so you can enable/disable features.
7
+ -- CFG tab : snapshot of all Linoria Toggles + Options + Monitor values.
8
+ --
9
+ -- Toggle via _PulseDevGuiToggle() or the Debug page in the Linoria menu.
10
+
11
+ -- ── Cleanup any previous instance ────────────────────────────────────────────
12
+ local function _destroyOld(parent)
13
+ pcall(function()
14
+ local old = parent and parent:FindFirstChild("PulseDevLog")
15
+ if old then old:Destroy() end
16
+ end)
17
+ end
18
+ pcall(function() _destroyOld(game:GetService("CoreGui")) end)
19
+ pcall(function() _destroyOld(game:GetService("Players").LocalPlayer.PlayerGui) end)
20
+
21
+ -- ── GUI parent ────────────────────────────────────────────────────────────────
22
+ local _guiParent
23
+ pcall(function() _guiParent = game:GetService("CoreGui") end)
24
+ if not _guiParent then
25
+ _guiParent = game:GetService("Players").LocalPlayer.PlayerGui
26
+ end
27
+
28
+ -- ── Linoria global accessors (use _G[] not rawget — respects metatables) ─────
29
+ local function _getToggles() local ok, t = pcall(function() return _G.Toggles end); return (ok and t) or nil end
30
+ local function _getOptions() local ok, t = pcall(function() return _G.Options end); return (ok and t) or nil end
31
+
32
+ -- ── Palette ───────────────────────────────────────────────────────────────────
33
+ local P = {
34
+ bg = Color3.fromRGB(12, 12, 19),
35
+ bg2 = Color3.fromRGB(17, 17, 26),
36
+ bg3 = Color3.fromRGB(22, 22, 34),
37
+ header = Color3.fromRGB(20, 20, 32),
38
+ filter = Color3.fromRGB(16, 16, 24),
39
+ border = Color3.fromRGB(50, 50, 75),
40
+ text = Color3.fromRGB(195, 195, 215),
41
+ dim = Color3.fromRGB(85, 85, 108),
42
+ tag = Color3.fromRGB(135, 135, 205),
43
+ active = Color3.fromRGB(80, 80, 175),
44
+ on = Color3.fromRGB(60, 175, 100),
45
+ off = Color3.fromRGB(175, 60, 60),
46
+ section = Color3.fromRGB(30, 30, 48),
47
+ TRC = Color3.fromRGB(72, 72, 90),
48
+ DBG = Color3.fromRGB(70, 175, 155),
49
+ INF = Color3.fromRGB(170, 195, 220),
50
+ WRN = Color3.fromRGB(215, 165, 45),
51
+ ERR = Color3.fromRGB(215, 65, 65),
52
+ }
53
+
54
+ local LEVEL_HEX = {
55
+ TRC = "#484858", DBG = "#46AF9B",
56
+ INF = "#AAC3DC", WRN = "#D7A52D", ERR = "#D74141",
57
+ }
58
+ local TAG_HEX = "#8787CD"
59
+ local TS_HEX = "#555570"
60
+ local DATA_HEX = "#555570"
61
+ local MSG_HEX = "#C3C3D7"
62
+
63
+ local WIN_W, WIN_H = 760, 430
64
+ local HDR_H = 30
65
+ local FLT_H = 28
66
+ local LOG_H = WIN_H - HDR_H - FLT_H
67
+ local BODY_H = WIN_H - HDR_H
68
+ local FONT = Enum.Font.Code
69
+ local TAB_FONT = Enum.Font.GothamBold
70
+ local FS = 13
71
+ local ENTRY_H = 18
72
+
73
+ -- ── Helpers ───────────────────────────────────────────────────────────────────
74
+ local function _esc(s)
75
+ return tostring(s):gsub("&","&amp;"):gsub("<","&lt;"):gsub(">","&gt;")
76
+ end
77
+
78
+ local function _mkFrame(props)
79
+ local f = Instance.new("Frame")
80
+ for k, v in pairs(props) do f[k] = v end
81
+ return f
82
+ end
83
+
84
+ local function _mkText(props)
85
+ local t = Instance.new("TextLabel")
86
+ t.BackgroundTransparency = 1
87
+ t.TextXAlignment = Enum.TextXAlignment.Left
88
+ t.Font = FONT
89
+ for k, v in pairs(props) do t[k] = v end
90
+ return t
91
+ end
92
+
93
+ local function _mkBtn(props, onClick)
94
+ local b = Instance.new("TextButton")
95
+ b.BackgroundTransparency = 1
96
+ b.Font = FONT
97
+ for k, v in pairs(props) do b[k] = v end
98
+ if onClick then b.MouseButton1Click:Connect(onClick) end
99
+ return b
100
+ end
101
+
102
+ local function _copyText(text)
103
+ local ok = false
104
+ if not ok then pcall(function() setclipboard(text); ok = true end) end
105
+ if not ok then pcall(function() toclipboard(text); ok = true end) end
106
+ if not ok then pcall(function() writeclipboard(text); ok = true end) end
107
+ if not ok then pcall(function() Clipboard:set(text); ok = true end) end
108
+ return ok
109
+ end
110
+
111
+ local function _mkTabBtn(text, xOffset, active)
112
+ local btn = _mkBtn({
113
+ Text = text,
114
+ Size = UDim2.new(0, 40, 0, 22),
115
+ Position = UDim2.new(1, xOffset, 0.5, -11),
116
+ TextColor3 = active and P.active or P.dim,
117
+ TextSize = 13,
118
+ Font = TAB_FONT,
119
+ BackgroundColor3 = P.bg2,
120
+ BackgroundTransparency = 0,
121
+ Parent = nil,
122
+ }, nil)
123
+ Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 4)
124
+ local s = Instance.new("UIStroke", btn)
125
+ s.Color = active and P.active or P.border
126
+ s.Thickness = 1
127
+ return btn, s
128
+ end
129
+
130
+ -- Forward declarations — used before their bodies are defined below.
131
+ local _rerender
132
+ local _enableMouseForOverlay
133
+
134
+ -- ── Build window ─────────────────────────────────────────────────────────────
135
+ local _gui = Instance.new("ScreenGui")
136
+ _gui.Name = "PulseDevLog"
137
+ _gui.ResetOnSpawn = false
138
+ _gui.IgnoreGuiInset = true
139
+ _gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
140
+ _gui.DisplayOrder = 999
141
+ _gui.Parent = _guiParent
142
+
143
+ local _win = _mkFrame({
144
+ Name = "Win",
145
+ Size = UDim2.new(0, WIN_W, 0, WIN_H),
146
+ Position = UDim2.new(1, -(WIN_W + 10), 0, 80),
147
+ BackgroundColor3 = P.bg,
148
+ BorderSizePixel = 0,
149
+ Visible = false,
150
+ Parent = _gui,
151
+ })
152
+ Instance.new("UIStroke", _win).Color = P.border
153
+
154
+ -- ── Header bar ────────────────────────────────────────────────────────────────
155
+ local _hdr = _mkFrame({
156
+ Size = UDim2.new(1, 0, 0, HDR_H),
157
+ BackgroundColor3 = P.header,
158
+ BorderSizePixel = 0,
159
+ Parent = _win,
160
+ })
161
+ Instance.new("UIStroke", _hdr).Color = P.border
162
+
163
+ _mkText({
164
+ Text = "◉ PULSE DEV",
165
+ Size = UDim2.new(0, 130, 1, 0),
166
+ Position = UDim2.new(0, 8, 0, 0),
167
+ TextColor3 = P.active,
168
+ TextSize = 13,
169
+ Font = Enum.Font.GothamBold,
170
+ Parent = _hdr,
171
+ })
172
+
173
+ local _monLabel = _mkText({
174
+ Text = "",
175
+ Size = UDim2.new(0, 300, 1, 0),
176
+ Position = UDim2.new(0, 145, 0, 0),
177
+ TextColor3 = P.dim,
178
+ TextSize = 12,
179
+ TextTruncate = Enum.TextTruncate.AtEnd,
180
+ Parent = _hdr,
181
+ })
182
+
183
+ -- ── Header: action buttons and tab buttons (right side) ───────────────────────
184
+ -- From right: ✕ SAVE CLR [CFG] [FEAT] [LOG]
185
+
186
+ local _btnClose = _mkBtn({
187
+ Text = "x",
188
+ Size = UDim2.new(0, 28, 0, 22),
189
+ Position = UDim2.new(1, -32, 0.5, -11),
190
+ TextColor3 = P.ERR,
191
+ TextSize = 14,
192
+ Font = Enum.Font.GothamBold,
193
+ BackgroundColor3 = P.bg2,
194
+ BackgroundTransparency = 0,
195
+ Parent = _hdr,
196
+ }, function() _win.Visible = false end)
197
+ Instance.new("UICorner", _btnClose).CornerRadius = UDim.new(0, 4)
198
+ do local s = Instance.new("UIStroke", _btnClose); s.Color = P.ERR; s.Thickness = 1 end
199
+
200
+ local _btnSave = _mkBtn({
201
+ Text = "SAVE",
202
+ Size = UDim2.new(0, 44, 0, 22),
203
+ Position = UDim2.new(1, -82, 0.5, -11),
204
+ TextColor3 = P.dim,
205
+ TextSize = 12,
206
+ BackgroundColor3 = P.bg2,
207
+ BackgroundTransparency = 0,
208
+ Parent = _hdr,
209
+ }, function() Pulse.Log.save("pulse_dev.log") end)
210
+ Instance.new("UICorner", _btnSave).CornerRadius = UDim.new(0, 4)
211
+
212
+ local _btnClear = _mkBtn({
213
+ Text = "CLR",
214
+ Size = UDim2.new(0, 36, 0, 22),
215
+ Position = UDim2.new(1, -124, 0.5, -11),
216
+ TextColor3 = P.dim,
217
+ TextSize = 12,
218
+ BackgroundColor3 = P.bg2,
219
+ BackgroundTransparency = 0,
220
+ Parent = _hdr,
221
+ }, function() Pulse.Log.clear(); _rerender() end)
222
+ Instance.new("UICorner", _btnClear).CornerRadius = UDim.new(0, 4)
223
+
224
+ -- Tab buttons: LOG / FEAT / CFG
225
+ local _mode = "log"
226
+
227
+ local _btnTabCfg, _cfgStroke = _mkTabBtn("CFG", -168, false)
228
+ local _btnTabFeat, _featStroke = _mkTabBtn("FEAT", -214, false)
229
+ local _btnTabLog, _logStroke = _mkTabBtn("LOG", -260, true)
230
+ _btnTabCfg.Parent = _hdr
231
+ _btnTabFeat.Parent = _hdr
232
+ _btnTabLog.Parent = _hdr
233
+ -- click handlers wired after panels are built
234
+
235
+ -- ── Filter bar (LOG view) ─────────────────────────────────────────────────────
236
+ local _flt = _mkFrame({
237
+ Size = UDim2.new(1, 0, 0, FLT_H),
238
+ Position = UDim2.new(0, 0, 0, HDR_H),
239
+ BackgroundColor3 = P.filter,
240
+ BorderSizePixel = 0,
241
+ Parent = _win,
242
+ })
243
+ Instance.new("UIStroke", _flt).Color = P.border
244
+
245
+ local _showLevels = { TRC=false, DBG=true, INF=true, WRN=true, ERR=true }
246
+ local _lvlBtns = {}
247
+ local LEVELS = { "TRC", "DBG", "INF", "WRN", "ERR" }
248
+ local function _lvlColor(lbl) return P[lbl] or P.text end
249
+
250
+ for i, lbl in ipairs(LEVELS) do
251
+ local on = _showLevels[lbl]
252
+ local btn = _mkBtn({
253
+ Text = lbl,
254
+ Size = UDim2.new(0, 36, 0, 20),
255
+ Position = UDim2.new(0, 6 + (i-1) * 40, 0.5, -10),
256
+ TextColor3 = on and _lvlColor(lbl) or P.dim,
257
+ TextSize = 11,
258
+ Font = Enum.Font.GothamBold,
259
+ BackgroundColor3 = P.bg,
260
+ BackgroundTransparency = on and 0.6 or 0.85,
261
+ Parent = _flt,
262
+ }, nil)
263
+ Instance.new("UICorner", btn).CornerRadius = UDim.new(0, 4)
264
+ local stroke = Instance.new("UIStroke", btn)
265
+ stroke.Color = on and _lvlColor(lbl) or P.border
266
+ stroke.Thickness = 1
267
+ btn.MouseButton1Click:Connect(function()
268
+ _showLevels[lbl] = not _showLevels[lbl]
269
+ local v = _showLevels[lbl]
270
+ btn.TextColor3 = v and _lvlColor(lbl) or P.dim
271
+ btn.BackgroundTransparency = v and 0.6 or 0.85
272
+ stroke.Color = v and _lvlColor(lbl) or P.border
273
+ end)
274
+ _lvlBtns[lbl] = btn
275
+ end
276
+
277
+ local _btnJumpErr = _mkBtn({
278
+ Text = "ERR↑",
279
+ Size = UDim2.new(0, 40, 0, 20),
280
+ Position = UDim2.new(0, 210, 0.5, -10),
281
+ TextColor3 = P.ERR,
282
+ TextSize = 11,
283
+ Font = Enum.Font.GothamBold,
284
+ BackgroundColor3 = P.bg2,
285
+ BackgroundTransparency = 0,
286
+ Parent = _flt,
287
+ }, nil)
288
+ Instance.new("UICorner", _btnJumpErr).CornerRadius = UDim.new(0, 4)
289
+ Instance.new("UIStroke", _btnJumpErr).Color = P.ERR
290
+
291
+ _mkText({
292
+ Text = "search:", Size = UDim2.new(0, 50, 1, 0),
293
+ Position = UDim2.new(0, 257, 0, 0), TextColor3 = P.dim, TextSize = 12, Parent = _flt,
294
+ })
295
+
296
+ local _tagInput = Instance.new("TextBox")
297
+ _tagInput.PlaceholderText = "tag or message…"
298
+ _tagInput.PlaceholderColor3 = P.dim
299
+ _tagInput.Text = ""
300
+ _tagInput.Size = UDim2.new(0, 190, 0, 20)
301
+ _tagInput.Position = UDim2.new(0, 309, 0.5, -10)
302
+ _tagInput.BackgroundColor3 = P.bg2
303
+ _tagInput.BorderSizePixel = 0
304
+ _tagInput.TextColor3 = P.tag
305
+ _tagInput.Font = FONT
306
+ _tagInput.TextSize = 12
307
+ _tagInput.TextXAlignment = Enum.TextXAlignment.Left
308
+ _tagInput.TextEditable = true
309
+ _tagInput.ClearTextOnFocus = false
310
+ _tagInput.Parent = _flt
311
+ do
312
+ local p = Instance.new("UIPadding", _tagInput); p.PaddingLeft = UDim.new(0, 6)
313
+ Instance.new("UICorner", _tagInput).CornerRadius = UDim.new(0, 4)
314
+ Instance.new("UIStroke", _tagInput).Color = P.border
315
+ end
316
+
317
+ local _countLabel = _mkText({
318
+ Text = "0 entries", Size = UDim2.new(0, 110, 1, 0),
319
+ Position = UDim2.new(1, -116, 0, 0), TextColor3 = P.dim, TextSize = 11,
320
+ TextXAlignment = Enum.TextXAlignment.Right, Parent = _flt,
321
+ })
322
+
323
+ local _btnCopyAll = _mkBtn({
324
+ Text = "COPY", Size = UDim2.new(0, 44, 0, 20),
325
+ Position = UDim2.new(0, 506, 0.5, -10), TextColor3 = P.dim, TextSize = 12,
326
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0, Parent = _flt,
327
+ }, function()
328
+ local all = Pulse.Log.getNewEntries(0)
329
+ local q = _tagInput.Text:lower():gsub("^%s+",""):gsub("%s+$","")
330
+ local lines = {}
331
+ for _, e in ipairs(all) do
332
+ if _showLevels[e.lbl] then
333
+ local pass = q == "" or e.tag:lower():find(q,1,true) or e.msg:lower():find(q,1,true)
334
+ if pass then lines[#lines+1] = e.line end
335
+ end
336
+ end
337
+ local feedback
338
+ if #lines == 0 then
339
+ feedback = "nothing visible"
340
+ elseif _copyText(table.concat(lines, "\n")) then
341
+ feedback = #lines .. " copied!"
342
+ else
343
+ feedback = "no clipboard fn"
344
+ end
345
+ _countLabel.Text = feedback
346
+ task.delay(1.5, function()
347
+ if _countLabel and _countLabel.Parent then _countLabel.Text = _totalShown .. " shown" end
348
+ end)
349
+ end)
350
+ Instance.new("UICorner", _btnCopyAll).CornerRadius = UDim.new(0, 4)
351
+
352
+ -- ── Log scroll area ───────────────────────────────────────────────────────────
353
+ local _scroll = Instance.new("ScrollingFrame")
354
+ _scroll.Size = UDim2.new(1, 0, 0, LOG_H)
355
+ _scroll.Position = UDim2.new(0, 0, 0, HDR_H + FLT_H)
356
+ _scroll.BackgroundColor3 = P.bg2
357
+ _scroll.BorderSizePixel = 0
358
+ _scroll.ScrollBarThickness = 5
359
+ _scroll.ScrollBarImageColor3 = P.border
360
+ _scroll.CanvasSize = UDim2.new(0, 0, 0, 0)
361
+ _scroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
362
+ _scroll.Parent = _win
363
+
364
+ local _list = Instance.new("UIListLayout", _scroll)
365
+ _list.SortOrder = Enum.SortOrder.LayoutOrder; _list.Padding = UDim.new(0, 0)
366
+
367
+ local _pad = Instance.new("UIPadding", _scroll)
368
+ _pad.PaddingLeft = UDim.new(0, 4); _pad.PaddingRight = UDim.new(0, 6)
369
+
370
+ -- ── Entry rendering ───────────────────────────────────────────────────────────
371
+ local MAX_LABELS = 400
372
+ local _labels = {}
373
+ local _lastId = 0
374
+ local _totalShown = 0
375
+
376
+ -- One-line header (no data — data lives in the expanded panel)
377
+ local function _entryHeaderText(e)
378
+ local hasData = e.data ~= nil
379
+ local dot = hasData and ('<font color="'..DATA_HEX..'"> ●</font>') or ""
380
+ return string.format(
381
+ '<font color="%s">%s</font> <font color="%s">%s</font> <font color="%s">%s</font> %s%s',
382
+ TS_HEX, _esc(e.ts),
383
+ LEVEL_HEX[e.lbl] or MSG_HEX, "["..e.lbl.."]",
384
+ TAG_HEX, _esc(e.tag),
385
+ '<font color="'..MSG_HEX..'">'.. _esc(e.msg) .."</font>",
386
+ dot
387
+ )
388
+ end
389
+
390
+ -- Plain-text value formatter for the expanded data panel
391
+ local function _fmtVal(v)
392
+ if type(v) == "number" then
393
+ return (math.floor(v) == v) and tostring(math.floor(v)) or string.format("%.4g", v)
394
+ elseif type(v) == "string" then return '"' .. v .. '"'
395
+ else return tostring(v)
396
+ end
397
+ end
398
+
399
+ -- Legacy: still used by COPY ALL
400
+ local function _entryRichText(e)
401
+ local dataStr = ""
402
+ if e.data ~= nil then
403
+ if type(e.data) == "table" then
404
+ local ok, s = pcall(function() return game:GetService("HttpService"):JSONEncode(e.data) end)
405
+ dataStr = ok and (" <font color=\""..DATA_HEX.."\">".. _esc(s) .."</font>") or ""
406
+ else
407
+ dataStr = " <font color=\""..DATA_HEX.."\">" .. _esc(tostring(e.data)) .. "</font>"
408
+ end
409
+ end
410
+ return string.format(
411
+ '<font color="%s">%s</font> <font color="%s">%s</font> <font color="%s">%s</font> %s%s',
412
+ TS_HEX, _esc(e.ts),
413
+ LEVEL_HEX[e.lbl] or MSG_HEX, "["..e.lbl.."]",
414
+ TAG_HEX, _esc(e.tag),
415
+ '<font color="'..MSG_HEX..'">'.. _esc(e.msg) .."</font>",
416
+ dataStr
417
+ )
418
+ end
419
+
420
+ local function _entryPassesFilter(e)
421
+ if not _showLevels[e.lbl] then return false end
422
+ local q = _tagInput.Text:lower():gsub("^%s+",""):gsub("%s+$","")
423
+ if q == "" then return true end
424
+ return e.tag:lower():find(q,1,true) ~= nil or e.msg:lower():find(q,1,true) ~= nil
425
+ end
426
+
427
+ local function _addEntry(e)
428
+ if not _entryPassesFilter(e) then return end
429
+
430
+ -- ── Outer container — grows when expanded ──────────────────────────────
431
+ local container = _mkFrame({
432
+ BackgroundTransparency = 1,
433
+ Size = UDim2.new(1, -10, 0, 0),
434
+ AutomaticSize = Enum.AutomaticSize.Y,
435
+ LayoutOrder = e.id,
436
+ Parent = _scroll,
437
+ })
438
+ local _innerList = Instance.new("UIListLayout", container)
439
+ _innerList.SortOrder = Enum.SortOrder.LayoutOrder
440
+ _innerList.Padding = UDim.new(0, 0)
441
+
442
+ -- ── Header row (one-line, click to expand) ─────────────────────────────
443
+ local header = _mkFrame({
444
+ BackgroundTransparency = 1,
445
+ Size = UDim2.new(1, 0, 0, ENTRY_H),
446
+ LayoutOrder = 0,
447
+ Parent = container,
448
+ })
449
+
450
+ local indicator = _mkText({
451
+ Text = "▶",
452
+ Size = UDim2.new(0, 12, 1, 0),
453
+ Position = UDim2.new(0, 2, 0, 0),
454
+ TextColor3 = P.dim,
455
+ TextSize = 10,
456
+ Font = Enum.Font.GothamBold,
457
+ Parent = header,
458
+ })
459
+
460
+ local summary = _mkText({
461
+ Text = _entryHeaderText(e),
462
+ Size = UDim2.new(1, -14, 1, 0),
463
+ Position = UDim2.new(0, 14, 0, 0),
464
+ TextColor3 = P.text,
465
+ TextSize = FS,
466
+ RichText = true,
467
+ TextTruncate = Enum.TextTruncate.AtEnd,
468
+ TextYAlignment = Enum.TextYAlignment.Center,
469
+ Parent = header,
470
+ })
471
+
472
+ -- ── Expanded panel ─────────────────────────────────────────────────────
473
+ local expPanel = _mkFrame({
474
+ BackgroundColor3 = P.bg3,
475
+ BorderSizePixel = 0,
476
+ Size = UDim2.new(1, 0, 0, 0), -- height=0 when collapsed
477
+ AutomaticSize = Enum.AutomaticSize.None,
478
+ ClipsDescendants = true,
479
+ LayoutOrder = 1,
480
+ Parent = container,
481
+ })
482
+ Instance.new("UIStroke", expPanel).Color = P.border
483
+
484
+ local _expList = Instance.new("UIListLayout", expPanel)
485
+ _expList.SortOrder = Enum.SortOrder.LayoutOrder
486
+ _expList.Padding = UDim.new(0, 1)
487
+
488
+ local _expPad = Instance.new("UIPadding", expPanel)
489
+ _expPad.PaddingLeft = UDim.new(0, 14)
490
+ _expPad.PaddingRight = UDim.new(0, 8)
491
+ _expPad.PaddingTop = UDim.new(0, 3)
492
+ _expPad.PaddingBottom = UDim.new(0, 4)
493
+
494
+ -- Full message (non-truncated, wraps)
495
+ _mkText({
496
+ Text = string.format("[%s] [%s] [%s] %s", _esc(e.ts), e.lbl, _esc(e.tag), _esc(e.msg)),
497
+ Size = UDim2.new(1, -52, 0, 0),
498
+ AutomaticSize = Enum.AutomaticSize.Y,
499
+ TextColor3 = P.text,
500
+ TextSize = FS,
501
+ TextWrapped = true,
502
+ LayoutOrder = 1,
503
+ Parent = expPanel,
504
+ })
505
+
506
+ -- Data rows (one per key, sorted)
507
+ if e.data ~= nil then
508
+ local order = 2
509
+ if type(e.data) == "table" then
510
+ local keys = {}
511
+ for k in pairs(e.data) do keys[#keys+1] = k end
512
+ table.sort(keys, function(a,b) return tostring(a) < tostring(b) end)
513
+ for _, k in ipairs(keys) do
514
+ _mkText({
515
+ Text = tostring(k) .. " = " .. _fmtVal(e.data[k]),
516
+ Size = UDim2.new(1, 0, 0, ENTRY_H - 2),
517
+ TextColor3 = P.dim,
518
+ TextSize = FS - 1,
519
+ LayoutOrder = order,
520
+ Parent = expPanel,
521
+ })
522
+ order = order + 1
523
+ end
524
+ else
525
+ _mkText({
526
+ Text = _fmtVal(e.data),
527
+ Size = UDim2.new(1, 0, 0, ENTRY_H - 2),
528
+ TextColor3 = P.dim,
529
+ TextSize = FS - 1,
530
+ LayoutOrder = 2,
531
+ Parent = expPanel,
532
+ })
533
+ end
534
+ end
535
+
536
+ -- Copy button row
537
+ local copyRow = _mkFrame({
538
+ BackgroundTransparency = 1,
539
+ Size = UDim2.new(1, 0, 0, 22),
540
+ LayoutOrder = 99,
541
+ Parent = expPanel,
542
+ })
543
+ local copyBtn = _mkBtn({
544
+ Text = "COPY",
545
+ Size = UDim2.new(0, 44, 0, 18),
546
+ Position = UDim2.new(0, 0, 0.5, -9),
547
+ TextColor3 = P.dim,
548
+ TextSize = 11,
549
+ BackgroundColor3 = P.bg2,
550
+ BackgroundTransparency = 0,
551
+ Parent = copyRow,
552
+ }, function()
553
+ _copyText(e.line)
554
+ copyBtn.Text = "✓"
555
+ task.delay(0.8, function() if copyBtn and copyBtn.Parent then copyBtn.Text = "COPY" end end)
556
+ end)
557
+ Instance.new("UICorner", copyBtn).CornerRadius = UDim.new(0, 3)
558
+
559
+ -- ── Toggle expand/collapse ─────────────────────────────────────────────
560
+ local _expanded = false
561
+ local function _toggle()
562
+ _expanded = not _expanded
563
+ if _expanded then
564
+ expPanel.AutomaticSize = Enum.AutomaticSize.Y
565
+ indicator.Text = "▼"
566
+ indicator.TextColor3 = P.active
567
+ else
568
+ expPanel.AutomaticSize = Enum.AutomaticSize.None
569
+ expPanel.Size = UDim2.new(1, 0, 0, 0)
570
+ indicator.Text = "▶"
571
+ indicator.TextColor3 = P.dim
572
+ end
573
+ end
574
+
575
+ -- Whole header row is the click target (Frame.InputBegan; TextLabels pass input through)
576
+ header.InputBegan:Connect(function(inp)
577
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then _toggle() end
578
+ end)
579
+
580
+ _labels[#_labels+1] = { container = container, e = e }
581
+ _totalShown = _totalShown + 1
582
+ if #_labels > MAX_LABELS then
583
+ local old = table.remove(_labels, 1)
584
+ if old and old.container and old.container.Parent then old.container:Destroy() end
585
+ end
586
+ end
587
+
588
+ local function _scrollToBottom()
589
+ task.wait()
590
+ local maxY = math.max(0, _scroll.AbsoluteCanvasSize.Y - _scroll.AbsoluteWindowSize.Y)
591
+ _scroll.CanvasPosition = Vector2.new(0, maxY)
592
+ end
593
+
594
+ local function _isAtBottom()
595
+ local maxY = _scroll.AbsoluteCanvasSize.Y - _scroll.AbsoluteWindowSize.Y
596
+ return _scroll.CanvasPosition.Y >= maxY - 20
597
+ end
598
+
599
+ _rerender = function()
600
+ for _, item in ipairs(_labels) do
601
+ if item and item.container and item.container.Parent then item.container:Destroy() end
602
+ end
603
+ _labels = {}; _totalShown = 0; _lastId = 0
604
+ local all = Pulse.Log.getNewEntries(0)
605
+ for _, e in ipairs(all) do
606
+ _lastId = math.max(_lastId, e.id)
607
+ _addEntry(e)
608
+ end
609
+ _countLabel.Text = _totalShown .. " shown"
610
+ _scrollToBottom()
611
+ end
612
+
613
+ -- ── Features panel (FEAT tab) ─────────────────────────────────────────────────
614
+ local _featPanel = _mkFrame({
615
+ Size = UDim2.new(1, 0, 0, BODY_H),
616
+ Position = UDim2.new(0, 0, 0, HDR_H),
617
+ BackgroundColor3 = P.bg,
618
+ BorderSizePixel = 0,
619
+ Visible = false,
620
+ Parent = _win,
621
+ })
622
+
623
+ local _featScroll = Instance.new("ScrollingFrame")
624
+ _featScroll.Size = UDim2.new(1, 0, 1, -32)
625
+ _featScroll.Position = UDim2.new(0, 0, 0, 32)
626
+ _featScroll.BackgroundColor3 = P.bg2
627
+ _featScroll.BorderSizePixel = 0
628
+ _featScroll.ScrollBarThickness = 5
629
+ _featScroll.ScrollBarImageColor3 = P.border
630
+ _featScroll.CanvasSize = UDim2.new(0, 0, 0, 0)
631
+ _featScroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
632
+ _featScroll.Parent = _featPanel
633
+
634
+ Instance.new("UIListLayout", _featScroll).SortOrder = Enum.SortOrder.LayoutOrder
635
+ do
636
+ local p = Instance.new("UIPadding", _featScroll)
637
+ p.PaddingLeft = UDim.new(0, 6); p.PaddingRight = UDim.new(0, 6); p.PaddingTop = UDim.new(0, 4)
638
+ end
639
+
640
+ local _featBar = _mkFrame({
641
+ Size = UDim2.new(1, 0, 0, 30), BackgroundColor3 = P.filter,
642
+ BorderSizePixel = 0, Parent = _featPanel,
643
+ })
644
+ Instance.new("UIStroke", _featBar).Color = P.border
645
+
646
+ _mkText({
647
+ Text = "Feature Toggles", Size = UDim2.new(1, -140, 1, 0),
648
+ Position = UDim2.new(0, 8, 0, 0), TextColor3 = P.dim, TextSize = 12,
649
+ Parent = _featBar,
650
+ })
651
+
652
+ local _featStatusLabel = _mkText({
653
+ Text = "", Size = UDim2.new(0, 100, 1, 0),
654
+ Position = UDim2.new(1, -300, 0, 0), TextColor3 = P.dim, TextSize = 11,
655
+ TextXAlignment = Enum.TextXAlignment.Right, Parent = _featBar,
656
+ })
657
+
658
+ local _btnEnableAll = _mkBtn({
659
+ Text = "ALL ON", Size = UDim2.new(0, 56, 0, 20),
660
+ Position = UDim2.new(1, -130, 0.5, -10), TextColor3 = P.on, TextSize = 11,
661
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0, Parent = _featBar,
662
+ }, nil)
663
+ Instance.new("UICorner", _btnEnableAll).CornerRadius = UDim.new(0, 4)
664
+
665
+ local _btnDisableAll = _mkBtn({
666
+ Text = "ALL OFF", Size = UDim2.new(0, 56, 0, 20),
667
+ Position = UDim2.new(1, -68, 0.5, -10), TextColor3 = P.off, TextSize = 11,
668
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0, Parent = _featBar,
669
+ }, nil)
670
+ Instance.new("UICorner", _btnDisableAll).CornerRadius = UDim.new(0, 4)
671
+
672
+ local _featRows = {}
673
+
674
+ local function _buildFeatRows()
675
+ for _, r in ipairs(_featRows) do
676
+ if r.row and r.row.Parent then r.row:Destroy() end
677
+ end
678
+ _featRows = {}
679
+
680
+ local toggles = _getToggles()
681
+ if not toggles or next(toggles) == nil then
682
+ local hint = _mkText({
683
+ Text = "No feature toggles registered yet — open the main menu once, then switch back to FEAT.",
684
+ Size = UDim2.new(1, -12, 0, 60), Position = UDim2.new(0, 6, 0, 6),
685
+ TextColor3 = P.dim, TextSize = 12, TextWrapped = true,
686
+ TextYAlignment = Enum.TextYAlignment.Top, LayoutOrder = 0, Parent = _featScroll,
687
+ })
688
+ _featRows[1] = { row = hint, id = "__hint" }
689
+ _featStatusLabel.Text = "not ready"
690
+ return
691
+ end
692
+
693
+ local ids = {}
694
+ for id in pairs(toggles) do ids[#ids+1] = id end
695
+ table.sort(ids)
696
+
697
+ for order, id in ipairs(ids) do
698
+ local toggle = toggles[id]
699
+ local val = toggle.Value
700
+
701
+ local row = _mkFrame({
702
+ Size = UDim2.new(1, 0, 0, 26), BackgroundColor3 = P.bg3,
703
+ BorderSizePixel = 0, LayoutOrder = order, Parent = _featScroll,
704
+ })
705
+
706
+ local nameLabel = _mkText({
707
+ Text = id, Size = UDim2.new(1, -80, 1, 0), Position = UDim2.new(0, 8, 0, 0),
708
+ TextColor3 = val and P.text or P.dim, TextSize = 12,
709
+ TextTruncate = Enum.TextTruncate.AtEnd, Parent = row,
710
+ })
711
+
712
+ local stateBtn = _mkBtn({
713
+ Text = val and "ON" or "OFF",
714
+ Size = UDim2.new(0, 52, 0, 20),
715
+ Position = UDim2.new(1, -58, 0.5, -10),
716
+ TextColor3 = val and P.on or P.off,
717
+ TextSize = 11, Font = Enum.Font.GothamBold,
718
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0,
719
+ Parent = row,
720
+ }, nil)
721
+ Instance.new("UICorner", stateBtn).CornerRadius = UDim.new(0, 4)
722
+ local stateStroke = Instance.new("UIStroke", stateBtn)
723
+ stateStroke.Color = val and P.on or P.off
724
+ stateStroke.Thickness = 1
725
+
726
+ local rowEntry = { row = row, id = id, nameLabel = nameLabel, stateBtn = stateBtn, stateStroke = stateStroke }
727
+ _featRows[#_featRows+1] = rowEntry
728
+
729
+ local function _flipToggle()
730
+ local t = _getToggles()
731
+ if not t or not t[id] then return end
732
+ local newVal = not t[id].Value
733
+ pcall(function() t[id]:SetValue(newVal) end)
734
+ stateBtn.Text = newVal and "ON" or "OFF"
735
+ stateBtn.TextColor3 = newVal and P.on or P.off
736
+ stateStroke.Color = newVal and P.on or P.off
737
+ nameLabel.TextColor3 = newVal and P.text or P.dim
738
+ Pulse.Log.debug("feat-panel", (newVal and "enabled" or "disabled") .. ": " .. id)
739
+ end
740
+
741
+ stateBtn.MouseButton1Click:Connect(_flipToggle)
742
+ row.InputBegan:Connect(function(inp)
743
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then _flipToggle() end
744
+ end)
745
+ end
746
+
747
+ _featStatusLabel.Text = #ids .. " toggles"
748
+ end
749
+
750
+ _btnEnableAll.MouseButton1Click:Connect(function()
751
+ local toggles = _getToggles()
752
+ if not toggles then return end
753
+ for _, r in ipairs(_featRows) do
754
+ if r.id ~= "__hint" then
755
+ pcall(function() toggles[r.id]:SetValue(true) end)
756
+ if r.stateBtn then r.stateBtn.Text = "ON"; r.stateBtn.TextColor3 = P.on end
757
+ if r.stateStroke then r.stateStroke.Color = P.on end
758
+ if r.nameLabel then r.nameLabel.TextColor3 = P.text end
759
+ end
760
+ end
761
+ Pulse.Log.info("feat-panel", "all features enabled")
762
+ end)
763
+
764
+ _btnDisableAll.MouseButton1Click:Connect(function()
765
+ local toggles = _getToggles()
766
+ if not toggles then return end
767
+ for _, r in ipairs(_featRows) do
768
+ if r.id ~= "__hint" then
769
+ pcall(function() toggles[r.id]:SetValue(false) end)
770
+ if r.stateBtn then r.stateBtn.Text = "OFF"; r.stateBtn.TextColor3 = P.off end
771
+ if r.stateStroke then r.stateStroke.Color = P.off end
772
+ if r.nameLabel then r.nameLabel.TextColor3 = P.dim end
773
+ end
774
+ end
775
+ Pulse.Log.info("feat-panel", "all features disabled")
776
+ end)
777
+
778
+ -- ── Config snapshot panel (CFG tab) ───────────────────────────────────────────
779
+ -- Shows all Toggles + Options current values + Monitor runtime values.
780
+ -- Use COPY to grab a plain-text snapshot for bug reports.
781
+
782
+ local _cfgPanel = _mkFrame({
783
+ Size = UDim2.new(1, 0, 0, BODY_H),
784
+ Position = UDim2.new(0, 0, 0, HDR_H),
785
+ BackgroundColor3 = P.bg,
786
+ BorderSizePixel = 0,
787
+ Visible = false,
788
+ Parent = _win,
789
+ })
790
+
791
+ local _cfgScroll = Instance.new("ScrollingFrame")
792
+ _cfgScroll.Size = UDim2.new(1, 0, 1, -32)
793
+ _cfgScroll.Position = UDim2.new(0, 0, 0, 32)
794
+ _cfgScroll.BackgroundColor3 = P.bg2
795
+ _cfgScroll.BorderSizePixel = 0
796
+ _cfgScroll.ScrollBarThickness = 5
797
+ _cfgScroll.ScrollBarImageColor3 = P.border
798
+ _cfgScroll.CanvasSize = UDim2.new(0, 0, 0, 0)
799
+ _cfgScroll.AutomaticCanvasSize = Enum.AutomaticSize.Y
800
+ _cfgScroll.Parent = _cfgPanel
801
+ Instance.new("UIListLayout", _cfgScroll).SortOrder = Enum.SortOrder.LayoutOrder
802
+ do
803
+ local p = Instance.new("UIPadding", _cfgScroll)
804
+ p.PaddingLeft = UDim.new(0, 6); p.PaddingRight = UDim.new(0, 6); p.PaddingTop = UDim.new(0, 4)
805
+ end
806
+
807
+ local _cfgBar = _mkFrame({
808
+ Size = UDim2.new(1, 0, 0, 30), BackgroundColor3 = P.filter,
809
+ BorderSizePixel = 0, Parent = _cfgPanel,
810
+ })
811
+ Instance.new("UIStroke", _cfgBar).Color = P.border
812
+
813
+ _mkText({
814
+ Text = "Config Snapshot", Size = UDim2.new(1, -160, 1, 0),
815
+ Position = UDim2.new(0, 8, 0, 0), TextColor3 = P.dim, TextSize = 12,
816
+ Parent = _cfgBar,
817
+ })
818
+
819
+ local _cfgCopyLabel = _mkText({
820
+ Text = "", Size = UDim2.new(0, 80, 1, 0),
821
+ Position = UDim2.new(1, -160, 0, 0), TextColor3 = P.dim, TextSize = 11,
822
+ TextXAlignment = Enum.TextXAlignment.Right, Parent = _cfgBar,
823
+ })
824
+
825
+ local _btnCfgRefresh = _mkBtn({
826
+ Text = "REFRESH", Size = UDim2.new(0, 64, 0, 20),
827
+ Position = UDim2.new(1, -130, 0.5, -10), TextColor3 = P.dim, TextSize = 11,
828
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0, Parent = _cfgBar,
829
+ }, nil) -- wired below
830
+ Instance.new("UICorner", _btnCfgRefresh).CornerRadius = UDim.new(0, 4)
831
+
832
+ local _btnCfgCopy = _mkBtn({
833
+ Text = "COPY", Size = UDim2.new(0, 44, 0, 20),
834
+ Position = UDim2.new(1, -60, 0.5, -10), TextColor3 = P.dim, TextSize = 11,
835
+ BackgroundColor3 = P.bg2, BackgroundTransparency = 0, Parent = _cfgBar,
836
+ }, nil) -- wired below
837
+ Instance.new("UICorner", _btnCfgCopy).CornerRadius = UDim.new(0, 4)
838
+
839
+ local _cfgRows = {} -- { row = Frame } list for cleanup
840
+
841
+ local function _addCfgSection(title, order)
842
+ local row = _mkFrame({
843
+ Size = UDim2.new(1, 0, 0, 22), BackgroundColor3 = P.section,
844
+ BorderSizePixel = 0, LayoutOrder = order, Parent = _cfgScroll,
845
+ })
846
+ _mkText({
847
+ Text = title, Size = UDim2.new(1, -8, 1, 0), Position = UDim2.new(0, 8, 0, 0),
848
+ TextColor3 = P.active, TextSize = 11, Font = Enum.Font.GothamBold, Parent = row,
849
+ })
850
+ _cfgRows[#_cfgRows+1] = { row = row }
851
+ return order + 1
852
+ end
853
+
854
+ local function _addCfgRow(key, value, isOn, order)
855
+ local row = _mkFrame({
856
+ Size = UDim2.new(1, 0, 0, 22), BackgroundColor3 = P.bg3,
857
+ BorderSizePixel = 0, LayoutOrder = order, Parent = _cfgScroll,
858
+ })
859
+ _mkText({
860
+ Text = tostring(key), Size = UDim2.new(1, -90, 1, 0), Position = UDim2.new(0, 8, 0, 0),
861
+ TextColor3 = P.text, TextSize = 11, TextTruncate = Enum.TextTruncate.AtEnd, Parent = row,
862
+ })
863
+ local valColor = P.dim
864
+ if isOn == true then valColor = P.on end
865
+ if isOn == false then valColor = P.off end
866
+ _mkText({
867
+ Text = tostring(value), Size = UDim2.new(0, 80, 1, 0),
868
+ Position = UDim2.new(1, -88, 0, 0),
869
+ TextColor3 = valColor, TextSize = 11,
870
+ TextXAlignment = Enum.TextXAlignment.Right,
871
+ TextTruncate = Enum.TextTruncate.AtEnd,
872
+ Parent = row,
873
+ })
874
+ _cfgRows[#_cfgRows+1] = { row = row }
875
+ return order + 1
876
+ end
877
+
878
+ local function _buildCfgContent()
879
+ for _, r in ipairs(_cfgRows) do
880
+ if r.row and r.row.Parent then r.row:Destroy() end
881
+ end
882
+ _cfgRows = {}
883
+
884
+ local order = 1
885
+ local copyLines = {}
886
+
887
+ -- Toggles section
888
+ local toggles = _getToggles()
889
+ order = _addCfgSection("TOGGLES", order)
890
+ copyLines[#copyLines+1] = "=== TOGGLES ==="
891
+ if toggles and next(toggles) then
892
+ local ids = {}
893
+ for id in pairs(toggles) do ids[#ids+1] = id end
894
+ table.sort(ids)
895
+ for _, id in ipairs(ids) do
896
+ local val = toggles[id].Value
897
+ order = _addCfgRow(id, val and "ON" or "OFF", val, order)
898
+ copyLines[#copyLines+1] = id .. ": " .. (val and "ON" or "OFF")
899
+ end
900
+ else
901
+ order = _addCfgRow("(not ready)", "open menu first", nil, order)
902
+ copyLines[#copyLines+1] = "(not ready)"
903
+ end
904
+
905
+ -- Options section
906
+ local options = _getOptions()
907
+ order = _addCfgSection("OPTIONS", order)
908
+ copyLines[#copyLines+1] = ""
909
+ copyLines[#copyLines+1] = "=== OPTIONS ==="
910
+ if options and next(options) then
911
+ local ids = {}
912
+ for id in pairs(options) do ids[#ids+1] = id end
913
+ table.sort(ids)
914
+ for _, id in ipairs(ids) do
915
+ local val = options[id].Value
916
+ order = _addCfgRow(id, tostring(val), nil, order)
917
+ copyLines[#copyLines+1] = id .. ": " .. tostring(val)
918
+ end
919
+ else
920
+ order = _addCfgRow("(not ready)", "open menu first", nil, order)
921
+ copyLines[#copyLines+1] = "(not ready)"
922
+ end
923
+
924
+ -- Monitor section
925
+ order = _addCfgSection("MONITOR (runtime)", order)
926
+ copyLines[#copyLines+1] = ""
927
+ copyLines[#copyLines+1] = "=== MONITOR ==="
928
+ local monitors = Pulse.Monitor.getAll()
929
+ local mkeys = {}
930
+ for k in pairs(monitors) do mkeys[#mkeys+1] = k end
931
+ table.sort(mkeys)
932
+ if #mkeys > 0 then
933
+ for _, k in ipairs(mkeys) do
934
+ order = _addCfgRow(k, tostring(monitors[k]), nil, order)
935
+ copyLines[#copyLines+1] = k .. ": " .. tostring(monitors[k])
936
+ end
937
+ else
938
+ order = _addCfgRow("(no monitor values)", "", nil, order)
939
+ end
940
+
941
+ return table.concat(copyLines, "\n")
942
+ end
943
+
944
+ local _cfgCopyBuffer = ""
945
+
946
+ _btnCfgRefresh.MouseButton1Click:Connect(function()
947
+ _cfgCopyBuffer = _buildCfgContent()
948
+ _cfgCopyLabel.Text = "refreshed"
949
+ task.delay(1.2, function() if _cfgCopyLabel and _cfgCopyLabel.Parent then _cfgCopyLabel.Text = "" end end)
950
+ end)
951
+
952
+ _btnCfgCopy.MouseButton1Click:Connect(function()
953
+ if _cfgCopyBuffer == "" then _cfgCopyBuffer = _buildCfgContent() end
954
+ local feedback
955
+ if _copyText(_cfgCopyBuffer) then
956
+ feedback = "copied!"
957
+ else
958
+ feedback = "no clipboard fn"
959
+ end
960
+ _cfgCopyLabel.Text = feedback
961
+ task.delay(1.5, function() if _cfgCopyLabel and _cfgCopyLabel.Parent then _cfgCopyLabel.Text = "" end end)
962
+ end)
963
+
964
+ -- ── Tab switching ─────────────────────────────────────────────────────────────
965
+ -- Tools are now in the WindUI Dev tab (pulse/dev/ui/9_DevPanel.lua).
966
+ local _allTabBtns = {
967
+ { btn = _btnTabLog, stroke = _logStroke, tab = "log" },
968
+ { btn = _btnTabFeat, stroke = _featStroke, tab = "feat" },
969
+ { btn = _btnTabCfg, stroke = _cfgStroke, tab = "cfg" },
970
+ }
971
+
972
+ local function _setTab(tab)
973
+ _mode = tab
974
+ local isLog = tab == "log"
975
+ local isFeat = tab == "feat"
976
+ local isCfg = tab == "cfg"
977
+
978
+ _flt.Visible = isLog
979
+ _scroll.Visible = isLog
980
+ _featPanel.Visible = isFeat
981
+ _cfgPanel.Visible = isCfg
982
+ _btnClear.Visible = isLog
983
+ _btnSave.Visible = isLog
984
+
985
+ for _, t in ipairs(_allTabBtns) do
986
+ local active = t.tab == tab
987
+ t.btn.TextColor3 = active and P.active or P.dim
988
+ t.stroke.Color = active and P.active or P.border
989
+ end
990
+
991
+ if isFeat then _buildFeatRows() end
992
+ if isLog then _rerender() end
993
+ if isCfg then _cfgCopyBuffer = _buildCfgContent() end
994
+
995
+ _enableMouseForOverlay()
996
+ end
997
+
998
+ _btnTabLog.MouseButton1Click:Connect(function() _setTab("log") end)
999
+ _btnTabFeat.MouseButton1Click:Connect(function() _setTab("feat") end)
1000
+ _btnTabCfg.MouseButton1Click:Connect(function() _setTab("cfg") end)
1001
+
1002
+ -- ── Shared UIS reference ─────────────────────────────────────────────────────
1003
+ local _UIS = game:GetService("UserInputService")
1004
+
1005
+ -- ── Mouse cursor ──────────────────────────────────────────────────────────────
1006
+ -- Ensure the cursor is visible and unlocked while the overlay is open.
1007
+ -- Games can hide/lock the cursor; without this the overlay is unclickable.
1008
+ _enableMouseForOverlay = function()
1009
+ pcall(function() _UIS.MouseIconEnabled = true end)
1010
+ pcall(function() _UIS.MouseBehavior = Enum.MouseBehavior.Default end)
1011
+ end
1012
+
1013
+ -- ── Floating anchor (TanStack-style, draggable corner toggle) ─────────────────
1014
+ -- Click to open/close the dev window. Drag to reposition. Insert key also toggles.
1015
+
1016
+ local _anchor = _mkFrame({
1017
+ Name = "DevAnchor",
1018
+ Size = UDim2.new(0, 72, 0, 26),
1019
+ Position = UDim2.new(1, -82, 1, -36),
1020
+ BackgroundColor3 = P.bg,
1021
+ BorderSizePixel = 0,
1022
+ ZIndex = 10,
1023
+ Parent = _gui,
1024
+ })
1025
+ Instance.new("UICorner", _anchor).CornerRadius = UDim.new(0, 7)
1026
+ local _anchorStroke = Instance.new("UIStroke", _anchor)
1027
+ _anchorStroke.Color = P.border
1028
+ _anchorStroke.Thickness = 1
1029
+
1030
+ local _anchorBtn = _mkBtn({
1031
+ Text = "◉ DEV",
1032
+ Size = UDim2.new(1, 0, 1, 0),
1033
+ TextColor3 = P.dim,
1034
+ TextSize = 12,
1035
+ Font = Enum.Font.GothamBold,
1036
+ BackgroundTransparency = 1,
1037
+ ZIndex = 11,
1038
+ Parent = _anchor,
1039
+ }, nil)
1040
+
1041
+ local function _updateAnchorState()
1042
+ local open = _win.Visible
1043
+ _anchorBtn.TextColor3 = open and P.active or P.dim
1044
+ _anchorStroke.Color = open and P.active or P.border
1045
+ end
1046
+
1047
+ local _anchorDragging, _anchorDragStart, _anchorPosStart = false, nil, nil
1048
+ local _anchorMoved = false
1049
+
1050
+ -- Wire to _anchorBtn (TextButton), not _anchor (Frame) — TextButton consumes input,
1051
+ -- so Frame.InputBegan never fires when clicking the button area.
1052
+ _anchorBtn.InputBegan:Connect(function(inp)
1053
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then
1054
+ _anchorDragging = true
1055
+ _anchorMoved = false
1056
+ _anchorDragStart = inp.Position
1057
+ _anchorPosStart = _anchor.Position
1058
+ end
1059
+ end)
1060
+
1061
+ _anchorBtn.InputEnded:Connect(function(inp)
1062
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then
1063
+ _anchorDragging = false
1064
+ if not _anchorMoved then
1065
+ _win.Visible = not _win.Visible
1066
+ if _win.Visible then _setTab(_mode) end
1067
+ _updateAnchorState()
1068
+ end
1069
+ _anchorMoved = false
1070
+ end
1071
+ end)
1072
+
1073
+ _UIS.InputChanged:Connect(function(inp)
1074
+ if _anchorDragging and inp.UserInputType == Enum.UserInputType.MouseMovement then
1075
+ local d = inp.Position - _anchorDragStart
1076
+ if math.abs(d.X) > 4 or math.abs(d.Y) > 4 then _anchorMoved = true end
1077
+ if _anchorMoved then
1078
+ _anchor.Position = UDim2.new(
1079
+ _anchorPosStart.X.Scale, _anchorPosStart.X.Offset + d.X,
1080
+ _anchorPosStart.Y.Scale, _anchorPosStart.Y.Offset + d.Y
1081
+ )
1082
+ end
1083
+ end
1084
+ end)
1085
+
1086
+ -- Insert key toggles the dev window
1087
+ _UIS.InputBegan:Connect(function(inp, processed)
1088
+ if not processed and inp.KeyCode == Enum.KeyCode.Insert then
1089
+ _win.Visible = not _win.Visible
1090
+ if _win.Visible then _setTab(_mode) end
1091
+ _updateAnchorState()
1092
+ end
1093
+ end)
1094
+
1095
+ -- ── Window dragging ───────────────────────────────────────────────────────────
1096
+ local _dragging, _dragStart, _winStart = false, nil, nil
1097
+
1098
+ _hdr.InputBegan:Connect(function(inp)
1099
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then
1100
+ _dragging = true; _dragStart = inp.Position; _winStart = _win.Position
1101
+ end
1102
+ end)
1103
+ _hdr.InputEnded:Connect(function(inp)
1104
+ if inp.UserInputType == Enum.UserInputType.MouseButton1 then _dragging = false end
1105
+ end)
1106
+ _UIS.InputChanged:Connect(function(inp)
1107
+ if _dragging and inp.UserInputType == Enum.UserInputType.MouseMovement then
1108
+ local d = inp.Position - _dragStart
1109
+ -- Clamp Y so the header can't be dragged behind the TopBar (~36 px inset)
1110
+ local newY = math.max(36, _winStart.Y.Offset + d.Y)
1111
+ _win.Position = UDim2.new(_winStart.X.Scale, _winStart.X.Offset + d.X, _winStart.Y.Scale, newY)
1112
+ end
1113
+ end)
1114
+
1115
+ -- ── Update loop ───────────────────────────────────────────────────────────────
1116
+ task.spawn(function()
1117
+ while _gui.Parent do
1118
+ task.wait(0.25)
1119
+
1120
+ if _win.Visible and _mode == "log" then
1121
+ local atBottom = _isAtBottom()
1122
+ local newEntries = Pulse.Log.getNewEntries(_lastId)
1123
+ local added = 0
1124
+ for _, e in ipairs(newEntries) do
1125
+ _lastId = math.max(_lastId, e.id)
1126
+ _addEntry(e)
1127
+ added = added + 1
1128
+ end
1129
+ if added > 0 then
1130
+ _countLabel.Text = _totalShown .. " shown"
1131
+ if atBottom then _scrollToBottom() end
1132
+ end
1133
+ end
1134
+
1135
+ -- Keep anchor button state in sync (open=blue, closed=dim)
1136
+ _updateAnchorState()
1137
+
1138
+ -- Always update monitor bar (visible in all tabs)
1139
+ if _win.Visible then
1140
+ local monitors = Pulse.Monitor.getAll()
1141
+ local parts = {}
1142
+ for k, v in pairs(monitors) do parts[#parts+1] = k .. ":" .. tostring(v) end
1143
+ table.sort(parts)
1144
+ _monLabel.Text = table.concat(parts, " ")
1145
+ end
1146
+
1147
+ if _win.Visible and _mode == "feat" then
1148
+ local toggles = _getToggles()
1149
+ if toggles and next(toggles) then
1150
+ if #_featRows == 0 or (_featRows[1] and _featRows[1].id == "__hint") then
1151
+ _buildFeatRows()
1152
+ else
1153
+ for _, r in ipairs(_featRows) do
1154
+ if r.id ~= "__hint" and r.stateBtn and toggles[r.id] then
1155
+ local val = toggles[r.id].Value
1156
+ r.stateBtn.Text = val and "ON" or "OFF"
1157
+ r.stateBtn.TextColor3 = val and P.on or P.off
1158
+ if r.stateStroke then r.stateStroke.Color = val and P.on or P.off end
1159
+ if r.nameLabel then r.nameLabel.TextColor3 = val and P.text or P.dim end
1160
+ end
1161
+ end
1162
+ end
1163
+ end
1164
+ end
1165
+ end
1166
+ end)
1167
+
1168
+ -- ── Wire remaining handlers ───────────────────────────────────────────────────
1169
+ _btnJumpErr.MouseButton1Click:Connect(function()
1170
+ local lastErrIdx = nil
1171
+ for i, item in ipairs(_labels) do
1172
+ if item.e.lbl == "ERR" then lastErrIdx = i end
1173
+ end
1174
+ if lastErrIdx then
1175
+ task.wait()
1176
+ local y = (lastErrIdx - 1) * ENTRY_H
1177
+ local maxY = math.max(0, _scroll.AbsoluteCanvasSize.Y - _scroll.AbsoluteWindowSize.Y)
1178
+ _scroll.CanvasPosition = Vector2.new(0, math.min(math.max(0, y - LOG_H / 2), maxY))
1179
+ end
1180
+ end)
1181
+
1182
+ _tagInput:GetPropertyChangedSignal("Text"):Connect(function()
1183
+ task.delay(0.15, _rerender)
1184
+ end)
1185
+
1186
+ for _, lbl in ipairs(LEVELS) do
1187
+ _lvlBtns[lbl].MouseButton1Click:Connect(function()
1188
+ task.delay(0.05, _rerender)
1189
+ end)
1190
+ end
1191
+
1192
+ -- ── Public API ────────────────────────────────────────────────────────────────
1193
+ _PulseDevGuiToggle = function()
1194
+ _win.Visible = not _win.Visible
1195
+ if _win.Visible then _setTab(_mode) end
1196
+ end
1197
+
1198
+ _PulseDevGuiShow = function(v)
1199
+ _win.Visible = v
1200
+ if v then _setTab(_mode) end
1201
+ end
1202
+
1203
+ _PulseDevGuiOpenFeat = function()
1204
+ _win.Visible = true
1205
+ _setTab("feat")
1206
+ end