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:
@@ -2,6 +2,7 @@ import { Component, ElementRef, effect, inject, signal, viewChild } from '@angul
|
||||
import { NgIcon } from '@ng-icons/core';
|
||||
import { PanelsService } from '../panels.service';
|
||||
import { IS_MAC_CHROME } from '../platform';
|
||||
import { RpcService } from '../rpc.service';
|
||||
import { SettingsService } from '../settings.service';
|
||||
|
||||
const THEMES = ['system', 'light', 'dark'] as const;
|
||||
@@ -188,6 +189,55 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">Editor</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
<button
|
||||
(click)="settings.setEditor('builtin')"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.editor() === 'builtin'"
|
||||
[class.text-foreground]="settings.editor() === 'builtin'"
|
||||
[class.text-muted-foreground]="settings.editor() !== 'builtin'"
|
||||
>
|
||||
Built-in
|
||||
</button>
|
||||
<button
|
||||
(click)="settings.setEditor('custom')"
|
||||
class="rounded px-2 py-0.5 text-[11px]"
|
||||
[class.bg-accent]="settings.editor() === 'custom'"
|
||||
[class.text-foreground]="settings.editor() === 'custom'"
|
||||
[class.text-muted-foreground]="settings.editor() !== 'custom'"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (settings.editor() === 'custom') {
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2 pl-3">
|
||||
<span class="min-w-0 truncate text-xs text-muted-foreground" [title]="settings.editorApp()">
|
||||
{{ editorAppName() }}
|
||||
</span>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<button
|
||||
(click)="pickEditorApp()"
|
||||
class="rounded-md border border-border px-2 py-0.5 text-[11px] hover:bg-accent"
|
||||
>
|
||||
Choose…
|
||||
</button>
|
||||
@if (settings.editorApp()) {
|
||||
<button
|
||||
(click)="settings.setEditorApp('')"
|
||||
title="Reset to system default"
|
||||
class="grid size-5 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<ng-icon name="mynaX" size="0.7rem" />
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-2.5 flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-muted-foreground">Finished transfers</span>
|
||||
<div class="flex rounded-md border border-border p-0.5">
|
||||
@@ -219,6 +269,7 @@ const CONCURRENCY_CHOICES = [2, 3, 4, 6, 8] as const;
|
||||
export class TabBar {
|
||||
readonly panels = inject(PanelsService);
|
||||
readonly settings = inject(SettingsService);
|
||||
private readonly rpc = inject(RpcService);
|
||||
readonly settingsOpen = signal(false);
|
||||
readonly isMacChrome = IS_MAC_CHROME;
|
||||
readonly themes = THEMES;
|
||||
@@ -248,6 +299,19 @@ export class TabBar {
|
||||
this.panels.closeWorkspace(id);
|
||||
}
|
||||
|
||||
/** "Visual Studio Code.app" -> "Visual Studio Code"; empty -> system default. */
|
||||
editorAppName(): string {
|
||||
const path = this.settings.editorApp();
|
||||
if (!path) return 'System default';
|
||||
const base = path.split('/').filter(Boolean).pop() ?? path;
|
||||
return base.replace(/\.app$/i, '');
|
||||
}
|
||||
|
||||
async pickEditorApp(): Promise<void> {
|
||||
const res = await this.rpc.request.pickEditorApp({});
|
||||
if (res.ok && res.path) this.settings.setEditorApp(res.path);
|
||||
}
|
||||
|
||||
/** Insertion line at the drop position (inset shadow: no layout shift). */
|
||||
dropIndicatorShadow(index: number): string | null {
|
||||
const target = this.dropTarget();
|
||||
|
||||
Reference in New Issue
Block a user