Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c0e359
tramp: initial working version
MasterCsquare Jul 21, 2026
431fb64
tramp: performance improved
MasterCsquare Jul 21, 2026
514e667
tramp: fix sudo password keystroke leakage into file buffer
MasterCsquare Jul 21, 2026
81c9329
tramp: fix sudo password leaking into file buffer
MasterCsquare Jul 21, 2026
6c19601
tramp: fix tab completion for ssh and sudo paths
MasterCsquare Jul 21, 2026
8946318
fix contract.yml errors
MasterCsquare Jul 21, 2026
ea06bc3
tramp: fix completion filtering by partial input
MasterCsquare Jul 21, 2026
1ae444c
tramp: Add README
MasterCsquare Jul 22, 2026
1100231
tramp: restrict auto-enable to Unix platforms
MasterCsquare Jul 22, 2026
761871f
tramp: shorter for uiop:ensure-list
MasterCsquare Jul 22, 2026
abd86c9
tramp: remove tramp-* prefix
MasterCsquare Jul 22, 2026
36d951f
tramp: re-prompt password when input wrong password
MasterCsquare Jul 22, 2026
1691160
tramp: re-prompt password when input wrong password for ssh
MasterCsquare Jul 22, 2026
d7af587
tramp: add comments for virtual file system hooks
MasterCsquare Jul 22, 2026
90ef8f2
tramp: replace unnessassry :: with :
MasterCsquare Jul 22, 2026
2f568c4
file-utils: extract %call-virtual-handlers to DRY virtual hook dispatch
MasterCsquare Jul 24, 2026
93d8588
tramp: replace probe-file with exist-program-p
MasterCsquare Jul 24, 2026
d5d04d8
tramp: split ssh-ensure-auth into smaller parts
MasterCsquare Jul 24, 2026
df6368a
tramp: simplify file-completion
MasterCsquare Jul 25, 2026
c57d299
tramp: fix file completion replacing entire TRAMP path
MasterCsquare Jul 26, 2026
3c8b87a
tramp: fix password leaking into file content on sudo writes
MasterCsquare Jul 30, 2026
7d24c25
tramp: implement TRAMP-aware terminal support
MasterCsquare Jul 30, 2026
5553b87
tramp: update README
MasterCsquare Jul 30, 2026
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
12 changes: 9 additions & 3 deletions extensions/terminal/ffi.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,19 @@
(cb_sb_pushline :pointer)
(cb_sb_popline :pointer))

(defun terminal-new (directory id rows cols)
(defun terminal-new (directory id rows cols &key program argv)
"Create a new terminal PTY running PROGRAM with ARGV.
When PROGRAM/ARGV are not provided, defaults to the user's shell started
in DIRECTORY (via 'cd <directory>; <shell>')."
(let* ((shell (or (uiop:getenv "SHELL") "/bin/bash"))
(argv (list shell "-c" (concatenate 'string "cd " directory "; " shell))))
(program (or program shell))
(argv (or argv
(list shell "-c"
(concatenate 'string "cd " directory "; " shell)))))
(%terminal-new id
rows
cols
shell
program
argv
(cffi:callback cb-damage)
(cffi:callback cb-moverect)
Expand Down
54 changes: 47 additions & 7 deletions extensions/terminal/terminal-mode.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
lem-core::<mouse-event>
lem/frame-multiplexer:frame-multiplexer-advice))

(defun %remote-terminal-command (path)
"If PATH is a TRAMP-style remote path, return (values program argv) for
launching a terminal on the remote host. Returns nil for non-TRAMP paths.
Looks up lem-tramp:tramp-terminal-command at runtime so no compile-time
dependency on the TRAMP extension is needed."
(let* ((pkg (find-package :lem-tramp))
(fn (and pkg (find-symbol "TRAMP-TERMINAL-COMMAND" pkg))))
(when fn
(funcall (symbol-function fn) path))))

(define-major-mode terminal-mode ()
(:name "Terminal"
:keymap *terminal-mode-keymap*)
Expand Down Expand Up @@ -81,14 +91,44 @@
(resize-terminal (buffer-terminal buffer) window)
(setf (current-window) window)))

(defun create-terminal-with-command (program argv &key (name "*Terminal*"))
"Create a terminal buffer running PROGRAM with ARGV (a list of strings).
Unlike `create-terminal', this launches an arbitrary command instead of the
user's shell started in a directory."
(declare (type (string) program))
(let* ((buffer (make-buffer (unique-buffer-name name) :enable-undo-p nil))
(terminal (terminal:create :cols 80 :rows 24 :buffer buffer
:directory ""
:program program :argv argv)))
(setf (buffer-terminal buffer) terminal)
(change-buffer-mode buffer 'terminal-mode)
(let ((window (pop-to-buffer buffer)))
(resize-terminal (buffer-terminal buffer) window)
(setf (current-window) window))))

(define-command terminal (always-create-terminal-p) (:universal-nil)
(labels ((new-terminal ()
(create-terminal (buffer-directory (current-buffer)))))
(if always-create-terminal-p
(new-terminal)
(alexandria:if-let (buffer (terminal:find-terminal-buffer))
(setf (current-window) (pop-to-buffer buffer))
(new-terminal)))))
"Open a terminal buffer. When the current buffer visits a TRAMP path
\(e.g. /sudo::/etc or /ssh:user@host:/var/log), the terminal is opened
on the remote host with appropriate privileges.

With a universal argument, always create a new terminal buffer."
(let* ((buf (current-buffer))
;; Check both buffer-filename (file buffers) and buffer-directory
;; (directory-mode buffers) for a potential TRAMP path.
(path (or (buffer-filename buf) (buffer-directory buf))))
(multiple-value-bind (program argv)
(%remote-terminal-command path)
(cond
;; TRAMP/remote path: always create a dedicated terminal
((and program argv)
(create-terminal-with-command program argv))
;; Local: reuse or create
(always-create-terminal-p
(create-terminal (buffer-directory buf)))
(t
(alexandria:if-let (buffer (terminal:find-terminal-buffer))
(setf (current-window) (pop-to-buffer buffer))
(create-terminal (buffer-directory buf))))))))

(defun get-current-terminal ()
(buffer-terminal (current-buffer)))
Expand Down
8 changes: 6 additions & 2 deletions extensions/terminal/terminal.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -240,15 +240,19 @@ point is kept for manual/REPL use and tests."
(defun create (&key (rows (alexandria:required-argument :rows))
(cols (alexandria:required-argument :cols))
(buffer (alexandria:required-argument :buffer))
(directory (alexandria:required-argument :directory)))
(directory (alexandria:required-argument :directory))
(program nil)
(argv nil))
(declare (type (string) directory)
(type (integer) rows)
(type (integer) cols))
(let* ((id (generate-terminal-id))
(terminal
(make-instance 'terminal
:id id
:viscus (ffi::terminal-new directory id rows cols)
:viscus (ffi::terminal-new directory id rows cols
:program program
:argv argv)
:buffer buffer
:rows rows
:cols cols)))
Expand Down
90 changes: 90 additions & 0 deletions extensions/tramp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# lem-tramp

Transparent remote file editing for Lem. Works like
[Emacs TRAMP](https://www.gnu.org/software/tramp/) — type a remote path
into `C-x C-f` and edit the file as if it were local.

## Supported Methods

| Method | Syntax | Description |
|:----------:|-----------------------------------|----------------------------------------------------------|
| `ssh` | `/ssh:user@host:/remote/path` | Edit files on a remote host via SSH |
| `sudo` | `/sudo::/local/path` | Edit local files with root privileges via sudo |

## Usage

Open a file with `C-x C-f` using the TRAMP path syntax:

```
C-x C-f /ssh:root@example.com:/etc/nginx/nginx.conf
C-x C-f /sudo::/etc/hostname
```

The remote file is loaded into a buffer. Edit normally, then save with `C-x C-s`.

## Authentication

### SSH (`/ssh:`)

**Key-based auth** (recommended) — no prompt, works automatically if you have
SSH keys set up and your key is in the remote host's `authorized_keys`.

**Password auth** — requires the `sshpass` utility:

```bash
# Arch
sudo pacman -S sshpass
# Debian/Ubuntu
sudo apt install sshpass
```

When key auth is unavailable, a password prompt appears in the minibuffer.
The password is cached for the session; subsequent file operations on the
same host reuse it without re-prompting.

### Sudo (`/sudo::`)

A password prompt appears if your sudo timestamp has expired (typically
5-15 minutes since your last `sudo` invocation in a terminal). If you
have passwordless sudo configured (`NOPASSWD` in sudoers), no prompt is
shown.

## How It Works

lem-tramp hooks into Lem's virtual filesystem layer:

- **Reading** — runs `cat /remote/path` via SSH (or sudo), loads the
output directly into an in-memory buffer.
- **Writing** — pipes the buffer content through `cat > /remote/path`.
- **Directory listing** — uses `ls -1a` for completion and directory-mode.
- **Metadata** — uses `stat -c '%s %Y'` for file size and modification time.

No temporary files are created on either the local or remote side.

## Dependencies

- **sshpass** — only needed for SSH password authentication
- **flexi-streams**, **str**, **babel**, **ppcre** — Common Lisp libraries

## Terminal Integration

When a buffer is visiting a TRAMP path, `M-x terminal` opens a terminal
in the remote file's directory on the appropriate host:

| Buffer path | Terminal command |
|--------------------------------------|--------------------------------------------------------------------------------|
| `/sudo::/etc/nginx/` | `sudo bash -c "cd /etc/nginx; exec bash"` |
| `/sudo:root::/var/log/` | `sudo -u root bash -c "cd /var/log; exec bash"` |
| `/ssh:user@host:/var/log/` | `ssh -t user@host "cd /var/log; exec ${SHELL:-/bin/sh}"` |

Authentication happens interactively inside the terminal PTY — sudo and
ssh prompt for passwords naturally, without involving TRAMP's password
management. Key-based SSH auth works transparently.

## Performance

SSH connections use `ControlMaster` multiplexing — the first command
establishes the TCP connection, and all subsequent commands reuse it.
A 5-second filesystem cache eliminates redundant `test -d` / `test -f` /
`stat` calls when Lem probes the same path multiple times during a single
file-open operation.
4 changes: 4 additions & 0 deletions extensions/tramp/lem-tramp.asd
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(defsystem "lem-tramp"
:depends-on ("lem/core" "flexi-streams" "str")
:serial t
:components ((:file "tramp")))
Loading
Loading