refactor: modularize text injection with a factory and dedicated backend implementations, including a new Wayland clipboard option.
This commit is contained in:
113
IO/Injectors/WlClipboardTextInjector.cs
Normal file
113
IO/Injectors/WlClipboardTextInjector.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System.Diagnostics;
|
||||
using Toak.Core;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO.Injectors;
|
||||
|
||||
/// <summary>
|
||||
/// Text injector that writes text to the Wayland clipboard via <c>wl-copy</c>
|
||||
/// and then pastes it using Shift+Insert via <c>wtype</c>.
|
||||
/// Whitespace-only content is typed directly with <c>wtype</c> to avoid
|
||||
/// wl-copy silently stripping leading/trailing spaces from clipboard content.
|
||||
/// </summary>
|
||||
public class WlClipboardTextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text)
|
||||
{
|
||||
Logger.LogDebug("Injecting text using wl-clipboard...");
|
||||
if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
// Write the full text to wl-copy via stdin
|
||||
var copyInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.ClipboardWayland,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
using var copyProcess = Process.Start(copyInfo);
|
||||
if (copyProcess != null)
|
||||
{
|
||||
copyProcess.StandardInput.Write(text);
|
||||
copyProcess.StandardInput.Close();
|
||||
copyProcess.WaitForExit();
|
||||
}
|
||||
|
||||
Task.Delay(100).Wait();
|
||||
|
||||
// Simulate Shift+Insert to paste into the focused window
|
||||
var pasteInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = "-M shift -k Insert -m shift",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var pasteProcess = Process.Start(pasteInfo);
|
||||
pasteProcess?.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WlClipboardTextInjector] Error injecting text: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text into window.");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream)
|
||||
{
|
||||
Logger.LogDebug("Setting up stream injection using wl-clipboard...");
|
||||
var fullText = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
// Collect all tokens first
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Buffering token: '{token}'");
|
||||
fullText += token;
|
||||
}
|
||||
|
||||
// Write the full text to wl-copy via stdin
|
||||
var copyInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.ClipboardWayland,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
using var copyProcess = Process.Start(copyInfo);
|
||||
if (copyProcess != null)
|
||||
{
|
||||
await copyProcess.StandardInput.WriteAsync(fullText);
|
||||
copyProcess.StandardInput.Close();
|
||||
await copyProcess.WaitForExitAsync();
|
||||
}
|
||||
|
||||
await Task.Delay(100);
|
||||
|
||||
// Simulate Shift+Insert to paste into the focused window
|
||||
var pasteInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = "-M shift -k Insert -m shift",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var pasteProcess = Process.Start(pasteInfo);
|
||||
if (pasteProcess != null) await pasteProcess.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WlClipboardTextInjector] Error injecting text stream: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text stream into window.");
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
81
IO/Injectors/WtypeTextInjector.cs
Normal file
81
IO/Injectors/WtypeTextInjector.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Diagnostics;
|
||||
using Toak.Core;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO.Injectors;
|
||||
|
||||
/// <summary>
|
||||
/// Text injector that uses <c>wtype</c> to type text on Wayland.
|
||||
/// </summary>
|
||||
public class WtypeTextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text)
|
||||
{
|
||||
Logger.LogDebug("Injecting text using wtype...");
|
||||
if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var p = Process.Start(pInfo);
|
||||
p?.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WtypeTextInjector] Error injecting text: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text into window.");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream)
|
||||
{
|
||||
Logger.LogDebug("Setting up stream injection using wtype...");
|
||||
var fullText = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(pInfo);
|
||||
if (process == null) return string.Empty;
|
||||
|
||||
Logger.LogDebug("Started wtype stream process, waiting for tokens...");
|
||||
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Injecting token: '{token}'");
|
||||
fullText += token;
|
||||
await process.StandardInput.WriteAsync(token);
|
||||
await process.StandardInput.FlushAsync();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Stream injection complete. Closing standard input.");
|
||||
process.StandardInput.Close();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[WtypeTextInjector] Error injecting text stream: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text stream into window.");
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
81
IO/Injectors/XdotoolTextInjector.cs
Normal file
81
IO/Injectors/XdotoolTextInjector.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Diagnostics;
|
||||
using Toak.Core;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO.Injectors;
|
||||
|
||||
/// <summary>
|
||||
/// Text injector that uses <c>xdotool</c> to type text on X11.
|
||||
/// </summary>
|
||||
public class XdotoolTextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text)
|
||||
{
|
||||
Logger.LogDebug("Injecting text using xdotool...");
|
||||
if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var p = Process.Start(pInfo);
|
||||
p?.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[XdotoolTextInjector] Error injecting text: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text into window.");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream)
|
||||
{
|
||||
Logger.LogDebug("Setting up stream injection using xdotool...");
|
||||
var fullText = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} --file -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(pInfo);
|
||||
if (process == null) return string.Empty;
|
||||
|
||||
Logger.LogDebug("Started xdotool stream process, waiting for tokens...");
|
||||
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Injecting token: '{token}'");
|
||||
fullText += token;
|
||||
await process.StandardInput.WriteAsync(token);
|
||||
await process.StandardInput.FlushAsync();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Stream injection complete. Closing standard input.");
|
||||
process.StandardInput.Close();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[XdotoolTextInjector] Error injecting text stream: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text stream into window.");
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
71
IO/Injectors/YdotoolTextInjector.cs
Normal file
71
IO/Injectors/YdotoolTextInjector.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Diagnostics;
|
||||
using Toak.Core;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO.Injectors;
|
||||
|
||||
/// <summary>
|
||||
/// Text injector that uses <c>ydotool</c> to type text via virtual input (works on both X11 and Wayland).
|
||||
/// </summary>
|
||||
public class YdotoolTextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text)
|
||||
{
|
||||
Logger.LogDebug("Injecting text using ydotool...");
|
||||
if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
var pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var p = Process.Start(pInfo);
|
||||
p?.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[YdotoolTextInjector] Error injecting text: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text into window.");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream)
|
||||
{
|
||||
Logger.LogDebug("Setting up stream injection using ydotool (chunked)...");
|
||||
var fullText = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Injecting token: '{token}'");
|
||||
fullText += token;
|
||||
|
||||
var chunkInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{token.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var chunkP = Process.Start(chunkInfo);
|
||||
if (chunkP != null) await chunkP.WaitForExitAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[YdotoolTextInjector] Error injecting text stream: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text stream into window.");
|
||||
}
|
||||
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Toak.Core;
|
||||
using Toak.Core.Interfaces;
|
||||
|
||||
namespace Toak.IO;
|
||||
|
||||
public class TextInjector(INotifications notifications) : ITextInjector
|
||||
{
|
||||
private readonly INotifications _notifications = notifications;
|
||||
|
||||
public Task InjectTextAsync(string text, string backend = "xdotool")
|
||||
{
|
||||
Logger.LogDebug($"Injecting text: '{text}' with {backend}");
|
||||
if (string.IsNullOrWhiteSpace(text)) return Task.CompletedTask;
|
||||
|
||||
try
|
||||
{
|
||||
ProcessStartInfo pInfo;
|
||||
if (backend.ToLowerInvariant() == "wtype")
|
||||
{
|
||||
Logger.LogDebug($"Injecting text using wtype...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
else if (backend.ToLowerInvariant() == "ydotool")
|
||||
{
|
||||
Logger.LogDebug($"Injecting text using ydotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
else // xdotool
|
||||
{
|
||||
Logger.LogDebug($"Injecting text using xdotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} \"{text.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
}
|
||||
var process = Process.Start(pInfo);
|
||||
process?.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[TextInjector] Error injecting text: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text into window.");
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<string> InjectStreamAsync(IAsyncEnumerable<string> tokenStream, string backend)
|
||||
{
|
||||
var fullText = string.Empty;
|
||||
try
|
||||
{
|
||||
ProcessStartInfo pInfo;
|
||||
if (backend.ToLowerInvariant() == "wtype")
|
||||
{
|
||||
Logger.LogDebug($"Setting up stream injection using wtype...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeWayland,
|
||||
Arguments = $"-d {Constants.Defaults.DefaultTypeDelayMs} -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
}
|
||||
else if (backend.ToLowerInvariant() == "ydotool")
|
||||
{
|
||||
Logger.LogDebug($"Setting up stream injection using ydotool (chunked)...");
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Injecting token: '{token}'");
|
||||
fullText += token;
|
||||
var chunkInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeYdotool,
|
||||
Arguments = $"type \"{token.Replace("\"", "\\\"")}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
var chunkP = Process.Start(chunkInfo);
|
||||
if (chunkP != null) await chunkP.WaitForExitAsync();
|
||||
}
|
||||
return fullText;
|
||||
}
|
||||
else // xdotool
|
||||
{
|
||||
Logger.LogDebug($"Setting up stream injection using xdotool...");
|
||||
pInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Constants.Commands.TypeX11,
|
||||
Arguments = $"type --clearmodifiers --delay {Constants.Defaults.DefaultTypeDelayMs} --file -",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardInput = true
|
||||
};
|
||||
}
|
||||
|
||||
using var process = Process.Start(pInfo);
|
||||
if (process == null) return string.Empty;
|
||||
|
||||
Logger.LogDebug("Started stream injection process, waiting for tokens...");
|
||||
|
||||
await foreach (var token in tokenStream)
|
||||
{
|
||||
Logger.LogDebug($"Injecting token: '{token}'");
|
||||
fullText += token;
|
||||
await process.StandardInput.WriteAsync(token);
|
||||
await process.StandardInput.FlushAsync();
|
||||
}
|
||||
|
||||
Logger.LogDebug("Stream injection complete. Closing standard input.");
|
||||
process.StandardInput.Close();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[TextInjector] Error injecting text stream: {ex.Message}");
|
||||
_notifications.Notify("Injection Error", "Could not type text stream into window.");
|
||||
}
|
||||
return fullText;
|
||||
}
|
||||
}
|
||||
23
IO/TextInjectorFactory.cs
Normal file
23
IO/TextInjectorFactory.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Toak.Core.Interfaces;
|
||||
using Toak.IO.Injectors;
|
||||
|
||||
namespace Toak.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the correct <see cref="ITextInjector"/> implementation based on the configured backend name.
|
||||
/// </summary>
|
||||
public static class TextInjectorFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the appropriate <see cref="ITextInjector"/> for the given <paramref name="backend"/> string.
|
||||
/// Supported values: <c>wtype</c>, <c>wl-clipboard</c>, <c>ydotool</c>, <c>xdotool</c> (default).
|
||||
/// </summary>
|
||||
public static ITextInjector Create(string backend, INotifications notifications) =>
|
||||
backend.ToLowerInvariant() switch
|
||||
{
|
||||
"wtype" => new WtypeTextInjector(notifications),
|
||||
"wl-clipboard" => new WlClipboardTextInjector(notifications),
|
||||
"ydotool" => new YdotoolTextInjector(notifications),
|
||||
_ => new XdotoolTextInjector(notifications)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user