| Commit message (Collapse) | Author | Age | Files | Lines |
| |
|
|
|
|
|
|
|
|
| |
Phase 5 turns the pure transfer-specs from Phase 3 into running transfers. duet--run-transfer spawns the backend over make-process, and a serial queue (duet-max-concurrent-transfers, default 1) holds the rest until a slot frees. Each transfer carries a state machine: queued, running, stalled, cancelling, then a terminal done, failed, or cleanup-unverified.
The process filter is the hot path, so it stays cheap: it counts output, bounds it to a trailing window, and schedules one coalesced log render. It never refreshes panes. Pane refresh runs once per batch from the sentinel, also coalesced. A move deletes its sources only after the copy exits 0, so a failed move leaves the source untouched. Cancellation kills the process and, for a backend declaring verifiable cleanup, checks the destination for stray temp files before settling on failed versus cleanup-unverified.
The process boundary and the temp-file lister are injectable, so the queue and classification logic test against fake results while real rsync gets its own slow integration file. duet-copy, duet-move, duet-mkdir, and duet-delete are wired to the engine. The viewer actions stay stubs until their phase. In-process TRAMP and both-remote rsync still refuse to run here. They land with Phase 6.
172 tests, duet.el at 100% line coverage, compile/lint/complexity green.
|
| |
|
|
| |
In a pane, q ran dired's quit-window, which closes only the window it is in, so quitting left the other pane behind. Binding q to duet-quit in duet-mode-map makes it tear down both panes and restore the pre-launch layout, the same as F10. q is the key a commander user reaches for, so it should mean "leave the commander," not "close this window."
|
| |
|
|
|
|
|
|
|
|
| |
The duet command lays out two side-by-side dired panes, each in the buffer-local duet-mode, which carries the mc/Norton F-key map (F3 view, F4 edit, F5 copy, F6 move, F7 mkdir, F8 delete, F10 quit). The map is a minor-mode keymap, so the single-key actions fire only inside a pane and leave the global F-keys untouched elsewhere. duet-quit restores the window layout from before launch (Phase 4 in the design spec).
duet--other-pane is the explicit other-pane resolution that replaces dired-dwim-target, the dirvish#36 fix. Its logic lives in a pure helper, duet--sibling-pane, that takes the active window and the list of commander panes and returns the sibling, erroring clearly when there are not exactly two. Keeping the decision pure makes the dangerous part testable without a live frame.
DUET does not depend on dirvish. A pane is a plain dired buffer, and when the user has dirvish rendering dired buffers it renders as dirvish with no extra work. That keeps dirvish recommended but never required, and avoids binding the package to a specific dirvish entry point.
The transfer and file actions are stubs that announce themselves until their phases land, so the keys are bound and their precedence is tested now. The pure selector, the keymap, and minor-mode precedence are unit-tested; the live two-pane launch and the F-key feel in a running Emacs are a manual check.
|
| |
|
|
|
|
|
|
| |
Phase 3 shipped conflict and move sequencing but skipped the data-safety checks the design spec makes the pure planner's job. This adds them: same-file rejection, dir-into-itself rejection, trailing-slash destination resolution, special-file rejection, symlink surfacing, case-insensitive collision detection, and path-length and reserved-name checks.
Every check is pure. The filesystem facts each one needs (a path's lstat type, the names already at a destination, whether that filesystem folds case, its path-length limit, its reserved names) are injected, so the planner decides before a byte moves and the tests never touch a real file. Each problem is a plist with a class, a severity, the file, and a message. An error severity blocks a transfer, and a warning surfaces a decision, which is how a symlink is carried as follow-versus-preserve rather than silently chosen.
duet--plan-move-safe composes the checks with the move planner: a source carrying a blocking error is skipped with no copy and no delete, a safe source gets a copy and a delete gated on its copy success, and no delete is ever ungated. Coverage on duet.el rises to 92.3%.
|
| |
|
|
|
|
|
|
|
|
| |
duet--rsync-command built one direct rsync argv for every pair, rendering each remote endpoint as host:path. That is right when at most one endpoint is remote, but rsync refuses a source and destination that are both remote, so the argv was unexecutable for every remote-to-remote route, not just the round-trip.
Now rsync builds a single argv only for :local and :local-remote, the routes it can run in one invocation. A both-remote pair returns a deferred spec: nil argv, an :exec-mode marker, and the route. Phase 6 reads the route to run rsync on a host (same-host), route through this machine (round-trip), or go direct host-to-host when the override is set.
transfer-spec now carries the classified :src-endpoint and :dst-endpoint alongside the route, so execution has the structured endpoints and never has to reinterpret an argv string. The runnable-shape check from the in-process-mode fix extends to accept :exec-mode, so a deferred spec reads as a real plan rather than a broken nil-argv command.
Tests cover local-to-remote and remote-to-local argv shapes including a port, and the same-host, round-trip, and direct routes each producing a deferred spec.
|
| |
|
|
|
|
|
|
| |
Two coupled holes surfaced in the Phase 0-3 review. duet--transfer-spec copied only :argv, :default-directory, and :process-environment out of a backend's command result, dropping the :tramp marker, so a TRAMP-routed spec arrived as :argv nil with nothing telling the executor to copy in process. And duet--check-command leaned on listp, where nil is a list in Elisp, so a command builder returning nil or a bare :argv nil passed the minimum tier.
Both turn on the same idea, so they share a fix. duet--command-spec-executable-p defines a runnable spec: a non-empty plist with either a non-empty :argv of strings (a CLI backend) or a declared in-process mode such as :tramp. The contract checker rejects anything else, and transfer-spec now carries :tramp through, so the TRAMP fallback has a positive execution signal rather than an ambiguous nil argv.
The legitimate TRAMP backend keeps passing because it declares its mode. A broken backend that forgets argv no longer slips through.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
duet--transfer-spec classifies both endpoints, selects a backend through the registry, determines the route, and delegates argv construction to the backend (Phase 3 in the design spec). It returns the plist the executor will run: sources, destination, backend, route, argv, and async flag.
The route is decided independently of backend by duet--transfer-route: local, local-remote, remote-same-host, remote-direct, or remote-roundtrip. Different remote hosts default to the round-trip through this machine. Direct host-to-host runs only when a per-connection override asks for it, never automatically, because a direct route can silently fail where a round-trip always works.
This phase also registers the two stage-1 backends through the same duet-register-backend seam a plugin uses: rsync for local and ssh-reachable endpoints, TRAMP as the universal fallback that costs more so rsync wins whenever it applies. rsync receives its source and destination as separate argv elements, so a filename with spaces or shell metacharacters stays inert.
The two planners are pure and prompt-free, so the dangerous decisions are testable before a byte moves. duet--plan-conflicts resolves overwrite/skip/rename per file with an apply-to-all that stops asking, taking the existence check and the resolver as injected functions. duet--plan-move pairs each source's copy with a delete gated on that source's copy success, so a failed copy can never delete its source.
Remote-to-remote execution (honoring the round-trip route as a two-step through local) and TRAMP's in-process copy land in Phase 6. Here transfer-spec records the route and the rsync path the executor will use.
|
| |
|
|
|
|
|
|
|
|
| |
This is the seam every transport plugs into (Phase 2 in the design spec). A duet-backend struct carries a scorer, a command builder, capability flags, and contract metadata; duet-register-backend keeps a registry where re-registering a name replaces the prior backend, which is how a user or plugin overrides a built-in. duet--select-backend asks every backend to score an endpoint pair and picks the lowest cost, breaking ties toward the most recently registered.
The failure-normalizer interface turns a raw process failure into a class, a cause, evidence, a safety outcome, and next actions. Backends supply a pattern table via duet-define-cli-failure-patterns; anything unmatched falls back to a minimal normalizer that covers launch failure, missing executable, stall, signal, and unknown exit. This is what will let DUET explain a failure instead of dumping stderr.
Secrets are redacted before they reach a log: a pattern keeps its group-1 field label and strips the value, and a backend with no secret surface declares :none rather than an empty pattern list, so a forgotten declaration (nil) stays distinguishable from a deliberate one.
The tiered contract checks (minimum, publishable, capability) return a list of violations a backend author asserts is empty in one ERT test. The built-in rsync and TRAMP backends register through this same API in Phase 3, once their command builders land; here the machinery is exercised by fake backends.
|
| |
|
|
|
|
|
|
| |
duet--classify-path turns a path string into a plist describing where it lives and how to reach it: :locality (local or remote), :method, :user, :host, :port, :localname, and :hop for multi-hop paths. It's the pure foundation the transfer-spec and connection layers build on (Phase 1 in the design spec).
TRAMP does the dissection, so any path file-remote-p recognizes is remote and everything else is local. A local path is expanded, so a leading ~ resolves to the home directory. A remote localname is kept verbatim because a ~ there is the remote home, not this machine's. Classification never signals: an incomplete string like /ssh:host that TRAMP rejects as a remote name falls back to local, since validating raw TRAMP input belongs to the connection reader, not here.
I probed TRAMP's real contract before writing the tests (port comes back as a string, a multi-hop path reports the final host with the leading hops in :hop), so the Normal/Boundary/Error cases assert what TRAMP actually returns rather than what I'd have guessed.
|
|
|
Initial skeleton for Duet, a two-pane file commander built on dirvish/dired: the package header and entry-command stub, the README, and the test directory. Pre-alpha — no functionality yet.
|