diff --git a/src/logger.c b/src/logger.c index de2eead..997a975 100644 --- a/src/logger.c +++ b/src/logger.c @@ -71,9 +71,9 @@ typedef enum { LVL_INFO, LVL_OK, LVL_WARN, LVL_ERROR } LogLevel; static const char *level_tag(LogLevel lv) { switch (lv) { - case LVL_INFO: return "[INFO ]"; - case LVL_OK: return "[OK ]"; - case LVL_WARN: return "[WARN ]"; + case LVL_INFO: return "[INFO]"; + case LVL_OK: return "[OK]"; + case LVL_WARN: return "[WARN]"; case LVL_ERROR: return "[ERROR]"; } return "[ ]"; diff --git a/bin/gbuild b/bin/gbuild deleted file mode 100755 index dc9ad62..0000000 Binary files a/bin/gbuild and /dev/null differ diff --git a/bin/gconfig b/bin/gconfig deleted file mode 100755 index 2b1ee61..0000000 Binary files a/bin/gconfig and /dev/null differ diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm deleted file mode 100644 index 2ac5ced..0000000 Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz deleted file mode 100644 index 8e410d7..0000000 Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and /dev/null differ diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz deleted file mode 100644 index 4fea580..0000000 Binary files a/dist/gbuild-1.0.0.tar.gz and /dev/null differ diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm deleted file mode 100644 index c0bcf34..0000000 Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb deleted file mode 100644 index 58b5e08..0000000 Binary files a/dist/gbuild_1.0.0_amd64.deb and /dev/null differ diff --git a/bin/gbuild b/bin/gbuild deleted file mode 100755 index dc9ad62..0000000 Binary files a/bin/gbuild and /dev/null differ diff --git a/bin/gconfig b/bin/gconfig deleted file mode 100755 index 2b1ee61..0000000 Binary files a/bin/gconfig and /dev/null differ diff --git a/changelog.txt b/changelog.txt index 2367a82..374e581 100644 --- a/changelog.txt +++ b/changelog.txt @@ -15,3 +15,31 @@ index de2eead..997a975 100644 case LVL_ERROR: return "[ERROR]"; } return "[ ]"; +diff --git a/bin/gbuild b/bin/gbuild +deleted file mode 100755 +index dc9ad62..0000000 +Binary files a/bin/gbuild and /dev/null differ +diff --git a/bin/gconfig b/bin/gconfig +deleted file mode 100755 +index 2b1ee61..0000000 +Binary files a/bin/gconfig and /dev/null differ +diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm +deleted file mode 100644 +index 2ac5ced..0000000 +Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and /dev/null differ +diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz +deleted file mode 100644 +index 8e410d7..0000000 +Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and /dev/null differ +diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz +deleted file mode 100644 +index 4fea580..0000000 +Binary files a/dist/gbuild-1.0.0.tar.gz and /dev/null differ +diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm +deleted file mode 100644 +index c0bcf34..0000000 +Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and /dev/null differ +diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb +deleted file mode 100644 +index 58b5e08..0000000 +Binary files a/dist/gbuild_1.0.0_amd64.deb and /dev/null differ diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm deleted file mode 100644 index 2ac5ced..0000000 Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz deleted file mode 100644 index 8e410d7..0000000 Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and /dev/null differ diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz deleted file mode 100644 index 4fea580..0000000 Binary files a/dist/gbuild-1.0.0.tar.gz and /dev/null differ diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm deleted file mode 100644 index c0bcf34..0000000 Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb deleted file mode 100644 index 58b5e08..0000000 Binary files a/dist/gbuild_1.0.0_amd64.deb and /dev/null differ diff --git a/src/tui.c b/src/tui.c index b07ea65..a2465df 100644 --- a/src/tui.c +++ b/src/tui.c @@ -247,10 +247,10 @@ void tui_log_browser(const char *log_dir) if (has_colors()) { start_color(); use_default_colors(); - init_pair(1, COLOR_BLACK, COLOR_CYAN); /* selected row */ - init_pair(2, COLOR_CYAN, -1); /* border */ + init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ + init_pair(2, COLOR_WHITE, -1); /* border */ init_pair(3, COLOR_WHITE, -1); /* normal row */ - init_pair(4, COLOR_YELLOW,-1); /* preview text */ + init_pair(4, COLOR_WHITE,-1); /* preview text */ init_pair(5, COLOR_RED, -1); /* status/warn */ } @@ -282,7 +282,7 @@ void tui_log_browser(const char *log_dir) /* ---- hint bar ---- */ if (has_colors()) attron(COLOR_PAIR(2)); mvprintw(rows - 2, 0, - " \u2191\u2193 navigate Enter open in less d delete q quit " + " J - DOWN K - UP navigate Enter - open in less d - delete q - quit " " "); if (has_colors()) attroff(COLOR_PAIR(2)); diff --git a/src/tui.c b/src/tui.c index a2465df..103babc 100644 --- a/src/tui.c +++ b/src/tui.c @@ -282,7 +282,7 @@ void tui_log_browser(const char *log_dir) /* ---- hint bar ---- */ if (has_colors()) attron(COLOR_PAIR(2)); mvprintw(rows - 2, 0, - " J - DOWN K - UP navigate Enter - open in less d - delete q - quit " + " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit (Press J to start) " " "); if (has_colors()) attroff(COLOR_PAIR(2)); @@ -389,10 +389,10 @@ void tui_log_browser(const char *log_dir) if (has_colors()) { start_color(); use_default_colors(); - init_pair(1, COLOR_BLACK, COLOR_CYAN); - init_pair(2, COLOR_CYAN, -1); + init_pair(1, COLOR_BLACK, COLOR_WHITE); + init_pair(2, COLOR_WHITE, -1); init_pair(3, COLOR_WHITE, -1); - init_pair(4, COLOR_YELLOW,-1); + init_pair(4, COLOR_WHITE,-1); init_pair(5, COLOR_RED, -1); } clear(); diff --git a/bin/gbuild b/bin/gbuild index 3585f82..09371a5 100755 Binary files a/bin/gbuild and b/bin/gbuild differ diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm deleted file mode 100644 index 2343555..0000000 Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz deleted file mode 100644 index cdbc281..0000000 Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and /dev/null differ diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz deleted file mode 100644 index c8fea5c..0000000 Binary files a/dist/gbuild-1.0.0.tar.gz and /dev/null differ diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm deleted file mode 100644 index 04797e8..0000000 Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb deleted file mode 100644 index a0b5475..0000000 Binary files a/dist/gbuild_1.0.0_amd64.deb and /dev/null differ diff --git a/src/tui.c b/src/tui.c index 103babc..1442086 100644 --- a/src/tui.c +++ b/src/tui.c @@ -189,7 +189,7 @@ static int load_log_entries(const char *log_dir, * Returns the number of lines read. * Caller owns the memory and should free each pointer. */ -#define PREVIEW_MAX 128 +#define PREVIEW_MAX 200 static int read_tail(const char *path, char **lines, int max_lines) { @@ -279,11 +279,6 @@ void tui_log_browser(const char *log_dir) attroff(A_BOLD); clrtoeol(); - /* ---- hint bar ---- */ - if (has_colors()) attron(COLOR_PAIR(2)); - mvprintw(rows - 2, 0, - " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit (Press J to start) " - " "); if (has_colors()) attroff(COLOR_PAIR(2)); /* ---- vertical divider ---- */ @@ -351,7 +346,13 @@ void tui_log_browser(const char *log_dir) mvwprintw(wright, 2, 2, "(no log selected)"); } wrefresh(wright); - refresh(); + + /* ---- hint bar ---- */ + if (has_colors()) attron(COLOR_PAIR(2)); + mvprintw(rows - 2, 0, + " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit (Press J to start) " + " "); + refresh(); /* ---- input ---- */ int ch = getch(); diff --git a/bin/gbuild b/bin/gbuild index 09371a5..e95c8a0 100755 Binary files a/bin/gbuild and b/bin/gbuild differ diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm index 6df2d36..611a7ef 100644 Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and b/dist/gbuild-1.0.0-1.x86_64.rpm differ diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz index 60a5227..a8e0d0e 100644 Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and b/dist/gbuild-1.0.0-x86_64-bin.tar.gz differ diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz index 3a83082..6010827 100644 Binary files a/dist/gbuild-1.0.0.tar.gz and b/dist/gbuild-1.0.0.tar.gz differ diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm index 813ee98..ba7fe6d 100644 Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm differ diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb index 6f52b7e..a365173 100644 Binary files a/dist/gbuild_1.0.0_amd64.deb and b/dist/gbuild_1.0.0_amd64.deb differ diff --git a/src/tui.c b/src/tui.c index b5df72f..c5e660a 100644 --- a/src/tui.c +++ b/src/tui.c @@ -61,7 +61,7 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) /* header hint */ attron(A_DIM); mvprintw(win_y - 2, win_x, - "Select make target K - UP J - DOWN to navigate Enter - select \u00b7 q - cancel"); + "Select make target \u2191\u2193 navigate \u00b7 Enter select \u00b7 q cancel"); attroff(A_DIM); refresh(); @@ -189,7 +189,7 @@ static int load_log_entries(const char *log_dir, * Returns the number of lines read. * Caller owns the memory and should free each pointer. */ -#define PREVIEW_MAX 200 +#define PREVIEW_MAX 128 static int read_tail(const char *path, char **lines, int max_lines) { @@ -247,24 +247,24 @@ void tui_log_browser(const char *log_dir) if (has_colors()) { start_color(); use_default_colors(); - init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ - init_pair(2, COLOR_WHITE, -1); /* border */ - init_pair(3, COLOR_WHITE, -1); /* normal row */ - init_pair(4, COLOR_WHITE,-1); /* preview text */ - init_pair(5, COLOR_RED, -1); /* status/warn */ + init_pair(1, COLOR_BLACK, COLOR_WHITE); + init_pair(2, COLOR_WHITE, -1); + init_pair(3, COLOR_WHITE, -1); + init_pair(4, COLOR_WHITE, -1); + init_pair(5, COLOR_RED, -1); } int rows, cols; getmaxyx(stdscr, rows, cols); - /* layout: left pane = 40% width, right pane = rest */ int left_w = cols * 40 / 100; if (left_w < 30) left_w = 30; - int right_w = cols - left_w - 1; /* -1 for divider */ - int pane_h = rows - 3; /* -3 for title + hint bar */ + int right_w = cols - left_w - 1; + int pane_h = rows - 3; WINDOW *wleft = newwin(pane_h, left_w, 1, 0); WINDOW *wright = newwin(pane_h, right_w, 1, left_w + 1); + keypad(wleft, TRUE); int cur = 0, scroll = 0; int done = 0; @@ -279,6 +279,11 @@ void tui_log_browser(const char *log_dir) attroff(A_BOLD); clrtoeol(); + /* ---- hint bar ---- */ + if (has_colors()) attron(COLOR_PAIR(2)); + mvprintw(rows - 2, 0, + " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit " + " "); if (has_colors()) attroff(COLOR_PAIR(2)); /* ---- vertical divider ---- */ @@ -299,7 +304,6 @@ void tui_log_browser(const char *log_dir) } else { for (int i = 0; i < visible && (i + scroll) < count; i++) { int idx = i + scroll; - /* trim the .log extension for display */ char disp[256]; strncpy(disp, entries[idx].name, sizeof(disp) - 1); char *dot = strrchr(disp, '.'); @@ -318,7 +322,7 @@ void tui_log_browser(const char *log_dir) } } } - wrefresh(wleft); + /* NOTE: no wrefresh(wleft) here — flush everything together below */ /* ---- right pane: preview ---- */ werase(wright); @@ -345,17 +349,17 @@ void tui_log_browser(const char *log_dir) } else { mvwprintw(wright, 2, 2, "(no log selected)"); } - wrefresh(wright); - refresh(); - /* ---- hint bar ---- */ - if (has_colors()) attron(COLOR_PAIR(2)); - mvprintw(rows - 2, 0, - " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit (Press J to start) " - " "); - + /* Flush all layers atomically: stdscr first (background), then the two + * panes on top. doupdate() does one physical write so nothing + * overwrites anything else. */ + wnoutrefresh(stdscr); + wnoutrefresh(wleft); + wnoutrefresh(wright); + doupdate(); + /* ---- input ---- */ - int ch = getch(); + int ch = wgetch(wleft); switch (ch) { case KEY_UP: case 'k': @@ -381,7 +385,6 @@ void tui_log_browser(const char *log_dir) snprintf(cmd, sizeof(cmd), "less \"%s\"", entries[cur].path); system(cmd); - /* re-init after less exits */ initscr(); noecho(); cbreak(); @@ -391,24 +394,24 @@ void tui_log_browser(const char *log_dir) start_color(); use_default_colors(); init_pair(1, COLOR_BLACK, COLOR_WHITE); - init_pair(2, COLOR_WHITE, -1); + init_pair(2, COLOR_WHITE, -1); init_pair(3, COLOR_WHITE, -1); - init_pair(4, COLOR_WHITE,-1); + init_pair(4, COLOR_WHITE, -1); init_pair(5, COLOR_RED, -1); } + touchwin(wleft); + touchwin(wright); clear(); } break; case 'd': if (count > 0 && cur < count) { - /* confirm */ mvprintw(rows - 1, 0, "Delete %s? [y/N] ", entries[cur].name); refresh(); int confirm = getch(); if (confirm == 'y' || confirm == 'Y') { remove(entries[cur].path); - /* reload list */ count = load_log_entries(log_dir, entries, MAX_LOGS); if (cur >= count) cur = count > 0 ? count - 1 : 0; if (scroll > cur) scroll = cur; @@ -417,7 +420,7 @@ void tui_log_browser(const char *log_dir) } break; case 'q': - case 27: /* ESC */ + case 27: done = 1; break; } diff --git a/Makefile b/Makefile index 2c21f21..b2e3f5c 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,7 @@ COMMON_SRCS = src/config.c GBUILD_SRCS = \ src/gbuild.c \ src/git_ops.c \ + src/index.c \ src/logger.c \ src/make_ops.c \ src/tui.c \ diff --git a/README.md b/README.md index 0a8ef78..13e5301 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# gbuild +# gbuild — C rewrite A C rewrite of the original [gbuild](https://spdlab.hu/gbuild) bash tool. Clone, pull, pick a target, build — all from one command. @@ -18,7 +18,7 @@ Clone, pull, pick a target, build — all from one command. ## Dependencies -| Library | Purpose | +| Library | Purpose | |----------|------------------------------| | ncurses | TUI for picker & log browser | | git | Clone / pull | @@ -29,14 +29,9 @@ Clone, pull, pick a target, build — all from one command. ## Build -### Linux - ```sh -# Install these packages with your package manager -libncurses-dev make - -# Clone this repository -git clone https://github.com/jokerz/gbuild.git +# Install ncurses dev headers if needed +sudo apt install libncurses-dev # Build both binaries into bin/ make @@ -70,7 +65,7 @@ gbuild myproject [git] GIT_URL = http://localhost:3000 -GIT_USER = Username +GIT_USER = [auth] # Use token-based OR password-based auth; leave the other blank diff --git a/include/git_ops.h b/include/git_ops.h index bf22f63..bd5ae79 100644 --- a/include/git_ops.h +++ b/include/git_ops.h @@ -16,3 +16,7 @@ int git_clone(const char *base_url, const char *user, int git_pull(const char *repo_path, Logger *log); #endif /* GIT_OPS_H */ + +/* Read the current HEAD hash of repo_path into out (at least 41 bytes). + * Returns 0 on success, -1 on failure. */ +int git_head_hash(const char *repo_path, char *out, size_t n); diff --git a/include/tui.h b/include/tui.h index a2c035f..ca6cd1d 100644 --- a/include/tui.h +++ b/include/tui.h @@ -13,4 +13,17 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size * Keys: ↑↓ navigate Enter open in less d delete q quit */ void tui_log_browser(const char *log_dir); +#include "index.h" + +/* Full-screen project overview. + * Lists all projects in idx with last-build status, timestamp and HEAD hash. + * On Enter, fills selected_project and returns the row index. + * Returns -1 if the user quit without selecting. + * Keys: ↑↓ navigate Enter build l log-browser q/ESC quit */ +int tui_project_overview(ProjectIndex *idx, + const char *clone_dir, + const char *log_dir, + char *selected_project, + size_t sel_size); + #endif /* TUI_H */ diff --git a/src/config.c b/src/config.c index a89ba18..46b8b5f 100644 --- a/src/config.c +++ b/src/config.c @@ -1,12 +1,3 @@ -/* - -This file is licensed under the MIT license. - -Copyright (c) Schmidt Peter Daniel 2026 - - -*/ - #include "config.h" #include diff --git a/src/gbuild.c b/src/gbuild.c index 9bc8b8a..ff8514e 100644 --- a/src/gbuild.c +++ b/src/gbuild.c @@ -1,5 +1,6 @@ #include "config.h" #include "git_ops.h" +#include "index.h" #include "logger.h" #include "make_ops.h" #include "tui.h" @@ -20,20 +21,126 @@ static void print_usage(const char *prog) "gbuild %s — A build tool for your Linux distro\n\n" "Usage:\n" " %s [options] \n" + " %s (no args — opens project overview TUI)\n" " %s --logs\n\n" "Options:\n" - " --url Override Git base URL (default: ~/.gconfig)\n" - " --user Override Git username (default: ~/.gconfig)\n" - " --target Run target directly, skip interactive picker\n" + " --url Override Git base URL (default: ~/.gconfig)\n" + " --user Override Git username (default: ~/.gconfig)\n" + " --target Run target directly, skip interactive picker\n" " --logs Open the interactive log browser\n" + " --no-tui With no , print usage instead of opening TUI\n" " -h, --help Print this help and exit\n\n" "Examples:\n" + " gbuild (overview TUI — pick and build)\n" " gbuild myproject\n" " gbuild --target clean myproject\n" " gbuild --url http://10.0.0.5:3000 --user alice myproject\n" " gbuild --logs\n\n" "Configuration is read from ~/.gconfig. Run 'gconfig init' to set it up.\n", - VERSION, prog, prog); + VERSION, prog, prog, prog); +} + +/* ----------------------------------------------------------------- build one project */ + +static int build_project(const char *project, + const char *arg_target, + GConfig *cfg, + const char *clone_dir, + const char *log_dir, + ProjectIndex *idx) +{ + /* ---- open logger ---- */ + Logger log; + memset(&log, 0, sizeof(log)); + if (cfg->log_enabled) { + if (logger_open(&log, log_dir, project) == 0) + logger_info(&log, "Logging to: %s", log.path); + else + fprintf(stderr, "[WARN ] Could not open log file in %s\n", log_dir); + } + + logger_info(&log, "gbuild started for project: %s", project); + + /* ---- clone or pull ---- */ + char repo_path[CFG_MAX * 2]; + snprintf(repo_path, sizeof(repo_path), "%s/%s", clone_dir, project); + + if (git_repo_exists(clone_dir, project)) { + logger_info(&log, "Repo already cloned — pulling latest changes ..."); + int rc = git_pull(repo_path, &log); + if (rc != 0) { + logger_error(&log, "git pull failed (exit %d).", rc); + logger_close(&log); + return 1; + } + logger_ok(&log, "Repository updated."); + } else { + logger_info(&log, "Cloning %s/%s/%s ...", + cfg->git_url, cfg->git_user, project); + int rc = git_clone(cfg->git_url, cfg->git_user, + cfg->git_token, cfg->git_password, + project, clone_dir, &log); + if (rc != 0) { + logger_error(&log, "git clone failed (exit %d).", rc); + logger_close(&log); + return 1; + } + logger_ok(&log, "Repository cloned."); + } + + /* ---- record HEAD hash ---- */ + ProjectRecord *rec = index_upsert(idx, project); + if (rec) + git_head_hash(repo_path, rec->last_head_hash, IDX_HASH_LEN); + + /* ---- resolve make target ---- */ + char target[TARGET_NAME]; + memset(target, 0, sizeof(target)); + + if (arg_target) { + strncpy(target, arg_target, sizeof(target) - 1); + logger_info(&log, "Selected target: %s", target); + } else if (cfg->default_target[0]) { + strncpy(target, cfg->default_target, sizeof(target) - 1); + logger_info(&log, "Using default target: %s", target); + } else { + MakeTargets targets; + if (make_parse_targets(repo_path, &targets) != 0) { + logger_warn(&log, + "No targets found in Makefile — running 'make' with no target."); + target[0] = '\0'; + } else { + if (tui_pick_target(&targets, target, sizeof(target)) < 0) { + logger_warn(&log, "No target selected — aborting."); + logger_close(&log); + return 0; + } + } + logger_info(&log, "Selected target: %s", + target[0] ? target : "(default)"); + } + + /* ---- build ---- */ + logger_info(&log, "Building '%s' target '%s' ...", + project, target[0] ? target : "(default)"); + + int rc = make_build(repo_path, target, &log); + + /* ---- update index ---- */ + if (rec) { + rec->last_build_rc = rc; + rec->last_build_ts = time(NULL); + } + + if (rc != 0) { + logger_error(&log, "Build failed (exit %d).", rc); + logger_close(&log); + return 1; + } + + logger_ok(&log, "gbuild complete."); + logger_close(&log); + return 0; } /* ----------------------------------------------------------------- main */ @@ -46,6 +153,7 @@ int main(int argc, char *argv[]) const char *arg_target = NULL; const char *arg_project = NULL; int flag_logs = 0; + int flag_no_tui = 0; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { @@ -62,6 +170,8 @@ int main(int argc, char *argv[]) arg_target = argv[i]; } else if (!strcmp(argv[i], "--logs")) { flag_logs = 1; + } else if (!strcmp(argv[i], "--no-tui")) { + flag_no_tui = 1; } else if (argv[i][0] == '-') { fprintf(stderr, "error: unknown flag '%s'\n", argv[i]); return 1; @@ -86,109 +196,54 @@ int main(int argc, char *argv[]) config_defaults(&cfg); } - /* apply overrides */ if (arg_url) strncpy(cfg.git_url, arg_url, CFG_MAX - 1); if (arg_user) strncpy(cfg.git_user, arg_user, sizeof(cfg.git_user) - 1); - /* expand paths that might contain ~ */ char clone_dir[CFG_MAX], log_dir[CFG_MAX]; expand_tilde(cfg.clone_dir, clone_dir, sizeof(clone_dir)); expand_tilde(cfg.log_dir, log_dir, sizeof(log_dir)); + /* ---- load / scan project index ---- */ + ProjectIndex idx; + char idx_path[512]; + index_get_path(idx_path, sizeof(idx_path)); + index_load(&idx, idx_path); /* ok if file doesn't exist yet */ + index_scan(&idx, clone_dir); /* pick up any new repos on disk */ + /* ---- log browser mode ---- */ if (flag_logs) { tui_log_browser(log_dir); return 0; } - /* ---- need a project name ---- */ + /* ---- no project given: open overview TUI or print usage ---- */ if (!arg_project) { - fprintf(stderr, "error: no project specified.\n\n"); - print_usage(argv[0]); - return 1; - } - - /* ---- open logger ---- */ - Logger log; - memset(&log, 0, sizeof(log)); - if (cfg.log_enabled) { - if (logger_open(&log, log_dir, arg_project) == 0) - logger_info(&log, "Logging to: %s", log.path); - else - fprintf(stderr, "[WARN ] Could not open log file in %s\n", log_dir); - } - - logger_info(&log, "gbuild started for project: %s", arg_project); - - /* ---- clone or pull ---- */ - char repo_path[CFG_MAX * 2]; - snprintf(repo_path, sizeof(repo_path), "%s/%s", clone_dir, arg_project); - - if (git_repo_exists(clone_dir, arg_project)) { - logger_info(&log, "Repo already cloned — pulling latest changes ..."); - int rc = git_pull(repo_path, &log); - if (rc != 0) { - logger_error(&log, "git pull failed (exit %d).", rc); - logger_close(&log); - return 1; + if (flag_no_tui || idx.count == 0) { + if (idx.count == 0 && !flag_no_tui) + fprintf(stderr, + "No projects found in %s.\n" + "Run 'gbuild ' to clone and build one first.\n", + clone_dir); + else + print_usage(argv[0]); + return (idx.count == 0) ? 1 : 0; } - logger_ok(&log, "Repository updated."); - } else { - logger_info(&log, "Cloning %s/%s/%s ...", - cfg.git_url, cfg.git_user, arg_project); - int rc = git_clone(cfg.git_url, cfg.git_user, - cfg.git_token, cfg.git_password, - arg_project, clone_dir, &log); - if (rc != 0) { - logger_error(&log, "git clone failed (exit %d).", rc); - logger_close(&log); - return 1; - } - logger_ok(&log, "Repository cloned."); - } - /* ---- resolve make target ---- */ - char target[TARGET_NAME]; - memset(target, 0, sizeof(target)); + static char chosen[IDX_NAME_LEN]; + memset(chosen, 0, sizeof(chosen)); + if (tui_project_overview(&idx, clone_dir, log_dir, + chosen, sizeof(chosen)) < 0) + return 0; /* user quit without picking */ - if (arg_target) { - /* explicit --target flag */ - strncpy(target, arg_target, sizeof(target) - 1); - logger_info(&log, "Selected target: %s", target); - } else if (cfg.default_target[0]) { - /* default from config */ - strncpy(target, cfg.default_target, sizeof(target) - 1); - logger_info(&log, "Using default target: %s", target); - } else { - /* interactive TUI picker */ - MakeTargets targets; - if (make_parse_targets(repo_path, &targets) != 0) { - logger_warn(&log, - "No targets found in Makefile — running 'make' with no target."); - target[0] = '\0'; - } else { - if (tui_pick_target(&targets, target, sizeof(target)) < 0) { - logger_warn(&log, "No target selected — aborting."); - logger_close(&log); - return 0; - } - } - logger_info(&log, "Selected target: %s", - target[0] ? target : "(default)"); + arg_project = chosen; } /* ---- build ---- */ - logger_info(&log, "gbuild: building '%s' target '%s' ...", - arg_project, target[0] ? target : "(default)"); + int rc = build_project(arg_project, arg_target, + &cfg, clone_dir, log_dir, &idx); - int rc = make_build(repo_path, target, &log); - if (rc != 0) { - logger_error(&log, "Build failed (exit %d).", rc); - logger_close(&log); - return 1; - } + /* ---- persist updated index ---- */ + index_save(&idx, idx_path); - logger_ok(&log, "gbuild complete."); - logger_close(&log); - return 0; + return rc; } diff --git a/src/git_ops.c b/src/git_ops.c index 69588eb..3f995f1 100644 --- a/src/git_ops.c +++ b/src/git_ops.c @@ -1,13 +1,6 @@ -/* - -This file is licensed under the MIT license. -Copyright (c) Schmidt Peter Daniel 2026. - - -*/ - #include "git_ops.h" #include "config.h" +#include "index.h" #include #include @@ -131,3 +124,27 @@ int git_pull(const char *repo_path, Logger *log) "git -C \"%s\" pull 2>&1", repo_path); return run_stream(cmd, log); } + +int git_head_hash(const char *repo_path, char *out, size_t n) +{ + char cmd[IDX_PATH_LEN + 64]; + snprintf(cmd, sizeof(cmd), + "git -C \"%s\" rev-parse HEAD 2>/dev/null", repo_path); + + FILE *p = popen(cmd, "r"); + if (!p) return -1; + + char buf[64] = {0}; + int ok = (fgets(buf, sizeof(buf), p) != NULL); + pclose(p); + + if (!ok || buf[0] == '\0') return -1; + + /* strip newline */ + size_t l = strlen(buf); + if (l > 0 && buf[l-1] == '\n') buf[--l] = '\0'; + + strncpy(out, buf, n - 1); + out[n - 1] = '\0'; + return 0; +} diff --git a/src/logger.c b/src/logger.c index 997a975..de2eead 100644 --- a/src/logger.c +++ b/src/logger.c @@ -71,9 +71,9 @@ typedef enum { LVL_INFO, LVL_OK, LVL_WARN, LVL_ERROR } LogLevel; static const char *level_tag(LogLevel lv) { switch (lv) { - case LVL_INFO: return "[INFO]"; - case LVL_OK: return "[OK]"; - case LVL_WARN: return "[WARN]"; + case LVL_INFO: return "[INFO ]"; + case LVL_OK: return "[OK ]"; + case LVL_WARN: return "[WARN ]"; case LVL_ERROR: return "[ERROR]"; } return "[ ]"; diff --git a/src/tui.c b/src/tui.c index c5e660a..605fba6 100644 --- a/src/tui.c +++ b/src/tui.c @@ -10,7 +10,7 @@ #include /* ================================================================ - * SECTION 1 — TARGET PICKER + * SECTION 1 - TARGET PICKER * ================================================================ */ #define PICKER_MIN_W 40 @@ -29,18 +29,15 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) if (has_colors()) { start_color(); use_default_colors(); - init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ - init_pair(2, COLOR_WHITE, -1); /* border colour */ - init_pair(3, COLOR_WHITE, -1); /* normal row */ + init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ } int rows, cols; getmaxyx(stdscr, rows, cols); - int list_h = targets->count + 2; /* +2 for border */ + int list_h = targets->count + 2; int list_w = PICKER_MIN_W; - /* find longest target name and widen accordingly */ for (int i = 0; i < targets->count; i++) { int l = (int)strlen(targets->names[i]) + 4; if (l > list_w) list_w = l; @@ -51,17 +48,16 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) int win_y = (rows - list_h) / 2; int win_x = (cols - list_w) / 2; - /* instruction line above the box */ int cur = 0; int scroll_offset = 0; - int visible = list_h - 2; /* rows inside border */ + int visible = list_h - 2; WINDOW *win = newwin(list_h, list_w, win_y, win_x); + keypad(win, TRUE); - /* header hint */ attron(A_DIM); mvprintw(win_y - 2, win_x, - "Select make target \u2191\u2193 navigate \u00b7 Enter select \u00b7 q cancel"); + "Select make target J/K navigate | Enter select | q cancel"); attroff(A_DIM); refresh(); @@ -70,9 +66,7 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) while (!done) { werase(win); - if (has_colors()) wattron(win, COLOR_PAIR(2)); box(win, 0, 0); - if (has_colors()) wattroff(win, COLOR_PAIR(2)); for (int i = 0; i < visible && (i + scroll_offset) < targets->count; i++) { int idx = i + scroll_offset; @@ -84,10 +78,8 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) if (has_colors()) wattroff(win, COLOR_PAIR(1) | A_BOLD); else wattroff(win, A_REVERSE); } else { - if (has_colors()) wattron(win, COLOR_PAIR(3)); mvwprintw(win, i + 1, 1, " %-*s", list_w - 4, targets->names[idx]); - if (has_colors()) wattroff(win, COLOR_PAIR(3)); } } @@ -119,7 +111,7 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) done = 1; break; case 'q': - case 27: /* ESC */ + case 27: done = 1; break; } @@ -131,20 +123,19 @@ int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size) } /* ================================================================ - * SECTION 2 — LOG BROWSER + * SECTION 2 - LOG BROWSER * ================================================================ */ #define MAX_LOGS 512 typedef struct { - char name[256]; /* filename only */ - char path[768]; /* full path */ + char name[256]; + char path[768]; time_t mtime; } LogEntry; static int log_entry_cmp(const void *a, const void *b) { - /* newest first */ const LogEntry *la = (const LogEntry *)a; const LogEntry *lb = (const LogEntry *)b; if (lb->mtime > la->mtime) return 1; @@ -162,7 +153,6 @@ static int load_log_entries(const char *log_dir, struct dirent *de; while ((de = readdir(d)) && count < max) { if (de->d_name[0] == '.') continue; - /* only .log files */ const char *dot = strrchr(de->d_name, '.'); if (!dot || strcmp(dot, ".log")) continue; @@ -184,11 +174,6 @@ static int load_log_entries(const char *log_dir, return count; } -/* - * Read up to `max_lines` trailing lines from `path` into `lines`. - * Returns the number of lines read. - * Caller owns the memory and should free each pointer. - */ #define PREVIEW_MAX 128 static int read_tail(const char *path, char **lines, int max_lines) @@ -196,17 +181,14 @@ static int read_tail(const char *path, char **lines, int max_lines) FILE *f = fopen(path, "r"); if (!f) return 0; - /* Circular buffer approach */ char **buf = calloc(max_lines, sizeof(char *)); if (!buf) { fclose(f); return 0; } char tmp[1024]; int idx = 0, total = 0; while (fgets(tmp, sizeof(tmp), f)) { - /* strip newline */ size_t l = strlen(tmp); if (l > 0 && tmp[l-1] == '\n') tmp[l-1] = '\0'; - free(buf[idx % max_lines]); buf[idx % max_lines] = strdup(tmp); idx++; @@ -214,13 +196,12 @@ static int read_tail(const char *path, char **lines, int max_lines) } fclose(f); - int have = (total < max_lines) ? total : max_lines; + int have = (total < max_lines) ? total : max_lines; int start = (total >= max_lines) ? (idx % max_lines) : 0; for (int i = 0; i < have; i++) lines[i] = buf[(start + i) % max_lines]; - /* free slots that weren't returned */ for (int i = have; i < max_lines; i++) { free(buf[(start + i) % max_lines]); buf[(start + i) % max_lines] = NULL; @@ -247,11 +228,7 @@ void tui_log_browser(const char *log_dir) if (has_colors()) { start_color(); use_default_colors(); - init_pair(1, COLOR_BLACK, COLOR_WHITE); - init_pair(2, COLOR_WHITE, -1); - init_pair(3, COLOR_WHITE, -1); - init_pair(4, COLOR_WHITE, -1); - init_pair(5, COLOR_RED, -1); + init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ } int rows, cols; @@ -280,11 +257,11 @@ void tui_log_browser(const char *log_dir) clrtoeol(); /* ---- hint bar ---- */ - if (has_colors()) attron(COLOR_PAIR(2)); + attron(A_REVERSE); mvprintw(rows - 2, 0, - " K - UP J - DOWN to navigate Enter - open in less d - delete q - quit " + " J/K navigate Enter open in less d delete q quit " " "); - if (has_colors()) attroff(COLOR_PAIR(2)); + attroff(A_REVERSE); /* ---- vertical divider ---- */ for (int y = 1; y < rows - 2; y++) @@ -292,9 +269,7 @@ void tui_log_browser(const char *log_dir) /* ---- left pane: file list ---- */ werase(wleft); - if (has_colors()) wattron(wleft, COLOR_PAIR(2)); box(wleft, 0, 0); - if (has_colors()) wattroff(wleft, COLOR_PAIR(2)); int visible = pane_h - 2; if (visible < 1) visible = 1; @@ -322,20 +297,17 @@ void tui_log_browser(const char *log_dir) } } } - /* NOTE: no wrefresh(wleft) here — flush everything together below */ + /* NOTE: no wrefresh(wleft) here - flush everything together below */ /* ---- right pane: preview ---- */ werase(wright); - if (has_colors()) wattron(wright, COLOR_PAIR(2)); box(wright, 0, 0); - if (has_colors()) wattroff(wright, COLOR_PAIR(2)); if (count > 0 && cur < count) { int preview_lines = pane_h - 2; char **lines = calloc(preview_lines, sizeof(char *)); if (lines) { int n = read_tail(entries[cur].path, lines, preview_lines); - if (has_colors()) wattron(wright, COLOR_PAIR(4)); for (int i = 0; i < n; i++) { if (lines[i]) { mvwprintw(wright, i + 1, 1, "%-*.*s", @@ -343,16 +315,13 @@ void tui_log_browser(const char *log_dir) free(lines[i]); } } - if (has_colors()) wattroff(wright, COLOR_PAIR(4)); free(lines); } } else { mvwprintw(wright, 2, 2, "(no log selected)"); } - /* Flush all layers atomically: stdscr first (background), then the two - * panes on top. doupdate() does one physical write so nothing - * overwrites anything else. */ + /* Atomic flush - stdscr first, then both panes on top */ wnoutrefresh(stdscr); wnoutrefresh(wleft); wnoutrefresh(wright); @@ -394,10 +363,6 @@ void tui_log_browser(const char *log_dir) start_color(); use_default_colors(); init_pair(1, COLOR_BLACK, COLOR_WHITE); - init_pair(2, COLOR_WHITE, -1); - init_pair(3, COLOR_WHITE, -1); - init_pair(4, COLOR_WHITE, -1); - init_pair(5, COLOR_RED, -1); } touchwin(wleft); touchwin(wright); @@ -431,3 +396,202 @@ void tui_log_browser(const char *log_dir) endwin(); free(entries); } + +/* ================================================================ + * SECTION 3 - PROJECT OVERVIEW + * ================================================================ */ + +/* + * Column layout: + * ST PROJECT NAME LAST BUILD RESULT HEAD HASH + * + * Status symbols: + * * last build passed + * X last build failed + * - never built + * + * Keys: + * J/K navigate Enter build l logs q/ESC quit + */ + +#include "index.h" +#include "git_ops.h" + +#define COL_STATUS_W 3 +#define COL_NAME_W 24 +#define COL_TIME_W 18 +#define COL_RC_W 8 +#define COL_HASH_W 10 + +static void fmt_ts(time_t ts, char *out, size_t n) +{ + if (ts == 0) { + strncpy(out, "(never)", n - 1); + out[n-1] = '\0'; + return; + } + struct tm *t = localtime(&ts); + strftime(out, n, "%d %b %H:%M", t); +} + +int tui_project_overview(ProjectIndex *idx, + const char *clone_dir, + const char *log_dir, + char *selected_project, + size_t sel_size) +{ + (void)clone_dir; + + if (!idx || idx->count == 0) return -1; + + initscr(); + noecho(); + cbreak(); + keypad(stdscr, TRUE); + curs_set(0); + + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(1, COLOR_BLACK, COLOR_WHITE); /* selected row */ + } + + int rows, cols; + getmaxyx(stdscr, rows, cols); + + int cur = 0, scroll = 0; + int done = 0; + int result = -1; + + while (!done) { + getmaxyx(stdscr, rows, cols); + int visible = rows - 5; + if (visible < 1) visible = 1; + + erase(); + + /* ---- title bar ---- */ + attron(A_BOLD); + mvprintw(0, 2, "gbuild 1.0.0 - project overview"); + attroff(A_BOLD); + + /* ---- column header ---- */ + attron(A_BOLD); + mvprintw(2, 2, " %-*s %-*s %-*s %-*s", + COL_NAME_W, "PROJECT", + COL_TIME_W, "LAST BUILD", + COL_RC_W, "RESULT", + COL_HASH_W, "HEAD"); + attroff(A_BOLD); + + /* separator */ + mvhline(3, 0, ACS_HLINE, cols); + + /* ---- project rows ---- */ + for (int i = 0; i < visible && (i + scroll) < idx->count; i++) { + int idx_i = i + scroll; + ProjectRecord *r = &idx->projects[idx_i]; + + char ts_str[32]; + fmt_ts(r->last_build_ts, ts_str, sizeof(ts_str)); + + char shorthash[12] = "--------"; + if (r->last_head_hash[0]) + strncpy(shorthash, r->last_head_hash, 8); + shorthash[8] = '\0'; + + char rc_str[16]; + if (r->last_build_rc == -1) strncpy(rc_str, "-", sizeof(rc_str) - 1); + else if (r->last_build_rc == 0) strncpy(rc_str, "PASS", sizeof(rc_str) - 1); + else snprintf(rc_str, sizeof(rc_str), "FAIL(%d)", r->last_build_rc); + + const char *sym = (r->last_build_rc == 0) ? "*" : + (r->last_build_rc > 0) ? "X" : "-"; + + int y = 4 + i; + + if (idx_i == cur) { + /* fill the whole row first */ + if (has_colors()) attron(COLOR_PAIR(1) | A_BOLD); + else attron(A_REVERSE); + mvprintw(y, 0, "%*s", cols, ""); + mvprintw(y, 2, "%s %-*s %-*s %-*s %-*s", + sym, + COL_NAME_W, r->name, + COL_TIME_W, ts_str, + COL_RC_W, rc_str, + COL_HASH_W, shorthash); + if (has_colors()) attroff(COLOR_PAIR(1) | A_BOLD); + else attroff(A_REVERSE); + } else { + mvprintw(y, 2, "%s %-*s %-*s %-*s %-*s", + sym, + COL_NAME_W, r->name, + COL_TIME_W, ts_str, + COL_RC_W, rc_str, + COL_HASH_W, shorthash); + } + } + + /* ---- hint bar ---- */ + attron(A_REVERSE); + mvprintw(rows - 1, 0, + " J/K navigate Enter build l logs q quit" + " "); + attroff(A_REVERSE); + + wnoutrefresh(stdscr); + doupdate(); + + /* ---- input ---- */ + int ch = getch(); + switch (ch) { + case KEY_UP: + case 'k': + if (cur > 0) { + cur--; + if (cur < scroll) scroll = cur; + } + break; + case KEY_DOWN: + case 'j': + if (cur < idx->count - 1) { + cur++; + if (cur >= scroll + visible) + scroll = cur - visible + 1; + } + break; + case '\n': + case '\r': + case KEY_ENTER: + strncpy(selected_project, + idx->projects[cur].name, sel_size - 1); + selected_project[sel_size - 1] = '\0'; + result = cur; + done = 1; + break; + case 'l': + endwin(); + tui_log_browser(log_dir); + initscr(); + noecho(); + cbreak(); + keypad(stdscr, TRUE); + curs_set(0); + if (has_colors()) { + start_color(); + use_default_colors(); + init_pair(1, COLOR_BLACK, COLOR_WHITE); + } + clear(); + break; + case 'q': + case 27: + done = 1; + break; + } + } + + endwin(); + return result; +} diff --git a/bin/gbuild b/bin/gbuild deleted file mode 100755 index 23d9fe9..0000000 Binary files a/bin/gbuild and /dev/null differ diff --git a/bin/gconfig b/bin/gconfig deleted file mode 100755 index 2b1ee61..0000000 Binary files a/bin/gconfig and /dev/null differ diff --git a/dist/gbuild-1.0.0-1.x86_64.rpm b/dist/gbuild-1.0.0-1.x86_64.rpm deleted file mode 100644 index f0d9432..0000000 Binary files a/dist/gbuild-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild-1.0.0-x86_64-bin.tar.gz b/dist/gbuild-1.0.0-x86_64-bin.tar.gz deleted file mode 100644 index 40bbc22..0000000 Binary files a/dist/gbuild-1.0.0-x86_64-bin.tar.gz and /dev/null differ diff --git a/dist/gbuild-1.0.0.tar.gz b/dist/gbuild-1.0.0.tar.gz deleted file mode 100644 index 3cc865a..0000000 Binary files a/dist/gbuild-1.0.0.tar.gz and /dev/null differ diff --git a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm b/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm deleted file mode 100644 index 4903e10..0000000 Binary files a/dist/gbuild-debuginfo-1.0.0-1.x86_64.rpm and /dev/null differ diff --git a/dist/gbuild_1.0.0_amd64.deb b/dist/gbuild_1.0.0_amd64.deb deleted file mode 100644 index 0a722ed..0000000 Binary files a/dist/gbuild_1.0.0_amd64.deb and /dev/null differ