Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ pre-built binary** or **compile from source** (controlled by
`ghostel-module-auto-install`, default `ask`). You can also trigger these
manually:

- `M-x ghostel-download-module` — download a pre-built binary from GitHub releases
- `M-x ghostel-download-module` — download the minimum supported pre-built binary
- `C-u M-x ghostel-download-module` — choose a specific release tag (leave blank for latest)
- `M-x ghostel-module-compile` — build from source via `zig build`

## Building from source
Expand Down Expand Up @@ -116,10 +117,10 @@ zig build -Doptimize=ReleaseFast

When installed from MELPA, `M-x ghostel-module-compile` builds the native
module from source using `zig build`. Zig's package manager fetches the
ghostty dependency automatically — no git submodule needed.
ghostty dependency automatically.

Alternatively, download a **pre-built binary** via
`M-x ghostel-download-module`.
Alternatively, download a **pre-built binary** via `M-x ghostel-download-module`
(or `C-u M-x ghostel-download-module` to pick a specific release).

## Shell Integration

Expand Down
77 changes: 43 additions & 34 deletions ghostel.el
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,11 @@ before sending the input."
ghostel-color-bright-white]
"Color palette for the terminal (vector of 16 face names).")

(defvar ghostel-github-release-url
(defcustom ghostel-github-release-url
"https://github.com/dakra/ghostel/releases"
"Base URL for ghostel GitHub releases.")
"Base URL for Ghostel GitHub releases.
Customize this when downloading pre-built modules from a fork or mirror."
:type 'string)

(defconst ghostel--minimum-module-version "0.13.0"
"Minimum native module version required by this Elisp version.
Expand Down Expand Up @@ -423,22 +425,26 @@ Returns nil if the platform is not recognized."
(when tag
(format "ghostel-module-%s%s" tag module-file-suffix))))

(defun ghostel--module-download-url ()
"Return the download URL for the current platform's pre-built module."
(defun ghostel--module-download-url (&optional version)
"Return the download URL for the current platform's pre-built module.
When VERSION is nil, use the latest release download URL."
(let ((asset-name (ghostel--module-asset-name)))
(when asset-name
(let ((version (ghostel--package-version)))
(if version
(format "%s/download/v%s/%s"
ghostel-github-release-url version asset-name)
(format "%s/latest/download/%s"
ghostel-github-release-url asset-name))))))

(defun ghostel--download-module (dir)
(if version
(format "%s/download/v%s/%s"
ghostel-github-release-url version asset-name)
(format "%s/latest/download/%s"
ghostel-github-release-url asset-name)))))

(defun ghostel--download-module (dir &optional version latest-release)
"Download a pre-built module into DIR.
When VERSION is non-nil, download that release tag.
When LATEST-RELEASE is non-nil, use the latest release asset URL.
Returns non-nil on success."
(condition-case err
(let ((url (ghostel--module-download-url)))
(let* ((requested-version (unless latest-release
(or version ghostel--minimum-module-version)))
(url (ghostel--module-download-url requested-version)))
(when url
(unless (string-prefix-p "https://" url)
(error "Refusing non-HTTPS download URL: %s" url))
Expand Down Expand Up @@ -484,16 +490,28 @@ Behavior is controlled by `ghostel-module-auto-install'."
('compile (ghostel--compile-module dir))
(_ nil))))

(defun ghostel--read-module-download-version ()
"Prompt for a release tag to download, or nil for the latest release."
(let ((version (read-string
(format "Ghostel module version (>= %s, empty for latest): "
ghostel--minimum-module-version))))
(unless (string= version "")
(when (version< version ghostel--minimum-module-version)
(user-error "Version %s is older than minimum supported version %s"
version ghostel--minimum-module-version))
version)))

(defun ghostel--ask-install-action (_dir)
"Prompt the user to choose how to install the missing native module.
Returns \\='download, \\='compile, or nil."
(let* ((url (or (ghostel--module-download-url) "GitHub releases"))
(let* ((url (or (ghostel--module-download-url ghostel--minimum-module-version)
"GitHub releases"))
(choice (read-char-choice
(format "Ghostel native module not found.

[d] Download pre-built binary from:
%s
[c] Compile from source (requires Zig)
[c] Compile from source via build.sh
[s] Skip — install manually later

Choice: " url)
Expand All @@ -503,19 +521,6 @@ Choice: " url)
(?c 'compile)
(?s nil))))

(defun ghostel--package-version ()
"Return ghostel release version string, or nil.
Reads the Version header from ghostel.el so the download URL
matches the GitHub release tag even when MELPA rewrites the
version to a date-based string."
(require 'lisp-mnt nil t)
(when (fboundp 'lm-header)
(let ((lib (or load-file-name (locate-library "ghostel.el" t))))
(when lib
(with-temp-buffer
(insert-file-contents lib nil 0 1024)
(lm-header "Version"))))))

(defun ghostel--download-file (url dest)
"Download URL to DEST. Return non-nil on success."
(condition-case nil
Expand All @@ -539,18 +544,23 @@ version to a date-based string."
(kill-buffer buf))))))
(error nil)))

(defun ghostel-download-module ()
"Interactively download the pre-built native module for this platform."
(interactive)
(defun ghostel-download-module (&optional prompt-for-version)
"Interactively download the pre-built native module for this platform.
With PROMPT-FOR-VERSION, prompt for a release tag to download.
Leaving the prompt empty downloads the latest release."
(interactive "P")
(let* ((dir (file-name-directory (or load-file-name
(locate-library "ghostel")
buffer-file-name)))
(mod (expand-file-name
(concat "ghostel-module" module-file-suffix) dir)))
(concat "ghostel-module" module-file-suffix) dir))
(version (when prompt-for-version
(ghostel--read-module-download-version)))
(latest-release (and prompt-for-version (null version))))
(when (and (file-exists-p mod)
(not (yes-or-no-p "Module already exists. Re-download? ")))
(user-error "Cancelled"))
(if (ghostel--download-module dir)
(if (ghostel--download-module dir version latest-release)
(progn
(module-load mod)
(message "ghostel: module loaded successfully"))
Expand Down Expand Up @@ -601,7 +611,6 @@ DIR is the module directory."
(concat "Native module not found: " mod
"\nRun M-x ghostel-download-module or M-x ghostel-module-compile")))))


;;; Internal variables

(defvar-local ghostel--term nil
Expand Down
132 changes: 123 additions & 9 deletions test/ghostel-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1970,14 +1970,123 @@ buffer and hand nil to the native module."
;; -----------------------------------------------------------------------

;; -----------------------------------------------------------------------
;; Test: module version check
;; -----------------------------------------------------------------------

(ert-deftest ghostel-test-package-version ()
"Test that `ghostel--package-version' returns a version string."
(let ((ver (ghostel--package-version)))
(should (stringp ver))
(should (string-match-p "^[0-9]+\\.[0-9]+\\.[0-9]+" ver))))
;; Test: module download version selection
;; -----------------------------------------------------------------------

(ert-deftest ghostel-test-module-download-url-uses-requested-version ()
"Requested download versions are decoupled from the package version."
(let ((ghostel-github-release-url "https://example.invalid/releases"))
(cl-letf (((symbol-function 'ghostel--module-asset-name)
(lambda () "ghostel-module-x86_64-linux.so")))
(should (equal "https://example.invalid/releases/download/v0.7.1/ghostel-module-x86_64-linux.so"
(ghostel--module-download-url "0.7.1"))))))

(ert-deftest ghostel-test-module-download-url-uses-latest-release ()
"A nil download version uses the latest release asset."
(let ((ghostel-github-release-url "https://example.invalid/releases"))
(cl-letf (((symbol-function 'ghostel--module-asset-name)
(lambda () "ghostel-module-x86_64-linux.so")))
(should (equal "https://example.invalid/releases/latest/download/ghostel-module-x86_64-linux.so"
(ghostel--module-download-url nil))))))

(ert-deftest ghostel-test-download-module-defaults-to-minimum-version ()
"Automatic downloads pin to the minimum supported native module version."
(let ((ghostel--minimum-module-version "0.7.1")
(captured-version :unset)
(download-dest nil))
(cl-letf (((symbol-function 'ghostel--module-download-url)
(lambda (&optional version)
(setq captured-version version)
"https://example.invalid/releases/download/v0.7.1/ghostel-module-x86_64-linux.so"))
((symbol-function 'ghostel--download-file)
(lambda (_url dest)
(setq download-dest dest)
t))
((symbol-function 'message)
(lambda (&rest _))))
(should (ghostel--download-module "C:/ghostel/"))
(should (equal "0.7.1" captured-version))
(should (equal (downcase (expand-file-name
(concat "ghostel-module" module-file-suffix)
"C:/ghostel/"))
(downcase download-dest))))))

(ert-deftest ghostel-test-download-module-prefix-uses-requested-version ()
"Prefix downloads pass the requested release version through unchanged."
(let ((ghostel--minimum-module-version "0.7.1")
(captured-version :unset)
(captured-latest nil)
(loaded-module nil))
(let ((comp-enable-subr-trampolines nil)
(native-comp-enable-subr-trampolines nil))
(cl-letf (((symbol-function 'locate-library)
(lambda (_) "C:/ghostel/ghostel.el"))
((symbol-function 'file-exists-p)
(lambda (_) nil))
((symbol-function 'read-string)
(lambda (&rest _) "0.8.0"))
((symbol-function 'ghostel--download-module)
(lambda (_dir &optional version latest-release)
(setq captured-version version
captured-latest latest-release)
t))
((symbol-function 'module-load)
(lambda (path)
(setq loaded-module path)))
((symbol-function 'message)
(lambda (&rest _))))
(ghostel-download-module '(4))
(should (equal "0.8.0" captured-version))
(should-not captured-latest)
(should (equal (downcase (expand-file-name
(concat "ghostel-module" module-file-suffix)
"C:/ghostel/"))
(downcase loaded-module)))))))

(ert-deftest ghostel-test-download-module-prefix-empty-uses-latest ()
"Prefix download treats blank input as a request for the latest release."
(let ((captured-version :unset)
(captured-latest nil)
(loaded-module nil))
(let ((comp-enable-subr-trampolines nil)
(native-comp-enable-subr-trampolines nil))
(cl-letf (((symbol-function 'locate-library)
(lambda (_) "C:/ghostel/ghostel.el"))
((symbol-function 'file-exists-p)
(lambda (_) nil))
((symbol-function 'read-string)
(lambda (&rest _) ""))
((symbol-function 'ghostel--download-module)
(lambda (_dir &optional version latest-release)
(setq captured-version version
captured-latest latest-release)
t))
((symbol-function 'module-load)
(lambda (path)
(setq loaded-module path)))
((symbol-function 'message)
(lambda (&rest _))))
(ghostel-download-module '(4))
(should (null captured-version))
(should captured-latest)
(should (equal (downcase (expand-file-name
(concat "ghostel-module" module-file-suffix)
"C:/ghostel/"))
(downcase loaded-module)))))))

(ert-deftest ghostel-test-download-module-prefix-rejects-too-old-version ()
"Prefix download rejects versions below the minimum supported version."
(let ((ghostel--minimum-module-version "0.7.1"))
(let ((comp-enable-subr-trampolines nil)
(native-comp-enable-subr-trampolines nil))
(cl-letf (((symbol-function 'locate-library)
(lambda (_) "C:/ghostel/ghostel.el"))
((symbol-function 'file-exists-p)
(lambda (_) nil))
((symbol-function 'read-string)
(lambda (&rest _) "0.7.0")))
(should-error (ghostel-download-module '(4))
:type 'user-error)))))

(ert-deftest ghostel-test-compile-module-invokes-zig-build ()
"Source compilation runs zig build directly."
Expand Down Expand Up @@ -2787,9 +2896,14 @@ while :; do sleep 0.1; done'\n")
ghostel-test-project-universal-arg
ghostel-test-copy-all
ghostel-test-copy-mode-buffer-navigation
ghostel-test-package-version
ghostel-test-compile-module-invokes-zig-build
ghostel-test-module-compile-command-uses-zig-build
ghostel-test-module-download-url-uses-requested-version
ghostel-test-module-download-url-uses-latest-release
ghostel-test-download-module-defaults-to-minimum-version
ghostel-test-download-module-prefix-uses-requested-version
ghostel-test-download-module-prefix-empty-uses-latest
ghostel-test-download-module-prefix-rejects-too-old-version
ghostel-test-module-version-match
ghostel-test-module-version-mismatch
ghostel-test-module-version-newer-than-minimum
Expand Down
Loading