-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig_editor_input.zig
More file actions
499 lines (431 loc) · 17.1 KB
/
config_editor_input.zig
File metadata and controls
499 lines (431 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
// Config Editor Input Handler - Processes user input for the config editor
const std = @import("std");
const config_editor_state = @import("config_editor_state");
const ConfigEditorState = config_editor_state.ConfigEditorState;
const ConfigField = config_editor_state.ConfigField;
const FieldType = config_editor_state.FieldType;
/// Result of handling input
pub const InputResult = enum {
/// Continue showing editor
@"continue",
/// User wants to save and close
save_and_close,
/// User wants to cancel (discard changes)
cancel,
/// Screen needs redraw
redraw,
};
/// Handle input for the config editor
pub fn handleInput(
state: *ConfigEditorState,
input: []const u8,
) !InputResult {
// Handle escape sequences first (arrow keys, function keys, etc.)
if (input.len >= 3 and input[0] == 0x1B and input[1] == '[') {
return try handleEscapeSequence(state, input);
}
// Handle single-byte inputs
if (input.len == 1) {
return try handleSingleKey(state, input[0]);
}
// Handle multi-byte input (UTF-8, etc.)
return .@"continue";
}
/// Handle escape sequences (arrow keys, etc.)
fn handleEscapeSequence(state: *ConfigEditorState, input: []const u8) !InputResult {
// Up arrow: \x1b[A
if (input.len == 3 and input[2] == 'A') {
state.focusPrevious();
return .redraw;
}
// Down arrow: \x1b[B
if (input.len == 3 and input[2] == 'B') {
state.focusNext();
return .redraw;
}
// Left arrow: \x1b[D - cycle radio buttons backward
if (input.len == 3 and input[2] == 'D') {
if (state.getFocusedField()) |field| {
if (field.field_type == .radio) {
try cycleRadioBackward(state, field);
state.has_changes = true;
return .redraw;
}
}
return .@"continue";
}
// Right arrow: \x1b[C - cycle radio buttons forward
if (input.len == 3 and input[2] == 'C') {
if (state.getFocusedField()) |field| {
if (field.field_type == .radio) {
try cycleRadioForward(state, field);
state.has_changes = true;
return .redraw;
}
}
return .@"continue";
}
return .@"continue";
}
/// Handle single key presses
fn handleSingleKey(state: *ConfigEditorState, key: u8) !InputResult {
// If we're in edit mode for a text/number/masked input, pass most keys to input handler
// (except Tab and Ctrl keys which should still work for navigation/save)
if (state.getFocusedField()) |field| {
if (field.is_editing and (field.field_type == .text_input or field.field_type == .number_input or field.field_type == .masked_input)) {
// Allow Tab, Ctrl+S, Esc to still work in edit mode
if (key != '\t' and key != 0x13 and key != 0x1B) {
return try handleTextInput(state, field, key);
}
}
}
switch (key) {
// Tab: Move to next field
'\t' => {
// If in edit mode, commit changes first
if (state.getFocusedField()) |field| {
if (field.is_editing) {
if (field.field_type == .text_input or field.field_type == .masked_input) {
try commitTextEdit(state, field);
} else if (field.field_type == .number_input) {
try commitNumberEdit(state, field);
}
field.is_editing = false;
state.has_changes = true;
}
}
state.focusNext();
return .redraw;
},
// Enter: Toggle/activate current field
'\r', '\n' => {
return try handleEnterKey(state);
},
// Escape: Exit edit mode, or cancel and close if not editing
0x1B => {
if (state.getFocusedField()) |field| {
if (field.is_editing and (field.field_type == .text_input or field.field_type == .number_input or field.field_type == .masked_input)) {
// Exit edit mode without saving changes to this field
field.is_editing = false;
return .redraw;
}
}
return .cancel;
},
// Ctrl+S (Save): Save and close
0x13 => {
return .save_and_close;
},
// Ctrl+R (Reset): Reset to defaults
0x12 => {
// TODO: Implement reset to defaults
return .redraw;
},
// Space: Toggle boolean fields
' ' => {
if (state.getFocusedField()) |field| {
if (field.field_type == .toggle) {
try toggleField(state, field);
state.has_changes = true;
return .redraw;
}
}
return .@"continue";
},
else => {
// If in edit mode for text/masked input, handle character input
if (state.getFocusedField()) |field| {
if (field.is_editing and (field.field_type == .text_input or field.field_type == .masked_input)) {
return try handleTextInput(state, field, key);
}
}
return .@"continue";
},
}
}
/// Handle Enter key on current field
fn handleEnterKey(state: *ConfigEditorState) !InputResult {
if (state.getFocusedField()) |field| {
switch (field.field_type) {
.toggle => {
try toggleField(state, field);
state.has_changes = true;
return .redraw;
},
.radio => {
try cycleRadioForward(state, field);
state.has_changes = true;
return .redraw;
},
.text_input, .masked_input => {
if (field.is_editing) {
// Finish editing - save buffer to config
try commitTextEdit(state, field);
field.is_editing = false;
state.has_changes = true;
} else {
// Start editing - copy current value to edit buffer
try startTextEdit(state, field);
}
return .redraw;
},
.number_input => {
if (field.is_editing) {
// Finish editing - parse buffer and save to config
try commitNumberEdit(state, field);
field.is_editing = false;
state.has_changes = true;
} else {
// Start editing - copy current number to edit buffer
try startNumberEdit(state, field);
}
return .redraw;
},
}
}
return .@"continue";
}
/// Toggle a boolean field
fn toggleField(state: *ConfigEditorState, field: *ConfigField) !void {
const config = &state.temp_config;
const llm_provider = @import("llm_provider");
// Check if this is a shared toggle field
if (std.mem.eql(u8, field.key, "enable_thinking")) {
config.enable_thinking = !config.enable_thinking;
} else if (std.mem.eql(u8, field.key, "show_tool_json")) {
config.show_tool_json = !config.show_tool_json;
} else {
// Try provider-specific toggle fields
const current_value = config.getProviderField(config.provider, field.key);
if (current_value == .boolean) {
const new_value = llm_provider.ConfigValue{ .boolean = !current_value.boolean };
try config.setProviderField(state.allocator, config.provider, field.key, new_value);
}
}
}
/// Cycle radio button forward
fn cycleRadioForward(state: *ConfigEditorState, field: *ConfigField) !void {
if (field.options) |options| {
const current_value = getCurrentRadioValue(state, field.key);
// Find current index
var current_idx: ?usize = null;
for (options, 0..) |option, i| {
if (std.mem.eql(u8, current_value, option)) {
current_idx = i;
break;
}
}
// Move to next option (wrap around)
const next_idx = if (current_idx) |idx|
(idx + 1) % options.len
else
0;
try setRadioValue(state, field.key, options[next_idx]);
}
}
/// Cycle radio button backward
fn cycleRadioBackward(state: *ConfigEditorState, field: *ConfigField) !void {
if (field.options) |options| {
const current_value = getCurrentRadioValue(state, field.key);
var current_idx: ?usize = null;
for (options, 0..) |option, i| {
if (std.mem.eql(u8, current_value, option)) {
current_idx = i;
break;
}
}
const prev_idx = if (current_idx) |idx|
if (idx == 0) options.len - 1 else idx - 1
else
options.len - 1;
try setRadioValue(state, field.key, options[prev_idx]);
}
}
/// Start editing a text field
fn startTextEdit(state: *ConfigEditorState, field: *ConfigField) !void {
// Free old buffer if exists
if (field.edit_buffer) |old_buffer| {
state.allocator.free(old_buffer);
}
// Get current value from config for text fields
const current_value = getCurrentTextValue(state, field.key);
// Allocate edit buffer and copy current value
field.edit_buffer = try state.allocator.dupe(u8, current_value);
field.is_editing = true;
}
/// Commit text edit to config
fn commitTextEdit(state: *ConfigEditorState, field: *ConfigField) !void {
if (field.edit_buffer) |buffer| {
try setTextValue(state, field.key, buffer);
}
}
/// Start editing a number field
fn startNumberEdit(state: *ConfigEditorState, field: *ConfigField) !void {
// Get current number value from config and convert to string
const current_number = getCurrentNumberValue(state, field.key);
// Format number to string
var buf: [32]u8 = undefined;
const number_str = if (std.mem.eql(u8, field.key, "num_predict"))
try std.fmt.bufPrint(&buf, "{d}", .{@as(isize, @intCast(current_number))})
else
try std.fmt.bufPrint(&buf, "{d}", .{current_number});
// Allocate edit buffer and copy formatted number
const buffer = try state.allocator.dupe(u8, number_str);
// Free old buffer if exists
if (field.edit_buffer) |old_buffer| {
state.allocator.free(old_buffer);
}
field.edit_buffer = buffer;
field.is_editing = true;
}
/// Commit number edit to config
fn commitNumberEdit(state: *ConfigEditorState, field: *ConfigField) !void {
if (field.edit_buffer) |buffer| {
// Parse the string buffer to a number
const parsed = std.fmt.parseInt(isize, buffer, 10) catch {
// If parsing fails, just ignore the edit
return;
};
try setNumberValue(state, field.key, parsed);
}
}
/// Handle character input during text editing
fn handleTextInput(state: *ConfigEditorState, field: *ConfigField, key: u8) !InputResult {
if (field.edit_buffer) |old_buffer| {
// Handle backspace
if (key == 0x7F or key == 0x08) {
if (old_buffer.len > 0) {
// Shrink buffer by 1 character
const new_buffer = try state.allocator.alloc(u8, old_buffer.len - 1);
@memcpy(new_buffer, old_buffer[0..old_buffer.len - 1]);
state.allocator.free(old_buffer);
field.edit_buffer = new_buffer;
return .redraw;
}
return .@"continue";
}
// Handle printable characters
else if (key >= 0x20 and key <= 0x7E) {
// For number inputs, only accept digits and minus sign
if (field.field_type == .number_input) {
const is_digit = key >= '0' and key <= '9';
const is_minus = key == '-' and old_buffer.len == 0; // Only at start
if (!is_digit and !is_minus) {
return .@"continue";
}
}
// Grow buffer by 1 and append character
const new_buffer = try state.allocator.alloc(u8, old_buffer.len + 1);
@memcpy(new_buffer[0..old_buffer.len], old_buffer);
new_buffer[old_buffer.len] = key;
state.allocator.free(old_buffer);
field.edit_buffer = new_buffer;
return .redraw;
}
}
return .@"continue";
}
/// Get current radio value from config
fn getCurrentRadioValue(state: *const ConfigEditorState, key: []const u8) []const u8 {
const config = &state.temp_config;
if (std.mem.eql(u8, key, "provider")) return config.provider;
return "";
}
/// Set radio value in config
fn setRadioValue(state: *ConfigEditorState, key: []const u8, value: []const u8) !void {
const config = &state.temp_config;
if (std.mem.eql(u8, key, "provider")) {
const old_provider = config.provider;
const changed = !std.mem.eql(u8, old_provider, value);
state.allocator.free(config.provider);
config.provider = try state.allocator.dupe(u8, value);
// Rebuild sections when provider changes to show provider-specific fields
if (changed) {
try state.rebuildSections();
}
}
}
/// Get current text value from config
fn getCurrentTextValue(state: *const ConfigEditorState, key: []const u8) []const u8 {
const config = &state.temp_config;
// Profile name (special case - stored in state, not config)
if (std.mem.eql(u8, key, "profile_name")) return state.profile_name;
// Shared fields
if (std.mem.eql(u8, key, "model")) return config.model;
// Google Search API fields
if (std.mem.eql(u8, key, "google_search_api_key")) return config.google_search_api_key orelse "";
if (std.mem.eql(u8, key, "google_search_engine_id")) return config.google_search_engine_id orelse "";
// Try provider-specific fields
const provider_value = config.getProviderField(config.provider, key);
if (provider_value == .text) {
return provider_value.text;
}
return "";
}
/// Set text value in config
fn setTextValue(state: *ConfigEditorState, key: []const u8, value: []const u8) !void {
const config = &state.temp_config;
const llm_provider = @import("llm_provider");
// Profile name (special case - stored in state, not config)
if (std.mem.eql(u8, key, "profile_name")) {
state.allocator.free(state.profile_name);
state.profile_name = try state.allocator.dupe(u8, value);
return;
}
// Shared fields
if (std.mem.eql(u8, key, "model")) {
state.allocator.free(config.model);
config.model = try state.allocator.dupe(u8, value);
return;
}
// Google Search API fields
if (std.mem.eql(u8, key, "google_search_api_key")) {
if (config.google_search_api_key) |old_key| {
state.allocator.free(old_key);
}
config.google_search_api_key = if (value.len > 0) try state.allocator.dupe(u8, value) else null;
return;
}
if (std.mem.eql(u8, key, "google_search_engine_id")) {
if (config.google_search_engine_id) |old_id| {
state.allocator.free(old_id);
}
config.google_search_engine_id = if (value.len > 0) try state.allocator.dupe(u8, value) else null;
return;
}
// Try provider-specific text fields
const new_value = llm_provider.ConfigValue{ .text = value };
try config.setProviderField(state.allocator, config.provider, key, new_value);
}
/// Get current number value from config
fn getCurrentNumberValue(state: *const ConfigEditorState, key: []const u8) isize {
const config = &state.temp_config;
if (std.mem.eql(u8, key, "num_ctx")) return @as(isize, @intCast(config.num_ctx));
if (std.mem.eql(u8, key, "num_predict")) return config.num_predict;
if (std.mem.eql(u8, key, "scroll_lines")) return @as(isize, @intCast(config.scroll_lines));
if (std.mem.eql(u8, key, "file_read_small_threshold")) return @as(isize, @intCast(config.file_read_small_threshold));
// Try provider-specific number fields
const provider_value = config.getProviderField(config.provider, key);
if (provider_value == .number) {
return provider_value.number;
}
return 0;
}
/// Set number value in config
fn setNumberValue(state: *ConfigEditorState, key: []const u8, value: isize) !void {
const config = &state.temp_config;
if (std.mem.eql(u8, key, "num_ctx")) {
config.num_ctx = @as(usize, @intCast(@max(0, value)));
} else if (std.mem.eql(u8, key, "num_predict")) {
config.num_predict = value;
} else if (std.mem.eql(u8, key, "scroll_lines")) {
config.scroll_lines = @as(usize, @intCast(@max(1, value)));
} else if (std.mem.eql(u8, key, "file_read_small_threshold")) {
config.file_read_small_threshold = @as(usize, @intCast(@max(0, value)));
} else {
// Try provider-specific number fields
const llm_provider = @import("llm_provider");
const new_value = llm_provider.ConfigValue{ .number = value };
try config.setProviderField(state.allocator, config.provider, key, new_value);
}
}