@rbxts/centurion-ui 1.0.7 → 2.0.0-rc.1
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.
- package/README.md +60 -15
- package/centurion.luau +10 -0
- package/package.json +13 -23
- package/src/analyse/cursor.luau +25 -0
- package/src/analyse/highlight.luau +91 -0
- package/src/analyse/init.luau +318 -0
- package/src/analyse/leventine.luau +298 -0
- package/src/analyse/match.luau +39 -0
- package/src/analyse/suggest.luau +41 -0
- package/src/app/header.luau +128 -0
- package/src/app/history.luau +258 -0
- package/src/app/init.luau +699 -0
- package/src/app/suggestions.luau +419 -0
- package/src/button.luau +13 -0
- package/src/components/background.luau +37 -0
- package/src/components/container.luau +33 -0
- package/src/components/corner.luau +11 -0
- package/src/components/divider.luau +28 -0
- package/src/components/flex.luau +108 -0
- package/src/components/gap.luau +25 -0
- package/src/components/image.luau +33 -0
- package/src/components/padding.luau +59 -0
- package/src/components/stroke.luau +20 -0
- package/src/components/text.luau +57 -0
- package/src/components/textbox.luau +186 -0
- package/src/fade.luau +23 -0
- package/src/init.luau +142 -0
- package/src/input.luau +16 -0
- package/src/px.luau +68 -0
- package/src/rich.luau +14 -0
- package/src/state.luau +33 -0
- package/src/theme.luau +70 -0
- package/src/ts/index.d.ts +60 -0
- package/src/ts/init.luau +5 -0
- package/src/types.luau +85 -0
- package/vide/init.luau +8 -0
- package/vide/src/action.luau +25 -0
- package/vide/src/apply.luau +162 -0
- package/vide/src/batch.luau +23 -0
- package/vide/src/branch.luau +40 -0
- package/vide/src/changed.luau +18 -0
- package/vide/src/cleanup.luau +43 -0
- package/vide/src/context.luau +74 -0
- package/vide/src/create.luau +107 -0
- package/vide/src/defaults.luau +113 -0
- package/vide/src/derive.luau +18 -0
- package/vide/src/effect.luau +12 -0
- package/vide/src/flags.luau +12 -0
- package/vide/src/graph.luau +321 -0
- package/vide/src/implicit_effect.luau +125 -0
- package/vide/src/indexes.luau +121 -0
- package/vide/src/init.luau +10 -0
- package/vide/src/lib.luau +120 -0
- package/vide/src/mount.luau +11 -0
- package/vide/src/read.luau +5 -0
- package/vide/src/root.luau +35 -0
- package/vide/src/show.luau +36 -0
- package/vide/src/source.luau +31 -0
- package/vide/src/spring.luau +328 -0
- package/vide/src/switch.luau +114 -0
- package/vide/src/timeout.luau +27 -0
- package/vide/src/untrack.luau +25 -0
- package/vide/src/values.luau +131 -0
- package/out/app/centurion-app.d.ts +0 -3
- package/out/app/centurion-app.luau +0 -86
- package/out/app/index.d.ts +0 -32
- package/out/app/init.luau +0 -105
- package/out/components/history/history-line.d.ts +0 -9
- package/out/components/history/history-line.luau +0 -88
- package/out/components/history/history-list.d.ts +0 -11
- package/out/components/history/history-list.luau +0 -67
- package/out/components/history/index.d.ts +0 -1
- package/out/components/history/init.luau +0 -7
- package/out/components/index.d.ts +0 -2
- package/out/components/init.luau +0 -10
- package/out/components/suggestions/badge.d.ts +0 -16
- package/out/components/suggestions/badge.luau +0 -40
- package/out/components/suggestions/index.d.ts +0 -1
- package/out/components/suggestions/init.luau +0 -7
- package/out/components/suggestions/main-suggestion.d.ts +0 -9
- package/out/components/suggestions/main-suggestion.luau +0 -232
- package/out/components/suggestions/suggestion-list.d.ts +0 -8
- package/out/components/suggestions/suggestion-list.luau +0 -96
- package/out/components/suggestions/suggestions.d.ts +0 -2
- package/out/components/suggestions/suggestions.luau +0 -64
- package/out/components/suggestions/types.d.ts +0 -6
- package/out/components/suggestions/types.luau +0 -2
- package/out/components/suggestions/util.d.ts +0 -1
- package/out/components/suggestions/util.luau +0 -22
- package/out/components/terminal/command.d.ts +0 -6
- package/out/components/terminal/command.luau +0 -74
- package/out/components/terminal/index.d.ts +0 -1
- package/out/components/terminal/init.luau +0 -7
- package/out/components/terminal/suggestion.d.ts +0 -26
- package/out/components/terminal/suggestion.luau +0 -205
- package/out/components/terminal/terminal-text-field/index.d.ts +0 -1
- package/out/components/terminal/terminal-text-field/init.luau +0 -7
- package/out/components/terminal/terminal-text-field/suggestion.d.ts +0 -5
- package/out/components/terminal/terminal-text-field/suggestion.luau +0 -124
- package/out/components/terminal/terminal-text-field/terminal-text-field.d.ts +0 -13
- package/out/components/terminal/terminal-text-field/terminal-text-field.luau +0 -273
- package/out/components/terminal/terminal.d.ts +0 -2
- package/out/components/terminal/terminal.luau +0 -304
- package/out/components/ui/frame.d.ts +0 -17
- package/out/components/ui/frame.luau +0 -32
- package/out/components/ui/group.d.ts +0 -15
- package/out/components/ui/group.luau +0 -29
- package/out/components/ui/layer.d.ts +0 -8
- package/out/components/ui/layer.luau +0 -26
- package/out/components/ui/outline.d.ts +0 -15
- package/out/components/ui/outline.luau +0 -116
- package/out/components/ui/padding.d.ts +0 -10
- package/out/components/ui/padding.luau +0 -19
- package/out/components/ui/scrolling-frame.d.ts +0 -14
- package/out/components/ui/scrolling-frame.luau +0 -44
- package/out/components/ui/text-field.d.ts +0 -12
- package/out/components/ui/text-field.luau +0 -60
- package/out/components/ui/text.d.ts +0 -17
- package/out/components/ui/text.luau +0 -61
- package/out/constants/options.d.ts +0 -2
- package/out/constants/options.luau +0 -27
- package/out/constants/text.d.ts +0 -3
- package/out/constants/text.luau +0 -9
- package/out/constants/util.d.ts +0 -1
- package/out/constants/util.luau +0 -7
- package/out/hooks/use-client.d.ts +0 -2
- package/out/hooks/use-client.luau +0 -17
- package/out/hooks/use-event.d.ts +0 -14
- package/out/hooks/use-event.luau +0 -58
- package/out/hooks/use-px.d.ts +0 -19
- package/out/hooks/use-px.luau +0 -78
- package/out/index.d.ts +0 -2
- package/out/init.luau +0 -10
- package/out/palette.d.ts +0 -47
- package/out/palette.luau +0 -42
- package/out/store.d.ts +0 -12
- package/out/store.luau +0 -26
- package/out/types.d.ts +0 -44
- package/out/types.luau +0 -2
- package/out/utils/string.d.ts +0 -5
- package/out/utils/string.luau +0 -25
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
local ContextActionService = game:GetService("ContextActionService")
|
|
2
|
+
local Players = game:GetService("Players")
|
|
3
|
+
local RunService = game:GetService("RunService")
|
|
4
|
+
local UserInputService = game:GetService("UserInputService")
|
|
5
|
+
|
|
6
|
+
local analyse = require("./analyse")
|
|
7
|
+
local background = require("./components/background")
|
|
8
|
+
local centurion = require("../centurion")
|
|
9
|
+
local container = require("./components/container")
|
|
10
|
+
local corner = require("./components/corner")
|
|
11
|
+
local divider = require("./components/divider")
|
|
12
|
+
local fade = require("./fade")
|
|
13
|
+
local flex = require("./components/flex")
|
|
14
|
+
local header = require("@self/header")
|
|
15
|
+
local highlight = require("./analyse/highlight")
|
|
16
|
+
local history = require("@self/history")
|
|
17
|
+
local padding = require("./components/padding")
|
|
18
|
+
local preferred_input = require("./input")
|
|
19
|
+
local px = require("./px")
|
|
20
|
+
local state = require("./state")
|
|
21
|
+
local stroke = require("./components/stroke")
|
|
22
|
+
local suggestions = require("@self/suggestions")
|
|
23
|
+
local text = require("./components/text")
|
|
24
|
+
local textbox = require("./components/textbox")
|
|
25
|
+
local theme = require("./theme")
|
|
26
|
+
local types = require("./types")
|
|
27
|
+
local vide = require("../vide")
|
|
28
|
+
|
|
29
|
+
local action = vide.action
|
|
30
|
+
local cleanup = vide.cleanup
|
|
31
|
+
local changed = vide.changed
|
|
32
|
+
local derive = vide.derive
|
|
33
|
+
local effect = vide.effect
|
|
34
|
+
local source = vide.source
|
|
35
|
+
local spring = vide.spring
|
|
36
|
+
local untrack = vide.untrack
|
|
37
|
+
|
|
38
|
+
local default_analysis = {
|
|
39
|
+
result = {
|
|
40
|
+
success = true,
|
|
41
|
+
value = nil,
|
|
42
|
+
},
|
|
43
|
+
suggestions = {},
|
|
44
|
+
} :: types.AnalysisResult
|
|
45
|
+
local max_history_lines = 10
|
|
46
|
+
local spinner_frames = { "|", "/", "-", "\\" }
|
|
47
|
+
|
|
48
|
+
local function app()
|
|
49
|
+
local raw_text = source("")
|
|
50
|
+
local cursor_pos = source(0)
|
|
51
|
+
local analysis = source(default_analysis)
|
|
52
|
+
|
|
53
|
+
-- same as analysis, but isn't reset when submitting (used for animations)
|
|
54
|
+
local display_analysis = source(default_analysis)
|
|
55
|
+
|
|
56
|
+
-- commands waiting on a response
|
|
57
|
+
local executing = source(0)
|
|
58
|
+
local executing_command = source("")
|
|
59
|
+
local busy = source(false)
|
|
60
|
+
local spinner_frame = source(1)
|
|
61
|
+
|
|
62
|
+
local selected_suggestion = source(1)
|
|
63
|
+
-- set once the textbox has mounted
|
|
64
|
+
local open_keyboard: () -> () = function() end
|
|
65
|
+
local over_interface = source(false)
|
|
66
|
+
|
|
67
|
+
-- touch devices draw a focused textbox with the native text field, which
|
|
68
|
+
-- ignores TextTransparency and sits at a different offset, so the
|
|
69
|
+
-- highlighted and suggested text overlays can't line up with it
|
|
70
|
+
local preferred = preferred_input()
|
|
71
|
+
local touch = derive(function()
|
|
72
|
+
return preferred() == Enum.PreferredInput.Touch
|
|
73
|
+
end)
|
|
74
|
+
local gamepad = derive(function()
|
|
75
|
+
return preferred() == Enum.PreferredInput.Gamepad
|
|
76
|
+
end)
|
|
77
|
+
local keyboard_visible = source(UserInputService.OnScreenKeyboardVisible)
|
|
78
|
+
cleanup(UserInputService:GetPropertyChangedSignal("OnScreenKeyboardVisible"):Connect(function()
|
|
79
|
+
keyboard_visible(UserInputService.OnScreenKeyboardVisible)
|
|
80
|
+
end))
|
|
81
|
+
|
|
82
|
+
local next_cursor_pos = false
|
|
83
|
+
local refocus_deadline = 0
|
|
84
|
+
local cmd_history = {}
|
|
85
|
+
local cmd_history_index = 0
|
|
86
|
+
|
|
87
|
+
-- shakes the input and flashes its outline when a command fails
|
|
88
|
+
local shake, shake_settings = spring(function()
|
|
89
|
+
return 0
|
|
90
|
+
end, 0.12, 0.25)
|
|
91
|
+
local failed = source(false)
|
|
92
|
+
local failures = 0
|
|
93
|
+
|
|
94
|
+
local function signal_failure()
|
|
95
|
+
failures += 1
|
|
96
|
+
local id = failures
|
|
97
|
+
|
|
98
|
+
shake_settings({ impulse = 300 })
|
|
99
|
+
failed(true)
|
|
100
|
+
task.delay(0.4, function()
|
|
101
|
+
if failures == id then
|
|
102
|
+
failed(false)
|
|
103
|
+
end
|
|
104
|
+
end)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
local function catch_hover()
|
|
108
|
+
return action(function(instance: Frame)
|
|
109
|
+
cleanup(instance.MouseEnter:Connect(function()
|
|
110
|
+
over_interface(true)
|
|
111
|
+
end))
|
|
112
|
+
cleanup(instance.MouseLeave:Connect(function()
|
|
113
|
+
over_interface(false)
|
|
114
|
+
end))
|
|
115
|
+
end)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
local function log(command: string, response: centurion.Response)
|
|
119
|
+
local logs = state.logs()
|
|
120
|
+
table.insert(logs, 1, {
|
|
121
|
+
command = command,
|
|
122
|
+
response = response,
|
|
123
|
+
})
|
|
124
|
+
table.remove(logs, 100)
|
|
125
|
+
state.logs(logs)
|
|
126
|
+
|
|
127
|
+
if not response.success then
|
|
128
|
+
signal_failure()
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
-- whether a space should follow the completed suggestion, so that the next
|
|
133
|
+
-- token can be typed straight away
|
|
134
|
+
local function needs_space(suggestion: types.AnalysisSuggestion): boolean
|
|
135
|
+
if suggestion.kind == "command" then
|
|
136
|
+
local cmd = centurion.registry.commands[suggestion.name]
|
|
137
|
+
if cmd and cmd.arguments and select("#", cmd.arguments()) > 0 then
|
|
138
|
+
return true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
-- subcommands still have to be typed after it
|
|
142
|
+
local prefix = suggestion.name .. " "
|
|
143
|
+
for name in centurion.registry.commands do
|
|
144
|
+
if name:sub(1, #prefix) == prefix then
|
|
145
|
+
return true
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
return false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
local arg = analysis().argument
|
|
152
|
+
if arg ~= nil then
|
|
153
|
+
local cmd = centurion.registry.commands[analysis().command]
|
|
154
|
+
local args = { cmd.arguments() }
|
|
155
|
+
if arg.index == #args or arg.kind == "list" then
|
|
156
|
+
return false
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
return true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
local function autofill(value: string, suggestion: types.AnalysisSuggestion, update_cursor_pos: boolean)
|
|
163
|
+
local token, with = suggestion.span, suggestion.replace
|
|
164
|
+
local span_start = token.x - 1
|
|
165
|
+
local before = string.sub(value, 1, span_start)
|
|
166
|
+
local after = string.sub(value, token.y + 1, -1)
|
|
167
|
+
local space = if needs_space(suggestion) then " " else ""
|
|
168
|
+
|
|
169
|
+
if update_cursor_pos then
|
|
170
|
+
-- moving the cursor shows it on consoles even with the keyboard
|
|
171
|
+
-- closed, and suggestions there are always for the end of the input
|
|
172
|
+
if not gamepad() then
|
|
173
|
+
task.defer(cursor_pos, span_start + #with + #space + 1)
|
|
174
|
+
end
|
|
175
|
+
selected_suggestion(1)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return before .. with .. space .. after
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
-- tapping a history entry or suggestion releases the textbox, but on touch
|
|
182
|
+
-- devices FocusLost can fire after the button activates, which would undo
|
|
183
|
+
-- an immediate refocus. so also recapture focus if it's lost shortly after
|
|
184
|
+
local function refocus()
|
|
185
|
+
refocus_deadline = os.clock() + 0.5
|
|
186
|
+
state.focused(true)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
local activation_keys = derive(function()
|
|
190
|
+
local keys = {}
|
|
191
|
+
for _, key in ipairs(state.activation_keys()) do
|
|
192
|
+
keys[key] = true
|
|
193
|
+
end
|
|
194
|
+
return keys
|
|
195
|
+
end)
|
|
196
|
+
|
|
197
|
+
local function handle_navigation(up: boolean)
|
|
198
|
+
local suggested = analysis().suggestions
|
|
199
|
+
|
|
200
|
+
if
|
|
201
|
+
#suggested == 0
|
|
202
|
+
or UserInputService:IsKeyDown(Enum.KeyCode.LeftShift)
|
|
203
|
+
or (selected_suggestion() == 1 and up and #cmd_history > 0)
|
|
204
|
+
then
|
|
205
|
+
cmd_history_index = math.clamp(cmd_history_index + (up and 1 or -1), 0, #cmd_history)
|
|
206
|
+
raw_text(cmd_history[cmd_history_index] or "")
|
|
207
|
+
cursor_pos(#raw_text() + 1)
|
|
208
|
+
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
selected_suggestion(math.clamp(selected_suggestion() + (up and -1 or 1), 1, #suggested))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
effect(function()
|
|
216
|
+
if raw_text() == "" then
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
display_analysis(analysis())
|
|
220
|
+
end)
|
|
221
|
+
|
|
222
|
+
cleanup(UserInputService.InputBegan:Connect(function(input)
|
|
223
|
+
if input.UserInputState ~= Enum.UserInputState.Begin then
|
|
224
|
+
return
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if
|
|
228
|
+
state.hide_on_lost_focus()
|
|
229
|
+
and not over_interface()
|
|
230
|
+
and (
|
|
231
|
+
input.UserInputType == Enum.UserInputType.MouseButton1
|
|
232
|
+
or input.UserInputType == Enum.UserInputType.Touch
|
|
233
|
+
)
|
|
234
|
+
then
|
|
235
|
+
state.opened(false)
|
|
236
|
+
return
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
if state.opened() then
|
|
240
|
+
if input.KeyCode == Enum.KeyCode.Up then
|
|
241
|
+
handle_navigation(true)
|
|
242
|
+
elseif input.KeyCode == Enum.KeyCode.Down then
|
|
243
|
+
handle_navigation(false)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if not activation_keys()[input.KeyCode] then
|
|
248
|
+
return
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
local opened = not state.opened()
|
|
252
|
+
state.opened(opened)
|
|
253
|
+
state.focused(opened)
|
|
254
|
+
end))
|
|
255
|
+
|
|
256
|
+
-- refresh suggestions for leftover input whenever the console opens, which
|
|
257
|
+
-- can also happen through the public open/toggle functions
|
|
258
|
+
effect(function()
|
|
259
|
+
if not state.opened() then
|
|
260
|
+
return
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
untrack(function()
|
|
264
|
+
local input_text = raw_text()
|
|
265
|
+
if #input_text > 0 then
|
|
266
|
+
analysis(analyse(input_text, #input_text + 1, state.max_suggestions()))
|
|
267
|
+
end
|
|
268
|
+
end)
|
|
269
|
+
end)
|
|
270
|
+
|
|
271
|
+
effect(function()
|
|
272
|
+
if executing() == 0 then
|
|
273
|
+
busy(false)
|
|
274
|
+
return
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
local thread = task.delay(0.1, busy, true)
|
|
278
|
+
cleanup(function()
|
|
279
|
+
task.cancel(thread)
|
|
280
|
+
end)
|
|
281
|
+
end)
|
|
282
|
+
|
|
283
|
+
effect(function()
|
|
284
|
+
if not busy() then
|
|
285
|
+
return
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
cleanup(RunService.Heartbeat:Connect(function()
|
|
289
|
+
spinner_frame(math.floor(os.clock() * 10) % #spinner_frames + 1)
|
|
290
|
+
end))
|
|
291
|
+
end)
|
|
292
|
+
|
|
293
|
+
local analysing = false
|
|
294
|
+
effect(function()
|
|
295
|
+
raw_text()
|
|
296
|
+
cursor_pos()
|
|
297
|
+
|
|
298
|
+
if analysing then
|
|
299
|
+
return
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
analysing = true
|
|
303
|
+
task.defer(function()
|
|
304
|
+
analysing = false
|
|
305
|
+
|
|
306
|
+
-- the textbox reports -1 while unfocused, which is the only time
|
|
307
|
+
-- consoles see the input, since their keyboard covers the screen
|
|
308
|
+
local pos = cursor_pos()
|
|
309
|
+
if pos == -1 then
|
|
310
|
+
pos = #raw_text() + 1
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
analysis(analyse(raw_text(), pos, state.max_suggestions()))
|
|
314
|
+
end)
|
|
315
|
+
end)
|
|
316
|
+
|
|
317
|
+
local function submit(value: string)
|
|
318
|
+
-- resolve from the submitted text rather than the analysis, which only
|
|
319
|
+
-- carries arguments when the cursor happens to sit on one
|
|
320
|
+
local submitted = centurion.resolve_command(value)
|
|
321
|
+
raw_text("")
|
|
322
|
+
analysis(default_analysis)
|
|
323
|
+
|
|
324
|
+
if #value:gsub("%s", "") == 0 then
|
|
325
|
+
state.opened(false)
|
|
326
|
+
return
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
-- on consoles the keyboard would cover the result, so leave it closed
|
|
330
|
+
if not gamepad() then
|
|
331
|
+
task.delay(0.1, function()
|
|
332
|
+
state.focused(true)
|
|
333
|
+
end)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
cmd_history_index = 0
|
|
337
|
+
table.insert(cmd_history, 1, value)
|
|
338
|
+
|
|
339
|
+
local cmd = submitted.name
|
|
340
|
+
if not cmd then
|
|
341
|
+
log(value, {
|
|
342
|
+
success = false,
|
|
343
|
+
message = "Unknown command",
|
|
344
|
+
timestamp = os.time(),
|
|
345
|
+
})
|
|
346
|
+
return
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
executing_command(cmd)
|
|
350
|
+
executing(executing() + 1)
|
|
351
|
+
|
|
352
|
+
-- pass the submitted text so that ctx.input keeps its original quoting
|
|
353
|
+
local ok, result = pcall(centurion.execute_command, Players.LocalPlayer, cmd, submitted.args, value)
|
|
354
|
+
executing(executing() - 1)
|
|
355
|
+
|
|
356
|
+
if not ok then
|
|
357
|
+
warn(`Failed to execute '{value}': {result}`)
|
|
358
|
+
log(value, {
|
|
359
|
+
success = false,
|
|
360
|
+
message = "Failed to execute command",
|
|
361
|
+
timestamp = os.time(),
|
|
362
|
+
})
|
|
363
|
+
return
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
if result.response then
|
|
367
|
+
if result.response.message ~= "" then
|
|
368
|
+
log(value, result.response)
|
|
369
|
+
end
|
|
370
|
+
else
|
|
371
|
+
log(value, {
|
|
372
|
+
success = true,
|
|
373
|
+
message = "Command executed",
|
|
374
|
+
timestamp = os.time(),
|
|
375
|
+
})
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
-- consoles can only browse suggestions while their keyboard is closed, so
|
|
380
|
+
-- bind the gamepad to them until the textbox is focused again. this is
|
|
381
|
+
-- also the only way to run a command, since confirming the keyboard doesn't
|
|
382
|
+
effect(function()
|
|
383
|
+
-- dismissing the keyboard can leave the textbox focused, so only treat it
|
|
384
|
+
-- as busy while the keyboard is actually showing
|
|
385
|
+
if not state.opened() or not gamepad() or (state.focused() and keyboard_visible()) then
|
|
386
|
+
return
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
ContextActionService:BindActionAtPriority(
|
|
390
|
+
"centurion_gamepad",
|
|
391
|
+
function(_, input_state, input)
|
|
392
|
+
if input_state ~= Enum.UserInputState.Begin then
|
|
393
|
+
return Enum.ContextActionResult.Sink
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
if input.KeyCode == Enum.KeyCode.DPadUp then
|
|
397
|
+
handle_navigation(true)
|
|
398
|
+
elseif input.KeyCode == Enum.KeyCode.DPadDown then
|
|
399
|
+
handle_navigation(false)
|
|
400
|
+
elseif input.KeyCode == Enum.KeyCode.ButtonA then
|
|
401
|
+
local selected = analysis().suggestions[selected_suggestion()]
|
|
402
|
+
if selected then
|
|
403
|
+
raw_text(autofill(raw_text(), selected, true))
|
|
404
|
+
end
|
|
405
|
+
elseif input.KeyCode == Enum.KeyCode.ButtonX then
|
|
406
|
+
open_keyboard()
|
|
407
|
+
elseif input.KeyCode == Enum.KeyCode.ButtonY then
|
|
408
|
+
submit(raw_text())
|
|
409
|
+
elseif input.KeyCode == Enum.KeyCode.ButtonB then
|
|
410
|
+
state.opened(false)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
return Enum.ContextActionResult.Sink
|
|
414
|
+
end,
|
|
415
|
+
false,
|
|
416
|
+
Enum.ContextActionPriority.High.Value,
|
|
417
|
+
Enum.KeyCode.DPadUp,
|
|
418
|
+
Enum.KeyCode.DPadDown,
|
|
419
|
+
Enum.KeyCode.ButtonA,
|
|
420
|
+
Enum.KeyCode.ButtonX,
|
|
421
|
+
Enum.KeyCode.ButtonY,
|
|
422
|
+
Enum.KeyCode.ButtonB
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
cleanup(function()
|
|
426
|
+
ContextActionService:UnbindAction("centurion_gamepad")
|
|
427
|
+
end)
|
|
428
|
+
end)
|
|
429
|
+
|
|
430
|
+
-- 0 when closed, 1 when open. The console fades and slides with it, and
|
|
431
|
+
-- stays visible until it has finished closing
|
|
432
|
+
local reveal = spring(function()
|
|
433
|
+
return if state.opened() then 1 else 0
|
|
434
|
+
end, 0.15)
|
|
435
|
+
|
|
436
|
+
-- the popover only shows when there's something to say about the input
|
|
437
|
+
local has_popover = derive(function()
|
|
438
|
+
local value = analysis()
|
|
439
|
+
return not value.result.success or value.argument ~= nil or #value.suggestions > 0
|
|
440
|
+
end)
|
|
441
|
+
local pop = spring(function()
|
|
442
|
+
return if has_popover() then 1 else 0
|
|
443
|
+
end, 0.12)
|
|
444
|
+
|
|
445
|
+
-- created inside the console so that it fades with it
|
|
446
|
+
local function input_field()
|
|
447
|
+
return background {
|
|
448
|
+
auto = "XY",
|
|
449
|
+
size = UDim2.fromScale(1, 0),
|
|
450
|
+
color = function()
|
|
451
|
+
return state.theme().surface
|
|
452
|
+
end,
|
|
453
|
+
transparency = state.transparency,
|
|
454
|
+
|
|
455
|
+
corner(px(6)),
|
|
456
|
+
padding {
|
|
457
|
+
y = px(8),
|
|
458
|
+
left = px(function()
|
|
459
|
+
return 34 + shake()
|
|
460
|
+
end),
|
|
461
|
+
right = px(function()
|
|
462
|
+
return 10 - shake()
|
|
463
|
+
end),
|
|
464
|
+
},
|
|
465
|
+
stroke {
|
|
466
|
+
color = spring(function()
|
|
467
|
+
local current = state.theme()
|
|
468
|
+
if failed() or not analysis().result.success then
|
|
469
|
+
return current.error
|
|
470
|
+
elseif busy() or state.focused() then
|
|
471
|
+
return current.highlight
|
|
472
|
+
end
|
|
473
|
+
return theme.border(current)
|
|
474
|
+
end, 0.15),
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
text {
|
|
478
|
+
text = function()
|
|
479
|
+
return if busy() then spinner_frames[spinner_frame()] else ">"
|
|
480
|
+
end,
|
|
481
|
+
text_style = function()
|
|
482
|
+
return (busy() or analysis().result.success) and "highlight" or "error"
|
|
483
|
+
end,
|
|
484
|
+
weight = Enum.FontWeight.Bold,
|
|
485
|
+
auto = "XY",
|
|
486
|
+
|
|
487
|
+
{
|
|
488
|
+
AnchorPoint = Vector2.new(0, 0.5),
|
|
489
|
+
Position = px(UDim2.new(0, -22, 0.5, 0)),
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
-- the rest of the selected suggestion, shown after the input
|
|
494
|
+
text {
|
|
495
|
+
size = UDim2.fromScale(1, 1),
|
|
496
|
+
text = function()
|
|
497
|
+
local analysis_suggestions = analysis().suggestions
|
|
498
|
+
if touch() or #analysis_suggestions == 0 then
|
|
499
|
+
return ""
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
local selected = analysis_suggestions[selected_suggestion()]
|
|
503
|
+
local suggested = autofill(raw_text(), selected, false)
|
|
504
|
+
if suggested:sub(1, #raw_text()) ~= raw_text() then
|
|
505
|
+
return ""
|
|
506
|
+
end
|
|
507
|
+
return suggested
|
|
508
|
+
end,
|
|
509
|
+
text_style = "subtext",
|
|
510
|
+
transparency = 0.3,
|
|
511
|
+
xalignment = Enum.TextXAlignment.Left,
|
|
512
|
+
zindex = -1,
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
-- syntax highlighted copy of the input, drawn over the textbox
|
|
516
|
+
text {
|
|
517
|
+
size = UDim2.fromScale(1, 1),
|
|
518
|
+
text = function()
|
|
519
|
+
return if touch() then "" else highlight(raw_text(), state.theme())
|
|
520
|
+
end,
|
|
521
|
+
rich_text = true,
|
|
522
|
+
xalignment = Enum.TextXAlignment.Left,
|
|
523
|
+
zindex = 2,
|
|
524
|
+
|
|
525
|
+
{ Interactable = false },
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
textbox {
|
|
529
|
+
placeholder = function()
|
|
530
|
+
return if busy() then `Running {executing_command()}...` else "Type a command"
|
|
531
|
+
end,
|
|
532
|
+
xalignment = Enum.TextXAlignment.Left,
|
|
533
|
+
-- the highlighted overlay draws the text, but hiding it completely would also hide the cursor
|
|
534
|
+
transparency = function()
|
|
535
|
+
return if raw_text() == "" or touch() then 0 else 0.5
|
|
536
|
+
end,
|
|
537
|
+
|
|
538
|
+
text = raw_text,
|
|
539
|
+
update_text = function(value)
|
|
540
|
+
local pos = next_cursor_pos or cursor_pos()
|
|
541
|
+
local last_char = string.sub(value, pos - 1, pos - 1)
|
|
542
|
+
|
|
543
|
+
local suggested = analysis().suggestions
|
|
544
|
+
if last_char == "\t" and suggested[1] then
|
|
545
|
+
local selected = suggested[selected_suggestion()]
|
|
546
|
+
value = autofill(raw_text(), selected, true)
|
|
547
|
+
end
|
|
548
|
+
raw_text(value)
|
|
549
|
+
end,
|
|
550
|
+
|
|
551
|
+
focused = state.focused,
|
|
552
|
+
update_focused = function(focused)
|
|
553
|
+
if focused then
|
|
554
|
+
refocus_deadline = 0
|
|
555
|
+
elseif os.clock() < refocus_deadline then
|
|
556
|
+
-- the tap that selected an entry released the textbox, so take it back
|
|
557
|
+
refocus_deadline = 0
|
|
558
|
+
task.defer(state.focused, true)
|
|
559
|
+
end
|
|
560
|
+
state.focused(focused)
|
|
561
|
+
end,
|
|
562
|
+
|
|
563
|
+
enter = submit,
|
|
564
|
+
-- the console keyboard only writes its text back when confirmed, so
|
|
565
|
+
-- confirming has to show suggestions instead of running the command
|
|
566
|
+
submit_on_keyboard_return = false,
|
|
567
|
+
bind_open_keyboard = function(open)
|
|
568
|
+
open_keyboard = open
|
|
569
|
+
end,
|
|
570
|
+
|
|
571
|
+
{
|
|
572
|
+
AutomaticSize = Enum.AutomaticSize.None,
|
|
573
|
+
Size = UDim2.fromScale(1, 1),
|
|
574
|
+
CursorPosition = cursor_pos,
|
|
575
|
+
|
|
576
|
+
changed("CursorPosition", function(new)
|
|
577
|
+
if next_cursor_pos then
|
|
578
|
+
next_cursor_pos = new
|
|
579
|
+
return
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
next_cursor_pos = new
|
|
583
|
+
task.defer(function()
|
|
584
|
+
cursor_pos(next_cursor_pos)
|
|
585
|
+
next_cursor_pos = false
|
|
586
|
+
end)
|
|
587
|
+
end),
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
}
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
return fade.opacity(reveal, function()
|
|
594
|
+
return container {
|
|
595
|
+
auto = "Y",
|
|
596
|
+
anchor = state.anchor,
|
|
597
|
+
position = function()
|
|
598
|
+
local offset = px.convert((reveal() - 1) * 12)
|
|
599
|
+
return px.get(state.position) + UDim2.fromOffset(0, offset)
|
|
600
|
+
end,
|
|
601
|
+
size = px(function()
|
|
602
|
+
local w = state.width()
|
|
603
|
+
return UDim2.new(w.Scale, w.Offset, 0, 0)
|
|
604
|
+
end),
|
|
605
|
+
|
|
606
|
+
padding(px(12)),
|
|
607
|
+
flex():row():gap(2),
|
|
608
|
+
|
|
609
|
+
{
|
|
610
|
+
Visible = function()
|
|
611
|
+
return state.opened() or reveal() > 0.01
|
|
612
|
+
end,
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
-- the console itself: header, history and input
|
|
616
|
+
background {
|
|
617
|
+
auto = "Y",
|
|
618
|
+
size = UDim2.fromScale(1, 0),
|
|
619
|
+
order = 1,
|
|
620
|
+
color = function()
|
|
621
|
+
return state.theme().background
|
|
622
|
+
end,
|
|
623
|
+
transparency = state.transparency,
|
|
624
|
+
|
|
625
|
+
corner(px(10)),
|
|
626
|
+
stroke {
|
|
627
|
+
color = function()
|
|
628
|
+
return theme.border(state.theme())
|
|
629
|
+
end,
|
|
630
|
+
},
|
|
631
|
+
flex():row():fill("horizontal"),
|
|
632
|
+
catch_hover(),
|
|
633
|
+
|
|
634
|
+
header { order = 1 },
|
|
635
|
+
divider { order = 2 },
|
|
636
|
+
|
|
637
|
+
history {
|
|
638
|
+
order = 3,
|
|
639
|
+
max_lines = max_history_lines,
|
|
640
|
+
on_select = function(command)
|
|
641
|
+
cmd_history_index = 0
|
|
642
|
+
raw_text(command)
|
|
643
|
+
refocus()
|
|
644
|
+
-- after the textbox has recaptured focus, move the cursor to the end
|
|
645
|
+
task.defer(cursor_pos, #command + 1)
|
|
646
|
+
end,
|
|
647
|
+
},
|
|
648
|
+
|
|
649
|
+
container {
|
|
650
|
+
auto = "Y",
|
|
651
|
+
size = UDim2.fromScale(1, 0),
|
|
652
|
+
order = 4,
|
|
653
|
+
|
|
654
|
+
padding(px(8)),
|
|
655
|
+
|
|
656
|
+
input_field(),
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
-- completion popover, dropping down from the input
|
|
661
|
+
fade.opacity(function()
|
|
662
|
+
return reveal() * pop()
|
|
663
|
+
end, function()
|
|
664
|
+
return container {
|
|
665
|
+
auto = "XY",
|
|
666
|
+
order = 2,
|
|
667
|
+
|
|
668
|
+
padding {
|
|
669
|
+
left = px(8),
|
|
670
|
+
top = px(function()
|
|
671
|
+
return 6 * pop()
|
|
672
|
+
end),
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
{
|
|
676
|
+
Visible = function()
|
|
677
|
+
return pop() > 0.01
|
|
678
|
+
end,
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
suggestions {
|
|
682
|
+
analysis = display_analysis,
|
|
683
|
+
selected_suggestion = selected_suggestion,
|
|
684
|
+
on_select = function(suggestion: types.AnalysisSuggestion)
|
|
685
|
+
raw_text(autofill(raw_text(), suggestion, true))
|
|
686
|
+
if not gamepad() then
|
|
687
|
+
refocus()
|
|
688
|
+
end
|
|
689
|
+
end,
|
|
690
|
+
|
|
691
|
+
catch_hover(),
|
|
692
|
+
},
|
|
693
|
+
}
|
|
694
|
+
end),
|
|
695
|
+
}
|
|
696
|
+
end)
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
return app
|