From e0a66c7a8ce4b55d987e01c1884ef7fe2c6e9d6b Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Wed, 12 Aug 2026 15:06:30 +0800 Subject: [PATCH] Convert desktop to EchoType Bridge: pipe injection via TIP - Add EchoType to TextInjectionMode enum (becomes default) - Add WriteToEchoTypePipe(): write [UINT32 cch][WCHAR text] to \.\pipe\EchoType\pid_ via NamedPipeClientStream - InsertText(): route to EchoType pipe when mode is EchoType - TrayApplicationContext: add 'EchoType (recommended)' to input mode menu - Update tray branding: 'EchoType Bridge' in tooltips and balloon tips - AppLogger: log to %LocalAppData%\EchoType\bridge.log The bridge keeps WebSocket server + UDP discovery + audio control unchanged. Text injection now goes through the EchoType TIP's composition-commit mechanism for atomic insertion, falling back to clipboard and SendInput as secondary options. Co-Authored-By: Claude --- .../Services/AppLogger.cs | 4 +- .../Services/KeyboardInjectionService.cs | 58 ++++++++++++++++++- .../Tray/TrayApplicationContext.cs | 28 ++++++--- 3 files changed, 78 insertions(+), 12 deletions(-) diff --git a/WirelessTextSyncer.Windows/Services/AppLogger.cs b/WirelessTextSyncer.Windows/Services/AppLogger.cs index 9a98e43..2ea7ccd 100644 --- a/WirelessTextSyncer.Windows/Services/AppLogger.cs +++ b/WirelessTextSyncer.Windows/Services/AppLogger.cs @@ -5,9 +5,9 @@ public static class AppLogger private static readonly object SyncRoot = new(); private static readonly string LogDirectory = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "WirelessTextSyncer"); + "EchoType"); - public static string LogPath => Path.Combine(LogDirectory, "desktop.log"); + public static string LogPath => Path.Combine(LogDirectory, "bridge.log"); public static void Info(string message) { diff --git a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs index 4d611c7..9f79365 100644 --- a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs +++ b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs @@ -1,9 +1,11 @@ -namespace WirelessTextSyncer.Windows.Services; - +using System.IO.Pipes; using System.Runtime.InteropServices; +namespace WirelessTextSyncer.Windows.Services; + public enum TextInjectionMode { + EchoType, ClipboardPaste, SendInputTyping } @@ -22,7 +24,7 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService private const ushort VirtualKeyV = 0x56; private static readonly IntPtr EnglishKeyboardLayout = LoadKeyboardLayout("00000409", KlfActivate); - public TextInjectionMode Mode { get; set; } = TextInjectionMode.ClipboardPaste; + public TextInjectionMode Mode { get; set; } = TextInjectionMode.EchoType; public void InsertText(string text) { @@ -32,6 +34,12 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService } AppLogger.Info($"Inserting text length: {text.Length}. Mode: {Mode}."); + if (Mode == TextInjectionMode.EchoType) + { + WriteToEchoTypePipe(text); + return; + } + if (Mode == TextInjectionMode.SendInputTyping) { TypeTextWithSendInput(text); @@ -41,6 +49,50 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService PasteTextViaClipboard(text); } + private static void WriteToEchoTypePipe(string text) + { + var fgWindow = GetForegroundWindow(); + if (fgWindow == IntPtr.Zero) + { + AppLogger.Error("EchoType pipe: no foreground window."); + return; + } + + GetWindowThreadProcessId(fgWindow, out var pid); + if (pid == 0) + { + AppLogger.Error("EchoType pipe: failed to get PID."); + return; + } + + var pipeName = $@"EchoType\pid_{pid}"; + + try + { + using var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.Out); + pipe.Connect(timeout: 2000); + + var utf16 = System.Text.Encoding.Unicode.GetBytes(text); + var cch = (uint)text.Length; + var header = BitConverter.GetBytes(cch); + + pipe.Write(header, 0, 4); + pipe.Write(utf16, 0, utf16.Length); + pipe.Flush(); + + AppLogger.Info($"EchoType pipe: sent {cch} chars to PID {pid}."); + } + catch (TimeoutException) + { + AppLogger.Error( + $"EchoType pipe: connection timeout for PID {pid}. Is EchoType the active IME in that app?"); + } + catch (Exception ex) + { + AppLogger.Error($"EchoType pipe write failed for PID {pid}.", ex); + } + } + private static void TypeTextWithSendInput(string text) { AppLogger.Info($"Falling back to SendInput text length: {text.Length}"); diff --git a/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs index d6d59fd..6354457 100644 --- a/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs +++ b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs @@ -12,6 +12,7 @@ public sealed class TrayApplicationContext : ApplicationContext private readonly Icon connectedIcon; private readonly Icon waitIcon; private readonly SynchronizationContext uiContext; + private ToolStripMenuItem? echoTypeMenuItem; private ToolStripMenuItem? clipboardPasteMenuItem; private ToolStripMenuItem? sendInputTypingMenuItem; private bool? lastConnectionState; @@ -34,7 +35,7 @@ public sealed class TrayApplicationContext : ApplicationContext notifyIcon = new NotifyIcon { Icon = waitIcon, - Text = "WirelessTextSyncer starting...", + Text = "EchoType Bridge starting...", Visible = true, ContextMenuStrip = BuildMenu() }; @@ -49,7 +50,7 @@ public sealed class TrayApplicationContext : ApplicationContext catch (Exception ex) { AppLogger.Error("Failed to start tray application.", ex); - notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipTitle = "EchoType Bridge"; notifyIcon.BalloonTipText = $"Failed to start: {ex.Message}"; notifyIcon.ShowBalloonTip(5000); } @@ -83,6 +84,10 @@ public sealed class TrayApplicationContext : ApplicationContext private ToolStripMenuItem BuildInputModeMenu() { var inputModeMenu = new ToolStripMenuItem("Input mode"); + echoTypeMenuItem = new ToolStripMenuItem("EchoType (recommended)") + { + CheckOnClick = false + }; clipboardPasteMenuItem = new ToolStripMenuItem("Clipboard paste") { CheckOnClick = false @@ -92,8 +97,10 @@ public sealed class TrayApplicationContext : ApplicationContext CheckOnClick = false }; + echoTypeMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.EchoType); clipboardPasteMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.ClipboardPaste); sendInputTypingMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.SendInputTyping); + inputModeMenu.DropDownItems.Add(echoTypeMenuItem); inputModeMenu.DropDownItems.Add(clipboardPasteMenuItem); inputModeMenu.DropDownItems.Add(sendInputTypingMenuItem); UpdateInputModeMenuChecks(); @@ -106,13 +113,18 @@ public sealed class TrayApplicationContext : ApplicationContext AppLogger.Info($"Text injection mode changed to {mode}."); UpdateInputModeMenuChecks(); - notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipTitle = "EchoType Bridge"; notifyIcon.BalloonTipText = $"Input mode: {GetInputModeLabel(mode)}"; notifyIcon.ShowBalloonTip(1500); } private void UpdateInputModeMenuChecks() { + if (echoTypeMenuItem is not null) + { + echoTypeMenuItem.Checked = keyboard.Mode == TextInjectionMode.EchoType; + } + if (clipboardPasteMenuItem is not null) { clipboardPasteMenuItem.Checked = keyboard.Mode == TextInjectionMode.ClipboardPaste; @@ -126,7 +138,9 @@ public sealed class TrayApplicationContext : ApplicationContext private static string GetInputModeLabel(TextInjectionMode mode) { - return mode == TextInjectionMode.ClipboardPaste ? "Clipboard paste" : "SendInput typing"; + return mode == TextInjectionMode.EchoType ? "EchoType" + : mode == TextInjectionMode.ClipboardPaste ? "Clipboard paste" + : "SendInput typing"; } private void OnTrayIconClicked(object? sender, MouseEventArgs e) @@ -140,7 +154,7 @@ public sealed class TrayApplicationContext : ApplicationContext private void CopyConnectionAddress() { Clipboard.SetText($"ws://{server.LocalIpAddress}:{server.Port}"); - notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipTitle = "EchoType Bridge"; notifyIcon.BalloonTipText = "Connection address copied."; notifyIcon.ShowBalloonTip(1500); } @@ -154,7 +168,7 @@ public sealed class TrayApplicationContext : ApplicationContext var connected = server.HasClient; var status = connected ? "connected" : "waiting"; - var text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})"; + var text = $"EchoType Bridge {server.LocalIpAddress}:{server.Port} ({status})"; notifyIcon.Icon = connected ? connectedIcon : waitIcon; notifyIcon.Text = text.Length > 63 ? text[..63] : text; @@ -168,7 +182,7 @@ public sealed class TrayApplicationContext : ApplicationContext private void ShowConnectionToast(bool connected) { - notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipTitle = "EchoType Bridge"; notifyIcon.BalloonTipText = connected ? $"Device connected to {server.LocalIpAddress}:{server.Port}." : "Device disconnected. Waiting for connection.";