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