Project is published.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
# ============================================================
|
||||
# gbuild — C rewrite build system
|
||||
# ============================================================
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2 -Iinclude
|
||||
LDLIBS_GBUILD = -lncurses
|
||||
LDLIBS_GCONFIG =
|
||||
|
||||
# ---- package metadata --------------------------------------
|
||||
|
||||
NAME = gbuild
|
||||
VERSION = 1.0.0
|
||||
ARCH := $(shell uname -m)
|
||||
DEB_ARCH := $(shell dpkg --print-architecture 2>/dev/null || \
|
||||
echo $(ARCH) | sed 's/x86_64/amd64/;s/aarch64/arm64/')
|
||||
RPM_DATE := $(shell date "+%a %b %d %Y")
|
||||
PREFIX = /usr/local
|
||||
|
||||
# ---- release output dirs -----------------------------------
|
||||
|
||||
DIST = dist
|
||||
DEB_ROOT = $(DIST)/deb
|
||||
RPM_ROOT = $(DIST)/rpmbuild
|
||||
|
||||
# ---- sources -----------------------------------------------
|
||||
|
||||
COMMON_SRCS = src/config.c
|
||||
|
||||
GBUILD_SRCS = \
|
||||
src/gbuild.c \
|
||||
src/git_ops.c \
|
||||
src/logger.c \
|
||||
src/make_ops.c \
|
||||
src/tui.c \
|
||||
$(COMMON_SRCS)
|
||||
|
||||
GCONFIG_SRCS = \
|
||||
src/gconfig.c \
|
||||
$(COMMON_SRCS)
|
||||
|
||||
HEADERS = $(wildcard include/*.h)
|
||||
|
||||
# ============================================================
|
||||
# Standard targets
|
||||
# ============================================================
|
||||
|
||||
.PHONY: all clean install uninstall \
|
||||
release release-src release-bin release-deb release-rpm
|
||||
|
||||
all: bin/gbuild bin/gconfig
|
||||
|
||||
bin/gbuild: $(GBUILD_SRCS) $(HEADERS) | bin
|
||||
$(CC) $(CFLAGS) -o $@ $(GBUILD_SRCS) $(LDLIBS_GBUILD)
|
||||
|
||||
bin/gconfig: $(GCONFIG_SRCS) $(HEADERS) | bin
|
||||
$(CC) $(CFLAGS) -o $@ $(GCONFIG_SRCS) $(LDLIBS_GCONFIG)
|
||||
|
||||
bin:
|
||||
mkdir -p bin
|
||||
|
||||
clean:
|
||||
rm -rf bin/ dist/
|
||||
|
||||
install: all
|
||||
install -Dm755 bin/gbuild $(DESTDIR)$(PREFIX)/bin/gbuild
|
||||
install -Dm755 bin/gconfig $(DESTDIR)$(PREFIX)/bin/gconfig
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)$(PREFIX)/bin/gbuild
|
||||
rm -f $(DESTDIR)$(PREFIX)/bin/gconfig
|
||||
|
||||
# ============================================================
|
||||
# release — build all four artefacts into dist/
|
||||
# ============================================================
|
||||
#
|
||||
# dist/
|
||||
# ├── gbuild-1.0.0.tar.gz ← source tarball
|
||||
# ├── gbuild-1.0.0-x86_64-bin.tar.gz ← stripped binaries
|
||||
# ├── gbuild_1.0.0_amd64.deb ← Debian package
|
||||
# └── gbuild-1.0.0-1.x86_64.rpm ← RPM package
|
||||
#
|
||||
release: release-src release-bin release-deb release-rpm
|
||||
@echo ""
|
||||
@echo "Release artefacts:"
|
||||
@ls -1sh $(DIST)/*.tar.gz $(DIST)/*.deb $(DIST)/*.rpm 2>/dev/null \
|
||||
| awk '{print " " $$0}'
|
||||
|
||||
# ============================================================
|
||||
# release-src — source tarball
|
||||
# ============================================================
|
||||
|
||||
release-src:
|
||||
@echo ">>> [1/4] Source tarball ..."
|
||||
mkdir -p $(DIST)
|
||||
mkdir -p $(DIST)/$(NAME)-$(VERSION)
|
||||
cp -r src include pkg Makefile README.md $(DIST)/$(NAME)-$(VERSION)/
|
||||
tar -C $(DIST) -czf $(DIST)/$(NAME)-$(VERSION).tar.gz \
|
||||
$(NAME)-$(VERSION)
|
||||
rm -rf $(DIST)/$(NAME)-$(VERSION)
|
||||
@echo " OK $(DIST)/$(NAME)-$(VERSION).tar.gz"
|
||||
|
||||
# ============================================================
|
||||
# release-bin — stripped binary tarball
|
||||
# ============================================================
|
||||
|
||||
release-bin: all
|
||||
@echo ">>> [2/4] Binary tarball ..."
|
||||
mkdir -p $(DIST)
|
||||
mkdir -p $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin/usr/local/bin
|
||||
cp bin/gbuild $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin/usr/local/bin/
|
||||
cp bin/gconfig $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin/usr/local/bin/
|
||||
strip $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin/usr/local/bin/gbuild
|
||||
strip $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin/usr/local/bin/gconfig
|
||||
tar -C $(DIST) -czf \
|
||||
$(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin.tar.gz \
|
||||
$(NAME)-$(VERSION)-$(ARCH)-bin
|
||||
rm -rf $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin
|
||||
@echo " OK $(DIST)/$(NAME)-$(VERSION)-$(ARCH)-bin.tar.gz"
|
||||
|
||||
# ============================================================
|
||||
# release-deb — Debian .deb package
|
||||
# ============================================================
|
||||
|
||||
release-deb: all
|
||||
@echo ">>> [3/4] .deb package ..."
|
||||
mkdir -p $(DIST)
|
||||
rm -rf $(DEB_ROOT)
|
||||
|
||||
install -Dm755 bin/gbuild $(DEB_ROOT)/usr/local/bin/gbuild
|
||||
install -Dm755 bin/gconfig $(DEB_ROOT)/usr/local/bin/gconfig
|
||||
strip $(DEB_ROOT)/usr/local/bin/gbuild
|
||||
strip $(DEB_ROOT)/usr/local/bin/gconfig
|
||||
|
||||
install -dm755 $(DEB_ROOT)/usr/share/doc/$(NAME)
|
||||
cp README.md $(DEB_ROOT)/usr/share/doc/$(NAME)/
|
||||
|
||||
install -dm755 $(DEB_ROOT)/DEBIAN
|
||||
sed -e 's/@VERSION@/$(VERSION)/g' \
|
||||
-e 's/@DEB_ARCH@/$(DEB_ARCH)/g' \
|
||||
pkg/deb/DEBIAN/control.in > $(DEB_ROOT)/DEBIAN/control
|
||||
sed -e 's/@VERSION@/$(VERSION)/g' \
|
||||
pkg/deb/DEBIAN/postinst.in > $(DEB_ROOT)/DEBIAN/postinst
|
||||
chmod 755 $(DEB_ROOT)/DEBIAN/postinst
|
||||
|
||||
dpkg-deb --build --root-owner-group $(DEB_ROOT) \
|
||||
$(DIST)/$(NAME)_$(VERSION)_$(DEB_ARCH).deb
|
||||
rm -rf $(DEB_ROOT)
|
||||
@echo " OK $(DIST)/$(NAME)_$(VERSION)_$(DEB_ARCH).deb"
|
||||
|
||||
# ============================================================
|
||||
# release-rpm — RPM package
|
||||
# ============================================================
|
||||
|
||||
release-rpm: release-src
|
||||
@echo ">>> [4/4] .rpm package ..."
|
||||
rm -rf $(RPM_ROOT)
|
||||
mkdir -p $(RPM_ROOT)/BUILD $(RPM_ROOT)/BUILDROOT $(RPM_ROOT)/RPMS $(RPM_ROOT)/SOURCES $(RPM_ROOT)/SPECS $(RPM_ROOT)/SRPMS
|
||||
|
||||
cp $(DIST)/$(NAME)-$(VERSION).tar.gz \
|
||||
$(RPM_ROOT)/SOURCES/
|
||||
|
||||
sed -e 's/@VERSION@/$(VERSION)/g' \
|
||||
-e 's/@RPM_DATE@/$(RPM_DATE)/g' \
|
||||
pkg/rpm/gbuild.spec.in > $(RPM_ROOT)/SPECS/$(NAME).spec
|
||||
|
||||
rpmbuild -bb --nodeps \
|
||||
--define "_topdir $(CURDIR)/$(RPM_ROOT)" \
|
||||
$(RPM_ROOT)/SPECS/$(NAME).spec
|
||||
|
||||
find $(RPM_ROOT)/RPMS -name "*.rpm" -exec cp {} $(DIST)/ \;
|
||||
rm -rf $(RPM_ROOT)
|
||||
@echo " OK $(DIST)/$(NAME)-$(VERSION)-1.$(ARCH).rpm"
|
||||
@@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Clone or pull** — automatically clones a fresh repo on first run; pulls on subsequent runs
|
||||
- **Interactive target picker** — reads your `Makefile` and presents an ncurses TUI to select the build target; pass `--target` to skip it
|
||||
- **Timestamped log files** — every run writes a log to `~/.local/log/gbuild/` named `<timestamp>_<project>.log`
|
||||
- **Log browser** — built-in two-pane TUI: file list on the left, live preview on the right; open in `less`, delete, or quit
|
||||
- **Configurable** — reads `~/.gconfig`; override with `--url` and `--user` at runtime
|
||||
- **`gconfig` companion** — manages `~/.gconfig` with `init`, `show`, and `help` commands
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Library | Purpose | Install (Debian/Ubuntu) |
|
||||
|----------|------------------------------|------------------------------|
|
||||
| ncurses | TUI for picker & log browser | `sudo apt install libncurses-dev` |
|
||||
| git | Clone / pull | `sudo apt install git` |
|
||||
| make | Build projects | `sudo apt install make` |
|
||||
| less | Open logs from browser | `sudo apt install less` |
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
# Install ncurses dev headers if needed
|
||||
sudo apt install libncurses-dev
|
||||
|
||||
# Build both binaries into bin/
|
||||
make
|
||||
|
||||
# Optionally install system-wide
|
||||
sudo make install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick setup
|
||||
|
||||
```sh
|
||||
# 1. Initialise ~/.gconfig with defaults
|
||||
gconfig init
|
||||
|
||||
# 2. Fill in your values
|
||||
$EDITOR ~/.gconfig
|
||||
|
||||
# 3. Verify
|
||||
gconfig show
|
||||
|
||||
# 4. Build a project
|
||||
gbuild myproject
|
||||
```
|
||||
|
||||
### `~/.gconfig` format
|
||||
|
||||
```ini
|
||||
# ~/.gconfig — managed by gconfig
|
||||
|
||||
[git]
|
||||
GIT_URL = http://localhost:3000
|
||||
GIT_USER = jokerz
|
||||
|
||||
[auth]
|
||||
# Use token-based OR password-based auth; leave the other blank
|
||||
GIT_TOKEN =
|
||||
GIT_PASSWORD =
|
||||
|
||||
[build]
|
||||
DEFAULT_TARGET =
|
||||
CLONE_DIR = ~/projects
|
||||
|
||||
[log]
|
||||
LOG_ENABLED = true
|
||||
LOG_DIR = ~/.local/log/gbuild
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
gbuild [options] <project>
|
||||
gbuild --logs
|
||||
|
||||
Options:
|
||||
--url <url> Override Git base URL (default: ~/.gconfig)
|
||||
--user <name> Override Git username (default: ~/.gconfig)
|
||||
--target <tgt> Run target directly, skip interactive picker
|
||||
--logs Open the interactive log browser
|
||||
-h, --help Print this help and exit
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
# Build with defaults
|
||||
gbuild myproject
|
||||
|
||||
# Skip the target picker
|
||||
gbuild --target clean myproject
|
||||
|
||||
# Different server and user for this run
|
||||
gbuild --url http://10.0.0.5:3000 --user alice myproject
|
||||
|
||||
# Browse past build logs
|
||||
gbuild --logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `gconfig` commands
|
||||
|
||||
| Command | Description |
|
||||
|---------------------|-------------------------------------------------------------|
|
||||
| `gconfig init` | Create `~/.gconfig` with defaults (skips if already exists) |
|
||||
| `gconfig init --force` | Overwrite; backs up old file as `~/.gconfig.bak.<ts>` |
|
||||
| `gconfig show` | Print current config (auth fields masked) |
|
||||
| `gconfig help` | Print usage |
|
||||
|
||||
---
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
gbuild/
|
||||
├── Makefile
|
||||
├── README.md
|
||||
├── include/
|
||||
│ ├── config.h — GConfig struct, INI load/save/init/show
|
||||
│ ├── git_ops.h — clone / pull
|
||||
│ ├── logger.h — timestamped coloured logging
|
||||
│ ├── make_ops.h — Makefile target parser + build runner
|
||||
│ └── tui.h — ncurses target picker + log browser
|
||||
└── src/
|
||||
├── config.c
|
||||
├── gbuild.c — gbuild main()
|
||||
├── gconfig.c — gconfig main()
|
||||
├── git_ops.c
|
||||
├── logger.c
|
||||
├── make_ops.c
|
||||
└── tui.c
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
MIT License — maintained by jokerz / spdlab.hu
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define CFG_MAX 512
|
||||
|
||||
typedef struct {
|
||||
char git_url[CFG_MAX];
|
||||
char git_user[256];
|
||||
char git_token[256];
|
||||
char git_password[256];
|
||||
char default_target[128];
|
||||
char clone_dir[CFG_MAX];
|
||||
bool log_enabled;
|
||||
char log_dir[CFG_MAX];
|
||||
} GConfig;
|
||||
|
||||
void config_defaults(GConfig *cfg);
|
||||
int config_load(GConfig *cfg, const char *path);
|
||||
int config_save(const GConfig *cfg, const char *path);
|
||||
int config_init(const char *path, bool force);
|
||||
void config_show(const GConfig *cfg);
|
||||
void config_get_path(char *out, size_t n);
|
||||
char *expand_tilde(const char *in, char *out, size_t n);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
@@ -0,0 +1,18 @@
|
||||
#ifndef GIT_OPS_H
|
||||
#define GIT_OPS_H
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
/* Returns 1 if clone_dir/project/.git exists, 0 otherwise */
|
||||
int git_repo_exists(const char *clone_dir, const char *project);
|
||||
|
||||
/* Clone repo under clone_dir/project using token or password auth */
|
||||
int git_clone(const char *base_url, const char *user,
|
||||
const char *token, const char *password,
|
||||
const char *project, const char *clone_dir,
|
||||
Logger *log);
|
||||
|
||||
/* Pull latest in repo_path */
|
||||
int git_pull(const char *repo_path, Logger *log);
|
||||
|
||||
#endif /* GIT_OPS_H */
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef LOGGER_H
|
||||
#define LOGGER_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
FILE *fp;
|
||||
char path[512];
|
||||
bool enabled;
|
||||
} Logger;
|
||||
|
||||
int logger_open (Logger *log, const char *log_dir, const char *project);
|
||||
void logger_close(Logger *log);
|
||||
|
||||
void logger_info (Logger *log, const char *fmt, ...);
|
||||
void logger_ok (Logger *log, const char *fmt, ...);
|
||||
void logger_warn (Logger *log, const char *fmt, ...);
|
||||
void logger_error(Logger *log, const char *fmt, ...);
|
||||
void logger_raw (Logger *log, const char *fmt, ...);
|
||||
|
||||
#endif /* LOGGER_H */
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef MAKE_OPS_H
|
||||
#define MAKE_OPS_H
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#define MAX_TARGETS 256
|
||||
#define TARGET_NAME 128
|
||||
|
||||
typedef struct {
|
||||
char names[MAX_TARGETS][TARGET_NAME];
|
||||
int count;
|
||||
} MakeTargets;
|
||||
|
||||
int make_parse_targets(const char *repo_path, MakeTargets *out);
|
||||
int make_build(const char *repo_path, const char *target, Logger *log);
|
||||
|
||||
#endif /* MAKE_OPS_H */
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef TUI_H
|
||||
#define TUI_H
|
||||
|
||||
#include "make_ops.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/* Arrow-key driven target picker.
|
||||
* Fills `selected` with the chosen target name.
|
||||
* Returns 0 on selection, -1 if the user cancelled (q / ESC). */
|
||||
int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size);
|
||||
|
||||
/* Two-pane log browser: list on left, preview on right.
|
||||
* Keys: ↑↓ navigate Enter open in less d delete q quit */
|
||||
void tui_log_browser(const char *log_dir);
|
||||
|
||||
#endif /* TUI_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
Package: gbuild
|
||||
Version: @VERSION@
|
||||
Architecture: @DEB_ARCH@
|
||||
Maintainer: jokerz <jokerz@spdlab.hu>
|
||||
Depends: git, make, less, libncurses6
|
||||
Section: devel
|
||||
Priority: optional
|
||||
Description: Build tool for your Linux distro
|
||||
Clone, pull, pick a target, build — all from one command.
|
||||
Includes the gconfig companion for managing ~/.gconfig.
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
echo "gbuild @VERSION@ installed. Run: gconfig init"
|
||||
@@ -0,0 +1,34 @@
|
||||
Name: gbuild
|
||||
Version: @VERSION@
|
||||
Release: 1%{?dist}
|
||||
Summary: Build tool for your Linux distro
|
||||
License: MIT
|
||||
URL: https://spdlab.hu/gbuild
|
||||
Source0: gbuild-@VERSION@.tar.gz
|
||||
|
||||
BuildRequires: gcc make ncurses-devel
|
||||
Requires: git make less ncurses-libs
|
||||
|
||||
%description
|
||||
Clone, pull, pick a target, build — all from one command.
|
||||
Includes the gconfig companion for managing ~/.gconfig.
|
||||
|
||||
%prep
|
||||
%autosetup
|
||||
|
||||
%build
|
||||
make all
|
||||
|
||||
%install
|
||||
make install DESTDIR=%{buildroot} PREFIX=/usr/local
|
||||
|
||||
%files
|
||||
/usr/local/bin/gbuild
|
||||
/usr/local/bin/gconfig
|
||||
|
||||
%post
|
||||
echo "gbuild @VERSION@ installed. Run: gconfig init"
|
||||
|
||||
%changelog
|
||||
* @RPM_DATE@ jokerz <jokerz@spdlab.hu> - @VERSION@-1
|
||||
- Initial C rewrite release
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ------------------------------------------------------------------ helpers */
|
||||
|
||||
char *expand_tilde(const char *in, char *out, size_t n)
|
||||
{
|
||||
if (!in || in[0] != '~') {
|
||||
strncpy(out, in ? in : "", n - 1);
|
||||
out[n - 1] = '\0';
|
||||
return out;
|
||||
}
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
snprintf(out, n, "%s%s", home, in + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
void config_get_path(char *out, size_t n)
|
||||
{
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
snprintf(out, n, "%s/.gconfig", home);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- defaults */
|
||||
|
||||
void config_defaults(GConfig *cfg)
|
||||
{
|
||||
memset(cfg, 0, sizeof(*cfg));
|
||||
strncpy(cfg->git_url, "http://localhost:3000", CFG_MAX - 1);
|
||||
strncpy(cfg->git_user, "", sizeof(cfg->git_user) - 1);
|
||||
cfg->log_enabled = true;
|
||||
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) home = "/tmp";
|
||||
snprintf(cfg->clone_dir, CFG_MAX, "%s/projects", home);
|
||||
snprintf(cfg->log_dir, CFG_MAX, "%s/.local/log/gbuild", home);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ loader */
|
||||
|
||||
static void strip(char *s)
|
||||
{
|
||||
/* strip leading whitespace */
|
||||
char *p = s;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (p != s) memmove(s, p, strlen(p) + 1);
|
||||
/* strip trailing whitespace / newline */
|
||||
size_t l = strlen(s);
|
||||
while (l > 0 && (s[l-1] == '\n' || s[l-1] == '\r' ||
|
||||
s[l-1] == ' ' || s[l-1] == '\t'))
|
||||
s[--l] = '\0';
|
||||
}
|
||||
|
||||
int config_load(GConfig *cfg, const char *path)
|
||||
{
|
||||
config_defaults(cfg);
|
||||
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return -1;
|
||||
|
||||
char line[512];
|
||||
char section[64] = "";
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
strip(line);
|
||||
if (line[0] == '\0' || line[0] == '#') continue;
|
||||
|
||||
/* section header */
|
||||
if (line[0] == '[') {
|
||||
char *end = strchr(line, ']');
|
||||
if (end) {
|
||||
*end = '\0';
|
||||
strncpy(section, line + 1, sizeof(section) - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* key = value */
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
*eq = '\0';
|
||||
char *key = line;
|
||||
char *val = eq + 1;
|
||||
strip(key);
|
||||
strip(val);
|
||||
|
||||
char expanded[CFG_MAX];
|
||||
expand_tilde(val, expanded, sizeof(expanded));
|
||||
|
||||
if (!strcmp(key, "GIT_URL")) strncpy(cfg->git_url, expanded, CFG_MAX - 1);
|
||||
else if (!strcmp(key, "GIT_USER")) strncpy(cfg->git_user, expanded, sizeof(cfg->git_user) - 1);
|
||||
else if (!strcmp(key, "GIT_TOKEN")) strncpy(cfg->git_token, expanded, sizeof(cfg->git_token) - 1);
|
||||
else if (!strcmp(key, "GIT_PASSWORD")) strncpy(cfg->git_password, expanded, sizeof(cfg->git_password) - 1);
|
||||
else if (!strcmp(key, "DEFAULT_TARGET")) strncpy(cfg->default_target, expanded, sizeof(cfg->default_target) - 1);
|
||||
else if (!strcmp(key, "CLONE_DIR")) strncpy(cfg->clone_dir, expanded, CFG_MAX - 1);
|
||||
else if (!strcmp(key, "LOG_DIR")) strncpy(cfg->log_dir, expanded, CFG_MAX - 1);
|
||||
else if (!strcmp(key, "LOG_ENABLED")) cfg->log_enabled = (strcmp(expanded, "false") != 0 &&
|
||||
strcmp(expanded, "0") != 0);
|
||||
(void)section;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ saver */
|
||||
|
||||
int config_save(const GConfig *cfg, const char *path)
|
||||
{
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) return -1;
|
||||
|
||||
fprintf(f,
|
||||
"# ~/.gconfig — managed by gconfig\n\n"
|
||||
"[git]\n"
|
||||
"GIT_URL = %s\n"
|
||||
"GIT_USER = %s\n\n"
|
||||
"[auth]\n"
|
||||
"# Use token-based OR password-based auth; leave the other blank\n"
|
||||
"GIT_TOKEN = %s\n"
|
||||
"GIT_PASSWORD = %s\n\n"
|
||||
"[build]\n"
|
||||
"DEFAULT_TARGET = %s\n"
|
||||
"CLONE_DIR = %s\n\n"
|
||||
"[log]\n"
|
||||
"LOG_ENABLED = %s\n"
|
||||
"LOG_DIR = %s\n",
|
||||
cfg->git_url,
|
||||
cfg->git_user,
|
||||
cfg->git_token,
|
||||
cfg->git_password,
|
||||
cfg->default_target,
|
||||
cfg->clone_dir,
|
||||
cfg->log_enabled ? "true" : "false",
|
||||
cfg->log_dir);
|
||||
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ init */
|
||||
|
||||
int config_init(const char *path, bool force)
|
||||
{
|
||||
struct stat st;
|
||||
if (!force && stat(path, &st) == 0) {
|
||||
/* file exists and we're not forcing */
|
||||
return 1; /* already exists */
|
||||
}
|
||||
|
||||
if (force && stat(path, &st) == 0) {
|
||||
/* back up existing file */
|
||||
time_t now = time(NULL);
|
||||
char backup[CFG_MAX + 32];
|
||||
snprintf(backup, sizeof(backup), "%s.bak.%ld", path, (long)now);
|
||||
rename(path, backup);
|
||||
printf("Backed up existing config to: %s\n", backup);
|
||||
}
|
||||
|
||||
GConfig cfg;
|
||||
config_defaults(&cfg);
|
||||
return config_save(&cfg, path);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ show */
|
||||
|
||||
void config_show(const GConfig *cfg)
|
||||
{
|
||||
const char *tok = cfg->git_token[0] ? "****" : "(not set)";
|
||||
const char *pass = cfg->git_password[0] ? "****" : "(not set)";
|
||||
|
||||
printf("\n [git]\n");
|
||||
printf(" GIT_URL = %s\n", cfg->git_url[0] ? cfg->git_url : "(not set)");
|
||||
printf(" GIT_USER = %s\n", cfg->git_user[0] ? cfg->git_user : "(not set)");
|
||||
|
||||
printf("\n [auth]\n");
|
||||
printf(" GIT_TOKEN = %s\n", tok);
|
||||
printf(" GIT_PASSWORD = %s\n", pass);
|
||||
|
||||
printf("\n [build]\n");
|
||||
printf(" DEFAULT_TARGET = %s\n", cfg->default_target[0] ? cfg->default_target : "(not set)");
|
||||
printf(" CLONE_DIR = %s\n", cfg->clone_dir);
|
||||
|
||||
printf("\n [log]\n");
|
||||
printf(" LOG_ENABLED = %s\n", cfg->log_enabled ? "true" : "false");
|
||||
printf(" LOG_DIR = %s\n\n", cfg->log_dir);
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
#include "config.h"
|
||||
#include "git_ops.h"
|
||||
#include "logger.h"
|
||||
#include "make_ops.h"
|
||||
#include "tui.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define VERSION "1.0.0"
|
||||
|
||||
/* ----------------------------------------------------------------- usage */
|
||||
|
||||
static void print_usage(const char *prog)
|
||||
{
|
||||
printf(
|
||||
"gbuild %s — A build tool for your Linux distro\n\n"
|
||||
"Usage:\n"
|
||||
" %s [options] <project>\n"
|
||||
" %s --logs\n\n"
|
||||
"Options:\n"
|
||||
" --url <url> Override Git base URL (default: ~/.gconfig)\n"
|
||||
" --user <name> Override Git username (default: ~/.gconfig)\n"
|
||||
" --target <tgt> Run target directly, skip interactive picker\n"
|
||||
" --logs Open the interactive log browser\n"
|
||||
" -h, --help Print this help and exit\n\n"
|
||||
"Examples:\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);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- main */
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
/* ---- parse arguments ---- */
|
||||
const char *arg_url = NULL;
|
||||
const char *arg_user = NULL;
|
||||
const char *arg_target = NULL;
|
||||
const char *arg_project = NULL;
|
||||
int flag_logs = 0;
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||||
print_usage(argv[0]);
|
||||
return 0;
|
||||
} else if (!strcmp(argv[i], "--url")) {
|
||||
if (++i >= argc) { fprintf(stderr, "error: --url requires a value\n"); return 1; }
|
||||
arg_url = argv[i];
|
||||
} else if (!strcmp(argv[i], "--user")) {
|
||||
if (++i >= argc) { fprintf(stderr, "error: --user requires a value\n"); return 1; }
|
||||
arg_user = argv[i];
|
||||
} else if (!strcmp(argv[i], "--target")) {
|
||||
if (++i >= argc) { fprintf(stderr, "error: --target requires a value\n"); return 1; }
|
||||
arg_target = argv[i];
|
||||
} else if (!strcmp(argv[i], "--logs")) {
|
||||
flag_logs = 1;
|
||||
} else if (argv[i][0] == '-') {
|
||||
fprintf(stderr, "error: unknown flag '%s'\n", argv[i]);
|
||||
return 1;
|
||||
} else {
|
||||
if (arg_project) {
|
||||
fprintf(stderr, "error: unexpected argument '%s'\n", argv[i]);
|
||||
return 1;
|
||||
}
|
||||
arg_project = argv[i];
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- load config ---- */
|
||||
GConfig cfg;
|
||||
char cfg_path[512];
|
||||
config_get_path(cfg_path, sizeof(cfg_path));
|
||||
|
||||
if (config_load(&cfg, cfg_path) != 0) {
|
||||
fprintf(stderr,
|
||||
"[WARN ] ~/.gconfig not found — using built-in defaults.\n"
|
||||
" Run 'gconfig init' to create one.\n");
|
||||
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));
|
||||
|
||||
/* ---- log browser mode ---- */
|
||||
if (flag_logs) {
|
||||
tui_log_browser(log_dir);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- need a project name ---- */
|
||||
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;
|
||||
}
|
||||
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));
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
/* ---- build ---- */
|
||||
logger_info(&log, "gbuild: building '%s' target '%s' ...",
|
||||
arg_project, target[0] ? target : "(default)");
|
||||
|
||||
int rc = make_build(repo_path, target, &log);
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define VERSION "1.0.0"
|
||||
|
||||
/* ----------------------------------------------------------------- usage */
|
||||
|
||||
static void print_usage(void)
|
||||
{
|
||||
printf(
|
||||
"gconfig %s — Manage ~/.gconfig for gbuild\n\n"
|
||||
"Usage:\n"
|
||||
" gconfig <command> [flags]\n\n"
|
||||
"Commands:\n"
|
||||
" init Create ~/.gconfig with defaults (skips if already exists)\n"
|
||||
" init --force Overwrite existing config; backs up the old file first\n"
|
||||
" show Print current config values (auth fields masked)\n"
|
||||
" help Print this help and exit\n\n"
|
||||
"Quick setup:\n"
|
||||
" gconfig init\n"
|
||||
" $EDITOR ~/.gconfig\n"
|
||||
" gconfig show\n",
|
||||
VERSION);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- main */
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
if (argc < 2) {
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
char cfg_path[512];
|
||||
config_get_path(cfg_path, sizeof(cfg_path));
|
||||
|
||||
const char *cmd = argv[1];
|
||||
|
||||
/* ---- help ---- */
|
||||
if (!strcmp(cmd, "help") || !strcmp(cmd, "-h") || !strcmp(cmd, "--help")) {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- init ---- */
|
||||
if (!strcmp(cmd, "init")) {
|
||||
int force = 0;
|
||||
for (int i = 2; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--force") || !strcmp(argv[i], "-f"))
|
||||
force = 1;
|
||||
}
|
||||
|
||||
int rc = config_init(cfg_path, force);
|
||||
if (rc < 0) {
|
||||
fprintf(stderr, "error: could not write %s: ", cfg_path);
|
||||
perror(NULL);
|
||||
return 1;
|
||||
}
|
||||
if (rc == 1) {
|
||||
printf("~/.gconfig already exists. Use 'gconfig init --force' to overwrite.\n");
|
||||
return 0;
|
||||
}
|
||||
printf("Created %s with default values.\n", cfg_path);
|
||||
printf("Edit it with: $EDITOR %s\n", cfg_path);
|
||||
printf("Verify with: gconfig show\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- show ---- */
|
||||
if (!strcmp(cmd, "show")) {
|
||||
GConfig cfg;
|
||||
if (config_load(&cfg, cfg_path) != 0) {
|
||||
fprintf(stderr,
|
||||
"error: could not read %s\n"
|
||||
"Run 'gconfig init' to create it.\n", cfg_path);
|
||||
return 1;
|
||||
}
|
||||
printf("\n%s\n", cfg_path);
|
||||
config_show(&cfg);
|
||||
return 0;
|
||||
}
|
||||
|
||||
fprintf(stderr, "error: unknown command '%s'\n\n", cmd);
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
#include "git_ops.h"
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ---------------------------------------------------------------- helpers */
|
||||
|
||||
/*
|
||||
* Run `cmd`, streaming output to stdout and log simultaneously.
|
||||
* Returns the exit status of the child process.
|
||||
*/
|
||||
static int run_stream(const char *cmd, Logger *log)
|
||||
{
|
||||
FILE *pipe = popen(cmd, "r");
|
||||
if (!pipe) {
|
||||
logger_error(log, "popen failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), pipe)) {
|
||||
/* strip trailing newline for logger_raw so it adds its own */
|
||||
size_t l = strlen(line);
|
||||
if (l > 0 && line[l-1] == '\n') line[l-1] = '\0';
|
||||
logger_raw(log, "%s\n", line);
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
return (status == -1) ? -1 : WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
/*
|
||||
* Inject credentials into a URL of the form scheme://host/path
|
||||
* producing scheme://user:auth@host/path.
|
||||
*
|
||||
* If both token and password are empty the URL is left unmodified
|
||||
* (useful for public repos).
|
||||
*/
|
||||
static void build_auth_url(char *out, size_t n,
|
||||
const char *base_url, const char *user,
|
||||
const char *token, const char *password,
|
||||
const char *project)
|
||||
{
|
||||
const char *auth = (token && token[0]) ? token :
|
||||
(password && password[0]) ? password : NULL;
|
||||
|
||||
/* find end of scheme:// */
|
||||
const char *sep = strstr(base_url, "://");
|
||||
if (!sep) {
|
||||
/* fallback: treat whole thing as host */
|
||||
if (auth)
|
||||
snprintf(out, n, "http://%s:%s@%s/%s/%s",
|
||||
user, auth, base_url, user, project);
|
||||
else
|
||||
snprintf(out, n, "http://%s/%s/%s", base_url, user, project);
|
||||
return;
|
||||
}
|
||||
|
||||
/* scheme = base_url[0 .. sep+3) */
|
||||
char scheme[16] = {0};
|
||||
size_t slen = (size_t)(sep + 3 - base_url);
|
||||
if (slen >= sizeof(scheme)) slen = sizeof(scheme) - 1;
|
||||
strncpy(scheme, base_url, slen);
|
||||
|
||||
const char *host = base_url + slen;
|
||||
|
||||
/* strip trailing slash from host fragment */
|
||||
char hostbuf[512];
|
||||
strncpy(hostbuf, host, sizeof(hostbuf) - 1);
|
||||
hostbuf[sizeof(hostbuf) - 1] = '\0';
|
||||
size_t hl = strlen(hostbuf);
|
||||
while (hl > 0 && hostbuf[hl-1] == '/') hostbuf[--hl] = '\0';
|
||||
|
||||
if (auth && user && user[0])
|
||||
snprintf(out, n, "%s%s:%s@%s/%s/%s",
|
||||
scheme, user, auth, hostbuf, user, project);
|
||||
else if (user && user[0])
|
||||
snprintf(out, n, "%s%s@%s/%s/%s",
|
||||
scheme, user, hostbuf, user, project);
|
||||
else
|
||||
snprintf(out, n, "%s%s/%s/%s", scheme, hostbuf, user, project);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------- public API */
|
||||
|
||||
int git_repo_exists(const char *clone_dir, const char *project)
|
||||
{
|
||||
char git_path[CFG_MAX * 2];
|
||||
snprintf(git_path, sizeof(git_path), "%s/%s/.git", clone_dir, project);
|
||||
struct stat st;
|
||||
return (stat(git_path, &st) == 0 && S_ISDIR(st.st_mode)) ? 1 : 0;
|
||||
}
|
||||
|
||||
int git_clone(const char *base_url, const char *user,
|
||||
const char *token, const char *password,
|
||||
const char *project, const char *clone_dir,
|
||||
Logger *log)
|
||||
{
|
||||
char url[CFG_MAX * 2];
|
||||
build_auth_url(url, sizeof(url),
|
||||
base_url, user, token, password, project);
|
||||
|
||||
/* NOTE: credentials appear in the URL which git masks in its own output,
|
||||
* but they may briefly be visible in /proc/<pid>/cmdline.
|
||||
* For a higher-security setup, consider GIT_ASKPASS instead. */
|
||||
char cmd[CFG_MAX * 4];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"git clone \"%s\" \"%s/%s\" 2>&1",
|
||||
url, clone_dir, project);
|
||||
|
||||
return run_stream(cmd, log);
|
||||
}
|
||||
|
||||
int git_pull(const char *repo_path, Logger *log)
|
||||
{
|
||||
char cmd[CFG_MAX * 2];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"git -C \"%s\" pull 2>&1", repo_path);
|
||||
return run_stream(cmd, log);
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
#include "logger.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ANSI colours */
|
||||
#define COL_RESET "\033[0m"
|
||||
#define COL_CYAN "\033[36m"
|
||||
#define COL_GREEN "\033[32m"
|
||||
#define COL_YELLOW "\033[33m"
|
||||
#define COL_RED "\033[31m"
|
||||
#define COL_BOLD "\033[1m"
|
||||
|
||||
/* ---------------------------------------------------------------- helpers */
|
||||
|
||||
static void mkdirs(const char *path)
|
||||
{
|
||||
char tmp[512];
|
||||
strncpy(tmp, path, sizeof(tmp) - 1);
|
||||
tmp[sizeof(tmp) - 1] = '\0';
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = '\0';
|
||||
mkdir(tmp, 0755);
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
mkdir(tmp, 0755);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ open */
|
||||
|
||||
int logger_open(Logger *log, const char *log_dir, const char *project)
|
||||
{
|
||||
memset(log, 0, sizeof(*log));
|
||||
|
||||
mkdirs(log_dir);
|
||||
|
||||
time_t now = time(NULL);
|
||||
struct tm *t = localtime(&now);
|
||||
char ts[32];
|
||||
strftime(ts, sizeof(ts), "%Y%m%d_%H%M%S", t);
|
||||
|
||||
snprintf(log->path, sizeof(log->path),
|
||||
"%s/%s_%s.log", log_dir, ts, project);
|
||||
|
||||
log->fp = fopen(log->path, "w");
|
||||
if (!log->fp) return -1;
|
||||
|
||||
log->enabled = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void logger_close(Logger *log)
|
||||
{
|
||||
if (log->fp) {
|
||||
fclose(log->fp);
|
||||
log->fp = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- write */
|
||||
|
||||
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_ERROR: return "[ERROR]";
|
||||
}
|
||||
return "[ ]";
|
||||
}
|
||||
|
||||
static const char *level_color(LogLevel lv)
|
||||
{
|
||||
switch (lv) {
|
||||
case LVL_INFO: return COL_CYAN;
|
||||
case LVL_OK: return COL_GREEN COL_BOLD;
|
||||
case LVL_WARN: return COL_YELLOW;
|
||||
case LVL_ERROR: return COL_RED COL_BOLD;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static void write_level(Logger *log, LogLevel lv, const char *fmt, va_list ap)
|
||||
{
|
||||
char msg[2048];
|
||||
vsnprintf(msg, sizeof(msg), fmt, ap);
|
||||
|
||||
/* stdout — coloured */
|
||||
printf("%s%s%s %s\n", level_color(lv), level_tag(lv), COL_RESET, msg);
|
||||
fflush(stdout);
|
||||
|
||||
/* log file — plain */
|
||||
if (log && log->fp) {
|
||||
/* prepend a timestamp in the file */
|
||||
time_t now = time(NULL);
|
||||
struct tm *t = localtime(&now);
|
||||
char ts[32];
|
||||
strftime(ts, sizeof(ts), "%H:%M:%S", t);
|
||||
fprintf(log->fp, "%s %s %s\n", ts, level_tag(lv), msg);
|
||||
fflush(log->fp);
|
||||
}
|
||||
}
|
||||
|
||||
void logger_info(Logger *log, const char *fmt, ...)
|
||||
{
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_level(log, LVL_INFO, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void logger_ok(Logger *log, const char *fmt, ...)
|
||||
{
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_level(log, LVL_OK, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void logger_warn(Logger *log, const char *fmt, ...)
|
||||
{
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_level(log, LVL_WARN, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void logger_error(Logger *log, const char *fmt, ...)
|
||||
{
|
||||
va_list ap; va_start(ap, fmt);
|
||||
write_level(log, LVL_ERROR, fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
void logger_raw(Logger *log, const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vprintf(fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
fflush(stdout);
|
||||
|
||||
if (log && log->fp) {
|
||||
va_start(ap, fmt);
|
||||
vfprintf(log->fp, fmt, ap);
|
||||
va_end(ap);
|
||||
fflush(log->fp);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#include "make_ops.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ------------------------------------------------------------ helpers */
|
||||
|
||||
/* Special make targets we want to skip in the picker */
|
||||
static const char *SKIP_TARGETS[] = {
|
||||
".PHONY", ".SUFFIXES", ".DEFAULT", ".PRECIOUS",
|
||||
".INTERMEDIATE", ".SECONDARY", ".NOTPARALLEL",
|
||||
".DELETE_ON_ERROR", ".IGNORE", ".LOW_RESOLUTION_TIME",
|
||||
".SILENT", ".EXPORT_ALL_VARIABLES", ".ONESHELL",
|
||||
NULL
|
||||
};
|
||||
|
||||
static int is_special(const char *name)
|
||||
{
|
||||
for (int i = 0; SKIP_TARGETS[i]; i++)
|
||||
if (!strcmp(name, SKIP_TARGETS[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int is_valid_target_char(char c)
|
||||
{
|
||||
return isalnum((unsigned char)c) || c == '_' || c == '-' || c == '.';
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- parser */
|
||||
|
||||
int make_parse_targets(const char *repo_path, MakeTargets *out)
|
||||
{
|
||||
/* Try Makefile then makefile */
|
||||
char mf_path[1024];
|
||||
FILE *f = NULL;
|
||||
|
||||
snprintf(mf_path, sizeof(mf_path), "%s/Makefile", repo_path);
|
||||
f = fopen(mf_path, "r");
|
||||
if (!f) {
|
||||
snprintf(mf_path, sizeof(mf_path), "%s/makefile", repo_path);
|
||||
f = fopen(mf_path, "r");
|
||||
}
|
||||
if (!f) return -1;
|
||||
|
||||
out->count = 0;
|
||||
|
||||
char line[2048];
|
||||
while (fgets(line, sizeof(line), f) && out->count < MAX_TARGETS) {
|
||||
/* skip comment lines and recipe lines (start with tab) */
|
||||
if (line[0] == '#' || line[0] == '\t' || line[0] == '\n') continue;
|
||||
|
||||
/* find first ':' */
|
||||
char *colon = strchr(line, ':');
|
||||
if (!colon) continue;
|
||||
|
||||
/* must not be := += ?= (variable assignment) */
|
||||
if (colon > line && (*(colon-1) == ':' ||
|
||||
*(colon-1) == '+' ||
|
||||
*(colon-1) == '?')) continue;
|
||||
|
||||
/* extract the name part before ':' */
|
||||
size_t name_len = (size_t)(colon - line);
|
||||
if (name_len == 0 || name_len >= TARGET_NAME) continue;
|
||||
|
||||
char name[TARGET_NAME];
|
||||
strncpy(name, line, name_len);
|
||||
name[name_len] = '\0';
|
||||
|
||||
/* strip surrounding whitespace */
|
||||
/* (shouldn't have leading whitespace since we skipped tabs,
|
||||
* but strip trailing) */
|
||||
while (name_len > 0 && isspace((unsigned char)name[name_len - 1]))
|
||||
name[--name_len] = '\0';
|
||||
|
||||
if (name_len == 0) continue;
|
||||
|
||||
/* skip if name contains non-target chars (%, =, space) */
|
||||
int valid = 1;
|
||||
for (size_t i = 0; i < name_len; i++) {
|
||||
if (!is_valid_target_char(name[i])) { valid = 0; break; }
|
||||
}
|
||||
if (!valid) continue;
|
||||
|
||||
/* skip special targets */
|
||||
if (is_special(name)) continue;
|
||||
|
||||
/* skip duplicates */
|
||||
int dup = 0;
|
||||
for (int i = 0; i < out->count; i++)
|
||||
if (!strcmp(out->names[i], name)) { dup = 1; break; }
|
||||
if (dup) continue;
|
||||
|
||||
strncpy(out->names[out->count], name, TARGET_NAME - 1);
|
||||
out->names[out->count][TARGET_NAME - 1] = '\0';
|
||||
out->count++;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return (out->count > 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------- build */
|
||||
|
||||
int make_build(const char *repo_path, const char *target, Logger *log)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"make -C \"%s\" %s 2>&1", repo_path, target);
|
||||
|
||||
FILE *pipe = popen(cmd, "r");
|
||||
if (!pipe) {
|
||||
logger_error(log, "popen failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), pipe)) {
|
||||
size_t l = strlen(line);
|
||||
if (l > 0 && line[l-1] == '\n') line[l-1] = '\0';
|
||||
logger_raw(log, "%s\n", line);
|
||||
}
|
||||
|
||||
int status = pclose(pipe);
|
||||
return (status == -1) ? -1 : WEXITSTATUS(status);
|
||||
}
|
||||
@@ -0,0 +1,429 @@
|
||||
#include "tui.h"
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* ================================================================
|
||||
* SECTION 1 — TARGET PICKER
|
||||
* ================================================================ */
|
||||
|
||||
#define PICKER_MIN_W 40
|
||||
#define PICKER_MIN_H 8
|
||||
|
||||
int tui_pick_target(const MakeTargets *targets, char *selected, size_t sel_size)
|
||||
{
|
||||
if (!targets || targets->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_CYAN); /* selected row */
|
||||
init_pair(2, COLOR_CYAN, -1); /* border colour */
|
||||
init_pair(3, COLOR_WHITE, -1); /* normal row */
|
||||
}
|
||||
|
||||
int rows, cols;
|
||||
getmaxyx(stdscr, rows, cols);
|
||||
|
||||
int list_h = targets->count + 2; /* +2 for border */
|
||||
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;
|
||||
}
|
||||
if (list_w > cols - 4) list_w = cols - 4;
|
||||
if (list_h > rows - 4) list_h = rows - 4;
|
||||
|
||||
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 */
|
||||
|
||||
WINDOW *win = newwin(list_h, list_w, win_y, win_x);
|
||||
|
||||
/* 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");
|
||||
attroff(A_DIM);
|
||||
refresh();
|
||||
|
||||
int done = 0;
|
||||
int result = -1;
|
||||
|
||||
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;
|
||||
if (idx == cur) {
|
||||
if (has_colors()) wattron(win, COLOR_PAIR(1) | A_BOLD);
|
||||
else wattron(win, A_REVERSE);
|
||||
mvwprintw(win, i + 1, 1, " %-*s ",
|
||||
list_w - 4, targets->names[idx]);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
wrefresh(win);
|
||||
|
||||
int ch = wgetch(win);
|
||||
switch (ch) {
|
||||
case KEY_UP:
|
||||
case 'k':
|
||||
if (cur > 0) {
|
||||
cur--;
|
||||
if (cur < scroll_offset) scroll_offset = cur;
|
||||
}
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
case 'j':
|
||||
if (cur < targets->count - 1) {
|
||||
cur++;
|
||||
if (cur >= scroll_offset + visible)
|
||||
scroll_offset = cur - visible + 1;
|
||||
}
|
||||
break;
|
||||
case '\n':
|
||||
case '\r':
|
||||
case KEY_ENTER:
|
||||
strncpy(selected, targets->names[cur], sel_size - 1);
|
||||
selected[sel_size - 1] = '\0';
|
||||
result = cur;
|
||||
done = 1;
|
||||
break;
|
||||
case 'q':
|
||||
case 27: /* ESC */
|
||||
done = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delwin(win);
|
||||
endwin();
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
* SECTION 2 — LOG BROWSER
|
||||
* ================================================================ */
|
||||
|
||||
#define MAX_LOGS 512
|
||||
|
||||
typedef struct {
|
||||
char name[256]; /* filename only */
|
||||
char path[768]; /* full path */
|
||||
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;
|
||||
if (lb->mtime < la->mtime) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_log_entries(const char *log_dir,
|
||||
LogEntry *entries, int max)
|
||||
{
|
||||
DIR *d = opendir(log_dir);
|
||||
if (!d) return 0;
|
||||
|
||||
int count = 0;
|
||||
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;
|
||||
|
||||
strncpy(entries[count].name, de->d_name,
|
||||
sizeof(entries[count].name) - 1);
|
||||
snprintf(entries[count].path, sizeof(entries[count].path),
|
||||
"%s/%s", log_dir, de->d_name);
|
||||
|
||||
struct stat st;
|
||||
if (stat(entries[count].path, &st) == 0)
|
||||
entries[count].mtime = st.st_mtime;
|
||||
else
|
||||
entries[count].mtime = 0;
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
|
||||
qsort(entries, count, sizeof(LogEntry), log_entry_cmp);
|
||||
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)
|
||||
{
|
||||
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++;
|
||||
total++;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
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;
|
||||
}
|
||||
free(buf);
|
||||
return have;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- browser */
|
||||
|
||||
void tui_log_browser(const char *log_dir)
|
||||
{
|
||||
LogEntry *entries = calloc(MAX_LOGS, sizeof(LogEntry));
|
||||
if (!entries) return;
|
||||
|
||||
int count = load_log_entries(log_dir, entries, MAX_LOGS);
|
||||
|
||||
initscr();
|
||||
noecho();
|
||||
cbreak();
|
||||
keypad(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
|
||||
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(3, COLOR_WHITE, -1); /* normal row */
|
||||
init_pair(4, COLOR_YELLOW,-1); /* preview text */
|
||||
init_pair(5, COLOR_RED, -1); /* status/warn */
|
||||
}
|
||||
|
||||
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 */
|
||||
|
||||
WINDOW *wleft = newwin(pane_h, left_w, 1, 0);
|
||||
WINDOW *wright = newwin(pane_h, right_w, 1, left_w + 1);
|
||||
|
||||
int cur = 0, scroll = 0;
|
||||
int done = 0;
|
||||
|
||||
while (!done) {
|
||||
getmaxyx(stdscr, rows, cols);
|
||||
pane_h = rows - 3;
|
||||
|
||||
/* ---- title bar ---- */
|
||||
attron(A_BOLD);
|
||||
mvprintw(0, 2, "gbuild log browser");
|
||||
attroff(A_BOLD);
|
||||
clrtoeol();
|
||||
|
||||
/* ---- hint bar ---- */
|
||||
if (has_colors()) attron(COLOR_PAIR(2));
|
||||
mvprintw(rows - 2, 0,
|
||||
" \u2191\u2193 navigate Enter open in less d delete q quit "
|
||||
" ");
|
||||
if (has_colors()) attroff(COLOR_PAIR(2));
|
||||
|
||||
/* ---- vertical divider ---- */
|
||||
for (int y = 1; y < rows - 2; y++)
|
||||
mvaddch(y, left_w, ACS_VLINE);
|
||||
|
||||
/* ---- 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;
|
||||
|
||||
if (count == 0) {
|
||||
mvwprintw(wleft, 2, 2, "(no logs found)");
|
||||
} 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, '.');
|
||||
if (dot) *dot = '\0';
|
||||
|
||||
if (idx == cur) {
|
||||
if (has_colors()) wattron(wleft, COLOR_PAIR(1) | A_BOLD);
|
||||
else wattron(wleft, A_REVERSE);
|
||||
mvwprintw(wleft, i + 1, 1, " %-*s ",
|
||||
left_w - 4, disp);
|
||||
if (has_colors()) wattroff(wleft, COLOR_PAIR(1) | A_BOLD);
|
||||
else wattroff(wleft, A_REVERSE);
|
||||
} else {
|
||||
mvwprintw(wleft, i + 1, 2, "%-*s",
|
||||
left_w - 4, disp);
|
||||
}
|
||||
}
|
||||
}
|
||||
wrefresh(wleft);
|
||||
|
||||
/* ---- 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",
|
||||
right_w - 2, right_w - 2, lines[i]);
|
||||
free(lines[i]);
|
||||
}
|
||||
}
|
||||
if (has_colors()) wattroff(wright, COLOR_PAIR(4));
|
||||
free(lines);
|
||||
}
|
||||
} else {
|
||||
mvwprintw(wright, 2, 2, "(no log selected)");
|
||||
}
|
||||
wrefresh(wright);
|
||||
refresh();
|
||||
|
||||
/* ---- 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 < count - 1) {
|
||||
cur++;
|
||||
if (cur >= scroll + visible)
|
||||
scroll = cur - visible + 1;
|
||||
}
|
||||
break;
|
||||
case '\n':
|
||||
case '\r':
|
||||
case KEY_ENTER:
|
||||
if (count > 0 && cur < count) {
|
||||
endwin();
|
||||
char cmd[1024];
|
||||
snprintf(cmd, sizeof(cmd),
|
||||
"less \"%s\"", entries[cur].path);
|
||||
system(cmd);
|
||||
/* re-init after less exits */
|
||||
initscr();
|
||||
noecho();
|
||||
cbreak();
|
||||
keypad(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
if (has_colors()) {
|
||||
start_color();
|
||||
use_default_colors();
|
||||
init_pair(1, COLOR_BLACK, COLOR_CYAN);
|
||||
init_pair(2, COLOR_CYAN, -1);
|
||||
init_pair(3, COLOR_WHITE, -1);
|
||||
init_pair(4, COLOR_YELLOW,-1);
|
||||
init_pair(5, COLOR_RED, -1);
|
||||
}
|
||||
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;
|
||||
}
|
||||
clear();
|
||||
}
|
||||
break;
|
||||
case 'q':
|
||||
case 27: /* ESC */
|
||||
done = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delwin(wleft);
|
||||
delwin(wright);
|
||||
endwin();
|
||||
free(entries);
|
||||
}
|
||||
Reference in New Issue
Block a user