@el4cteo/rbx-studio-mcp 0.3.1 → 0.3.6

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 (43) hide show
  1. package/README.md +20 -26
  2. package/dist/index.js +1 -1
  3. package/dist/lib/format.js +18 -2
  4. package/dist/lib/format.js.map +1 -1
  5. package/dist/tools/api.js +20 -1
  6. package/dist/tools/api.js.map +1 -1
  7. package/dist/tools/debug.js +18 -8
  8. package/dist/tools/debug.js.map +1 -1
  9. package/dist/tools/discover.js +5 -0
  10. package/dist/tools/discover.js.map +1 -1
  11. package/dist/tools/input.js +38 -5
  12. package/dist/tools/input.js.map +1 -1
  13. package/dist/tools/instances.js +49 -2
  14. package/dist/tools/instances.js.map +1 -1
  15. package/dist/tools/perf.js +9 -8
  16. package/dist/tools/perf.js.map +1 -1
  17. package/dist/tools/scripts.js +10 -1
  18. package/dist/tools/scripts.js.map +1 -1
  19. package/package.json +1 -1
  20. package/plugin/src/Config.luau +1 -1
  21. package/plugin/src/Console.luau +301 -117
  22. package/plugin/src/Mirror.luau +126 -0
  23. package/plugin/src/Serialize.luau +43 -3
  24. package/plugin/src/ThemePicker.luau +458 -0
  25. package/plugin/src/Themes/Aurora.luau +161 -0
  26. package/plugin/src/Themes/Blueprint.luau +213 -0
  27. package/plugin/src/Themes/Draw.luau +177 -0
  28. package/plugin/src/Themes/Lattice.luau +304 -0
  29. package/plugin/src/Themes/Nebula.luau +182 -0
  30. package/plugin/src/Themes/Observatory.luau +200 -0
  31. package/plugin/src/Themes/Orbit.luau +268 -0
  32. package/plugin/src/Themes/Phosphor.luau +200 -0
  33. package/plugin/src/Themes/Theme.luau +121 -0
  34. package/plugin/src/Themes/Void.luau +266 -0
  35. package/plugin/src/Themes/init.luau +116 -0
  36. package/plugin/src/Undo.luau +29 -0
  37. package/plugin/src/Visuals.luau +838 -907
  38. package/plugin/src/handlers/Device.luau +21 -1
  39. package/plugin/src/handlers/Discover.luau +75 -1
  40. package/plugin/src/handlers/Exec.luau +17 -5
  41. package/plugin/src/handlers/Input.luau +583 -493
  42. package/plugin/src/handlers/Instances.luau +35 -3
  43. package/plugin/src/init.server.luau +209 -11
@@ -18,39 +18,87 @@
18
18
  Instances, and colour spans do the same job in a fraction of the code.
19
19
  ]]
20
20
 
21
+ local ThemePicker = require(script.Parent.ThemePicker)
22
+ local Themes = require(script.Parent.Themes)
21
23
  local Visuals = require(script.Parent.Visuals)
22
24
 
23
25
  local Console = {}
24
26
 
25
- -- Ring buffer bound. Long enough to cover a working session, short enough that
26
- -- re-rendering the whole string on every append stays cheap.
27
- local MAX_LINES = 300
28
-
29
- -- Violet-tinted near-black. The cast is slight but it stops the panel reading
30
- -- as the same grey as every other Studio dock.
31
- local PALETTE = {
32
- background = Color3.fromRGB(11, 12, 20),
33
- surface = Color3.fromRGB(18, 20, 31),
34
- rule = Color3.fromRGB(167, 139, 250),
35
- text = Color3.fromRGB(206, 212, 226),
36
- dim = Color3.fromRGB(90, 97, 120),
37
- green = Color3.fromRGB(110, 231, 168),
38
- red = Color3.fromRGB(251, 113, 133),
39
- amber = Color3.fromRGB(251, 191, 36),
40
- cyan = Color3.fromRGB(103, 232, 249),
41
- violet = Color3.fromRGB(167, 139, 250),
42
- }
27
+ --[[
28
+ Ring buffer bound, counted in logged rows rather than in rendered lines.
29
+
30
+ It used to bound the rendered lines, which meant a burst of failures -- each
31
+ writing a message plus two wrapped lines of explanation -- evicted three
32
+ times as much history as a burst of successes. Bounding the records makes
33
+ "the last three hundred things that happened" mean what it says.
34
+ ]]
35
+ local MAX_RECORDS = 300
36
+
37
+ --[[
38
+ The active preset's colours, rebound rather than re-read.
39
+
40
+ Every use site in this file is a plain `PALETTE.dim`, and there are about
41
+ forty of them. Turning each into a function call to satisfy theming would
42
+ have been forty edits to working code for no gain: the palette changes only
43
+ when the user picks a different preset, so it is simply reassigned there and
44
+ the reads stay as they were.
45
+
46
+ The catch is that anything which COPIES a colour out of here keeps the old
47
+ one. Instances built in `mount` do exactly that, which is why `applyTheme`
48
+ has to walk them by hand.
49
+ ]]
50
+ local PALETTE: Themes.Palette = Themes.palette()
51
+
52
+ --[[
53
+ An optional listener on everything this console is told.
54
+
55
+ Exists for `Mirror`, which relays a playtest server's activity to the client
56
+ view that cannot reach the bridge itself. Kept as one hook rather than as
57
+ calls sprinkled through the file so there is a single place where "what the
58
+ console was told" is defined, and so the client half -- which sets no
59
+ observer -- cannot echo what it replays back into the channel.
60
+ ]]
61
+ local observer: ((string, { any }) -> ())? = nil
62
+
63
+ function Console.setObserver(fn: ((string, { any }) -> ())?)
64
+ observer = fn
65
+ end
66
+
67
+ local function notify(kind: string, arguments: { any })
68
+ local listener = observer
69
+ if listener ~= nil then
70
+ task.spawn(listener, kind, arguments)
71
+ end
72
+ end
73
+
74
+ --[[
75
+ Set on the console that is REPLAYING someone else's events.
76
+
77
+ A replayed call must not grow its own consequences. `recordCall` schedules
78
+ the "agent idle" summary, so a mirrored session scheduled one of its own and
79
+ then received the original over the channel too -- the same line twice, a
80
+ few pixels apart, which is exactly the sort of thing that makes a panel look
81
+ broken. The origin is the only session entitled to decide the burst ended.
82
+ ]]
83
+ local mirroring = false
84
+
85
+ function Console.setMirroring(value: boolean)
86
+ mirroring = value
87
+ end
43
88
 
44
89
  -- Sigil plus colour per level. The sigil is what makes the log scannable at a
45
90
  -- glance; colour alone fails for anyone who cannot separate red from green.
46
- local LEVELS: { [string]: { sigil: string, color: Color3 } } = {
47
- ok = { sigil = "\u{25C6}", color = PALETTE.green },
48
- error = { sigil = "\u{2715}", color = PALETTE.red },
49
- warn = { sigil = "\u{25B2}", color = PALETTE.amber },
50
- info = { sigil = "\u{25C6}", color = PALETTE.violet },
51
- dim = { sigil = "\u{00B7}", color = PALETTE.dim },
52
- call = { sigil = "\u{25B8}", color = PALETTE.cyan },
53
- reply = { sigil = "\u{25C2}", color = PALETTE.violet },
91
+ -- Named by palette key rather than by colour, because this table is built once
92
+ -- at load and a colour captured then belongs to whichever preset happened to
93
+ -- be active at the time.
94
+ local LEVELS: { [string]: { sigil: string, key: string } } = {
95
+ ok = { sigil = "\u{25C6}", key = "green" },
96
+ error = { sigil = "\u{2715}", key = "red" },
97
+ warn = { sigil = "\u{25B2}", key = "amber" },
98
+ info = { sigil = "\u{25C6}", key = "violet" },
99
+ dim = { sigil = "\u{00B7}", key = "dim" },
100
+ call = { sigil = "\u{25B8}", key = "cyan" },
101
+ reply = { sigil = "\u{25C2}", key = "violet" },
54
102
  }
55
103
 
56
104
  export type Level = "ok" | "error" | "warn" | "info" | "dim" | "call" | "reply"
@@ -105,8 +153,28 @@ local function wrapDetail(detail: string): { string }
105
153
  return lines
106
154
  end
107
155
 
156
+ --[[
157
+ One logged row, before it is coloured.
158
+
159
+ Holding the parts rather than the finished string is what lets a theme
160
+ switch repaint history: see `renderRecord`.
161
+ ]]
162
+ type Record = {
163
+ level: Level,
164
+ message: string,
165
+ detail: string?,
166
+ stamp: string,
167
+ }
168
+
108
169
  type State = {
109
- lines: { string },
170
+ records: { Record },
171
+ -- The last status reported, replayed after a theme switch. Without it the
172
+ -- header has no way to know what colour it should be wearing.
173
+ status: string,
174
+ statusMeta: string,
175
+ -- The accent rule's gradient. A ColorSequence rather than a Color3, so it
176
+ -- cannot ride the generic role list.
177
+ ruleFade: UIGradient?,
110
178
  --[[
111
179
  Rows written, as opposed to lines rendered.
112
180
 
@@ -144,7 +212,10 @@ type State = {
144
212
  }
145
213
 
146
214
  local state: State = {
147
- lines = {},
215
+ records = {},
216
+ ruleFade = nil,
217
+ status = "disconnected",
218
+ statusMeta = "",
148
219
  entries = 0,
149
220
  label = nil,
150
221
  scroller = nil,
@@ -189,13 +260,89 @@ local function span(color: Color3, value: string): string
189
260
  return string.format('<font color="%s">%s</font>', hex(color), escape(value))
190
261
  end
191
262
 
263
+ --[[
264
+ Turns one record into the lines it occupies.
265
+
266
+ Rendering is deferred to here rather than done when the row is logged, which
267
+ is the change that made theming possible at all: a line that has already had
268
+ `#A78BFA` baked into it cannot be recoloured, so switching preset used to
269
+ leave the whole session's history in the previous theme's palette while new
270
+ rows arrived in the new one. Now nothing is coloured until it is painted.
271
+ ]]
272
+ local function renderRecord(record: Record): { string }
273
+ local spec = LEVELS[record.level] or LEVELS.info
274
+ local detail = record.detail
275
+
276
+ --[[
277
+ Measured in characters, not bytes.
278
+
279
+ `#body` counts bytes, and every sigil in this console is a 3-byte UTF-8
280
+ glyph, so it over-counted each line by two and pushed the latency column
281
+ two places left on exactly the lines that had a latency. The column was
282
+ never straight, and the cause was invisible until two different sigils
283
+ sat next to each other.
284
+ ]]
285
+ --[[
286
+ Only a detail that will actually sit in the column costs the message any
287
+ of its width. Cutting the message to make room for a detail that then
288
+ goes on its own line below would shorten it for nothing.
289
+ ]]
290
+ local detailWidth = if detail then (utf8.len(detail) or #detail) else 0
291
+ local inlineDetail = detail ~= nil
292
+ and (utf8.len(record.message) or #record.message) + detailWidth + 3
293
+ <= DETAIL_COLUMN + INLINE_DETAIL
294
+
295
+ local trimmed = record.message
296
+ local budget = DETAIL_COLUMN - 3
297
+ if inlineDetail and (utf8.len(trimmed) or #trimmed) > budget then
298
+ -- Cut rather than wrap. A wrapped line destroys the alignment and
299
+ -- carries the detail off the end of the visible width as well.
300
+ local offset = utf8.offset(trimmed, budget) or budget
301
+ trimmed = string.sub(trimmed, 1, offset - 1) .. utf8.char(0x2026)
302
+ end
303
+
304
+ local body = string.format("%s %s", spec.sigil, trimmed)
305
+ local bodyWidth = utf8.len(body) or #body
306
+ local lines = { span(PALETTE.dim, record.stamp) .. " " .. span(PALETTE[spec.key], body) }
307
+
308
+ --[[
309
+ Short details ride the right-hand column; long ones get their own lines.
310
+
311
+ The column exists for latencies -- "12ms" stacking into something
312
+ readable -- and it was applied to every detail regardless of length. A
313
+ sentence of prose therefore started at column 52, ran off the widget, and
314
+ wrapped back to column 0, so the explanation of a standby session came
315
+ out as a ragged block that began in the middle of the screen and ended at
316
+ the left edge. Anything that will not fit beside the message is better
317
+ off beneath it.
318
+ ]]
319
+ if detail then
320
+ if inlineDetail then
321
+ local padding = math.max(1, DETAIL_COLUMN - bodyWidth)
322
+ lines[1] ..= span(PALETTE.dim, string.rep(" ", padding) .. detail)
323
+ else
324
+ for _, wrapped in wrapDetail(detail) do
325
+ table.insert(lines, span(PALETTE.dim, DETAIL_INDENT .. wrapped))
326
+ end
327
+ end
328
+ end
329
+ return lines
330
+ end
331
+
192
332
  local function paint()
193
333
  state.dirty = false
194
334
  local label = state.label
195
335
  if not label then
196
336
  return
197
337
  end
198
- label.Text = table.concat(state.lines, "\n")
338
+
339
+ local lines: { string } = {}
340
+ for _, record in state.records do
341
+ for _, line in renderRecord(record) do
342
+ table.insert(lines, line)
343
+ end
344
+ end
345
+ label.Text = table.concat(lines, "\n")
199
346
 
200
347
  -- Only follow the tail when the user has not scrolled up to read history.
201
348
  local scroller = state.scroller
@@ -261,66 +408,21 @@ end
261
408
  ragged offset.
262
409
  ]]
263
410
  function Console.log(level: Level, message: string, detail: string?)
264
- local spec = LEVELS[level] or LEVELS.info
265
-
266
- --[[
267
- Measured in characters, not bytes.
268
-
269
- `#body` counts bytes, and every sigil in this console is a 3-byte UTF-8
270
- glyph, so it over-counted each line by two and pushed the latency column
271
- two places left on exactly the lines that had a latency. The column was
272
- never straight, and the cause was invisible until two different sigils
273
- sat next to each other.
274
- ]]
275
- --[[
276
- Only a detail that will actually sit in the column costs the message any
277
- of its width. Cutting the message to make room for a detail that then
278
- goes on its own line below would shorten it for nothing.
279
- ]]
280
- local detailWidth = if detail then (utf8.len(detail) or #detail) else 0
281
- local inlineDetail = detail ~= nil
282
- and (utf8.len(message) or #message) + detailWidth + 3 <= DETAIL_COLUMN + INLINE_DETAIL
283
-
284
- local trimmed = message
285
- local budget = DETAIL_COLUMN - 3
286
- if inlineDetail and (utf8.len(trimmed) or #trimmed) > budget then
287
- -- Cut rather than wrap. A wrapped line destroys the alignment and
288
- -- carries the detail off the end of the visible width as well.
289
- local offset = utf8.offset(trimmed, budget) or budget
290
- trimmed = string.sub(trimmed, 1, offset - 1) .. utf8.char(0x2026)
291
- end
292
-
293
- local body = string.format("%s %s", spec.sigil, trimmed)
294
- local bodyWidth = utf8.len(body) or #body
295
- local line = span(PALETTE.dim, os.date("%H:%M:%S") :: string) .. " " .. span(spec.color, body)
296
-
297
- --[[
298
- Short details ride the right-hand column; long ones get their own lines.
299
-
300
- The column exists for latencies -- "12ms" stacking into something
301
- readable -- and it was applied to every detail regardless of length. A
302
- sentence of prose therefore started at column 52, ran off the widget, and
303
- wrapped back to column 0, so the explanation of a standby session came
304
- out as a ragged block that began in the middle of the screen and ended at
305
- the left edge. Anything that will not fit beside the message is better
306
- off beneath it.
307
- ]]
308
- table.insert(state.lines, line)
411
+ table.insert(state.records, {
412
+ level = level,
413
+ message = message,
414
+ detail = detail,
415
+ -- Stamped when the row happened, not when it is painted. A repaint after
416
+ -- a theme switch re-renders every line, and re-reading the clock there
417
+ -- would restamp the whole session to the moment the user changed colour.
418
+ stamp = os.date("%H:%M:%S") :: string,
419
+ })
309
420
  state.entries += 1
310
- if detail then
311
- if inlineDetail then
312
- local padding = math.max(1, DETAIL_COLUMN - bodyWidth)
313
- state.lines[#state.lines] ..= span(PALETTE.dim, string.rep(" ", padding) .. detail)
314
- else
315
- for _, wrapped in wrapDetail(detail) do
316
- table.insert(state.lines, span(PALETTE.dim, DETAIL_INDENT .. wrapped))
317
- end
318
- end
319
- end
320
- while #state.lines > MAX_LINES do
321
- table.remove(state.lines, 1)
421
+ while #state.records > MAX_RECORDS do
422
+ table.remove(state.records, 1)
322
423
  end
323
424
  redraw()
425
+ notify("log", { level, message, detail })
324
426
  end
325
427
 
326
428
  --[[
@@ -344,11 +446,11 @@ end
344
446
  and heavier still. Urgency drives both the spin rate and how strongly the
345
447
  colour takes over, so the two never disagree.
346
448
  ]]
347
- local KIND_LOOK: { [string]: { color: Color3, urgency: number } } = {
348
- read = { color = PALETTE.cyan, urgency = 0.3 },
349
- write = { color = PALETTE.violet, urgency = 0.7 },
350
- run = { color = PALETTE.green, urgency = 0.9 },
351
- debug = { color = PALETTE.amber, urgency = 0.5 },
449
+ local KIND_LOOK: { [string]: { key: string, urgency: number } } = {
450
+ read = { key = "cyan", urgency = 0.3 },
451
+ write = { key = "violet", urgency = 0.7 },
452
+ run = { key = "green", urgency = 0.9 },
453
+ debug = { key = "amber", urgency = 0.5 },
352
454
  }
353
455
 
354
456
  function Console.beginCall(title: string, kind: string)
@@ -356,8 +458,9 @@ function Console.beginCall(title: string, kind: string)
356
458
  state.generation += 1
357
459
  local look = KIND_LOOK[kind] or KIND_LOOK.read
358
460
  Visuals.setCaption(title)
359
- Visuals.setKind(look.color, look.urgency)
461
+ Visuals.setKind(PALETTE[look.key], look.urgency)
360
462
  refreshCounters()
463
+ notify("beginCall", { title, kind })
361
464
  end
362
465
 
363
466
  --[[
@@ -382,6 +485,7 @@ function Console.recordCall(ok: boolean, milliseconds: number)
382
485
  -- The band plots it: bar height is how long it took, colour is whether it
383
486
  -- worked, and a failure knocks the solid off its axis as well.
384
487
  Visuals.recordCall(milliseconds, ok, title)
488
+ notify("recordCall", { ok, milliseconds, title })
385
489
 
386
490
  --[[
387
491
  Says so when the session goes quiet, once per burst.
@@ -395,6 +499,9 @@ function Console.recordCall(ok: boolean, milliseconds: number)
395
499
  ]]
396
500
  state.generation += 1
397
501
  local mine = state.generation
502
+ if mirroring then
503
+ return
504
+ end
398
505
  task.delay(QUIET_SECONDS, function()
399
506
  if state.generation ~= mine or state.calls == 0 then
400
507
  return
@@ -499,7 +606,7 @@ function Console.clear()
499
606
  the one that went.
500
607
  ]]
501
608
  local cleared = state.entries
502
- table.clear(state.lines)
609
+ table.clear(state.records)
503
610
  state.entries = 0
504
611
  state.calls = 0
505
612
  state.errors = 0
@@ -524,6 +631,9 @@ end
524
631
  visible without scrolling, however long the session has run.
525
632
  ]]
526
633
  function Console.setStatus(status: string, meta: string)
634
+ state.status = status
635
+ state.statusMeta = meta
636
+
527
637
  local dot = state.statusDot
528
638
  local text = state.statusText
529
639
  local metaLabel = state.metaText
@@ -537,7 +647,7 @@ function Console.setStatus(status: string, meta: string)
537
647
  local color = if status == "connected"
538
648
  then PALETTE.green
539
649
  elseif status == "connecting" then PALETTE.amber
540
- elseif status == "standby" then PALETTE.dim
650
+ elseif status == "standby" or status == "mirroring" then PALETTE.dim
541
651
  else PALETTE.red
542
652
 
543
653
  dot.TextColor3 = color
@@ -550,14 +660,37 @@ function Console.setStatus(status: string, meta: string)
550
660
  Visuals.setTint(color)
551
661
  end
552
662
 
663
+ --[[
664
+ Records which palette colour an Instance is wearing, and puts it on.
665
+
666
+ The console builds about thirty Instances and copies a colour into each. A
667
+ copy does not follow the palette when the user picks a different preset, so
668
+ the panel would keep the old theme's header, rule, scrollbar and buttons
669
+ while the log underneath repainted -- which looks less like a theme than
670
+ like a half-finished render.
671
+
672
+ Rather than keep thirty named references and re-set thirty properties by
673
+ hand, each one declares the property and palette key it is wearing when it
674
+ is built, and `Console.applyTheme` walks the list. Adding a widget therefore
675
+ cannot forget to theme it: the same call that colours it registers it.
676
+ ]]
677
+ type Role = { instance: Instance, property: string, key: string }
678
+ local roles: { Role } = {}
679
+
680
+ local function themed<T>(instance: T & Instance, property: string, key: string): T
681
+ table.insert(roles, { instance = instance, property = property, key = key })
682
+ ;(instance :: any)[property] = PALETTE[key]
683
+ return instance
684
+ end
685
+
553
686
  local function makeButton(parent: Instance, text: string, order: number): TextButton
554
687
  local button = Instance.new("TextButton")
555
688
  button.Name = text
556
689
  button.Text = text
557
690
  button.Font = Enum.Font.Code
558
691
  button.TextSize = 11
559
- button.TextColor3 = PALETTE.dim
560
- button.BackgroundColor3 = PALETTE.background
692
+ themed(button, "TextColor3", "dim")
693
+ themed(button, "BackgroundColor3", "background")
561
694
  button.AutoButtonColor = false
562
695
  button.BorderSizePixel = 0
563
696
  button.Size = UDim2.new(0, 76, 0, 20)
@@ -569,7 +702,7 @@ local function makeButton(parent: Instance, text: string, order: number): TextBu
569
702
  corner.Parent = button
570
703
 
571
704
  local stroke = Instance.new("UIStroke")
572
- stroke.Color = PALETTE.dim
705
+ themed(stroke, "Color", "dim")
573
706
  stroke.Transparency = 0.6
574
707
  stroke.Parent = button
575
708
 
@@ -590,6 +723,10 @@ end
590
723
  export type Handlers = {
591
724
  onReconnect: () -> (),
592
725
  onClear: () -> (),
726
+ -- Called after a preset switch has already been applied, so the caller's
727
+ -- only job is to remember it. Persistence lives there because
728
+ -- `plugin:SetSetting` is not reachable from a ModuleScript.
729
+ onTheme: (string) -> (),
593
730
  }
594
731
 
595
732
  --[[
@@ -599,14 +736,14 @@ export type Handlers = {
599
736
  function Console.mount(parent: Instance, handlers: Handlers)
600
737
  local root = Instance.new("Frame")
601
738
  root.Size = UDim2.fromScale(1, 1)
602
- root.BackgroundColor3 = PALETTE.background
739
+ themed(root, "BackgroundColor3", "background")
603
740
  root.BorderSizePixel = 0
604
741
  root.Parent = parent
605
742
 
606
743
  -- Header ---------------------------------------------------------------
607
744
  local header = Instance.new("Frame")
608
745
  header.Size = UDim2.new(1, 0, 0, 32)
609
- header.BackgroundColor3 = PALETTE.surface
746
+ themed(header, "BackgroundColor3", "surface")
610
747
  header.BorderSizePixel = 0
611
748
  header.Parent = root
612
749
 
@@ -619,6 +756,10 @@ function Console.mount(parent: Instance, handlers: Handlers)
619
756
  dot.Text = "\u{25CF}"
620
757
  dot.Font = Enum.Font.Code
621
758
  dot.TextSize = 13
759
+ -- Deliberately NOT registered with `themed`. Red here is the value it
760
+ -- STARTS at, not the colour it wears: what it should be is whatever
761
+ -- `setStatus` last reported. Registering it meant every theme switch
762
+ -- repainted a connected session red.
622
763
  dot.TextColor3 = PALETTE.red
623
764
  dot.BackgroundTransparency = 1
624
765
  dot.Size = UDim2.new(0, 12, 1, 0)
@@ -641,7 +782,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
641
782
  meta.Text = ""
642
783
  meta.Font = Enum.Font.Code
643
784
  meta.TextSize = 11
644
- meta.TextColor3 = PALETTE.dim
785
+ themed(meta, "TextColor3", "dim")
645
786
  meta.TextXAlignment = Enum.TextXAlignment.Left
646
787
  meta.TextTruncate = Enum.TextTruncate.AtEnd
647
788
  meta.BackgroundTransparency = 1
@@ -663,11 +804,11 @@ function Console.mount(parent: Instance, handlers: Handlers)
663
804
  clientsChip.AnchorPoint = Vector2.new(1, 0.5)
664
805
  clientsChip.Position = UDim2.new(1, -252, 0.5, 0)
665
806
  clientsChip.Size = UDim2.new(0, 84, 0, 18)
666
- clientsChip.BackgroundColor3 = PALETTE.background
807
+ themed(clientsChip, "BackgroundColor3", "background")
667
808
  clientsChip.BorderSizePixel = 0
668
809
  clientsChip.Font = Enum.Font.Code
669
810
  clientsChip.TextSize = 11
670
- clientsChip.TextColor3 = PALETTE.cyan
811
+ themed(clientsChip, "TextColor3", "cyan")
671
812
  clientsChip.Text = ""
672
813
  clientsChip.Visible = false
673
814
  clientsChip.Parent = header
@@ -678,7 +819,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
678
819
  chipCorner.Parent = clientsChip
679
820
 
680
821
  local chipStroke = Instance.new("UIStroke")
681
- chipStroke.Color = PALETTE.cyan
822
+ themed(chipStroke, "Color", "cyan")
682
823
  chipStroke.Transparency = 0.6
683
824
  chipStroke.Parent = clientsChip
684
825
 
@@ -706,12 +847,13 @@ function Console.mount(parent: Instance, handlers: Handlers)
706
847
  local rule = Instance.new("Frame")
707
848
  rule.Position = UDim2.new(0, 0, 0, 32)
708
849
  rule.Size = UDim2.new(1, 0, 0, 1)
709
- rule.BackgroundColor3 = PALETTE.rule
850
+ themed(rule, "BackgroundColor3", "rule")
710
851
  rule.BorderSizePixel = 0
711
852
  rule.Parent = root
712
853
 
713
854
  local ruleFade = Instance.new("UIGradient")
714
855
  ruleFade.Color = ColorSequence.new(PALETTE.rule)
856
+ state.ruleFade = ruleFade
715
857
  ruleFade.Transparency = NumberSequence.new({
716
858
  NumberSequenceKeypoint.new(0, 0.15),
717
859
  NumberSequenceKeypoint.new(1, 0.85),
@@ -725,7 +867,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
725
867
  scroller.BackgroundTransparency = 1
726
868
  scroller.BorderSizePixel = 0
727
869
  scroller.ScrollBarThickness = 5
728
- scroller.ScrollBarImageColor3 = PALETTE.violet
870
+ themed(scroller, "ScrollBarImageColor3", "violet")
729
871
  scroller.ScrollBarImageTransparency = 0.5
730
872
  scroller.CanvasSize = UDim2.new()
731
873
  scroller.AutomaticCanvasSize = Enum.AutomaticSize.Y
@@ -747,7 +889,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
747
889
  label.Font = Enum.Font.Code
748
890
  label.TextSize = 12
749
891
  label.LineHeight = 1.25
750
- label.TextColor3 = PALETTE.text
892
+ themed(label, "TextColor3", "text")
751
893
  label.RichText = true
752
894
  label.TextWrapped = true
753
895
  label.TextXAlignment = Enum.TextXAlignment.Left
@@ -761,7 +903,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
761
903
  footer.AnchorPoint = Vector2.new(0, 1)
762
904
  footer.Position = UDim2.fromScale(0, 1)
763
905
  footer.Size = UDim2.new(1, 0, 0, 22)
764
- footer.BackgroundColor3 = PALETTE.surface
906
+ themed(footer, "BackgroundColor3", "surface")
765
907
  footer.BorderSizePixel = 0
766
908
  footer.Parent = root
767
909
 
@@ -774,7 +916,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
774
916
  prompt.Text = "rbx\u{00B7}studio"
775
917
  prompt.Font = Enum.Font.Code
776
918
  prompt.TextSize = 11
777
- prompt.TextColor3 = PALETTE.violet
919
+ themed(prompt, "TextColor3", "violet")
778
920
  prompt.TextXAlignment = Enum.TextXAlignment.Left
779
921
  prompt.BackgroundTransparency = 1
780
922
  prompt.Size = UDim2.new(0, 70, 1, 0)
@@ -793,7 +935,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
793
935
  counters.Text = "idle"
794
936
  counters.Font = Enum.Font.Code
795
937
  counters.TextSize = 11
796
- counters.TextColor3 = PALETTE.dim
938
+ themed(counters, "TextColor3", "dim")
797
939
  counters.TextXAlignment = Enum.TextXAlignment.Right
798
940
  counters.BackgroundTransparency = 1
799
941
  counters.AnchorPoint = Vector2.new(1, 0)
@@ -810,14 +952,7 @@ function Console.mount(parent: Instance, handlers: Handlers)
810
952
  costs legibility is a bad trade however good it looks, and this is a
811
953
  console before it is anything else.
812
954
  ]]
813
- Visuals.mount(root, {
814
- background = PALETTE.background,
815
- surface = PALETTE.surface,
816
- text = PALETTE.text,
817
- dim = PALETTE.dim,
818
- violet = PALETTE.violet,
819
- red = PALETTE.red,
820
- })
955
+ Visuals.mount(root)
821
956
  local band = root:FindFirstChild("ActivityBand") :: Frame
822
957
  band.Position = UDim2.new(0, 0, 0, 33)
823
958
  band.Size = UDim2.new(1, 0, 0, Visuals.BAND_HEIGHT)
@@ -846,6 +981,20 @@ function Console.mount(parent: Instance, handlers: Handlers)
846
981
  visualsButton.Text = if Visuals.isVisible() then "\u{25C6} visuals" else "visuals"
847
982
  end)
848
983
 
984
+ --[[
985
+ The preset drawer, mounted last so its tab sits over everything.
986
+
987
+ It has to be the last child of `root` as well as the highest ZIndex: the
988
+ band, the log and the footer are all built before it and a drawer that
989
+ opens behind the log is a drawer nobody can click.
990
+ ]]
991
+ ThemePicker.mount(root, function(id)
992
+ Console.applyTheme()
993
+ ThemePicker.applyTheme()
994
+ layoutLog()
995
+ handlers.onTheme(id)
996
+ end)
997
+
849
998
  -- Treat "scrolled away from the bottom" as the user reading history, and
850
999
  -- stop yanking the view down under them until they scroll back.
851
1000
  scroller:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
@@ -854,4 +1003,39 @@ function Console.mount(parent: Instance, handlers: Handlers)
854
1003
  end)
855
1004
  end
856
1005
 
1006
+ --[[
1007
+ Switches the whole panel to the active preset.
1008
+
1009
+ Three things have to move together or the switch looks broken: the palette
1010
+ this file reads, the Instances that copied a colour out of it, and the log,
1011
+ whose rows are re-rendered from records rather than recoloured in place. The
1012
+ band goes last because remounting a preset is the expensive part and there
1013
+ is no reason to make the text wait for it.
1014
+ ]]
1015
+ function Console.applyTheme()
1016
+ PALETTE = Themes.palette()
1017
+
1018
+ for _, role in roles do
1019
+ -- A widget can outlive its registration if the panel is rebuilt, and
1020
+ -- writing to a destroyed Instance throws.
1021
+ if role.instance.Parent ~= nil or role.instance:IsA("UIStroke") then
1022
+ pcall(function()
1023
+ (role.instance :: any)[role.property] = PALETTE[role.key]
1024
+ end)
1025
+ end
1026
+ end
1027
+
1028
+ local ruleFade = state.ruleFade
1029
+ if ruleFade then
1030
+ ruleFade.Color = ColorSequence.new(PALETTE.rule)
1031
+ end
1032
+
1033
+ refreshCounters()
1034
+ -- Replayed rather than recoloured: the status light's colour is a fact about
1035
+ -- the connection, and the only thing that knows it is `setStatus`.
1036
+ Console.setStatus(state.status, state.statusMeta)
1037
+ redraw()
1038
+ Visuals.applyTheme()
1039
+ end
1040
+
857
1041
  return Console