commit 498251ad79465e6c93258a8e16e7c1f4e38d11a0 Author: Misaka Date: Fri May 15 20:12:07 2026 +0800 Initialize Windows desktop skeleton diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e79d75d --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +bin/ +obj/ +.vs/ +.vscode/ +*.user +*.suo +*.log +.env +.env.* + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9e4d10 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# WirelessTextSyncer Desktop + +Windows tray server for WirelessTextSyncer. + +## Stack + +- .NET 8 +- WinForms tray application +- Fleck WebSocket server + +## Development + +```powershell +dotnet restore +dotnet build +dotnet run --project .\WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj +``` + +The initial server listens on `ws://0.0.0.0:8181`. + diff --git a/WirelessTextSyncer.Desktop.sln b/WirelessTextSyncer.Desktop.sln new file mode 100644 index 0000000..9c3c6f0 --- /dev/null +++ b/WirelessTextSyncer.Desktop.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WirelessTextSyncer.Windows", "WirelessTextSyncer.Windows\WirelessTextSyncer.Windows.csproj", "{7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD6081A-F4EB-45AA-917B-2CCFA54E0A67}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WirelessTextSyncer.Windows/Models/SyncAction.cs b/WirelessTextSyncer.Windows/Models/SyncAction.cs new file mode 100644 index 0000000..b784cab --- /dev/null +++ b/WirelessTextSyncer.Windows/Models/SyncAction.cs @@ -0,0 +1,9 @@ +namespace WirelessTextSyncer.Windows.Models; + +public enum SyncAction +{ + InsertText, + Backspace, + Enter, + Ping +} diff --git a/WirelessTextSyncer.Windows/Models/SyncMessage.cs b/WirelessTextSyncer.Windows/Models/SyncMessage.cs new file mode 100644 index 0000000..fb3565d --- /dev/null +++ b/WirelessTextSyncer.Windows/Models/SyncMessage.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace WirelessTextSyncer.Windows.Models; + +public sealed record SyncMessage +{ + [JsonPropertyName("action")] + public SyncAction Action { get; init; } + + [JsonPropertyName("text")] + public string? Text { get; init; } +} diff --git a/WirelessTextSyncer.Windows/Program.cs b/WirelessTextSyncer.Windows/Program.cs new file mode 100644 index 0000000..07a7c84 --- /dev/null +++ b/WirelessTextSyncer.Windows/Program.cs @@ -0,0 +1,16 @@ +namespace WirelessTextSyncer.Windows; + +using WirelessTextSyncer.Windows.Tray; + +static class Program +{ + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + ApplicationConfiguration.Initialize(); + Application.Run(new TrayApplicationContext()); + } +} diff --git a/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs new file mode 100644 index 0000000..351a7b7 --- /dev/null +++ b/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs @@ -0,0 +1,10 @@ +namespace WirelessTextSyncer.Windows.Services; + +public interface IKeyboardInjectionService +{ + void InsertText(string text); + + void Backspace(); + + void Enter(); +} diff --git a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs new file mode 100644 index 0000000..9e661dc --- /dev/null +++ b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs @@ -0,0 +1,24 @@ +namespace WirelessTextSyncer.Windows.Services; + +public sealed class KeyboardInjectionService : IKeyboardInjectionService +{ + public void InsertText(string text) + { + if (string.IsNullOrEmpty(text)) + { + return; + } + + SendKeys.SendWait(text); + } + + public void Backspace() + { + SendKeys.SendWait("{BACKSPACE}"); + } + + public void Enter() + { + SendKeys.SendWait("{ENTER}"); + } +} diff --git a/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs b/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs new file mode 100644 index 0000000..a5dbf75 --- /dev/null +++ b/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs @@ -0,0 +1,33 @@ +using WirelessTextSyncer.Windows.Models; + +namespace WirelessTextSyncer.Windows.Services; + +public sealed class SyncMessageHandler +{ + private readonly IKeyboardInjectionService keyboard; + + public SyncMessageHandler(IKeyboardInjectionService keyboard) + { + this.keyboard = keyboard; + } + + public void Handle(SyncMessage message) + { + switch (message.Action) + { + case SyncAction.InsertText: + keyboard.InsertText(message.Text ?? string.Empty); + break; + case SyncAction.Backspace: + keyboard.Backspace(); + break; + case SyncAction.Enter: + keyboard.Enter(); + break; + case SyncAction.Ping: + break; + default: + throw new InvalidOperationException($"Unsupported sync action: {message.Action}"); + } + } +} diff --git a/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs b/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs new file mode 100644 index 0000000..5c6c38d --- /dev/null +++ b/WirelessTextSyncer.Windows/Services/WebSocketServerService.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Net.Sockets; +using System.Text.Json; +using Fleck; +using WirelessTextSyncer.Windows.Models; + +namespace WirelessTextSyncer.Windows.Services; + +public sealed class WebSocketServerService : IDisposable +{ + private readonly SyncMessageHandler messageHandler; + private readonly List clients = []; + private WebSocketServer? server; + + public WebSocketServerService(SyncMessageHandler messageHandler) + { + this.messageHandler = messageHandler; + } + + public int Port { get; private set; } = 8181; + + public string LocalIpAddress { get; private set; } = "127.0.0.1"; + + public bool HasClient => clients.Count > 0; + + public event EventHandler? StatusChanged; + + public void Start(int port) + { + Port = port; + LocalIpAddress = ResolveLocalIpAddress(); + + server = new WebSocketServer($"ws://0.0.0.0:{Port}"); + server.Start(socket => + { + socket.OnOpen = () => + { + clients.Add(socket); + StatusChanged?.Invoke(this, EventArgs.Empty); + }; + socket.OnClose = () => + { + clients.Remove(socket); + StatusChanged?.Invoke(this, EventArgs.Empty); + }; + socket.OnMessage = HandleRawMessage; + }); + } + + public void Dispose() + { + foreach (var client in clients.ToArray()) + { + client.Close(); + } + + clients.Clear(); + server?.Dispose(); + } + + private void HandleRawMessage(string rawMessage) + { + var message = JsonSerializer.Deserialize(rawMessage); + if (message is null) + { + return; + } + + messageHandler.Handle(message); + } + + private static string ResolveLocalIpAddress() + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + var address = host.AddressList.FirstOrDefault( + address => address.AddressFamily == AddressFamily.InterNetwork + && !IPAddress.IsLoopback(address)); + + return address?.ToString() ?? "127.0.0.1"; + } +} diff --git a/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs new file mode 100644 index 0000000..38bdd2f --- /dev/null +++ b/WirelessTextSyncer.Windows/Tray/TrayApplicationContext.cs @@ -0,0 +1,80 @@ +using WirelessTextSyncer.Windows.Services; + +namespace WirelessTextSyncer.Windows.Tray; + +public sealed class TrayApplicationContext : ApplicationContext +{ + private readonly WebSocketServerService server; + private readonly NotifyIcon notifyIcon; + + public TrayApplicationContext() + { + var keyboard = new KeyboardInjectionService(); + var handler = new SyncMessageHandler(keyboard); + + server = new WebSocketServerService(handler); + server.StatusChanged += (_, _) => UpdateTrayText(); + + notifyIcon = new NotifyIcon + { + Icon = SystemIcons.Application, + Text = "WirelessTextSyncer starting...", + Visible = true, + ContextMenuStrip = BuildMenu() + }; + notifyIcon.MouseClick += OnTrayIconClicked; + + try + { + server.Start(8181); + UpdateTrayText(); + } + catch (Exception ex) + { + notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipText = $"Failed to start: {ex.Message}"; + notifyIcon.ShowBalloonTip(5000); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + notifyIcon.Dispose(); + server.Dispose(); + } + + base.Dispose(disposing); + } + + private ContextMenuStrip BuildMenu() + { + var menu = new ContextMenuStrip(); + menu.Items.Add("Copy connection address", null, (_, _) => CopyConnectionAddress()); + menu.Items.Add("Exit", null, (_, _) => ExitThread()); + return menu; + } + + private void OnTrayIconClicked(object? sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Left) + { + CopyConnectionAddress(); + } + } + + private void CopyConnectionAddress() + { + Clipboard.SetText($"ws://{server.LocalIpAddress}:{server.Port}"); + notifyIcon.BalloonTipTitle = "WirelessTextSyncer"; + notifyIcon.BalloonTipText = "Connection address copied."; + notifyIcon.ShowBalloonTip(1500); + } + + private void UpdateTrayText() + { + var status = server.HasClient ? "connected" : "waiting"; + notifyIcon.Text = $"WirelessTextSyncer {server.LocalIpAddress}:{server.Port} ({status})"; + } +} diff --git a/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj b/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj new file mode 100644 index 0000000..1454a5c --- /dev/null +++ b/WirelessTextSyncer.Windows/WirelessTextSyncer.Windows.csproj @@ -0,0 +1,17 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + + + \ No newline at end of file