Edit file: built-in editor + external editor with watch & auto-upload
- Built-in editor (default): large modal with monospace textarea, Cmd+S save, dirty guard; reads/writes through the provider streams so it works on local and remote files identically (2MB cap with a friendly error) - External editor: remote files download to appDataDir/edits/<id>, open via macOS `open -a`, the temp DIR is watched (survives atomic rename-saves) and every save enqueues a normal upload job (progress/errors in the transfer list); watchers + temp dirs are cleaned when the session disconnects - Editor setting: Built-in | Custom with a NATIVE app picker (electrobun openFileDialog, /Applications, .app filter) and reset-to-default - Leftover temp files from a crash prompt for cleanup at boot - F4, toolbar button (mynaFileText), and File > Edit File (Cmd+E) menu entry - README: feature notes + Portability notes (open/start/xdg-open seam documented in code and README per user request) Verified headless against the box: sftp read/write roundtrip incl. UTF-8, size cap, watch->save->auto-upload->remote-updated, session cleanup, orphan listing and clearing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+43
-2
@@ -1,6 +1,7 @@
|
||||
import { BrowserView } from "electrobun/bun";
|
||||
import { BrowserView, Utils } from "electrobun/bun";
|
||||
import type { AppRPC } from "../shared/rpc";
|
||||
import { effectiveConcurrency, type Result } from "../shared/types";
|
||||
import { readFileText, writeFileText, type EditManager } from "./edits";
|
||||
import type { SessionRegistry } from "./fs/registry";
|
||||
import { SftpProvider } from "./fs/sftp";
|
||||
import type { ConnectionStore } from "./store/connections";
|
||||
@@ -17,6 +18,7 @@ export interface AppDeps {
|
||||
engine: TransferEngine;
|
||||
store: ConnectionStore;
|
||||
settings: SettingsStore;
|
||||
edits: EditManager;
|
||||
/** Assigned after the rpc object exists (it needs rpc.send). */
|
||||
events: {
|
||||
sessionClosed: (info: { sessionId: string; reason: string }) => void;
|
||||
@@ -32,7 +34,7 @@ async function wrap<T extends object>(fn: () => Promise<T> | T): Promise<Result<
|
||||
}
|
||||
}
|
||||
|
||||
export function createAppRPC({ registry, engine, store, settings, events }: AppDeps) {
|
||||
export function createAppRPC({ registry, engine, store, settings, edits, events }: AppDeps) {
|
||||
return BrowserView.defineRPC<AppRPC>({
|
||||
maxRequestTime: MAX_REQUEST_TIME_MS,
|
||||
handlers: {
|
||||
@@ -45,6 +47,7 @@ export function createAppRPC({ registry, engine, store, settings, events }: AppD
|
||||
if (!sessionId) return;
|
||||
registry.drop(sessionId);
|
||||
engine.failSession(sessionId, reason);
|
||||
edits.stopForSession(sessionId);
|
||||
events.sessionClosed({ sessionId, reason });
|
||||
});
|
||||
sessionId = registry.add(provider);
|
||||
@@ -55,6 +58,7 @@ export function createAppRPC({ registry, engine, store, settings, events }: AppD
|
||||
|
||||
disconnect: ({ sessionId }) =>
|
||||
wrap(async () => {
|
||||
edits.stopForSession(sessionId);
|
||||
await registry.remove(sessionId);
|
||||
console.log(`[rpc] disconnected ${sessionId}`);
|
||||
return {};
|
||||
@@ -160,6 +164,43 @@ export function createAppRPC({ registry, engine, store, settings, events }: AppD
|
||||
engine.setConcurrency(effectiveConcurrency(next));
|
||||
return {};
|
||||
}),
|
||||
|
||||
// ---- file editing ----
|
||||
readFile: ({ sessionId, path }) =>
|
||||
wrap(async () => ({
|
||||
content: await readFileText(registry.get(sessionId), path),
|
||||
})),
|
||||
|
||||
writeFile: ({ sessionId, path, content }) =>
|
||||
wrap(async () => {
|
||||
await writeFileText(registry.get(sessionId), path, content);
|
||||
return {};
|
||||
}),
|
||||
|
||||
editExternal: ({ sessionId, path }) =>
|
||||
wrap(() => edits.editExternal(sessionId, path)),
|
||||
|
||||
pickEditorApp: () =>
|
||||
wrap(async () => {
|
||||
const picked = await Utils.openFileDialog({
|
||||
startingFolder: "/Applications",
|
||||
allowedFileTypes: "app",
|
||||
canChooseFiles: true,
|
||||
canChooseDirectory: false,
|
||||
allowsMultipleSelection: false,
|
||||
});
|
||||
const path = picked.find((p) => p.trim().length > 0) ?? null;
|
||||
return { path };
|
||||
}),
|
||||
|
||||
getEditLeftovers: () =>
|
||||
wrap(async () => ({ names: await edits.listLeftovers() })),
|
||||
|
||||
clearEditLeftovers: () =>
|
||||
wrap(async () => {
|
||||
await edits.clearLeftovers();
|
||||
return {};
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user