C · 23517 bytes Raw Blame History
1 /* Display and user interface functions
2 * Provides safe, accessible terminal output for gitswitch-c
3 */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <string.h>
8 #include <stdarg.h>
9 #include <unistd.h>
10 #include <ctype.h>
11 #include <termios.h>
12
13 #include "display.h"
14 #include "utils.h"
15 #include "error.h"
16 #include "git_ops.h"
17
18 /* Global display state */
19 static bool g_color_enabled = false;
20 static bool g_color_forced = false;
21 static int g_terminal_width = 80;
22 static int g_terminal_height = 24;
23
24 /* Color support detection */
25 static bool detect_color_support(void) {
26 const char *term = getenv("TERM");
27 const char *colorterm = getenv("COLORTERM");
28
29 /* Force color if COLORTERM is set */
30 if (colorterm && *colorterm) {
31 return true;
32 }
33
34 /* Check for common color-capable terminals */
35 if (term) {
36 if (strstr(term, "color") ||
37 strstr(term, "xterm") ||
38 strstr(term, "screen") ||
39 strstr(term, "tmux") ||
40 strcmp(term, "linux") == 0) {
41 return true;
42 }
43 }
44
45 return false;
46 }
47
48 /* Initialize display system */
49 int display_init(bool force_color, bool no_color) {
50 if (no_color) {
51 g_color_enabled = false;
52 g_color_forced = true;
53 } else if (force_color) {
54 g_color_enabled = true;
55 g_color_forced = true;
56 } else {
57 /* Auto-detect color support */
58 g_color_enabled = is_terminal(STDOUT_FILENO) && detect_color_support();
59 g_color_forced = false;
60 }
61
62 /* Get terminal size */
63 if (get_terminal_size(&g_terminal_width, &g_terminal_height) != 0) {
64 /* Use defaults if we can't get size */
65 g_terminal_width = 80;
66 g_terminal_height = 24;
67 }
68
69 log_debug("Display initialized: color=%s, size=%dx%d",
70 g_color_enabled ? "enabled" : "disabled",
71 g_terminal_width, g_terminal_height);
72
73 return 0;
74 }
75
76 /* Check if terminal supports color output */
77 bool display_supports_color(void) {
78 return g_color_enabled;
79 }
80
81 /* Format and colorize text based on content type */
82 const char *display_colorize(const char *text, const char *type) {
83 /* Use rotating buffers to allow multiple calls in same expression */
84 static char colored_buffers[4][512];
85 static int buffer_index = 0;
86 char *colored_buffer;
87 const char *color_code = "";
88
89 if (!g_color_enabled || !text || !type) {
90 return text;
91 }
92
93 /* Select color based on type */
94 if (strcmp(type, "success") == 0) {
95 color_code = COLOR_GREEN;
96 } else if (strcmp(type, "error") == 0) {
97 color_code = COLOR_RED;
98 } else if (strcmp(type, "warning") == 0) {
99 color_code = COLOR_YELLOW;
100 } else if (strcmp(type, "info") == 0) {
101 color_code = COLOR_BLUE;
102 } else if (strcmp(type, "header") == 0) {
103 color_code = COLOR_BOLD COLOR_CYAN;
104 } else if (strcmp(type, "current") == 0) {
105 color_code = COLOR_BOLD COLOR_GREEN;
106 } else if (strcmp(type, "inactive") == 0) {
107 color_code = COLOR_DIM;
108 } else {
109 return text; /* No coloring */
110 }
111
112 /* Use next buffer in rotation */
113 colored_buffer = colored_buffers[buffer_index];
114 buffer_index = (buffer_index + 1) % 4;
115
116 snprintf(colored_buffer, 512,
117 "%s%s%s", color_code, text, COLOR_RESET);
118
119 return colored_buffer;
120 }
121
122 /* Print formatted header with decorative border */
123 void display_header(const char *title) {
124 int title_len, padding, total_width;
125 int i;
126
127 if (!title) return;
128
129 title_len = strlen(title);
130 total_width = (title_len + 4 > 40) ? title_len + 4 : 40;
131 if (total_width > g_terminal_width - 2) {
132 total_width = g_terminal_width - 2;
133 }
134
135 padding = (total_width - title_len - 2) / 2;
136
137 /* Top border */
138 printf("┌");
139 for (i = 0; i < total_width - 2; i++) {
140 printf("─");
141 }
142 printf("┐\n");
143
144 /* Title line */
145 printf("│%*s%s%s%*s│\n",
146 padding, "",
147 display_colorize(title, "header"),
148 COLOR_RESET,
149 total_width - title_len - padding - 2, "");
150
151 /* Bottom border */
152 printf("└");
153 for (i = 0; i < total_width - 2; i++) {
154 printf("─");
155 }
156 printf("┘\n");
157 }
158
159 /* Print status message with appropriate color and icon */
160 void display_status(const char *level, const char *message, ...) {
161 va_list args;
162 char formatted_message[1024];
163 const char *icon = "";
164 const char *color_type = "";
165
166 if (!level || !message) return;
167
168 /* Format the message */
169 va_start(args, message);
170 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
171 va_end(args);
172
173 /* Select icon and color based on level */
174 if (strcmp(level, "success") == 0) {
175 icon = STATUS_SUCCESS;
176 color_type = "success";
177 } else if (strcmp(level, "error") == 0) {
178 icon = STATUS_ERROR;
179 color_type = "error";
180 } else if (strcmp(level, "warning") == 0) {
181 icon = STATUS_WARNING;
182 color_type = "warning";
183 } else if (strcmp(level, "info") == 0) {
184 icon = STATUS_INFO;
185 color_type = "info";
186 } else {
187 icon = "-";
188 color_type = "info";
189 }
190
191 if (strlen(formatted_message) > 0) {
192 printf("%s %s\n",
193 display_colorize(icon, color_type),
194 display_colorize(formatted_message, color_type));
195 } else {
196 printf("%s\n", display_colorize(icon, color_type));
197 }
198 fflush(stdout);
199 }
200
201 /* Print error message with context */
202 void display_error(const char *context, const char *message, ...) {
203 va_list args;
204 char formatted_message[1024];
205
206 if (!message) return;
207
208 va_start(args, message);
209 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
210 va_end(args);
211
212 /* Don't display if message is empty */
213 if (strlen(formatted_message) == 0) return;
214
215 if (context && strlen(context) > 0) {
216 display_status("error", "%s: %s", context, formatted_message);
217 } else {
218 display_status("error", "%s", formatted_message);
219 }
220 }
221
222 /* Print warning message */
223 void display_warning(const char *message, ...) {
224 va_list args;
225 char formatted_message[1024];
226
227 if (!message) return;
228
229 va_start(args, message);
230 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
231 va_end(args);
232
233 display_status("warning", "%s", formatted_message);
234 }
235
236 /* Print success message */
237 void display_success(const char *message, ...) {
238 va_list args;
239 char formatted_message[1024];
240
241 if (!message) return;
242
243 va_start(args, message);
244 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
245 va_end(args);
246
247 display_status("success", "%s", formatted_message);
248 }
249
250 /* Print info message */
251 void display_info(const char *message, ...) {
252 va_list args;
253 char formatted_message[1024];
254
255 if (!message) return;
256
257 va_start(args, message);
258 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
259 va_end(args);
260
261 display_status("info", "%s", formatted_message);
262 }
263
264 /* Format table with proper column alignment */
265 void display_table_header(const char **headers, const int *widths, int columns) {
266 int i;
267
268 if (!headers || !widths || columns <= 0) return;
269
270 /* Print header row */
271 printf("│");
272 for (i = 0; i < columns; i++) {
273 printf(" %s%-*s%s │",
274 display_colorize("", "header"),
275 widths[i] - 1, headers[i],
276 COLOR_RESET);
277 }
278 printf("\n");
279
280 /* Print separator */
281 printf("├");
282 for (i = 0; i < columns; i++) {
283 int j;
284 for (j = 0; j < widths[i] + 1; j++) {
285 printf("─");
286 }
287 printf(i < columns - 1 ? "┼" : "┤");
288 }
289 printf("\n");
290 }
291
292 void display_table_row(const char **values, const int *widths, int columns) {
293 int i;
294
295 if (!values || !widths || columns <= 0) return;
296
297 printf("│");
298 for (i = 0; i < columns; i++) {
299 printf(" %-*s │", widths[i] - 1, values[i] ? values[i] : "");
300 }
301 printf("\n");
302 }
303
304 void display_table_separator(const int *widths, int columns) {
305 int i;
306
307 if (!widths || columns <= 0) return;
308
309 printf("└");
310 for (i = 0; i < columns; i++) {
311 int j;
312 for (j = 0; j < widths[i] + 1; j++) {
313 printf("─");
314 }
315 printf(i < columns - 1 ? "┴" : "┘");
316 }
317 printf("\n");
318 }
319
320 /* Print account information in formatted table */
321 void display_account(const account_t *account, bool is_current) {
322 if (!account) return;
323
324 const char *marker = is_current ? "→" : " ";
325 const char *color_type = is_current ? "current" : "inactive";
326
327 printf("%s %s%3u%s │ %s%-20s%s │ %s%-30s%s │ %s%s%s\n",
328 display_colorize(marker, color_type),
329 display_colorize("", color_type), account->id, COLOR_RESET,
330 display_colorize("", color_type), account->name, COLOR_RESET,
331 display_colorize("", color_type), account->email, COLOR_RESET,
332 display_colorize("", color_type), account->description, COLOR_RESET);
333 }
334
335 /* Print accounts list in formatted table */
336 void display_accounts_list(const gitswitch_ctx_t *ctx) {
337 const char *headers[] = {"", "ID", "Name", "Email", "Description"};
338 const int widths[] = {3, 5, 22, 32, 30};
339 size_t i;
340
341 if (!ctx) return;
342
343 if (ctx->account_count == 0) {
344 display_info("No accounts configured");
345 display_info("Run 'gitswitch add' to create your first account");
346 return;
347 }
348
349 printf("\n");
350 display_header("Configured Accounts");
351 printf("\n");
352
353 /* Print table */
354 display_table_header(headers, widths, 5);
355
356 for (i = 0; i < ctx->account_count; i++) {
357 bool is_current = ctx->current_account &&
358 ctx->current_account->id == ctx->accounts[i].id;
359 display_account(&ctx->accounts[i], is_current);
360 }
361
362 display_table_separator(widths, 5);
363 printf("\n");
364 }
365
366 /* These functions will be implemented in later phases when we have git/ssh/gpg components
367
368 void display_current_status(const git_current_config_t *config) { ... }
369 void display_ssh_status(const ssh_config_t *ssh_config) { ... }
370 void display_gpg_status(const gpg_config_t *gpg_config) { ... }
371 void display_validation_results(const account_validation_t *validation) { ... }
372
373 */
374
375 /* Print health check results */
376 void display_health_check(const gitswitch_ctx_t *ctx) {
377 if (!ctx) return;
378
379 printf("\n");
380 display_header("System Health Check");
381 printf("\n");
382
383 /* Check git availability */
384 if (command_exists("git")) {
385 display_success("Git is available");
386 } else {
387 display_error("Git command not found", "Install git to use gitswitch");
388 }
389
390 /* Check SSH agent */
391 if (command_exists("ssh-agent")) {
392 display_success("SSH agent is available");
393 } else {
394 display_warning("SSH agent not found - SSH key management may not work");
395 }
396
397 /* Check GPG */
398 if (command_exists("gpg") || command_exists("gpg2")) {
399 display_success("GPG is available");
400 } else {
401 display_warning("GPG not found - GPG signing will not work");
402 }
403
404 /* Check configuration */
405 char config_path[MAX_PATH_LEN];
406 if (get_config_directory(config_path, sizeof(config_path)) == 0) {
407 if (is_directory(config_path)) {
408 display_success("Configuration directory exists: %s", config_path);
409 } else {
410 display_warning("Configuration directory not found: %s", config_path);
411 }
412 }
413
414 printf("\n");
415 }
416
417 /* Display interactive account selection menu */
418 uint32_t display_account_menu(const gitswitch_ctx_t *ctx) {
419 char input[64];
420 char *endptr;
421 unsigned long selected_id;
422
423 if (!ctx || ctx->account_count == 0) {
424 display_error(NULL, "No accounts available");
425 return 0;
426 }
427
428 display_accounts_list(ctx);
429
430 printf("Select account (ID or name): ");
431 fflush(stdout);
432
433 if (!fgets(input, sizeof(input), stdin)) {
434 display_error(NULL, "Failed to read input");
435 return 0;
436 }
437
438 /* Remove trailing newline */
439 input[strcspn(input, "\n")] = '\0';
440 trim_whitespace(input);
441
442 if (string_empty(input)) {
443 return 0; /* User cancelled */
444 }
445
446 /* Try to parse as number first */
447 selected_id = strtoul(input, &endptr, 10);
448 if (*endptr == '\0' && selected_id > 0) {
449 /* It's a valid number - verify it exists */
450 for (size_t i = 0; i < ctx->account_count; i++) {
451 if (ctx->accounts[i].id == (uint32_t)selected_id) {
452 return (uint32_t)selected_id;
453 }
454 }
455 display_error("Account ID not found", "%lu", selected_id);
456 return 0;
457 }
458
459 /* Search by name/description */
460 account_t *found = find_account_in_array((account_t *)ctx->accounts,
461 ctx->account_count, input);
462 if (found) {
463 return found->id;
464 }
465
466 display_error("Account not found", "%s", input);
467 return 0;
468 }
469
470 /* Prompt user for account information during add/edit */
471 int display_prompt_account_info(account_t *account, bool is_edit) {
472 char input[512];
473 char *trimmed;
474
475 if (!account) {
476 set_error(ERR_INVALID_ARGS, "NULL account to display_prompt_account_info");
477 return -1;
478 }
479
480 printf("\n");
481 display_header(is_edit ? "Edit Account" : "Add New Account");
482 printf("\n");
483
484 /* Name */
485 printf("Name%s: ", is_edit ? " (current: " : "");
486 if (is_edit && account->name[0] != '\0') {
487 printf("%s): ", account->name);
488 }
489 fflush(stdout);
490
491 if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
492 trimmed = trim_whitespace(input);
493 trimmed[strcspn(trimmed, "\n")] = '\0';
494 if (strlen(trimmed) > 0 && validate_name(trimmed)) {
495 safe_strncpy(account->name, trimmed, sizeof(account->name));
496 } else if (!is_edit) {
497 display_error("Invalid name", "Name cannot be empty");
498 return -1;
499 }
500 }
501
502 /* Email */
503 printf("Email%s: ", is_edit ? " (current: " : "");
504 if (is_edit && account->email[0] != '\0') {
505 printf("%s): ", account->email);
506 }
507 fflush(stdout);
508
509 if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
510 trimmed = trim_whitespace(input);
511 trimmed[strcspn(trimmed, "\n")] = '\0';
512 if (strlen(trimmed) > 0 && validate_email(trimmed)) {
513 safe_strncpy(account->email, trimmed, sizeof(account->email));
514 } else if (!is_edit) {
515 display_error("Invalid email", "Please enter a valid email address");
516 return -1;
517 }
518 }
519
520 /* Description */
521 printf("Description%s: ", is_edit ? " (current: " : "");
522 if (is_edit && account->description[0] != '\0') {
523 printf("%s): ", account->description);
524 }
525 fflush(stdout);
526
527 if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
528 trimmed = trim_whitespace(input);
529 trimmed[strcspn(trimmed, "\n")] = '\0';
530 if (strlen(trimmed) > 0) {
531 safe_strncpy(account->description, trimmed, sizeof(account->description));
532 }
533 }
534
535 /* SSH Key Path */
536 printf("SSH Key Path (optional)%s: ", is_edit ? " (current: " : "");
537 if (is_edit && account->ssh_key_path[0] != '\0') {
538 printf("%s): ", account->ssh_key_path);
539 }
540 fflush(stdout);
541
542 if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
543 trimmed = trim_whitespace(input);
544 trimmed[strcspn(trimmed, "\n")] = '\0';
545 if (strlen(trimmed) > 0) {
546 char expanded[MAX_PATH_LEN];
547 if (expand_path(trimmed, expanded, sizeof(expanded)) == 0 &&
548 path_exists(expanded)) {
549 safe_strncpy(account->ssh_key_path, expanded, sizeof(account->ssh_key_path));
550 account->ssh_enabled = true;
551 } else {
552 display_warning("SSH key file not found: %s", trimmed);
553 account->ssh_enabled = false;
554 }
555 }
556 }
557
558 /* GPG Key ID */
559 printf("GPG Key ID (optional)%s: ", is_edit ? " (current: " : "");
560 if (is_edit && account->gpg_key_id[0] != '\0') {
561 printf("%s): ", account->gpg_key_id);
562 }
563 fflush(stdout);
564
565 if (fgets(input, sizeof(input), stdin) && strlen(input) > 1) {
566 trimmed = trim_whitespace(input);
567 trimmed[strcspn(trimmed, "\n")] = '\0';
568 if (strlen(trimmed) > 0 && validate_key_id(trimmed)) {
569 safe_strncpy(account->gpg_key_id, trimmed, sizeof(account->gpg_key_id));
570 account->gpg_enabled = true;
571
572 /* Ask about GPG signing */
573 printf("Enable GPG signing? (y/N): ");
574 fflush(stdout);
575 if (fgets(input, sizeof(input), stdin)) {
576 trimmed = trim_whitespace(input);
577 account->gpg_signing_enabled = (tolower(trimmed[0]) == 'y');
578 }
579 }
580 }
581
582 return 0;
583 }
584
585 /* Confirm dangerous operations */
586 bool display_confirm(const char *message, ...) {
587 va_list args;
588 char formatted_message[1024];
589 char input[64];
590 char *trimmed;
591
592 if (!message) return false;
593
594 va_start(args, message);
595 vsnprintf(formatted_message, sizeof(formatted_message), message, args);
596 va_end(args);
597
598 printf("%s %s (y/N): ",
599 display_colorize(STATUS_WARNING, "warning"),
600 formatted_message);
601 fflush(stdout);
602
603 if (!fgets(input, sizeof(input), stdin)) {
604 return false;
605 }
606
607 trimmed = trim_whitespace(input);
608 return tolower(trimmed[0]) == 'y';
609 }
610
611 /* Display progress indicator for long operations */
612 void display_progress(const char *operation, int percent) {
613 const int bar_width = 40;
614 int filled = (percent * bar_width) / 100;
615 int i;
616
617 if (!operation) return;
618
619 printf("\r%s [", operation);
620
621 for (i = 0; i < bar_width; i++) {
622 if (i < filled) {
623 printf("█");
624 } else {
625 printf("░");
626 }
627 }
628
629 printf("] %3d%%", percent);
630 fflush(stdout);
631
632 if (percent >= 100) {
633 printf("\n");
634 }
635 }
636
637 /* Clear current line */
638 void display_clear_line(void) {
639 printf("\r\033[K");
640 fflush(stdout);
641 }
642
643 /* Get user input with prompt and validation */
644 int display_get_input(const char *prompt, char *buffer, size_t buffer_size,
645 bool (*validator)(const char *)) {
646 char *trimmed;
647
648 if (!prompt || !buffer || buffer_size == 0) {
649 set_error(ERR_INVALID_ARGS, "Invalid arguments to display_get_input");
650 return -1;
651 }
652
653 printf("%s: ", prompt);
654 fflush(stdout);
655
656 if (!fgets(buffer, buffer_size, stdin)) {
657 set_error(ERR_FILE_IO, "Failed to read user input");
658 return -1;
659 }
660
661 trimmed = trim_whitespace(buffer);
662 trimmed[strcspn(trimmed, "\n")] = '\0';
663
664 /* Move trimmed string to start of buffer */
665 if (trimmed != buffer) {
666 memmove(buffer, trimmed, strlen(trimmed) + 1);
667 }
668
669 /* Validate if validator provided */
670 if (validator && !validator(buffer)) {
671 set_error(ERR_INVALID_ARGS, "Input validation failed");
672 return -1;
673 }
674
675 return 0;
676 }
677
678 /* Get password/sensitive input (hidden) */
679 int display_get_password(const char *prompt, char *buffer, size_t buffer_size) {
680 char *trimmed;
681
682 if (!prompt || !buffer || buffer_size == 0) {
683 set_error(ERR_INVALID_ARGS, "Invalid arguments to display_get_password");
684 return -1;
685 }
686
687 printf("%s: ", prompt);
688 fflush(stdout);
689
690 /* Disable echo */
691 disable_echo();
692
693 if (!fgets(buffer, buffer_size, stdin)) {
694 enable_echo();
695 printf("\n");
696 set_error(ERR_FILE_IO, "Failed to read password input");
697 return -1;
698 }
699
700 /* Re-enable echo */
701 enable_echo();
702 printf("\n");
703
704 trimmed = trim_whitespace(buffer);
705 trimmed[strcspn(trimmed, "\n")] = '\0';
706
707 /* Move trimmed string to start of buffer */
708 if (trimmed != buffer) {
709 memmove(buffer, trimmed, strlen(trimmed) + 1);
710 }
711
712 return 0;
713 }
714
715 /* Show help text */
716 void display_help(const char *command) {
717 printf("\n");
718 display_header("gitswitch-c Help");
719 printf("\n");
720
721 if (!command) {
722 /* General help */
723 printf("Usage: gitswitch [OPTIONS] [COMMAND] [ARGS]\n\n");
724 printf("Commands:\n");
725 printf(" list List all configured accounts\n");
726 printf(" switch <account> Switch to specified account\n");
727 printf(" add Add new account interactively\n");
728 printf(" remove <account> Remove specified account\n");
729 printf(" status Show current git configuration\n");
730 printf(" doctor Run system health checks\n\n");
731 printf("Options:\n");
732 printf(" --global Set git config globally\n");
733 printf(" --local Set git config locally (default)\n");
734 printf(" --no-ssh Skip SSH key management\n");
735 printf(" --no-gpg Skip GPG key management\n");
736 printf(" --dry-run Show what would be done without executing\n");
737 printf(" --verbose Enable verbose output\n");
738 printf(" --color Force color output\n");
739 printf(" --no-color Disable color output\n");
740 printf(" --help Show this help message\n");
741 printf(" --version Show version information\n\n");
742 printf("Examples:\n");
743 printf(" gitswitch # Interactive account selection\n");
744 printf(" gitswitch list # List all accounts\n");
745 printf(" gitswitch switch 1 # Switch to account ID 1\n");
746 printf(" gitswitch switch work # Switch to account matching 'work'\n");
747 printf(" gitswitch add # Add new account\n");
748 printf(" gitswitch doctor # Check system health\n");
749 }
750
751 printf("\n");
752 }
753
754 /* Display version and build information */
755 void display_version(void) {
756 printf("%s version %s\n", GITSWITCH_NAME, GITSWITCH_VERSION);
757 printf("Safe git identity switching with SSH/GPG isolation\n");
758 printf("Built with security and reliability in mind\n\n");
759 printf("Features:\n");
760 printf("- Isolated SSH agents per account\n");
761 printf("- Separate GPG environments\n");
762 printf("- Comprehensive validation\n");
763 printf("- Secure memory handling\n");
764 }
765
766 /* Print configuration file location and status */
767 void display_config_info(const gitswitch_ctx_t *ctx) {
768 if (!ctx) return;
769
770 printf("\n");
771 printf("Configuration: %s\n", ctx->config.config_path);
772 printf("Accounts: %zu configured\n", ctx->account_count);
773
774 if (path_exists(ctx->config.config_path)) {
775 printf("Status: %s\n", display_colorize("exists", "success"));
776 } else {
777 printf("Status: %s\n", display_colorize("not found", "warning"));
778 }
779 }