diff --git a/.gitignore b/.gitignore index 9dfa06f..cb9a35f 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ dist/ # ── Go ──────────────────────────────────────────────────────────────────── vendor/ +.tools/ +.cache/ *.exe *.exe~ *.dll diff --git a/build-all.sh b/build-all.sh index 875900a..6d05e47 100755 --- a/build-all.sh +++ b/build-all.sh @@ -16,6 +16,92 @@ log() { echo -e "${GREEN}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } +MIN_GO_VERSION="1.25.0" +WAILS_VERSION="$(awk '$1 == "github.com/wailsapp/wails/v2" { print $2; exit }' go.mod)" +WAILS_BIN="" +WAILS_USE_GO_RUN=0 +BUILT_TARGETS=0 +SKIPPED_TARGETS=0 + +if [ -x "$SCRIPT_DIR/.tools/go/bin/go" ]; then + export PATH="$SCRIPT_DIR/.tools/go/bin:$PATH" +fi +export GOCACHE="${GOCACHE:-$SCRIPT_DIR/.cache/go-build}" +export GOPATH="${GOPATH:-$SCRIPT_DIR/.cache/go}" +mkdir -p "$GOCACHE" "$GOPATH" + +version_at_least() { + local have="${1#go}" + local want="$2" + local IFS=. + local have_parts want_parts + read -r -a have_parts <<< "$have" + read -r -a want_parts <<< "$want" + for i in 0 1 2; do + local h="${have_parts[$i]:-0}" + local w="${want_parts[$i]:-0}" + if (( h > w )); then return 0; fi + if (( h < w )); then return 1; fi + done + return 0 +} + +check_go() { + command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } + + local version + version="$(go env GOVERSION 2>/dev/null || go version | awk '{print $3}')" + if ! version_at_least "$version" "$MIN_GO_VERSION"; then + err "Go ${MIN_GO_VERSION}+ is required by go.mod; found ${version} at $(command -v go)" + err "Install a newer Go, or place it at .tools/go/bin/go for this project." + exit 1 + fi +} + +resolve_wails() { + if WAILS_BIN="$(command -v wails 2>/dev/null)"; then + WAILS_USE_GO_RUN=0 + return + fi + + local gopath + gopath="$(go env GOPATH 2>/dev/null || true)" + if [ -n "$gopath" ] && [ -x "$gopath/bin/wails" ]; then + WAILS_BIN="$gopath/bin/wails" + WAILS_USE_GO_RUN=0 + return + fi + + if [ -n "$WAILS_VERSION" ]; then + WAILS_BIN="github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION}" + WAILS_USE_GO_RUN=1 + warn "Wails CLI not found; using pinned ${WAILS_BIN} via go run" + return + fi + + err "Wails is not installed and no Wails version was found in go.mod" + exit 1 +} + +wails_cmd() { + if [ "$WAILS_USE_GO_RUN" -eq 1 ]; then + go run "$WAILS_BIN" "$@" + else + "$WAILS_BIN" "$@" + fi +} + +linux_deps_ok() { + command -v pkg-config >/dev/null 2>&1 || return 1 + pkg-config --exists gtk+-3.0 || return 1 + if ! pkg-config --exists webkit2gtk-4.1 && ! pkg-config --exists webkit2gtk-4.0; then + return 1 + fi + if ! pkg-config --exists ayatana-appindicator3-0.1 && ! pkg-config --exists appindicator3-0.1; then + return 1 + fi +} + build_tags() { if command -v pkg-config >/dev/null 2>&1 \ && pkg-config --exists webkit2gtk-4.1 \ @@ -42,22 +128,14 @@ build_ldflags() { } check_deps() { - command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } + check_go command -v node >/dev/null 2>&1 || { err "Node.js is not installed"; exit 1; } command -v npm >/dev/null 2>&1 || { err "npm is not installed"; exit 1; } - - if ! command -v wails >/dev/null 2>&1; then - if [ -f "$HOME/go/bin/wails" ]; then - export PATH="$PATH:$HOME/go/bin" - else - err "Wails is not installed. Run: go install github.com/wailsapp/wails/v2/cmd/wails@latest" - exit 1 - fi - fi + resolve_wails log "Go: $(go version)" log "Node: $(node --version)" - log "Wails: $(wails version 2>/dev/null | head -1)" + log "Wails: $(wails_cmd version 2>/dev/null | head -1)" } install_deps() { @@ -65,7 +143,11 @@ install_deps() { go mod download log "Installing frontend dependencies…" - cd frontend && npm install && cd .. + if [ -f frontend/package-lock.json ]; then + (cd frontend && npm ci) + else + (cd frontend && npm install) + fi } host_os() { @@ -76,6 +158,14 @@ host_os() { esac } +host_arch() { + case "$(uname -m)" in + x86_64|amd64) echo "amd64" ;; + arm64|aarch64) echo "arm64" ;; + *) echo "unknown" ;; + esac +} + pick_cc() { local target_os="$1" local target_arch="$2" @@ -105,7 +195,8 @@ build_target() { local target_os="$1" local target_arch="$2" local host="$3" - local version="$4" + local host_arch_value="$4" + local version="$5" local ldflags ldflags="$(build_ldflags "${version}")" local tags @@ -114,10 +205,17 @@ build_target() { local platform="${target_os}/${target_arch}" local cc="" - if [ "$target_os" != "$host" ]; then + if [ "$target_os" = "linux" ] && ! linux_deps_ok; then + warn "Skipping ${platform} (missing Linux GTK/WebKit build dependencies)" + ((SKIPPED_TARGETS += 1)) + return 0 + fi + + if [ "$target_os" != "$host" ] || [ "$target_arch" != "$host_arch_value" ]; then cc="$(pick_cc "$target_os" "$target_arch")" if [ -n "$cc" ] && ! command -v "$cc" >/dev/null 2>&1; then warn "Skipping ${platform} (missing C compiler: ${cc})" + ((SKIPPED_TARGETS += 1)) return 0 fi fi @@ -125,17 +223,18 @@ build_target() { log "Building ${platform}…" if [ -n "$cc" ]; then if [ -n "$tags" ]; then - CC="$cc" CGO_ENABLED=1 wails build -platform "$platform" -tags "$tags" -ldflags "$ldflags" + CC="$cc" CGO_ENABLED=1 wails_cmd build -platform "$platform" -tags "$tags" -ldflags "$ldflags" else - CC="$cc" CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" + CC="$cc" CGO_ENABLED=1 wails_cmd build -platform "$platform" -ldflags "$ldflags" fi else if [ -n "$tags" ]; then - CGO_ENABLED=1 wails build -platform "$platform" -tags "$tags" -ldflags "$ldflags" + CGO_ENABLED=1 wails_cmd build -platform "$platform" -tags "$tags" -ldflags "$ldflags" else - CGO_ENABLED=1 wails build -platform "$platform" -ldflags "$ldflags" + CGO_ENABLED=1 wails_cmd build -platform "$platform" -ldflags "$ldflags" fi fi + ((BUILT_TARGETS += 1)) } main() { @@ -147,24 +246,33 @@ main() { local host host="$(host_os)" - if [ "$host" = "unknown" ]; then - err "Unsupported host OS for this script. Use macOS or Linux." + local arch + arch="$(host_arch)" + if [ "$host" = "unknown" ] || [ "$arch" = "unknown" ]; then + err "Unsupported host platform: $(uname -s)/$(uname -m)" exit 1 fi - # Build host targets first - build_target "$host" "amd64" "$host" "$version" - build_target "$host" "arm64" "$host" "$version" + build_target "$host" "$arch" "$host" "$arch" "$version" # Cross-build Linux and Windows targets where toolchains are available if [ "$host" = "darwin" ] || [ "$host" = "linux" ]; then - build_target "linux" "amd64" "$host" "$version" - build_target "linux" "arm64" "$host" "$version" - build_target "windows" "amd64" "$host" "$version" - build_target "windows" "arm64" "$host" "$version" + for target in linux/amd64 linux/arm64 windows/amd64 windows/arm64; do + local target_os="${target%%/*}" + local target_arch="${target##*/}" + if [ "$target_os" = "$host" ] && [ "$target_arch" = "$arch" ]; then + continue + fi + build_target "$target_os" "$target_arch" "$host" "$arch" "$version" + done fi - log "Builds complete. Output: build/bin/" + if [ "$BUILT_TARGETS" -eq 0 ]; then + err "No targets were built; ${SKIPPED_TARGETS} target(s) were skipped because required system toolchains are missing." + exit 1 + fi + + log "Builds complete (${BUILT_TARGETS} built, ${SKIPPED_TARGETS} skipped). Output: build/bin/" } main "$@" diff --git a/build.sh b/build.sh index ac711d1..f2c316a 100755 --- a/build.sh +++ b/build.sh @@ -17,6 +17,106 @@ log() { echo -e "${GREEN}[nexus]${NC} $1"; } warn() { echo -e "${YELLOW}[nexus]${NC} $1"; } err() { echo -e "${RED}[nexus]${NC} $1" >&2; } +MIN_GO_VERSION="1.25.0" +WAILS_VERSION="$(awk '$1 == "github.com/wailsapp/wails/v2" { print $2; exit }' go.mod)" +WAILS_BIN="" +WAILS_USE_GO_RUN=0 + +if [ -x "$SCRIPT_DIR/.tools/go/bin/go" ]; then + export PATH="$SCRIPT_DIR/.tools/go/bin:$PATH" +fi +export GOCACHE="${GOCACHE:-$SCRIPT_DIR/.cache/go-build}" +export GOPATH="${GOPATH:-$SCRIPT_DIR/.cache/go}" +mkdir -p "$GOCACHE" "$GOPATH" + +version_at_least() { + local have="${1#go}" + local want="$2" + local IFS=. + local have_parts want_parts + read -r -a have_parts <<< "$have" + read -r -a want_parts <<< "$want" + for i in 0 1 2; do + local h="${have_parts[$i]:-0}" + local w="${want_parts[$i]:-0}" + if (( h > w )); then return 0; fi + if (( h < w )); then return 1; fi + done + return 0 +} + +check_go() { + command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } + + local version + version="$(go env GOVERSION 2>/dev/null || go version | awk '{print $3}')" + if ! version_at_least "$version" "$MIN_GO_VERSION"; then + err "Go ${MIN_GO_VERSION}+ is required by go.mod; found ${version} at $(command -v go)" + err "Install a newer Go, or place it at .tools/go/bin/go for this project." + exit 1 + fi +} + +resolve_wails() { + if WAILS_BIN="$(command -v wails 2>/dev/null)"; then + WAILS_USE_GO_RUN=0 + return + fi + + local gopath + gopath="$(go env GOPATH 2>/dev/null || true)" + if [ -n "$gopath" ] && [ -x "$gopath/bin/wails" ]; then + WAILS_BIN="$gopath/bin/wails" + WAILS_USE_GO_RUN=0 + return + fi + + if [ -n "$WAILS_VERSION" ]; then + WAILS_BIN="github.com/wailsapp/wails/v2/cmd/wails@${WAILS_VERSION}" + WAILS_USE_GO_RUN=1 + warn "Wails CLI not found; using pinned ${WAILS_BIN} via go run" + return + fi + + err "Wails is not installed and no Wails version was found in go.mod" + exit 1 +} + +wails_cmd() { + if [ "$WAILS_USE_GO_RUN" -eq 1 ]; then + go run "$WAILS_BIN" "$@" + else + "$WAILS_BIN" "$@" + fi +} + +check_linux_deps() { + if [ "$(uname -s)" != "Linux" ]; then + return + fi + + command -v pkg-config >/dev/null 2>&1 || { + err "pkg-config is required for Linux Wails builds." + err "On Ubuntu/Pop!_OS install: sudo apt-get install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev" + exit 1 + } + + local missing=() + pkg-config --exists gtk+-3.0 || missing+=("gtk+-3.0") + if ! pkg-config --exists webkit2gtk-4.1 && ! pkg-config --exists webkit2gtk-4.0; then + missing+=("webkit2gtk-4.1 or webkit2gtk-4.0") + fi + if ! pkg-config --exists ayatana-appindicator3-0.1 && ! pkg-config --exists appindicator3-0.1; then + missing+=("ayatana-appindicator3-0.1 or appindicator3-0.1") + fi + + if [ "${#missing[@]}" -gt 0 ]; then + err "Missing Linux build dependencies: ${missing[*]}" + err "On Ubuntu/Pop!_OS install: sudo apt-get install -y build-essential pkg-config libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev" + exit 1 + fi +} + build_tags() { if command -v pkg-config >/dev/null 2>&1 \ && pkg-config --exists webkit2gtk-4.1 \ @@ -44,23 +144,16 @@ build_ldflags() { # Check prerequisites check_deps() { - command -v go >/dev/null 2>&1 || { err "Go is not installed"; exit 1; } + check_go command -v node >/dev/null 2>&1 || { err "Node.js is not installed"; exit 1; } command -v npm >/dev/null 2>&1 || { err "npm is not installed"; exit 1; } - - if ! command -v wails >/dev/null 2>&1; then - if [ -f "$HOME/go/bin/wails" ]; then - export PATH="$PATH:$HOME/go/bin" - else - err "Wails is not installed. Run: go install github.com/wailsapp/wails/v2/cmd/wails@latest" - exit 1 - fi - fi + check_linux_deps + resolve_wails log "All prerequisites found" log "Go: $(go version)" log "Node: $(node --version)" - log "Wails: $(wails version 2>/dev/null | head -1)" + log "Wails: $(wails_cmd version 2>/dev/null | head -1)" } # Install dependencies @@ -69,7 +162,11 @@ install_deps() { go mod download log "Installing frontend dependencies…" - cd frontend && npm install && cd .. + if [ -f frontend/package-lock.json ]; then + (cd frontend && npm ci) + else + (cd frontend && npm install) + fi log "Dependencies installed" } @@ -79,7 +176,7 @@ dev() { check_deps install_deps log "Starting development mode…" - wails dev + wails_cmd dev } # Production build @@ -93,9 +190,9 @@ build() { tags="$(build_tags)" if [ -n "$tags" ]; then log "Using Wails build tags: ${tags}" - wails build -tags "$tags" -ldflags "$(build_ldflags "${version}")" + wails_cmd build -tags "$tags" -ldflags "$(build_ldflags "${version}")" else - wails build -ldflags "$(build_ldflags "${version}")" + wails_cmd build -ldflags "$(build_ldflags "${version}")" fi # Replace Wails-generated icon with custom app icon on macOS. @@ -113,6 +210,7 @@ build() { # Run tests run_tests() { + check_go log "Running tests…" go test ./... -v -count=1 } @@ -123,8 +221,9 @@ clean() { rm -rf build/bin rm -rf frontend/node_modules rm -rf frontend/src/js/lib - rm -rf frontend/src/css/lib - go clean -testcache + if command -v go >/dev/null 2>&1; then + go clean -testcache + fi log "Clean complete" } diff --git a/frontend/src/css/sidebar.css b/frontend/src/css/sidebar.css index 33e5ed1..afe5c0b 100644 --- a/frontend/src/css/sidebar.css +++ b/frontend/src/css/sidebar.css @@ -401,6 +401,52 @@ /* ----------------------------------------------------------------------- Sidebar empty list placeholder ----------------------------------------------------------------------- */ +.sidebar-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + margin: var(--space-6) var(--space-3) 0; + padding: var(--space-5) var(--space-3); + color: var(--text-tertiary); + text-align: center; +} + +.sidebar-empty-icon { + display: grid; + place-items: center; + width: 34px; + height: 34px; + border: 1px solid var(--border-default); + border-radius: var(--radius-lg); + background: var(--bg-elevated); + color: var(--accent); + font-size: var(--text-xl); + font-weight: var(--weight-semibold); + line-height: 1; +} + +.sidebar-empty-title { + color: var(--text-secondary); + font-size: var(--text-sm); + font-weight: var(--weight-semibold); + line-height: var(--leading-tight); +} + +.sidebar-empty-text { + max-width: 190px; + margin: 0; + color: var(--text-tertiary); + font-size: var(--text-xs); + line-height: var(--leading-normal); +} + +.sidebar-empty-actions { + display: flex; + justify-content: center; + margin-top: var(--space-1); +} + .sidebar-list-empty { padding: var(--space-3) var(--space-2); font-size: var(--text-xs); diff --git a/frontend/src/js/sidebar.js b/frontend/src/js/sidebar.js index 78c962e..cf76ab3 100644 --- a/frontend/src/js/sidebar.js +++ b/frontend/src/js/sidebar.js @@ -117,11 +117,7 @@ // Empty state if (profiles.length === 0) { - const empty = createElement("div", "sidebar-empty"); - empty.innerHTML = ` -
- `; - this.el.appendChild(empty); + this.el.appendChild(this._renderEmptyState()); } } @@ -162,6 +158,53 @@ // ── Rendering helpers ──────────────────────────────────────────── + /** + * Renders the sidebar empty state for first-run and no-search-results cases. + * @returns {HTMLElement} + * @private + */ + _renderEmptyState() { + const isSearching = this.searchQuery.length > 0; + const empty = createElement("div", "sidebar-empty"); + + const icon = createElement("div", "sidebar-empty-icon", { + textContent: isSearching ? "⌕" : "+", + "aria-hidden": "true", + }); + empty.appendChild(icon); + + empty.appendChild( + createElement("div", "sidebar-empty-title", { + textContent: isSearching ? "No matching profiles" : "No saved profiles", + }), + ); + + empty.appendChild( + createElement("p", "sidebar-empty-text", { + textContent: isSearching + ? "Try a different name, host, group, or protocol." + : "Create a profile for the servers and tools you use often.", + }), + ); + + if (!isSearching) { + const actions = createElement("div", "sidebar-empty-actions"); + const addButton = createElement("button", "btn btn-primary btn-sm", { + type: "button", + textContent: "New Profile", + }); + addButton.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.app.profileEditor.openNew(); + }); + actions.appendChild(addButton); + empty.appendChild(actions); + } + + return empty; + } + /** * Renders a collapsible section of profiles. * @param {string} title diff --git a/wails.json b/wails.json index 587d749..824ea28 100644 --- a/wails.json +++ b/wails.json @@ -2,7 +2,7 @@ "$schema": "https://wails.io/schemas/config.v2.json", "name": "nexus-terminal", "outputfilename": "NexusTerminal", - "frontend:install": "npm install", + "frontend:install": "npm ci", "frontend:build": "npm run build", "frontend:dev:watcher": "npm run dev", "frontend:dev:serverUrl": "auto",