using System; using System.Collections.Generic; using System.IO; using System.Linq; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Platform.Storage; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels; namespace ClaudeDo.Ui.Views; public partial class InteractiveTerminalView : UserControl { public InteractiveTerminalView() { InitializeComponent(); TerminalHost.Loaded += OnTerminalHostLoaded; DataContextChanged += OnDataContextChanged; // Tunnel: TerminalControl handles KeyDown itself and forwards Ctrl+V to the child as a raw // control byte, where nothing acts on it — paste has to be intercepted before it gets there. AddHandler(KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); AddHandler(DragDrop.DragOverEvent, OnDragOver); AddHandler(DragDrop.DropEvent, OnDrop); } private void OnDataContextChanged(object? sender, EventArgs e) { if (DataContext is InteractiveTerminalViewModel vm && TerminalHost.IsLoaded) vm.AttachControl(TerminalHost); } private void OnTerminalHostLoaded(object? sender, RoutedEventArgs e) { if (DataContext is InteractiveTerminalViewModel vm) vm.AttachControl(TerminalHost); } private void OnPreviewKeyDown(object? sender, KeyEventArgs e) { // AltGr arrives as Ctrl+Alt; the library would turn it into a control sequence (and mark it handled). if (IsAltGrText(e.KeyModifiers, e.KeySymbol)) { if (DataContext is not InteractiveTerminalViewModel altGrVm) return; e.Handled = true; _ = SendTextAndReportAsync(altGrVm, e.KeySymbol!); return; } if (e.Key != Key.V || !e.KeyModifiers.HasFlag(KeyModifiers.Control) || e.KeyModifiers.HasFlag(KeyModifiers.Alt)) return; if (DataContext is not InteractiveTerminalViewModel vm) return; e.Handled = true; _ = PasteAndReportAsync(this, vm); } // A clipboard image (e.g. a screenshot) can't be typed into the child process, so it's written // to a temp PNG and its path is inserted the same way a dropped file is — Claude reads the file // itself. Plain-text clipboard content keeps going through the existing bracketed-paste flow. // Once a bitmap is confirmed present, any failure (dir/file write, send) is a real paste // failure — never silently fall back to a text paste of the same clipboard. private static async System.Threading.Tasks.Task PasteAndReportAsync(InteractiveTerminalView view, InteractiveTerminalViewModel vm) { Avalonia.Media.Imaging.Bitmap? bitmap = null; try { bitmap = await TryGetClipboardBitmapAsync(TopLevel.GetTopLevel(view)); if (bitmap is null) { if (!await vm.PasteAsync()) vm.ReportSendFailed(); return; } var path = ClipboardImageTempStore.NewPngPath(DateTime.UtcNow); Directory.CreateDirectory(ClipboardImageTempStore.Directory); bitmap.Save(path); if (!await vm.SendTextAsync(FormatDroppedPaths(new[] { path }))) vm.ReportSendFailed(); } catch (Exception) { vm.ReportSendFailed(); } finally { bitmap?.Dispose(); } } private static async System.Threading.Tasks.Task TryGetClipboardBitmapAsync(TopLevel? topLevel) { if (topLevel?.Clipboard is not { } clipboard) return null; try { using var data = await clipboard.TryGetDataAsync(); return data is null ? null : await Avalonia.Input.AsyncDataTransferExtensions.TryGetBitmapAsync(data); } catch (Exception) { return null; } } private static bool IsFilesDrop(DragEventArgs e) => e.DataTransfer?.Contains(DataFormat.File) == true; private void OnDragOver(object? sender, DragEventArgs e) { e.DragEffects = IsFilesDrop(e) ? DragDropEffects.Copy : DragDropEffects.None; e.Handled = true; } // Dropping files types their paths into the session's composer (no Enter) — the common case is // "look at this file", not "run something". private void OnDrop(object? sender, DragEventArgs e) { if (DataContext is not InteractiveTerminalViewModel vm || !IsFilesDrop(e)) return; e.Handled = true; var text = FormatDroppedPaths(e.DataTransfer!.TryGetFiles()?.Select(i => i.TryGetLocalPath())); if (text.Length > 0) _ = SendTextAndReportAsync(vm, text); } private static async System.Threading.Tasks.Task SendTextAndReportAsync(InteractiveTerminalViewModel vm, string text) { if (!await vm.SendTextAsync(text)) vm.ReportSendFailed(); } /// True for an AltGr keystroke (Windows reports it as Ctrl+Alt) whose /// is already a single printable character, e.g. "@" or "\". internal static bool IsAltGrText(KeyModifiers modifiers, string? keySymbol) => modifiers.HasFlag(KeyModifiers.Control) && modifiers.HasFlag(KeyModifiers.Alt) && keySymbol is { Length: 1 } && !char.IsControl(keySymbol[0]); /// Joins dropped file paths into one insertable chunk: quoted when they contain a /// space, space-separated, with a trailing space so the user can keep typing. internal static string FormatDroppedPaths(IEnumerable? paths) { var usable = (paths ?? Enumerable.Empty()) .Where(p => !string.IsNullOrWhiteSpace(p)) .Select(p => p!.Contains(' ') ? $"\"{p}\"" : p) .ToList(); return usable.Count == 0 ? string.Empty : string.Join(" ", usable) + " "; } }