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_<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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,9 @@ public static class AppLogger
|
|||||||
private static readonly object SyncRoot = new();
|
private static readonly object SyncRoot = new();
|
||||||
private static readonly string LogDirectory = Path.Combine(
|
private static readonly string LogDirectory = Path.Combine(
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
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)
|
public static void Info(string message)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
namespace WirelessTextSyncer.Windows.Services;
|
using System.IO.Pipes;
|
||||||
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace WirelessTextSyncer.Windows.Services;
|
||||||
|
|
||||||
public enum TextInjectionMode
|
public enum TextInjectionMode
|
||||||
{
|
{
|
||||||
|
EchoType,
|
||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
SendInputTyping
|
SendInputTyping
|
||||||
}
|
}
|
||||||
@@ -22,7 +24,7 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
|||||||
private const ushort VirtualKeyV = 0x56;
|
private const ushort VirtualKeyV = 0x56;
|
||||||
private static readonly IntPtr EnglishKeyboardLayout = LoadKeyboardLayout("00000409", KlfActivate);
|
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)
|
public void InsertText(string text)
|
||||||
{
|
{
|
||||||
@@ -32,6 +34,12 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
|||||||
}
|
}
|
||||||
|
|
||||||
AppLogger.Info($"Inserting text length: {text.Length}. Mode: {Mode}.");
|
AppLogger.Info($"Inserting text length: {text.Length}. Mode: {Mode}.");
|
||||||
|
if (Mode == TextInjectionMode.EchoType)
|
||||||
|
{
|
||||||
|
WriteToEchoTypePipe(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Mode == TextInjectionMode.SendInputTyping)
|
if (Mode == TextInjectionMode.SendInputTyping)
|
||||||
{
|
{
|
||||||
TypeTextWithSendInput(text);
|
TypeTextWithSendInput(text);
|
||||||
@@ -41,6 +49,50 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService
|
|||||||
PasteTextViaClipboard(text);
|
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)
|
private static void TypeTextWithSendInput(string text)
|
||||||
{
|
{
|
||||||
AppLogger.Info($"Falling back to SendInput text length: {text.Length}");
|
AppLogger.Info($"Falling back to SendInput text length: {text.Length}");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
private readonly Icon connectedIcon;
|
private readonly Icon connectedIcon;
|
||||||
private readonly Icon waitIcon;
|
private readonly Icon waitIcon;
|
||||||
private readonly SynchronizationContext uiContext;
|
private readonly SynchronizationContext uiContext;
|
||||||
|
private ToolStripMenuItem? echoTypeMenuItem;
|
||||||
private ToolStripMenuItem? clipboardPasteMenuItem;
|
private ToolStripMenuItem? clipboardPasteMenuItem;
|
||||||
private ToolStripMenuItem? sendInputTypingMenuItem;
|
private ToolStripMenuItem? sendInputTypingMenuItem;
|
||||||
private bool? lastConnectionState;
|
private bool? lastConnectionState;
|
||||||
@@ -34,7 +35,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
notifyIcon = new NotifyIcon
|
notifyIcon = new NotifyIcon
|
||||||
{
|
{
|
||||||
Icon = waitIcon,
|
Icon = waitIcon,
|
||||||
Text = "WirelessTextSyncer starting...",
|
Text = "EchoType Bridge starting...",
|
||||||
Visible = true,
|
Visible = true,
|
||||||
ContextMenuStrip = BuildMenu()
|
ContextMenuStrip = BuildMenu()
|
||||||
};
|
};
|
||||||
@@ -49,7 +50,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
AppLogger.Error("Failed to start tray application.", ex);
|
AppLogger.Error("Failed to start tray application.", ex);
|
||||||
notifyIcon.BalloonTipTitle = "WirelessTextSyncer";
|
notifyIcon.BalloonTipTitle = "EchoType Bridge";
|
||||||
notifyIcon.BalloonTipText = $"Failed to start: {ex.Message}";
|
notifyIcon.BalloonTipText = $"Failed to start: {ex.Message}";
|
||||||
notifyIcon.ShowBalloonTip(5000);
|
notifyIcon.ShowBalloonTip(5000);
|
||||||
}
|
}
|
||||||
@@ -83,6 +84,10 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
private ToolStripMenuItem BuildInputModeMenu()
|
private ToolStripMenuItem BuildInputModeMenu()
|
||||||
{
|
{
|
||||||
var inputModeMenu = new ToolStripMenuItem("Input mode");
|
var inputModeMenu = new ToolStripMenuItem("Input mode");
|
||||||
|
echoTypeMenuItem = new ToolStripMenuItem("EchoType (recommended)")
|
||||||
|
{
|
||||||
|
CheckOnClick = false
|
||||||
|
};
|
||||||
clipboardPasteMenuItem = new ToolStripMenuItem("Clipboard paste")
|
clipboardPasteMenuItem = new ToolStripMenuItem("Clipboard paste")
|
||||||
{
|
{
|
||||||
CheckOnClick = false
|
CheckOnClick = false
|
||||||
@@ -92,8 +97,10 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
CheckOnClick = false
|
CheckOnClick = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
echoTypeMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.EchoType);
|
||||||
clipboardPasteMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.ClipboardPaste);
|
clipboardPasteMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.ClipboardPaste);
|
||||||
sendInputTypingMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.SendInputTyping);
|
sendInputTypingMenuItem.Click += (_, _) => SetInputMode(TextInjectionMode.SendInputTyping);
|
||||||
|
inputModeMenu.DropDownItems.Add(echoTypeMenuItem);
|
||||||
inputModeMenu.DropDownItems.Add(clipboardPasteMenuItem);
|
inputModeMenu.DropDownItems.Add(clipboardPasteMenuItem);
|
||||||
inputModeMenu.DropDownItems.Add(sendInputTypingMenuItem);
|
inputModeMenu.DropDownItems.Add(sendInputTypingMenuItem);
|
||||||
UpdateInputModeMenuChecks();
|
UpdateInputModeMenuChecks();
|
||||||
@@ -106,13 +113,18 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
AppLogger.Info($"Text injection mode changed to {mode}.");
|
AppLogger.Info($"Text injection mode changed to {mode}.");
|
||||||
UpdateInputModeMenuChecks();
|
UpdateInputModeMenuChecks();
|
||||||
|
|
||||||
notifyIcon.BalloonTipTitle = "WirelessTextSyncer";
|
notifyIcon.BalloonTipTitle = "EchoType Bridge";
|
||||||
notifyIcon.BalloonTipText = $"Input mode: {GetInputModeLabel(mode)}";
|
notifyIcon.BalloonTipText = $"Input mode: {GetInputModeLabel(mode)}";
|
||||||
notifyIcon.ShowBalloonTip(1500);
|
notifyIcon.ShowBalloonTip(1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInputModeMenuChecks()
|
private void UpdateInputModeMenuChecks()
|
||||||
{
|
{
|
||||||
|
if (echoTypeMenuItem is not null)
|
||||||
|
{
|
||||||
|
echoTypeMenuItem.Checked = keyboard.Mode == TextInjectionMode.EchoType;
|
||||||
|
}
|
||||||
|
|
||||||
if (clipboardPasteMenuItem is not null)
|
if (clipboardPasteMenuItem is not null)
|
||||||
{
|
{
|
||||||
clipboardPasteMenuItem.Checked = keyboard.Mode == TextInjectionMode.ClipboardPaste;
|
clipboardPasteMenuItem.Checked = keyboard.Mode == TextInjectionMode.ClipboardPaste;
|
||||||
@@ -126,7 +138,9 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
|
|
||||||
private static string GetInputModeLabel(TextInjectionMode mode)
|
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)
|
private void OnTrayIconClicked(object? sender, MouseEventArgs e)
|
||||||
@@ -140,7 +154,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
private void CopyConnectionAddress()
|
private void CopyConnectionAddress()
|
||||||
{
|
{
|
||||||
Clipboard.SetText($"ws://{server.LocalIpAddress}:{server.Port}");
|
Clipboard.SetText($"ws://{server.LocalIpAddress}:{server.Port}");
|
||||||
notifyIcon.BalloonTipTitle = "WirelessTextSyncer";
|
notifyIcon.BalloonTipTitle = "EchoType Bridge";
|
||||||
notifyIcon.BalloonTipText = "Connection address copied.";
|
notifyIcon.BalloonTipText = "Connection address copied.";
|
||||||
notifyIcon.ShowBalloonTip(1500);
|
notifyIcon.ShowBalloonTip(1500);
|
||||||
}
|
}
|
||||||
@@ -154,7 +168,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
|
|
||||||
var connected = server.HasClient;
|
var connected = server.HasClient;
|
||||||
var status = connected ? "connected" : "waiting";
|
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.Icon = connected ? connectedIcon : waitIcon;
|
||||||
notifyIcon.Text = text.Length > 63 ? text[..63] : text;
|
notifyIcon.Text = text.Length > 63 ? text[..63] : text;
|
||||||
|
|
||||||
@@ -168,7 +182,7 @@ public sealed class TrayApplicationContext : ApplicationContext
|
|||||||
|
|
||||||
private void ShowConnectionToast(bool connected)
|
private void ShowConnectionToast(bool connected)
|
||||||
{
|
{
|
||||||
notifyIcon.BalloonTipTitle = "WirelessTextSyncer";
|
notifyIcon.BalloonTipTitle = "EchoType Bridge";
|
||||||
notifyIcon.BalloonTipText = connected
|
notifyIcon.BalloonTipText = connected
|
||||||
? $"Device connected to {server.LocalIpAddress}:{server.Port}."
|
? $"Device connected to {server.LocalIpAddress}:{server.Port}."
|
||||||
: "Device disconnected. Waiting for connection.";
|
: "Device disconnected. Waiting for connection.";
|
||||||
|
|||||||
Reference in New Issue
Block a user