-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig_editor_renderer.zig
More file actions
348 lines (293 loc) · 12.9 KB
/
config_editor_renderer.zig
File metadata and controls
348 lines (293 loc) · 12.9 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
// Config Editor Renderer - Draws the configuration UI to the terminal
const std = @import("std");
const config_editor_state = @import("config_editor_state");
const ui = @import("ui");
const text_utils = @import("text_utils");
const ConfigEditorState = config_editor_state.ConfigEditorState;
const ConfigSection = config_editor_state.ConfigSection;
const ConfigField = config_editor_state.ConfigField;
const FieldType = config_editor_state.FieldType;
/// Render the config editor screen
pub fn render(
state: *ConfigEditorState,
writer: anytype,
terminal_width: u16,
terminal_height: u16,
) !void {
// Clear screen
try writer.writeAll("\x1b[2J\x1b[H");
// Calculate layout dimensions
const box_width = @min(terminal_width - 4, 70); // Leave 2 chars padding on each side
const box_start_x = (terminal_width - box_width) / 2; // Center horizontally
var current_y: usize = 2; // Start 2 rows from top
// Draw title
try drawCentered(writer, "Configuration Editor", terminal_width, current_y);
current_y += 1;
try drawCentered(writer, "Press Tab/Shift+Tab to navigate, Enter to edit, Ctrl+S to save, Esc to cancel", terminal_width, current_y);
current_y += 1;
// Draw profile indicator
const profile_manager = @import("profile_manager");
const active_profile = profile_manager.getActiveProfileName(state.allocator) catch "unknown";
defer state.allocator.free(active_profile);
const profile_text = std.fmt.allocPrint(state.allocator, "Editing profile: \x1b[1m{s}\x1b[0m", .{active_profile}) catch "Editing profile: unknown";
defer state.allocator.free(profile_text);
try drawCentered(writer, profile_text, terminal_width, current_y);
current_y += 2;
// Draw each section
var global_field_index: usize = 0;
for (state.sections) |section| {
// Section header
try writer.print("\x1b[{d};{d}H", .{ current_y, box_start_x });
try writer.print("\x1b[1;36m{s}\x1b[0m", .{section.title}); // Cyan, bold
current_y += 1;
// Section box top border
try writer.print("\x1b[{d};{d}H┌", .{ current_y, box_start_x });
for (0..box_width - 2) |_| try writer.writeAll("─");
try writer.writeAll("┐");
current_y += 1;
// Draw fields
for (section.fields) |field| {
const is_focused = global_field_index == state.focused_field_index;
try drawField(
writer,
&field,
state,
box_start_x,
current_y,
box_width,
is_focused,
);
current_y += 2; // Each field takes 2 lines (field + help text)
global_field_index += 1;
}
// Section box bottom border
try writer.print("\x1b[{d};{d}H└", .{ current_y, box_start_x });
for (0..box_width - 2) |_| try writer.writeAll("─");
try writer.writeAll("┘");
current_y += 2; // Space between sections
}
// Draw provider-specific warnings
try drawProviderWarnings(writer, state, box_start_x, box_width, ¤t_y, terminal_height);
// Draw action buttons at bottom
current_y = terminal_height - 3;
try drawCentered(writer, "[Ctrl+S] Save [Esc] Cancel [Ctrl+R] Reset to Defaults", terminal_width, current_y);
// Show change indicator if modified
if (state.has_changes) {
try writer.print("\x1b[{d};{d}H\x1b[33m● Unsaved changes\x1b[0m", .{ terminal_height - 1, box_start_x });
}
}
/// Draw a centered line of text
fn drawCentered(writer: anytype, text: []const u8, terminal_width: u16, y: usize) !void {
// Calculate visible width (strip ANSI codes for centering calculation)
const visible_len = text.len; // Simplified - in production you'd strip ANSI
const start_x = if (terminal_width > visible_len)
(terminal_width - @as(u16, @intCast(visible_len))) / 2
else
0;
try writer.print("\x1b[{d};{d}H{s}", .{ y, start_x, text });
}
/// Draw a single field
fn drawField(
writer: anytype,
field: *const ConfigField,
state: *const ConfigEditorState,
box_x: u16,
y: usize,
box_width: u16,
is_focused: bool,
) !void {
// Calculate available content width (box_width - borders - padding)
// Format: "│ content │" so we need to subtract: left border (1) + spaces (2) + right border (1) = 4
const content_width = box_width -| 4;
// Create a buffer to accumulate the field content
var content_buffer: [512]u8 = undefined;
var content_stream = std.io.fixedBufferStream(&content_buffer);
const content_writer = content_stream.writer();
// Build the field content (label + value)
content_writer.print("{s}: ", .{field.label}) catch {};
// Field value based on type
switch (field.field_type) {
.radio => {
drawRadioFieldToWriter(content_writer, field, state) catch {};
},
.toggle => {
drawToggleFieldToWriter(content_writer, field, state) catch {};
},
.text_input => {
drawTextInputFieldToWriter(content_writer, field, state) catch {};
},
.number_input => {
drawNumberInputFieldToWriter(content_writer, field, state) catch {};
},
.masked_input => {
drawMaskedInputFieldToWriter(content_writer, field, state) catch {};
},
}
const content = content_stream.getWritten();
// Truncate content if it exceeds available width
var truncate_buffer: [512]u8 = undefined;
const display_content = text_utils.truncateText(content, content_width, &truncate_buffer);
// Now render the line
try writer.print("\x1b[{d};{d}H│ ", .{ y, box_x });
// Highlight if focused
if (is_focused) {
try writer.writeAll("\x1b[7m"); // Reverse video
}
try writer.writeAll(display_content);
if (is_focused) {
try writer.writeAll("\x1b[0m"); // Reset formatting
}
// Pad remaining space and draw right border
// Use visual width (UTF-8 codepoint count) instead of byte length
const used_width = text_utils.visualWidth(display_content) + 2; // +2 for "│ " prefix
const padding_needed = box_width -| used_width -| 1; // -1 for right border
for (0..padding_needed) |_| {
try writer.writeAll(" ");
}
try writer.writeAll("│");
// Help text line
if (field.help_text) |help| {
var help_buffer: [512]u8 = undefined;
const display_help = text_utils.truncateText(help, content_width, &help_buffer);
try writer.print("\x1b[{d};{d}H│ \x1b[2m{s}\x1b[0m", .{ y + 1, box_x, display_help });
// Pad and draw right border for help text line
// Use visual width (UTF-8 codepoint count) instead of byte length
const help_used_width = text_utils.visualWidth(display_help) + 2; // +2 for "│ " prefix
const help_padding_needed = box_width -| help_used_width -| 1;
for (0..help_padding_needed) |_| {
try writer.writeAll(" ");
}
try writer.writeAll("│");
}
}
/// Draw radio button field to any writer (for buffering)
fn drawRadioFieldToWriter(writer: anytype, field: *const ConfigField, state: *const ConfigEditorState) !void {
if (field.options) |options| {
// Get current value from config
const current_value = getFieldValue(state, field.key);
for (options, 0..) |option, i| {
if (i > 0) try writer.writeAll(" ");
const is_selected = std.mem.eql(u8, current_value, option);
if (is_selected) {
try writer.print("[●] {s}", .{option});
} else {
try writer.print("[ ] {s}", .{option});
}
}
}
}
/// Draw toggle field to any writer (for buffering)
fn drawToggleFieldToWriter(writer: anytype, field: *const ConfigField, state: *const ConfigEditorState) !void {
const is_enabled = getFieldBoolValue(state, field.key);
if (is_enabled) {
try writer.writeAll("[✓] ON");
} else {
try writer.writeAll("[ ] OFF");
}
}
/// Draw text input field to any writer (for buffering)
fn drawTextInputFieldToWriter(writer: anytype, field: *const ConfigField, state: *const ConfigEditorState) !void {
const current_value = getFieldValue(state, field.key);
if (field.is_editing and field.edit_buffer != null) {
// Show edit buffer with cursor
try writer.print("{s}█", .{field.edit_buffer.?});
} else {
// Show current value
try writer.print("{s}", .{current_value});
}
}
/// Draw number input field to any writer (for buffering)
fn drawNumberInputFieldToWriter(writer: anytype, field: *const ConfigField, state: *const ConfigEditorState) !void {
const config = &state.temp_config;
if (field.is_editing and field.edit_buffer != null) {
// Show edit buffer with cursor
try writer.print("{s}█", .{field.edit_buffer.?});
} else {
// Get actual number value from config and format it
if (std.mem.eql(u8, field.key, "num_ctx")) {
try writer.print("{d}", .{config.num_ctx});
} else if (std.mem.eql(u8, field.key, "num_predict")) {
try writer.print("{d}", .{config.num_predict});
} else if (std.mem.eql(u8, field.key, "scroll_lines")) {
try writer.print("{d}", .{config.scroll_lines});
} else if (std.mem.eql(u8, field.key, "file_read_small_threshold")) {
try writer.print("{d}", .{config.file_read_small_threshold});
} else {
try writer.writeAll("0");
}
}
}
/// Draw masked input field to any writer (for buffering)
/// Shows full value when editing, masked value (first 4 + last 3 chars) when not editing
fn drawMaskedInputFieldToWriter(writer: anytype, field: *const ConfigField, state: *const ConfigEditorState) !void {
const current_value = getFieldValue(state, field.key);
if (field.is_editing and field.edit_buffer != null) {
// Show full edit buffer with cursor when editing
try writer.print("{s}█", .{field.edit_buffer.?});
} else {
// Show masked value when not editing
if (current_value.len == 0) {
try writer.writeAll("\x1b[2m(not set)\x1b[0m");
} else if (current_value.len <= 10) {
// Too short to mask meaningfully, just show dots
for (0..@min(current_value.len, 8)) |_| {
try writer.writeAll("•");
}
} else {
// Show first 4 chars + *** + last 3 chars
try writer.print("{s}***{s}", .{
current_value[0..4],
current_value[current_value.len - 3 ..],
});
}
}
}
/// Get string value from config based on field key
fn getFieldValue(state: *const ConfigEditorState, key: []const u8) []const u8 {
const config = &state.temp_config;
if (std.mem.eql(u8, key, "profile_name")) return state.profile_name;
if (std.mem.eql(u8, key, "provider")) return config.provider;
if (std.mem.eql(u8, key, "ollama_host")) return config.ollama_host;
if (std.mem.eql(u8, key, "lmstudio_host")) return config.lmstudio_host;
if (std.mem.eql(u8, key, "model")) return config.model;
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 "";
return "";
}
/// Get boolean value from config based on field key
fn getFieldBoolValue(state: *const ConfigEditorState, key: []const u8) bool {
const config = &state.temp_config;
// Check global boolean fields first
if (std.mem.eql(u8, key, "enable_thinking")) return config.enable_thinking;
if (std.mem.eql(u8, key, "show_tool_json")) return config.show_tool_json;
// Try provider-specific boolean fields
const provider_value = config.getProviderField(config.provider, key);
if (provider_value == .boolean) {
return provider_value.boolean;
}
return false;
}
/// Draw provider-specific warnings and helpful tips
fn drawProviderWarnings(
writer: anytype,
state: *const ConfigEditorState,
box_x: u16,
box_width: u16,
current_y: *usize,
terminal_height: u16,
) !void {
_ = box_width; // Not used currently, but kept for future enhancements
const config = &state.temp_config;
const llm_provider = @import("llm_provider");
// Check if we have enough space for warnings (need at least 2 lines before footer)
if (current_y.* + 2 >= terminal_height - 4) {
return; // Not enough space, skip warning
}
// Get provider capabilities from registry
const caps = llm_provider.ProviderRegistry.get(config.provider) orelse return;
// Display all warnings for this provider (data-driven!)
for (caps.config_warnings) |warning| {
try writer.print("\x1b[{d};{d}H\x1b[33m⚠ Note: {s}\x1b[0m", .{ current_y.*, box_x, warning.message });
current_y.* += 2; // Space after warning
}
}