Skip to content
Open
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# TP-7 CLI

`tp7` is a macOS command-line tool for browsing and moving files on a Teenage Engineering TP-7 field recorder without relying on FieldKit, Android File Transfer, or Finder mounting.
`tp7` is a macOS command-line tool for browsing, moving, and mounting files on a Teenage Engineering TP-7 field recorder without relying on FieldKit or Android File Transfer.

The TP-7 normally appears as a USB audio/MIDI device. `tp7` can send the device-specific MIDI mode switch, wait for the recorder to re-enumerate as MTP, open a direct MTP session, perform the file operation, and close the session again.

Expand All @@ -21,12 +21,16 @@ tp7 -a pull /recordings ./recordings --recursive --skip-existing
tp7 -a push ./clip.wav /memo/clip.wav --dry-run
tp7 -a push ./clip.wav /memo/clip.wav --overwrite
tp7 -a rm /memo/clip.wav --dry-run
tp7 mount
tp7 unmount
```

For normal use, prefer `-a` / `--auto-connect`. Each command then handles the full TP-7 lifecycle: detect the recorder, switch to MTP if needed, open MTP, do the operation, and close cleanly.

`tp7 connect` and `tp7 eject` are diagnostic/manual-control commands. They are useful for checking whether MTP can be opened and released, but they are not a "mount once, run many commands, eject later" workflow.

`tp7 mount` exposes the TP-7 as a read-only Finder volume through a local WebDAV bridge backed by the same MTP session layer. With no mount point argument it chooses `/Volumes/TP-7`, or `/Volumes/TP-7 2` and so on when needed. The command stays running while the mount is active; use `tp7 unmount <mountpoint>` from another shell, or `tp7 unmount` to unmount all TP-7 mounts recorded by the CLI.

## Command surface

```text
Expand All @@ -42,6 +46,8 @@ push Upload a file or directory to the TP-7
mkdir Create a remote folder
rm Delete a remote file or folder
rename Rename a remote object without moving it
mount Mount the TP-7 in Finder through a local WebDAV bridge
unmount Unmount TP-7 WebDAV mount(s)
eject Open and close an MTP session cleanly
```

Expand Down Expand Up @@ -91,15 +97,15 @@ The TP-7 firmware tested here (`1.1.9`) accepts file upload, rename, delete, and
- `push --recursive` uploads into an existing remote folder tree only.
- Missing remote folders are detected before any recursive upload starts.

This is a direct MTP CLI, not a Finder mount. A future FUSE mount is documented as a separate research track in `docs/spec.md`.
Finder mounting is currently read-only. It uses macOS' built-in WebDAV filesystem support and does not require macFUSE, Fuse-T, a kernel extension, or a system extension. Read-write mounting is intentionally not enabled yet because Finder-style writes need staged whole-file uploads and careful handling for safe-save, rename, delete, `.DS_Store`, and unsupported TP-7 folder creation behavior.

## Local requirements

- macOS
- A Teenage Engineering TP-7 connected over USB
- Rust 1.88 or newer for source builds and development

No Android File Transfer, FieldKit, libmtp, or kernel extension is required for the direct CLI workflow.
No Android File Transfer, FieldKit, libmtp, macFUSE, or kernel extension is required for the direct CLI or read-only Finder mount workflow.

## Install

Expand Down
65 changes: 48 additions & 17 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

Build a macOS CLI for Teenage Engineering TP-7 file access.

The first implementation target is a robust Rust-based MTP file manager that can detect the TP-7, switch or validate MTP mode, list files, and transfer recordings. Finder-style mounting is valuable, but should be treated as a later layer because MTP is object-based and macOS mounting requires FUSE infrastructure.
The first implementation target is a robust Rust-based MTP file manager that can detect the TP-7, switch or validate MTP mode, list files, transfer recordings, and expose a read-only Finder volume without third-party filesystem extensions.

## Current Decision

Expand All @@ -16,7 +16,8 @@ Recommended initial stack:
- `nusb` indirectly through `mtp-rs` for USB access.
- `clap` for CLI parsing.
- `tracing` or `log` for diagnostics.
- Later: `fuser` plus macFUSE or Fuse-T for filesystem mounting.
- macOS' built-in WebDAV filesystem client for read-only Finder mounting.
- Later, only if needed: File Provider, FSKit, or FUSE-style mounting for deeper Finder integration.

Avoid `libmtp` in the first prototype unless `mtp-rs` cannot handle the TP-7 reliably. FieldKit uses `libmtp`, so it remains a proven fallback.

Expand Down Expand Up @@ -72,6 +73,8 @@ Observed from local binary metadata:
- It links against bundled `libusb.dylib`.
- It has the `com.apple.security.device.usb` entitlement.
- It does not appear to bundle macFUSE/Fuse-T components.
- User-observed behavior confirms FieldKit can present a Finder-visible drive
after activation inside the app.

Observed strings and logs indicate:

Expand All @@ -82,10 +85,12 @@ Observed strings and logs indicate:
- FieldKit can re-enumerate the device.
- FieldKit sends MIDI `greet` and `mode` requests.
- FieldKit then opens the MTP device with libmtp.
- FieldKit has strings such as `mountQueue`, `volumeIcon`, `NSFilePromiseProvider`,
download, upload, and internal copy.

Practical inference:

FieldKit sends a Teenage Engineering-specific MIDI request to switch the TP-7 from audio/MIDI mode into MTP mode, waits for USB re-enumeration, and then opens the re-enumerated MTP device through libmtp/libusb.
FieldKit sends a Teenage Engineering-specific MIDI request to switch the TP-7 from audio/MIDI mode into MTP mode, waits for USB re-enumeration, and then opens the re-enumerated MTP device through libmtp/libusb. Its Finder-visible drive does not appear to come from bundled FUSE components or an embedded File Provider extension, so it is likely built on macOS facilities such as a local network filesystem mount or app-coordinated file presentation.

The exact mode-switch payload has been validated independently through CoreMIDI and implemented directly in Rust. FieldKit remains a research reference only, not a runtime dependency.

Expand Down Expand Up @@ -187,19 +192,23 @@ Potential process conflicts:

The CLI should detect and report likely conflicts rather than failing with a generic USB error.

For mounting, macOS requires a FUSE-compatible runtime:
For true arbitrary POSIX filesystem mounting, macOS requires a filesystem runtime:

- macFUSE: mature, but requires kernel extension approval or newer FSKit paths depending on version/macOS.
- Fuse-T: kext-less, attractive for distribution, but needs validation with Rust FUSE tooling.
- FSKit: native user-space filesystem modules on newer macOS, but delivered as an app extension rather than a standalone Rust CLI.
- File Provider: native Finder integration for replicated/cloud-style domains, but also requires an app extension and domain registration.
- WebDAV: built into macOS and mountable from a CLI through `mount_webdav`; suitable for a read-only TP-7 bridge without extensions.

V1 should not require FUSE.
V1 mounting should not require FUSE or app extensions.

## Rust Tooling

Preferred:

- `mtp-rs`: pure Rust MTP implementation, built on `nusb`. This avoids C dependencies and should be the first prototype path.
- `fuser`: Rust FUSE library for a future mount layer.
- A small loopback WebDAV server for the first read-only Finder mount layer.
- `fuser`, FSKit, or File Provider only if WebDAV proves insufficient.

Fallback:

Expand Down Expand Up @@ -358,13 +367,21 @@ reverse mode-switch command is discovered later, this can optionally use it.

### Future Commands

`tp7 mount <mountpoint>`
`tp7 mount [mountpoint]`

Mount the TP-7 as a read-only filesystem. Requires macFUSE or Fuse-T.
Implemented as a read-only Finder volume through a loopback WebDAV server and
macOS `mount_webdav`. With no mount point, it chooses `/Volumes/TP-7`, then
`/Volumes/TP-7 2`, etc. The command keeps running while the mount is active.

`tp7 mount <mountpoint> --read-write`
`tp7 unmount [mountpoint]`

Read-write mount with local write staging and upload-on-close semantics.
Unmount one TP-7 WebDAV mount, or all recorded TP-7 mounts when no mount point
is passed.

`tp7 mount [mountpoint] --read-write`

Not implemented. Read-write mounting needs local write staging and
upload-on-close semantics.

`tp7 sync <remote-path> <local-path>`

Expand Down Expand Up @@ -400,11 +417,14 @@ Current choices:
- Retry transient CoreMIDI endpoint discovery during `--auto-connect` for the
same 12-second window used for MTP visibility because the USB device can
reappear before its MIDI endpoints are ready.
- Keep v1 mount-free.
- `tp7 mount` uses an explicit long-lived MTP session and exposes it via a
local read-only WebDAV bridge.
- `tp7 unmount` without a mount point unmounts all TP-7 mounts recorded by the
CLI.

## Implementation Plan

Phases 0 through 4 now have working prototype coverage: CLI scaffolding, USB detection, diagnostics, CoreMIDI mode switching, MTP status validation, shared session handling, and read-only file exploration. Phase 5 has started with direct `tp7 pull` downloads.
Phases 0 through 7 now have working prototype coverage: CLI scaffolding, USB detection, diagnostics, CoreMIDI mode switching, MTP status validation, shared session handling, read-only file exploration, transfers, mutations, and a read-only WebDAV-backed Finder mount.

### Phase 0: Baseline Project

Expand Down Expand Up @@ -483,14 +503,19 @@ Deliverable:
- `tp7 eject` (implemented as MTP open/close validation)
- dry-run behavior

### Phase 7: Mount Research
### Phase 7: Read-Only Finder Mount

Prototype a read-only FUSE mount after the direct MTP CLI is stable.
Expose the TP-7 through Finder without macFUSE, Fuse-T, kernel extensions, or
app extensions.

Deliverable:

- decision between macFUSE and Fuse-T
- read-only `tp7 mount <mountpoint>` prototype
- `tp7 mount [mountpoint]` (implemented through a local WebDAV server and
`mount_webdav`)
- default `/Volumes/TP-7` mountpoint selection
- `tp7 unmount [mountpoint]`, where no mountpoint unmounts all recorded TP-7
mounts
- byte-range `GET` handling backed by MTP partial reads when available

## Risks

Expand All @@ -516,7 +541,13 @@ Library risk:

Mount risk:

- Finder-style mounting is a separate product surface with caching, partial writes, metadata, and macOS FUSE distribution concerns.
- Finder-style mounting is a separate product surface with caching, byte ranges,
metadata, and many macOS filesystem client behaviors.
- WebDAV keeps distribution simple but must be validated against Finder, Quick
Look, Spotlight, and large TP-7 recordings on hardware.
- Read-write mounting is higher risk because MTP writes are whole-object uploads
while Finder and POSIX clients expect partial writes, safe-save, rename,
metadata, and delete semantics.

## References

Expand Down
3 changes: 3 additions & 0 deletions docs/tp7-handshake.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ What each step taught us:
- MTP support lives behind our own session layer. `ls`, `tree`, `stat`, and
`pull`, `push`, `rename`, and `rm` now use that same switch/open/work/close
flow.
- `mount` keeps a long-lived MTP session open and exposes it to Finder through a
local read-only WebDAV server plus macOS `mount_webdav`; `unmount` tears down
one recorded mount or all recorded TP-7 mounts.
- TP-7 firmware `1.1.9` accepted file upload, rename, and delete in smoke tests,
but rejected folder creation with MTP `GeneralError`.
- `push --overwrite` stages a replacement under a temporary remote name before
Expand Down
24 changes: 24 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ pub enum Command {
#[command(about = "Rename a remote object without moving it")]
Rename(RenameArgs),

#[command(about = "Mount the TP-7 in Finder through a local WebDAV bridge")]
Mount(MountArgs),

#[command(about = "Unmount TP-7 WebDAV mount(s)")]
Unmount(UnmountArgs),

#[command(about = "Open and close an MTP session cleanly")]
Eject,
}
Expand Down Expand Up @@ -221,3 +227,21 @@ pub struct RenameArgs {
#[arg(help = "New name in the same remote folder")]
pub new_name: String,
}

#[derive(Debug, Args)]
pub struct MountArgs {
#[arg(help = "Local mount point; defaults to /Volumes/TP-7")]
pub mountpoint: Option<String>,

#[arg(
long = "read-write",
help = "Allow write operations through the mount; not available yet"
)]
pub read_write: bool,
}

#[derive(Debug, Args)]
pub struct UnmountArgs {
#[arg(help = "Mount point to unmount; omit to unmount all TP-7 mounts")]
pub mountpoint: Option<String>,
}
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod doctor;
mod eject;
mod ls;
mod midi;
mod mount;
mod mtp_session;
mod output;
mod pull;
Expand Down Expand Up @@ -153,6 +154,19 @@ pub fn run() -> Result<(), AppError> {
)?;
output::write_rename(&report, cli.json)
}
Command::Mount(args) => {
let guard = mount::run_mount(
cli.device.as_deref(),
args.mountpoint.as_deref(),
args.read_write,
)?;
output::write_mount(&guard.report, cli.json)?;
guard.wait()
}
Command::Unmount(args) => {
let report = mount::run_unmount(args.mountpoint.as_deref())?;
output::write_unmount(&report, cli.json)
}
Command::Eject => {
let report = eject::run_eject(cli.device.as_deref(), cli.auto_connect)?;
output::write_eject(&report, cli.json)
Expand Down
Loading
Loading