Repository navigation
Expand file tree
/
Copy pathfzfa-tramp.el
More file actions
144 lines (122 loc) · 5.36 KB
/
Copy pathfzfa-tramp.el
File metadata and controls
144 lines (122 loc) · 5.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
;;; fzfa-tramp.el --- TRAMP-aware spawning for `fzfa' -*- lexical-binding: t; -*-
;; Copyright (C) 2026 James Nguyen
;; Author: James Nguyen <james@jojojames.com>
;; Version: 1.0
;; Package-Requires: ((emacs "29.1"))
;; Homepage: https://github.com/jojojames/fzfa
;; Assisted-by: Claude:claude-opus-4-7
;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; TRAMP support for fzfa.
;;
;; Makes fzfa's shell sources spawn transparently against TRAMP `ssh'
;; paths: for a TRAMP `ssh'-method `default-directory' the shell
;; command is wrapped as `ssh HOST 'cd LOCALNAME && CMD'' and
;; fzf-native is handed a safe local chdir target. The C-layer's
;; fork+pipe+parallel-score pipeline is unchanged; the remote binary
;; streams over ssh into the same pipe reader as a local process.
;;
;; When `tramp-use-connection-share' is non-nil (Emacs 30 default on
;; non-Windows), TRAMP has already opened a ControlMaster socket for
;; the host, so the wrapped ssh call piggybacks on that socket with
;; no handshake. With sharing off, each fzfa invocation pays one
;; handshake — acceptable for interactive use.
;;
;; Commands:
;; `fzfa-ssh' Pick a host from ~/.ssh/config and visit /ssh:HOST:.
;; `fzfa-tramp' Pick a host from ~/.ssh/config and open the browse
;; picker on it (via `fzfa-browse-files').
;;; Code:
(require 'cl-lib)
(require 'fzfa)
(declare-function tramp-dissect-file-name "tramp" (name &optional nodefault))
;; `cl-defstruct' slot accessors — check-declare can't see auto-generated
;; bodies, only literal `defun's. ARGLIST is `t' (unspecified) not `nil'
;; (which would mean "takes 0 args" and trip byte-compile on 1-arg sites).
(declare-function tramp-file-name-method "tramp" t t)
(declare-function tramp-file-name-user "tramp" t t)
(declare-function tramp-file-name-host "tramp" t t)
(declare-function tramp-file-name-port "tramp" t t)
(declare-function tramp-file-name-localname "tramp" t t)
(declare-function fzfa-browse-files "fzfa-emacs" (&optional dir))
(defcustom fzfa-tramp-safe-local-dir "/"
"Local directory handed to `fzf-native-async-start' for ssh-wrapped calls.
The C layer's `chdir' runs before the wrapped `ssh HOST cmd', so the
choice is irrelevant to the actual working directory of the remote
command (that's set inside the ssh payload). It only needs to be a
`chdir'-able local path."
:type 'directory
:group 'fzfa)
(defun fzfa-tramp--build-ssh-cmd (vec cmd)
"Build `ssh USER@HOST -p PORT \\='cd LOCALNAME && CMD\\='' for VEC.
VEC is a `tramp-file-name' struct. Returns a shell string suitable
for `fzf-native-async-start' (which execs it via /bin/sh -c)."
(let* ((user (tramp-file-name-user vec))
(host (tramp-file-name-host vec))
(port (tramp-file-name-port vec))
(localname (tramp-file-name-localname vec))
(target (if user (format "%s@%s" user host) host))
(port-args (if port (format " -p %s" port) ""))
(remote-cmd (format "cd %s && %s"
(shell-quote-argument localname)
cmd)))
(format "ssh%s %s %s"
port-args
(shell-quote-argument target)
(shell-quote-argument remote-cmd))))
(defun fzfa-tramp--transform (cmd dir)
"Spawn transform for `fzfa-source-spawn-transform-function'.
For TRAMP `ssh'-method paths, wraps CMD as
`ssh HOST \\='cd LOCALNAME && CMD\\='' and returns
`fzfa-tramp-safe-local-dir' as the chdir target. Non-`ssh' TRAMP
methods and local paths pass through unchanged."
(if (not (file-remote-p dir))
(cons cmd dir)
(require 'tramp)
(let ((vec (tramp-dissect-file-name dir)))
(if (equal (tramp-file-name-method vec) "ssh")
(cons (fzfa-tramp--build-ssh-cmd vec cmd)
fzfa-tramp-safe-local-dir)
(cons cmd dir)))))
(defun fzfa-tramp--ssh-hosts ()
"Return the list of non-wildcard `Host' entries from ~/.ssh/config."
(let ((config (expand-file-name "~/.ssh/config"))
hosts)
(when (file-readable-p config)
(with-temp-buffer
(insert-file-contents config)
(while (re-search-forward
"^[Hh]ost[[:space:]]+\\(.+\\)" nil t)
(dolist (host (split-string (match-string 1)))
(unless (string-match-p "[*?!]" host)
(push host hosts))))))
(nreverse hosts)))
(defun fzfa-tramp--pick-host ()
"Pick an SSH host from ~/.ssh/config; return the TRAMP root `/ssh:HOST:'.
Signals `user-error' when the config is missing or has no hosts."
(when-let* ((hosts (or (fzfa-tramp--ssh-hosts)
(user-error "No SSH hosts in ~/.ssh/config")))
(host (fzfa-completing-read
:candidates hosts :prompt "ssh: ")))
(concat "/ssh:" host ":")))
;;;###autoload
(defun fzfa-ssh ()
"Pick a host from ~/.ssh/config and visit `/ssh:HOST:' as a directory."
(interactive)
(when-let* ((path (fzfa-tramp--pick-host)))
(fzfa-visit-file path)))
;;;###autoload
(defun fzfa-tramp ()
"Pick a host from ~/.ssh/config and open `fzfa-browse-files' rooted there."
(interactive)
(when-let* ((path (fzfa-tramp--pick-host)))
(fzfa-browse-files path)))
;;;###autoload
(defun fzfa-tramp-setup ()
"Install fzfa's TRAMP-aware source spawn transform."
(setq fzfa-source-spawn-transform-function #'fzfa-tramp--transform))
(provide 'fzfa-tramp)
;; Local Variables:
;; package-lint-main-file: "fzfa.el"
;; End:
;;; fzfa-tramp.el ends here