diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b6b3aaa..c595039 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,10 +4,11 @@ ## Why - + ## Tested - [ ] `bash phpvm.sh` runs without errors -- [ ] `shellcheck phpvm.sh` passes +- [ ] `shellcheck -S warning phpvm.sh install.sh uninstall.sh shell/php-auto.bash shell/shim-php tests/test_cli.sh tests/test_gui.sh` passes +- [ ] `bash tests/test_cli.sh phpvm.sh` passes - [ ] Tested on Bash version: diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index 578e864..db905a4 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -36,6 +36,7 @@ jobs: install.sh \ uninstall.sh \ shell/php-auto.bash \ + shell/shim-php \ tests/test_cli.sh \ tests/test_gui.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b005397..445f4d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,6 +21,7 @@ jobs: missing=0 for f in phpvm.sh phpvm-gui.py install.sh uninstall.sh \ shell/php-auto.bash shell/php-auto.zsh shell/php-auto.fish \ + shell/shim-php \ assets/phpvm.svg CHANGELOG.md; do if [[ ! -f "$f" ]]; then echo "::error::Missing file: $f" @@ -47,6 +48,7 @@ jobs: bash -n install.sh bash -n uninstall.sh bash -n shell/php-auto.bash + sh -n shell/shim-php - name: Extract changelog section for this tag id: notes diff --git a/CHANGELOG.md b/CHANGELOG.md index 462d593..a636b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,47 @@ All notable changes to phpvm. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning is [SemVer](https://semver.org/). +## [2.5.0] - 2026-05-28 + +### Added + +- Per-shell switching, now the default. `phpvm shell ` switches PHP for the current terminal only, with no sudo, + so two terminals can run two versions at once. It follows the rbenv / pyenv / asdf model: a `php` shim on `PATH` + (installed to `/shims/php`) reads `PHPVM_SHELL_VERSION` and execs the matching `/usr/bin/phpX.Y`, falling + back to the global symlink. `phpvm shell --unset` drops the pin. +- `phpvm local ` and `phpvm global ` as the project and system-wide verbs. `local` writes `.php-version` + (no sudo); `global` moves the `update-alternatives` symlink (sudo). The old `--set` and `--set-project` flags stay as + aliases, so existing usage and scripts keep working. +- A `php` shim template (`shell/shim-php`) plus a `phpvm()` shell wrapper in each hook. The wrapper routes `shell` and + the bare TUI through `eval` (fish uses `| source`) so they can change the current shell; everything else calls the + binary directly. +- Resolution is now three layers: shell pin (`PHPVM_SHELL_VERSION`), then project (`PHPVM_AUTO_VERSION`, set by the + cd-hook from `.php-version` / `composer.json`), then the global symlink. An explicit shell pin always wins, so it is + never overridden by a later `cd`. + +### Changed + +- The cd-hook no longer runs a sudo global switch. It now resolves the project version with `phpvm --auto --print` and + exports `PHPVM_AUTO_VERSION` (or unsets it on leaving), which the shim reads. The everyday path is sudo-free. +- The TUI now pins the current shell on Enter when launched through the wrapper (drawing to the terminal while emitting + the assignment on stdout, the way fzf does), with `g` for a global switch and `p` for the project. Run without the + wrapper, Enter falls back to a global switch and notes how to enable per-shell pinning. +- `phpvm --current` reports the shell pin, project, and global layers separately, plus the effective version. `--doctor` + gains a "Per-shell switching" section that checks the shim and whether the shim dir is on `PATH`. +- `install.sh` installs the shim, enables the shell hook by default (the everyday behavior depends on it), and reframes + the sudoers prompt as needed only for `phpvm global`. +- The GUI is documented as global by nature: the tray reflects and sets the system default, and a shell pinned with + `phpvm shell` can legitimately sit above it. + +### Fixed + +- `phpvm install` no longer risks hanging on an unattended `--yes` run. apt is now invoked as + `sudo env DEBIAN_FRONTEND=noninteractive apt-get ...`, so a package postinst (e.g. tzdata) can't block on an + interactive debconf prompt. `sudo` resets the environment, which is why the frontend is set through `env` rather than + an inline assignment; apt stays password-gated exactly as before. + +--- + ## [2.4.0] - 2026-05-27 ### Added @@ -30,15 +71,15 @@ is [SemVer](https://semver.org/). ### Changed - Repository moved to `github.com/rijverse/phpvm` (previously `rijoanul-shanto/phpvm`). The `PHPVM_REMOTE` default in - `install.sh` — used by the remote bootstrap clone and by the `--self-update` fallback when no URL was recorded at - install time — now points at the new location, along with every repo link in `README.md`, `CONTRIBUTING.md`, + `install.sh` (used by the remote bootstrap clone and by the `--self-update` fallback when no URL was recorded at + install time) now points at the new location, along with every repo link in `README.md`, `CONTRIBUTING.md`, `index.html`, and `social-preview.html`. ### Added -- Project landing page (`index.html`) — a feature showcase of the taskbar indicator, tray menu, GUI window, and TUI +- Project landing page (`index.html`): a feature showcase of the taskbar indicator, tray menu, GUI window, and TUI picker, each with an annotated screenshot. -- Social preview — `social-preview.html` rendered to `assets/showcase/social-preview.png` (1280×640) for the GitHub repo +- Social preview: `social-preview.html` rendered to `assets/showcase/social-preview.png` (1280×640) for the GitHub repo preview, plus Open Graph / Twitter Card meta tags on the landing page and a `rijverse` workspace badge linking to the organization. @@ -51,13 +92,13 @@ is [SemVer](https://semver.org/). - `install.sh` prompts were silently skipped under `curl … | sudo bash` because piping replaces stdin with the pipe, making `[[ -t 0 ]]` return false even when a real terminal is attached. All interactivity checks now use `{ true < /dev/tty; } 2>/dev/null` to detect a controlling terminal instead of testing stdin, and all `read` calls - redirect from `/dev/tty` directly. The one-line installer is now fully interactive — same prompts as running + redirect from `/dev/tty` directly. The one-line installer is now fully interactive, same prompts as running `bash install.sh` locally. Truly headless environments (CI, `nohup`, no controlling tty) still fall back to defaults. ### Changed - README: corrected the installer interactivity note to reflect `/dev/tty`-based detection. -- README: added `## Uninstalling` section — remote one-liner (`curl … | sudo bash`), local clone form, itemized list of +- README: added `## Uninstalling` section with a remote one-liner (`curl … | sudo bash`), local clone form, itemized list of what gets removed (binaries, hook dir, sudoers rule, desktop/autostart entries, icons, shell RC lines), RC backup behaviour, and the sudo-user note. @@ -67,22 +108,22 @@ is [SemVer](https://semver.org/). ### Added -- One-line remote installer — `install.sh` now self-bootstraps. When invoked without sibling repo files (e.g. +- One-line remote installer: `install.sh` now self-bootstraps. When invoked without sibling repo files (e.g. `curl -fsSL …/install.sh | sudo bash`), it git-clones the repo into a `mktemp -d`, retargets `SCRIPT_DIR` at the clone, and continues in the same process so the EXIT trap removes the tmp dir on exit (no `exec`, no orphaned clone). `PHPVM_REMOTE` and `PHPVM_REF` env vars override the default repo URL and ref (`main`); falls back to a default-branch clone + `git fetch origin && checkout FETCH_HEAD` when `--branch ` doesn't match a branch (so tags/SHAs work). Hard-fails with a clear message when `git` is missing. -- `phpvm --doctor` — full diagnostic that checks CLI install, PHP runtimes, composer, PHP-FPM units, sudoers rule, shell +- `phpvm --doctor`: full diagnostic that checks CLI install, PHP runtimes, composer, PHP-FPM units, sudoers rule, shell hook wiring, GUI/tray deps (python3-gi / GTK 3 / Ayatana or legacy AppIndicator3 / icon / `.desktop` entry / autostart / running process), and project detection. Counts pass / warn / fail and exits non-zero on any fail. - `install.sh` now offers to enable autostart on login. Writes `~/.config/autostart/phpvm-gui.desktop` and, under sudo, drops it into the invoking user's `$HOME` (resolved via `getent passwd`) with correct ownership. Upgrade mode refreshes the file in place if it already exists. -- CI compatibility matrix (`.github/workflows/compat.yml`) — CLI and GUI jobs build on `ubuntu:20.04 / 22.04 / 24.04` +- CI compatibility matrix (`.github/workflows/compat.yml`): CLI and GUI jobs build on `ubuntu:20.04 / 22.04 / 24.04` containers. Runs shellcheck (`-S warning`), CLI smoke tests, and a GUI import + xvfb `--help` smoke test on every push and PR touching `phpvm.sh`, `phpvm-gui.py`, `install.sh`, `uninstall.sh`, `shell/**`, or `tests/**`. -- `tests/test_cli.sh`, `tests/test_gui.sh`, `tests/local-compat.sh` — smoke tests for CLI flags, GUI imports, and a +- `tests/test_cli.sh`, `tests/test_gui.sh`, `tests/local-compat.sh`: smoke tests for CLI flags, GUI imports, and a Docker-driven local matrix runner. ### Changed @@ -90,7 +131,7 @@ is [SemVer](https://semver.org/). - README overhaul: centered logo + GUI screenshots (`assets/gui-window.png`, `assets/gui-tray-menu.png`, `assets/tui.png`), expanded `--doctor` row in the CLI table, new `--auto --print [dir]` row, "Things it won't do" limitations section, and explicit `Bash 4.3+` requirement (badge + "What you need"). -- Installer + GUI visual presentation polished — new box-drawing styles, clearer status labels in the GTK window, +- Installer + GUI visual presentation polished: new box-drawing styles, clearer status labels in the GTK window, refactored icon-install feedback. **Restart FPM** button now sits to the left of **Switch** in the row so the destructive-looking action isn't the primary target. - `install.sh` autostart heredoc deduplicated into a single `AUTOSTART_CONTENT` template; the root and non-root branches @@ -99,37 +140,37 @@ is [SemVer](https://semver.org/). install paths, matching the bash hook. - `phpvm-gui.py` docstring clarifies that **Ayatana** AppIndicator3 is preferred and legacy AppIndicator3 is accepted as a fallback. -- `tests/local-compat.sh` aligned with CI — Ubuntu 18.04 dropped from the local matrix (CI never tested it; README only +- `tests/local-compat.sh` aligned with CI; Ubuntu 18.04 dropped from the local matrix (CI never tested it; README only claims 20/22/24). -- `CONTRIBUTING.md` — real repo URL, Bash target tightened to `4.3+` (`local -n` is required), matching `phpvm.sh`'s +- `CONTRIBUTING.md`: real repo URL, Bash target tightened to `4.3+` (`local -n` is required), matching `phpvm.sh`'s guard. -- `.github/workflows/release.yml` — every step now earns its keep. Added a repo-integrity pre-check that fails the tag +- `.github/workflows/release.yml`: every step now earns its keep. Added a repo-integrity pre-check that fails the tag if any shipped file is missing (including `assets/phpvm.svg`, which the installer needs but the previous workflow never verified). Added a `bash -n` syntax gate across `phpvm.sh`, `install.sh`, `uninstall.sh`, and `shell/php-auto.bash`. Every `run:` block now uses `set -euo pipefail` so the changelog `awk` pipeline (and friends) can't silently produce empty output. `actions/checkout` and `softprops/action-gh-release` are pinned to commit SHAs with version comments for supply-chain hardening. Dropped the `shellcheck … || true` step (lint that always passes is - theater — lint lives in `compat.yml` now). Dropped the `files:` upload list and `fetch-depth: 0` — the installer and + theater; lint lives in `compat.yml` now). Dropped the `files:` upload list and `fetch-depth: 0`; the installer and `phpvm --self-update` both bootstrap via `git clone`, never via release artifacts, so the per-file uploads were decorative; GitHub's auto-attached source tarball still covers the "I want a versioned download" case. -- `.github/workflows/compat.yml` — `shellcheck` is now a real gate. Removed the `|| true` that silently swallowed every +- `.github/workflows/compat.yml`: `shellcheck` is now a real gate. Removed the `|| true` that silently swallowed every warning, hoisted lint into a dedicated `lint` job so it runs once instead of three times per matrix OS, and expanded the lint scope to include `tests/test_cli.sh` and `tests/test_gui.sh`. Split the GUI dependency install into a - required step (python3, python3-gi, GTK 3, xvfb, libglib2.0-0 — fails fast) and an optional AppIndicator step ( + required step (python3, python3-gi, GTK 3, xvfb, libglib2.0-0, fails fast) and an optional AppIndicator step ( Ayatana → legacy → `::warning::`), removing the blanket `|| true` that was masking missing-python3 failures. Pinned `actions/checkout` to a commit SHA and added `set -euo pipefail` to every script block. ### Fixed -- `phpvm.sh` header comment said `v2.1.0` while `VERSION="2.2.0"` — header bumped to v2.2.0. +- `phpvm.sh` header comment said `v2.1.0` while `VERSION="2.2.0"`; header bumped to v2.2.0. - `tests/test_cli.sh` was exercising non-existent subcommands (`list`, `current`, `use`) that the CLI never accepted; tests only passed because unknown commands return non-zero. Rewritten against the real flags (`--list`, `--current`, `--set`), with a regression test that asserts unknown positional `use` is rejected with `Unknown option`. -- `uninstall.sh` under `sudo` only cleaned the invoking user's autostart, desktop, and icon files — it left +- `uninstall.sh` under `sudo` only cleaned the invoking user's autostart, desktop, and icon files; it left `~/.local/bin/phpvm{,-gui}`, the `~/.phpvm` hook directory, and the user's shell rc lines untouched. `SUDO_HOME` now propagates to `BIN_DIRS`, `HOOK_DIRS`, and the rc-cleanup loop. - `set_project_tui` wrote `.php-version` without normalizing the version string or warning when an existing file held a - different value — diverged from `cmd_set_project`. TUI now normalizes via `normalize_version` and prints an overwrite + different value, which diverged from `cmd_set_project`. TUI now normalizes via `normalize_version` and prints an overwrite warning before the confirm prompt. - README CLI table missed `phpvm --auto --print [dir]` and undersold `--doctor` ("install location, sudoers rule, and shell-hook setup") versus its actual scope. @@ -142,7 +183,7 @@ is [SemVer](https://semver.org/). - `phpvm-gui` now falls back to `pkexec` (polkit graphical auth dialog) when passwordless sudo isn't configured. Switch / Restart FPM no longer silently no-op for users without the sudoers rule. -- Inline status label in the GTK window — switch and restart-fpm results render in the window itself (green/red), +- Inline status label in the GTK window: switch and restart-fpm results render in the window itself (green/red), replacing the desktop-notification round-trip. - `uninstall.sh` stops any running `phpvm-gui` (via `pkill -x`) before removing files. Avoids the "file in use" / stale tray icon after uninstall. @@ -153,7 +194,7 @@ is [SemVer](https://semver.org/). - Sudo prompts everywhere now carry a labeled `-p` string (`[phpvm] switching PHP — password for %u:`, `[phpvm] restarting phpX.Y-fpm — password for %u:`) so users see who's asking when no nopasswd rule is set. - Removed `sudo -n` quiet path and the rc=77 "password required" signaling from `do_switch` + `cmd_auto`. Shell-hook - auto-switch is now plain `sudo` — passwordless if sudoers is configured, interactive prompt otherwise. Net: 60+ lines + auto-switch is now plain `sudo`; passwordless if sudoers is configured, interactive prompt otherwise. Net: 60+ lines deleted from `phpvm.sh` and `phpvm-gui.py`. - `cmd_auto` quiet mode prints terse stdout (`phpvm: switched to PHP X.Y`) instead of dispatching `notify-send`. GUI handles its own notifications via the inline status label. @@ -164,27 +205,27 @@ is [SemVer](https://semver.org/). ### Added -- `phpvm --auto --print [dir]` — print resolved project PHP version without switching. Used by `phpvm-gui` so the GUI +- `phpvm --auto --print [dir]`: print resolved project PHP version without switching. Used by `phpvm-gui` so the GUI and CLI share one constraint solver. -- `phpvm-gui --foreground` / `-F` — keep the GUI attached to the terminal (errors visible, useful for debugging). +- `phpvm-gui --foreground` / `-F`: keep the GUI attached to the terminal (errors visible, useful for debugging). - `phpvm-gui` now double-forks on launch so the calling shell returns immediately and the GUI survives terminal close. `.desktop` launchers and `phpvm --window` benefit too. ### Changed - Auto-switch from shell hooks (`phpvm --auto --quiet`) now uses `sudo -n`. Without the nopasswd rule the hook no longer - hangs on a silent password prompt — it sends a labeled desktop notification telling you what's asking and how to fix + hangs on a silent password prompt; it sends a labeled desktop notification telling you what's asking and how to fix it. - `do_switch` failures return rc=77 when password is required; cmd_auto branches on this to show a contextual notification instead of a generic "failed to switch". -- Sudoers glob tightened from `/usr/bin/php*` to `/usr/bin/php[0-9].[0-9]` — the old glob also matched `phpunit`, +- Sudoers glob tightened from `/usr/bin/php*` to `/usr/bin/php[0-9].[0-9]`; the old glob also matched `phpunit`, `php-config`, etc. - `install.sh --upgrade` detects the old `php*` glob and rewrites the sudoers file to the tighter pattern. - `phpvm-gui` REFRESH_MS bumped 5s → 15s and per-version SAPI/xdebug/ini lookups are now memoized per session (cleared on switch). Was forking PHP for every installed version every 5 seconds. - `phpvm-gui` composer detection now shells out to `phpvm --auto --print` first so behavior matches the shell side exactly (supports `^`, `~`, ranges, `|`). -- `install.sh` no longer prompts when stdin isn't a tty (defaults to CLI+GUI, skips sudoers/hook prompts) — works under +- `install.sh` no longer prompts when stdin isn't a tty (defaults to CLI+GUI, skips sudoers/hook prompts); works under `curl … | sudo bash`. - `uninstall.sh` cleans both `/usr/local/bin`/`/etc/phpvm` AND `~/.local/bin`/`~/.phpvm` instead of either/or. - `install.sh` rewrites `git@host:owner/repo` remote URLs to `https://host/owner/repo` when recording REPO_URL, so @@ -192,7 +233,7 @@ is [SemVer](https://semver.org/). ### Fixed -- `do_switch` no longer swallows `update-alternatives` stderr — failure messages reach the user. +- `do_switch` no longer swallows `update-alternatives` stderr; failure messages reach the user. - `.php-version` parsing now normalizes `php8.2`, `8.2.0`, leading/trailing whitespace to `X.Y`. Was a silent miss before. - `phpvm --set-project` validates input and prompts before overwriting an existing `.php-version` with a different diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 239592d..f56ffed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Patches welcome. The core is intentionally simple — a bash script and a Python tray app, no build step. +Patches welcome. The core is intentionally simple: a bash script and a Python tray app, no build step. ## Setup @@ -29,7 +29,7 @@ Open an issue and include: - OS and bash version (`bash --version`) - Your registered PHP versions (`update-alternatives --list php`) - What you expected vs. what happened -- Terminal emulator — some TUI rendering quirks are terminal-specific +- Terminal emulator (some TUI rendering quirks are terminal-specific) ## Pull requests diff --git a/README.md b/README.md index 1737f8d..6eaaf12 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # phpvm -A small PHP version switcher for Linux. TUI in the terminal, optional system tray app, and a `cd`-hook that picks the right PHP for the project you just stepped into. +A small PHP version manager for Linux. TUI in the terminal, optional system tray app, per-shell switching, and a `cd`-hook that picks the right PHP for the project you just stepped into. If you've been juggling `update-alternatives --set php` by hand every time you switch between a Laravel 9 app on 8.1 and a fresh Symfony repo on 8.3, this is for you. @@ -26,9 +26,11 @@ If you've been juggling `update-alternatives --set php` by hand every time you s - A tray icon (and a separate GTK window if you'd rather not live in the panel) with per-version badges: which SAPIs are available, whether xdebug is loaded, whether FPM is running, whether the version is EOL. - `.php-version` (or `composer.json`'s `require.php`) drives a per-project version. Walks up the tree like `nvm` does. - A `cd`-hook for bash / zsh / fish that runs `phpvm --auto` so the right PHP is loaded by the time the prompt comes back. +- Per-shell switching: `phpvm shell 8.2` pins a version for the current terminal only via a `php` shim on `PATH`. Two terminals can run two PHP versions at once, no sudo. +- `phpvm install ` adds a new PHP version straight from the upstream repo (Ondřej Surý's PPA on Ubuntu, deb.sury.org on Debian) without hand-running `apt install`. - An installer that asks the obvious questions (CLI? GUI? wire up the shell hook? passwordless sudo?) and an uninstaller that backs up your shell rc before touching it. -Under the hood it's just `update-alternatives --set php`. Nothing exotic. The whole point is that you stop typing that command. +Global switches use `update-alternatives --set php`, installs drive the upstream PPA, and per-shell pins use a tiny `php` shim on your `PATH`. Nothing exotic. ## Installing @@ -77,7 +79,7 @@ phpvm --self-update https://github.com/rijverse/phpvm.git v2.2.0 - For the GUI: `python3-gi`, GTK3, AppIndicator3. The install command is in the GUI section below. ## CLI -Keyboard-driven picker right where you live. / to move, Enter to switch, p to pin as the project version, q to bail. +Keyboard-driven picker right where you live. / to move, Enter to pin the current shell, g for a system-wide switch, p to pin the project, q to bail. phpvm TUI @@ -85,22 +87,24 @@ Keyboard-driven picker right where you live. / to mo |---|---| | `phpvm` | Opens the TUI | | `phpvm --list` | Lists installed PHP versions | -| `phpvm --current` | Prints whichever one is active | -| `phpvm --set 8.2` | Switches globally to 8.2 | +| `phpvm --current` | Shows the effective version plus the shell / project / global breakdown | +| `phpvm shell 8.2` | Switches **this terminal only**, no sudo (see [Per-shell switching](#per-shell-switching)) | +| `phpvm shell --unset` | Drops the per-shell pin | +| `phpvm local 8.2` | Pins the project: writes `.php-version`, no sudo | +| `phpvm global 8.2` | Switches the system default via `update-alternatives` (sudo) | | `phpvm install 8.3` | Installs PHP 8.3 from Ondřej Surý's repo (see [Installing PHP versions](#installing-php-versions)) | | `phpvm --auto` | Reads `.php-version` / `composer.json` and switches | | `phpvm --auto --print [dir]` | Prints the resolved project PHP version without switching | -| `phpvm --set-project 8.2` | Writes `.php-version` here | -| `phpvm --enable-hook [shell]` | Adds the auto-switch hook to your rc | +| `phpvm --enable-hook [shell]` | Adds the shell hook + shim to your rc | | `phpvm --disable-hook [shell]` | Removes it (rc is backed up first) | | `phpvm --window` | Launches the GTK picker window, then frees the terminal | | `phpvm-gui` | Tray applet (see [The GUI](#the-gui)) | | `phpvm-gui --window` | Standalone GTK picker window, no tray | | `phpvm --self-update` | Re-runs the installer against the latest commit | -| `phpvm --doctor` | Full diagnostic: CLI install, PHP runtimes, FPM, sudoers, shell hook, GUI, project | +| `phpvm --doctor` | Full diagnostic: CLI install, PHP runtimes, FPM, sudoers, shell hook, shim, GUI, project | | `phpvm --help` | Everything else | -Vim users get k/j too. +`--set` is kept as an alias for `global`, and `--set-project` for `local`, so old muscle memory and scripts keep working. Vim users get k/j too. ## Installing PHP versions @@ -124,13 +128,38 @@ Versions are `X.Y` (or `latest`); patch levels like `8.2.13` are rejected. `apt` Other distros aren't supported: install PHP with your own package manager and phpvm will pick it up via `update-alternatives`. +## Per-shell switching + +This is the everyday switch, and it's the default. `phpvm shell 8.2` changes PHP for **the current terminal only**, with no sudo and no effect on any other shell: + +```bash +phpvm shell 8.2 # this terminal is now on 8.2 +phpvm shell 8.3 # ...and this one on 8.3, at the same time +phpvm shell --unset # back to the project / global default +``` + +Two terminals can run two PHP versions at once. It works the way rbenv, pyenv, and asdf do: a tiny `php` shim on your `PATH` reads a `PHPVM_SHELL_VERSION` env var and execs the matching `/usr/bin/phpX.Y`. The shim and the `phpvm()` shell wrapper come from the shell hook, so this needs the hook enabled (the installer does that by default; otherwise run `phpvm --enable-hook`). + +Resolution order, highest priority first: + +1. **shell** pin from `phpvm shell` (`PHPVM_SHELL_VERSION`), sticky until you `--unset` +2. **project** version from `.php-version` / `composer.json`, re-evaluated on every `cd` +3. **global** default from `update-alternatives` (`/usr/bin/php`) + +`phpvm --current` prints all three plus the effective one. A shell pin always wins, so an explicit `phpvm shell` is never silently overridden when you change directories. + +When to reach for the others: + +- `phpvm global ` (sudo): the system-wide default. This is what cron, systemd, other users, and PHP-FPM see, since none of them load your shell hook. Still aliased as `phpvm --set`. +- `phpvm local `: writes `.php-version` so the whole project gets that version automatically. Aliased as `phpvm --set-project`. + ## The GUI -A tray indicator sits in your panel showing whichever PHP is active. It updates live as `phpvm --auto` fires on `cd`, so the panel and your shell never disagree. +A tray indicator sits in your panel showing the **system-wide (global)** PHP. A tray app isn't attached to a terminal, so it works at the global level: clicking a version runs the same switch as `phpvm global`. The tray reflects the global default, and a terminal pinned with `phpvm shell` can sit above it, so the tray and a given shell may legitimately differ. phpvm tray indicator showing PHP 8.4 active -Click it and you get a menu for one-shot switching: +Click it and you get a menu for a one-click global switch: phpvm tray menu @@ -160,7 +189,7 @@ About FPM restart: it tries passwordless `sudo` first, and if that fails it pops ```bash echo "8.1" > .php-version # or -phpvm --set-project 8.1 +phpvm local 8.1 ``` phpvm walks up the directory tree looking for `.php-version`. If there isn't one, it reads `require.php` from `composer.json` and picks the highest installed version that satisfies the constraint. Caret, tilde, ranges, `|` unions, all the constraint syntaxes Composer accepts. @@ -195,9 +224,9 @@ source /etc/phpvm/php-auto.fish # or ~/.phpvm/php-auto.fish ## About sudo -Every switch ends up running `sudo update-alternatives --set php ...` +Only the **global** switch needs sudo. `phpvm global` (and its `--set` alias) moves the system-wide `/usr/bin/php` symlink via `sudo update-alternatives --set php ...`. Per-shell (`phpvm shell`) and per-project (`phpvm local`) switching touch only your own environment, so they never ask for a password. -By default that means a password prompt. The installer offers to drop a sudoers rule so you don't get one: +For the global switch, the installer offers to drop a sudoers rule so you don't get a prompt: ``` # /etc/sudoers.d/phpvm @@ -206,7 +235,7 @@ username ALL=(ALL) NOPASSWD: /usr/bin/update-alternatives --set php /usr/bin/php The glob is intentionally narrow, it matches `php8.2` but not `phpunit` or `php-config`. -If you skip the sudoers rule, the CLI just asks for a password the normal way (and labels the prompt so you know who's asking). The GUI tries passwordless sudo first, then falls back to the polkit dialog. +If you skip the sudoers rule, `phpvm global` just asks for a password the normal way (and labels the prompt so you know who's asking). The GUI, which is global by nature, tries passwordless sudo first, then falls back to the polkit dialog.
If phpvm reports no versions installed @@ -233,7 +262,8 @@ phpvm/ ├── shell/ │ ├── php-auto.bash │ ├── php-auto.zsh -│ └── php-auto.fish +│ ├── php-auto.fish +│ └── shim-php php resolver, installed to /shims/php ├── install.sh └── uninstall.sh ``` @@ -269,7 +299,7 @@ Shell RCs are backed up as `.phpvm-backup` before any edits. Running under A few things phpvm doesn't handle yet. Some are on the [Roadmap](#roadmap), some are out of scope for now. -- **Per-shell switching**: switches are currently system-wide via `update-alternatives`, so two shells on two versions at once isn't supported yet. A shim-based `phpvm shell ` is on the roadmap. +- **Per-shell pins are shell-only**: `phpvm shell` lives in your interactive shell's environment, so it's invisible to cron, systemd, other users, non-interactive scripts, and the GUI. Those all follow the global default, which is what `phpvm global` is for. - **Distros without `update-alternatives`**: Arch, Fedora, RHEL, openSUSE aren't supported. Adding a backend is welcome as a contribution. - **Web server config**: Apache/Nginx still point at whatever socket or module you wired up. FPM restart is per-version and assumes `systemctl restart phpX.Y-fpm` style unit names. - **Patch-level pinning**: everything is `X.Y`. If you need `8.2.13` exactly, you'll want a different tool. @@ -280,7 +310,7 @@ A few things phpvm doesn't handle yet. Some are on the [Roadmap](#roadmap), some Roughly in priority order. The top two are planned in detail in [ROADMAP.md](ROADMAP.md). Open an issue if you want to push one up the stack or claim one. - [x] **`phpvm install `** (shipped in v2.4.0): drives Ondřej Surý's PPA (Ubuntu) or Surý repo (Debian) under the hood so you don't have to `apt install` by hand. `phpvm install 8.3`, `phpvm install latest`. See [Installing PHP versions](#installing-php-versions). -- [ ] **Per-shell switching, as the new default** (planned, v2.5.0): `phpvm shell 8.2` flips PHP for the current terminal only, via a `~/.phpvm/shims/php` shim on `$PATH`. Two shells on two versions at once, no sudo. Global switching stays available as `phpvm global` (today's `--set`). +- [x] **Per-shell switching, as the new default** (shipped in v2.5.0): `phpvm shell 8.2` flips PHP for the current terminal only, via a shim on `$PATH`. Two shells on two versions at once, no sudo. Global switching stays available as `phpvm global` (and the `--set` alias). See [Per-shell switching](#per-shell-switching). - [ ] **Extension manager**: `phpvm ext install xdebug redis imagick` per version, with the matching `php-` packages and ini wiring. None of the existing PHP version managers do this well. - [ ] **`phpvm exec `**: run a one-off in a specific version without switching, like `nvm exec`. Handy for CI and quick sanity checks. - [ ] **Shell completion**: bash/zsh/fish completion for `shell`, `global`, `install`, etc. so `phpvm global ` lists installed versions. diff --git a/ROADMAP.md b/ROADMAP.md index e2b079b..bd04a44 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,7 +7,7 @@ idea-stage notes so the whole picture lives in one place. ## Status overview - [x] **1. `phpvm install `** (shipped, v2.4.0) -- [ ] **2. Per-shell switching, as the new default** (planned, v2.5.0) +- [x] **2. Per-shell switching, as the new default** (shipped, v2.5.0) - [ ] **3. Extension manager** (idea) - [ ] **4. `phpvm exec `** (idea) - [ ] **5. Shell completion** (idea) @@ -76,9 +76,12 @@ Reads `/etc/os-release`: --- -## 2. Per-shell switching, as the new default (v2.5.0) +## 2. Per-shell switching, as the new default (v2.5.0) - shipped -The architectural inversion. Gets its own release. +The architectural inversion. Shipped in v2.5.0; the spec below is what landed. +The shim lives at `shell/shim-php` (installed to `/shims/php`), the +`phpvm()` wrapper and cd-hook rewrite are in `shell/php-auto.{bash,zsh,fish}`, +and the verbs (`shell`, `sh-shell`, `local`, `global`) are in `phpvm.sh`. ### Why invert the default diff --git a/index.html b/index.html index 56a0f62..dec69d9 100644 --- a/index.html +++ b/index.html @@ -4,19 +4,19 @@ phpvm - + - - + + - - + + @@ -437,7 +437,7 @@

Four feature spotlights

phpvm
phpvm
-
// a small PHP version switcher for linux
+
// a small PHP version manager for linux
tray indicator @@ -463,8 +463,8 @@

Four feature spotlights

- tray indicator reflects phpvm --auto on every cd - no shell reload required + tray indicator reflects the system-wide (global) PHP + updates when you run phpvm global
@@ -537,7 +537,7 @@

Know which PHP is actually running

Switch PHP from the panel. Without leaving your work.

launch tray
-
$ phpvm-gui --tray
+
$ phpvm-gui
@@ -616,7 +616,7 @@

Every PHP, every status, at a glance.

04 Terminal UI - Keyboard-driven picker, right where you live. + Keyboard-driven picker. Enter pins the current shell.
-
↑/↓ navigate · enter to switch
+
↑/↓ navigate · enter pins this shell
- p pin as project version · q bail - uses update-alternatives under the hood + g global · p project · q quit + a php shim on your PATH, no sudo
-

A picker that lives in your terminal.

+

Switch PHP for just this terminal.

just run it
$ phpvm
diff --git a/install.sh b/install.sh index e1dda67..fc6150f 100755 --- a/install.sh +++ b/install.sh @@ -250,11 +250,19 @@ cp "$SCRIPT_DIR/shell/php-auto.zsh" "$HOOK_DIR/" cp "$SCRIPT_DIR/shell/php-auto.fish" "$HOOK_DIR/" success "Shell hooks installed" +# shim: the `php` resolver that makes per-shell / per-project switching work. +# Lives under HOOK_DIR (which the hook prepends to PATH, and uninstall removes). +mkdir -p "$HOOK_DIR/shims" +cp "$SCRIPT_DIR/shell/shim-php" "$HOOK_DIR/shims/php" +chmod +x "$HOOK_DIR/shims/php" +success "Shim installed at ${CYAN}${HOOK_DIR}/shims/php${NC}" + # passwordless sudo echo "" -echo -e " ${BOLD}Passwordless sudo for auto-switching${NC}" -echo -e " ${DIM}Without this, each auto-switch prompts for your password.${NC}" +echo -e " ${BOLD}Passwordless sudo (for phpvm global only)${NC}" +echo -e " ${DIM}Only the system-wide switch (phpvm global / --set) uses sudo.${NC}" +echo -e " ${DIM}Per-shell (phpvm shell) and per-project (phpvm local) need none.${NC}" echo "" if (( UPGRADE )); then if [[ -f /etc/sudoers.d/phpvm ]] && grep -q 'php\*' /etc/sudoers.d/phpvm 2>/dev/null; then @@ -294,8 +302,9 @@ fi # add hook to shell rc echo "" -echo -e " ${BOLD}Auto-switch hook${NC}" -echo -e " ${DIM}Automatically switches PHP when entering project directories.${NC}" +echo -e " ${BOLD}Shell hook${NC}" +echo -e " ${DIM}Powers per-shell switching (phpvm shell) and auto-switch on cd.${NC}" +echo -e " ${DIM}Puts the shim dir on PATH and adds the phpvm() wrapper to your shell.${NC}" echo "" RC="" @@ -321,11 +330,16 @@ if [[ -n "$RC" ]]; then (( UPGRADE )) || warn "Hook already present in ${RC}" elif (( UPGRADE )); then info "Skipping shell hook prompt (upgrade mode)" - elif (( ! INTERACTIVE )); then - info "Non-interactive — skipping shell hook (run: phpvm --enable-hook)" else - read -rp " Add auto-switch hook to ${RC}? [y/N] " ans < /dev/tty - if [[ "$ans" =~ ^[Yy]$ ]]; then + # default-enable: the everyday per-shell behavior depends on the hook + ans="y" + if (( INTERACTIVE )); then + read -rp " Enable the shell hook in ${RC}? [Y/n] " ans < /dev/tty + ans="${ans:-y}" + else + info "Non-interactive, enabling the shell hook by default" + fi + if [[ ! "$ans" =~ ^[Nn]$ ]]; then { echo "" echo "# phpvm auto-switch" diff --git a/phpvm.sh b/phpvm.sh index d8683a1..65cde2a 100755 --- a/phpvm.sh +++ b/phpvm.sh @@ -1,12 +1,12 @@ #!/bin/bash -# phpvm - PHP Version Manager v2.4.0 +# phpvm - PHP Version Manager v2.5.0 if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3) )); then echo "phpvm requires bash 4.3+. Current: ${BASH_VERSION}" >&2 exit 1 fi -VERSION="2.4.0" +VERSION="2.5.0" RED='\033[0;31m' GREEN='\033[0;32m' @@ -19,6 +19,7 @@ NC='\033[0m' REVERSE='\033[7m' selected_index=0 +tui_wrap=false # helpers @@ -100,6 +101,14 @@ apt_pkg_available() { apt-cache show "$1" 2>/dev/null | grep -q . } +# apt-get under sudo with a non-interactive debconf frontend, so an unattended +# `--yes` install can't wedge on a postinst prompt (e.g. tzdata). sudo resets the +# environment, so we set DEBIAN_FRONTEND via `env` rather than an inline +# assignment, which sudo would strip. apt stays password-gated as before. +_sudo_apt() { + sudo env DEBIAN_FRONTEND=noninteractive apt-get "$@" +} + # project detection find_php_version_file() { @@ -285,21 +294,59 @@ cmd_list() { } cmd_current() { - local current - current=$(get_current_php) - if [[ -z "$current" ]]; then + # Headline is ground truth: whatever `php` resolves to right now (the shim + # when the hook is loaded, else /usr/bin/php). The breakdown below shows the + # three resolution layers that feed it. The env vars are inherited from the + # caller's shell, so this is accurate when run through the wrapper. + local phpbin actual + phpbin=$(command -v php 2>/dev/null) + actual=$(php --version 2>/dev/null | head -1) + + local global_link global_name="" + global_link=$(get_current_php) + [[ -n "$global_link" ]] && global_name="${global_link##*/}" + + if [[ -z "$actual" && -z "$global_name" ]]; then echo -e "${YELLOW}No active PHP version found.${NC}" >&2 exit 1 fi - echo -e "${GREEN}$(basename "$current")${NC} ${DIM}($(php --version 2>/dev/null | head -1))${NC}" + + if [[ -n "$actual" ]]; then + echo -e "${GREEN}${BOLD}${actual}${NC}" + [[ -n "$phpbin" ]] && echo -e " ${DIM}via ${phpbin}${NC}" + fi + + echo "" + local shell_pin="${PHPVM_SHELL_VERSION:-}" + local auto_pin="${PHPVM_AUTO_VERSION:-}" + local proj="" + [[ -z "$auto_pin" ]] && proj=$(detect_project_php 2>/dev/null || true) + + if [[ -n "$shell_pin" ]]; then + echo -e " ${DIM}shell:${NC} ${CYAN}${shell_pin}${NC} ${DIM}(this terminal, phpvm shell)${NC}" + else + echo -e " ${DIM}shell:${NC} ${DIM}not pinned${NC}" + fi + if [[ -n "$auto_pin" ]]; then + echo -e " ${DIM}project:${NC} ${CYAN}${auto_pin}${NC} ${DIM}(auto, from cd-hook)${NC}" + elif [[ -n "$proj" ]]; then + echo -e " ${DIM}project:${NC} ${CYAN}${proj}${NC} ${DIM}(.php-version / composer.json)${NC}" + else + echo -e " ${DIM}project:${NC} ${DIM}none${NC}" + fi + if [[ -n "$global_name" ]]; then + echo -e " ${DIM}global:${NC} ${CYAN}${global_name#php}${NC} ${DIM}(system default, phpvm global)${NC}" + else + echo -e " ${DIM}global:${NC} ${DIM}none${NC}" + fi } -cmd_set() { +cmd_global() { require_update_alternatives local query="$1" if [[ -z "$query" ]]; then - echo -e "${RED}Usage: phpvm --set ${NC}" >&2 - echo -e "${DIM}Example: phpvm --set 8.2${NC}" >&2 + echo -e "${RED}Usage: phpvm global ${NC}" >&2 + echo -e "${DIM}Example: phpvm global 8.2${NC}" >&2 exit 1 fi @@ -365,11 +412,11 @@ cmd_auto() { return "$rc" } -cmd_set_project() { +cmd_local() { local ver="$1" if [[ -z "$ver" ]]; then - echo -e "${RED}Usage: phpvm --set-project ${NC}" >&2 - echo -e "${DIM}Example: phpvm --set-project 8.2${NC}" >&2 + echo -e "${RED}Usage: phpvm local ${NC}" >&2 + echo -e "${DIM}Example: phpvm local 8.2${NC}" >&2 exit 1 fi @@ -399,6 +446,68 @@ cmd_set_project() { echo -e "${GREEN}✓${NC} Created ${BOLD}.php-version${NC} → ${CYAN}${ver}${NC}" } +# per-shell switching + +# Emit shell code for the wrapper to eval. Default POSIX `export`; fish syntax +# under --fish. On any problem it emits a snippet that prints to stderr and runs +# `false`, so the eval in the caller's shell surfaces the error and returns +# non-zero without setting anything. +cmd_sh_shell() { + local fish=false do_unset=false ver="" + while [[ $# -gt 0 ]]; do + case "$1" in + --fish) fish=true ;; + --unset) do_unset=true ;; + -*) ;; + *) ver="$1" ;; + esac + shift + done + + if [[ "$do_unset" == "true" ]]; then + if [[ "$fish" == "true" ]]; then + echo "set -e PHPVM_SHELL_VERSION" + else + echo "unset PHPVM_SHELL_VERSION" + fi + return 0 + fi + + local norm + if [[ -z "$ver" ]] || ! norm=$(normalize_version "$ver"); then + echo "echo 'phpvm: usage: phpvm shell | --unset' >&2; false" + return 0 + fi + ver="$norm" + + if ! find_version_by_query "$ver" >/dev/null; then + echo "echo 'phpvm: PHP ${ver} is not installed (try: phpvm install ${ver})' >&2; false" + return 0 + fi + + if [[ "$fish" == "true" ]]; then + echo "set -gx PHPVM_SHELL_VERSION ${ver}" + else + echo "export PHPVM_SHELL_VERSION=${ver}" + fi +} + +# Direct-invoke fallback. The wrapper intercepts `shell` and routes it through +# `sh-shell` + eval, so reaching the binary here means the wrapper is not loaded +# and we cannot change the caller's shell. Tell them how to enable it. +cmd_shell() { + local v="${1:-}" + echo -e "${YELLOW}!${NC} ${BOLD}phpvm shell${NC} needs the shell wrapper to switch this terminal." >&2 + echo -e "${DIM}Enable it once (adds the hook + shim to your shell):${NC}" >&2 + echo -e "${DIM} phpvm --enable-hook${NC}" >&2 + if [[ -n "$v" ]]; then + echo -e "${DIM}Or for this shell only:${NC}" >&2 + echo -e "${DIM} eval \"\$(phpvm sh-shell ${v})\"${NC}" >&2 + fi + echo -e "${DIM}For a system-wide switch instead, use: phpvm global ${v:-}${NC}" >&2 + exit 1 +} + # install # Configure the upstream PHP repo for $ver if apt can't already see php$ver-cli. @@ -418,7 +527,7 @@ ensure_php_repo() { ubuntu) echo -e " ${BLUE}Adding${NC} ${BOLD}ppa:ondrej/php${NC}" if ! command -v add-apt-repository &>/dev/null; then - sudo apt-get install -y software-properties-common || return 1 + _sudo_apt install -y software-properties-common || return 1 fi sudo add-apt-repository -y ppa:ondrej/php || return 1 ;; @@ -426,7 +535,7 @@ ensure_php_repo() { local keyring="/etc/apt/keyrings/sury-php.gpg" local list="/etc/apt/sources.list.d/sury-php.list" echo -e " ${BLUE}Adding${NC} ${BOLD}deb.sury.org${NC} repo (${codename})" - sudo apt-get install -y ca-certificates curl || return 1 + _sudo_apt install -y ca-certificates curl || return 1 sudo install -d -m 0755 /etc/apt/keyrings || return 1 curl -fsSL https://packages.sury.org/php/apt.gpg | sudo tee "$keyring" >/dev/null || return 1 echo "deb [signed-by=${keyring}] https://packages.sury.org/php/ ${codename} main" \ @@ -437,7 +546,7 @@ ensure_php_repo() { ;; esac - sudo apt-get update + _sudo_apt update } cmd_install() { @@ -523,7 +632,7 @@ cmd_install() { # idempotency for an explicit version (latest is re-checked once it resolves) if [[ "$is_latest" != "true" ]] && { [[ -x "/usr/bin/php${ver}" ]] || get_php_versions | grep -qx "/usr/bin/php${ver}"; }; then echo -e "${GREEN}✓${NC} PHP ${BOLD}${ver}${NC} is already installed." - echo -e "${DIM}Switch with: phpvm --set ${ver} (per-project: phpvm --set-project ${ver})${NC}" + echo -e "${DIM}Use it: phpvm shell ${ver} (this terminal) | phpvm global ${ver} (system) | phpvm local ${ver} (project)${NC}" exit 0 fi @@ -577,7 +686,7 @@ cmd_install() { fi # shellcheck disable=SC2086 - if ! sudo apt-get install -y $packages; then + if ! _sudo_apt install -y $packages; then echo -e "${RED}✗${NC} apt-get install failed." >&2 exit 1 fi @@ -592,15 +701,17 @@ cmd_install() { echo -e "${GREEN}✓${NC} Installed PHP ${BOLD}${ver}${NC}" - # offer to switch + # offer to switch. install runs as a normal subprocess, so the only switch it + # can make durably is the global one (a per-shell pin can't reach back into + # the parent shell from here); the hint points at the no-sudo alternatives. if [[ "$auto_use" == "true" ]]; then do_switch "$bin" elif _tty_interactive; then local ans - read -rp " Switch to PHP ${ver} now? [y/N] " ans < /dev/tty + read -rp " Set PHP ${ver} as the system default now? [y/N] " ans < /dev/tty [[ "$ans" =~ ^[Yy]$ ]] && do_switch "$bin" else - echo -e "${DIM}Switch with: phpvm --set ${ver}${NC}" + echo -e "${DIM}Use it: phpvm shell ${ver} (this terminal) | phpvm global ${ver} (system)${NC}" fi } @@ -1019,6 +1130,44 @@ cmd_doctor() { _doc_warn "Cannot locate shell rc (shell=${shell_name})" fi + # per-shell switching (shim) + _doc_section "Per-shell switching (shim)" + + if [[ -n "$hook_dir" ]]; then + local shim="${hook_dir}/shims/php" + if [[ -x "$shim" ]]; then + _doc_ok "Shim present: ${BOLD}${shim}${NC}" + else + _doc_bad "Shim missing or not executable: ${shim}" + _doc_info "Fix: re-run install.sh" + fi + + # shims dir on PATH is the proxy for "hook active in this shell": the + # phpvm() function itself can't be seen from a subprocess, but the hook + # prepends this dir, and PATH is inherited. + local shims_dir="${hook_dir}/shims" + if [[ ":${PATH}:" == *":${shims_dir}:"* ]]; then + _doc_ok "Shims dir on PATH (per-shell switching active)" + local active_php + active_php=$(command -v php 2>/dev/null) + [[ "$active_php" == "${shims_dir}/php" ]] \ + && _doc_info "php resolves to the shim" \ + || _doc_info "php resolves to ${active_php:-nothing} (a non-shim php precedes it on PATH)" + else + _doc_warn "Shims dir not on PATH, so phpvm shell won't affect this terminal" + _doc_info "Fix: phpvm --enable-hook, then open a new shell" + fi + else + _doc_skip "No hook dir, shim unavailable" + fi + + if [[ -n "${PHPVM_SHELL_VERSION:-}" ]]; then + _doc_info "Shell pin: ${CYAN}PHPVM_SHELL_VERSION=${PHPVM_SHELL_VERSION}${NC}" + fi + if [[ -n "${PHPVM_AUTO_VERSION:-}" ]]; then + _doc_info "Project auto: ${CYAN}PHPVM_AUTO_VERSION=${PHPVM_AUTO_VERSION}${NC}" + fi + # gui (optional) _doc_section "GUI / tray (optional)" @@ -1172,15 +1321,18 @@ cmd_help() { echo -e "${BOLD}Usage:${NC}" echo -e " phpvm Interactive TUI" echo -e " phpvm --list List installed PHP versions" - echo -e " phpvm --current Show active PHP version" - echo -e " phpvm --set Switch to version (e.g. 8.2)" + echo -e " phpvm --current Show active PHP version (shell pin, project, global)" + echo -e " phpvm shell Switch this terminal only, no sudo (e.g. 8.2)" + echo -e " phpvm shell --unset Drop the per-shell pin" + echo -e " phpvm local Pin this project: writes .php-version, no sudo" + echo -e " phpvm global Switch the system default via update-alternatives (sudo)" + echo -e " ${DIM}aliases: --set = global, --set-project = local${NC}" echo -e " phpvm install Install PHP via Ondřej Surý's repo (e.g. 8.3, latest)" echo -e " ${DIM}--minimal drop -fpm --with curl,mbstring add extensions${NC}" echo -e " ${DIM}--use switch after install --yes skip prompts --print dry-run${NC}" echo -e " phpvm --auto [--quiet] Auto-switch from .php-version / composer.json" echo -e " phpvm --auto --print [dir] Print resolved project PHP version (no switch)" - echo -e " phpvm --set-project Write .php-version in current dir" - echo -e " phpvm --enable-hook [shell] Add auto-switch hook to shell rc (bash/zsh/fish)" + echo -e " phpvm --enable-hook [shell] Add the shell hook + shim to your rc (bash/zsh/fish)" echo -e " phpvm --disable-hook [shell] Remove auto-switch hook from shell rc" echo -e " phpvm --window Open detached GTK picker window (needs phpvm-gui)" echo -e " phpvm --self-update [URL] [REF] Pull latest from git and re-run installer" @@ -1239,7 +1391,12 @@ draw_menu() { fi echo "" - echo -e " ${DIM}↑/↓ navigate Enter select p set-project q quit${NC}" + if [[ "$tui_wrap" == "true" ]]; then + echo -e " ${DIM}↑/↓ navigate Enter pin this shell g global p project q quit${NC}" + else + echo -e " ${DIM}↑/↓ navigate Enter global switch p project q quit${NC}" + echo -e " ${DIM}(enable ${BOLD}phpvm --enable-hook${NC}${DIM} for per-shell pinning on Enter)${NC}" + fi echo "" echo -e " ${DIM}────────────────────────────────────────${NC}" echo "" @@ -1371,6 +1528,17 @@ tui_main() { fi done + # When launched through the shell wrapper, stdout is captured by `eval` (not a + # tty). Save that captured fd as 4 for the final `export` line, then point fd 1 + # at the terminal so every draw call below works unchanged. fzf uses the same + # split: UI to the tty, result to stdout. + tui_wrap=false + if [[ ! -t 1 ]] && { : >/dev/tty; } 2>/dev/null; then + tui_wrap=true + exec 4>&1 + exec 1>/dev/tty + fi + tput civis trap 'tput cnorm; tput rmcup; exit 0' EXIT INT TERM @@ -1397,6 +1565,30 @@ tui_main() { [[ "$selected_index" -ge "${#versions[@]}" ]] && selected_index=0 ;; '') + if [[ "$tui_wrap" == "true" ]]; then + # pin this terminal: emit the assignment on the saved fd (the + # wrapper's eval/source applies it once the TUI exits), then + # leave. fish wrapper sets PHPVM_SHELL_SYNTAX so we emit its + # syntax instead of POSIX export. + local _label _v + _label="${versions[$selected_index]##*/}" + _v=$(normalize_version "$_label" || echo "${_label#php}") + if [[ "${PHPVM_SHELL_SYNTAX:-}" == "fish" ]]; then + echo "set -gx PHPVM_SHELL_VERSION ${_v}" >&4 + else + echo "export PHPVM_SHELL_VERSION=${_v}" >&4 + fi + break + else + switch_version_tui "${versions[$selected_index]}" + mapfile -t versions < <(get_php_versions) + current_php=$(get_current_php) + tput civis + tput smcup + clear + fi + ;; + g | G) switch_version_tui "${versions[$selected_index]}" mapfile -t versions < <(get_php_versions) current_php=$(get_current_php) @@ -1433,8 +1625,14 @@ case "$CMD" in -c | --current) cmd_current ;; - -s | --set) - cmd_set "${1:-}" + -s | --set | global) + cmd_global "${1:-}" + ;; + shell) + cmd_shell "${1:-}" + ;; + sh-shell) + cmd_sh_shell "$@" ;; -a | --auto) QUIET=false @@ -1450,8 +1648,8 @@ case "$CMD" in done cmd_auto "$QUIET" "$PRINT_ONLY" "$DIR" ;; - -p | --set-project) - cmd_set_project "${1:-}" + -p | --set-project | local) + cmd_local "${1:-}" ;; install) cmd_install "$@" diff --git a/roadmap.html b/roadmap.html index 4869b04..1a527ae 100644 --- a/roadmap.html +++ b/roadmap.html @@ -3,20 +3,20 @@ - phpvm — Roadmap - + phpvm - Roadmap + - - + + - - + + @@ -160,7 +160,7 @@
phpvm

Roadmap

-

What's planned next. Items are in priority order — the top two are fully specced, the rest are idea-stage. Full detail lives in ROADMAP.md.

+

What's planned next. Items are in priority order: the top two are fully specced, the rest are idea-stage. Full detail lives in ROADMAP.md.