-
-
Notifications
You must be signed in to change notification settings - Fork 266
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
f156e89
commit 0ec62b9
Showing
23 changed files
with
1,000 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
namespace LenovoLegionToolkit.Lib.Macro; | ||
|
||
public enum MacroDirection | ||
{ | ||
Unknown, | ||
Down, | ||
Up | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
using Autofac; | ||
using LenovoLegionToolkit.Lib.Extensions; | ||
using LenovoLegionToolkit.Lib.Macro.Utils; | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro; | ||
|
||
public class IoCModule : Module | ||
{ | ||
protected override void Load(ContainerBuilder builder) | ||
{ | ||
builder.Register<MacroSettings>(); | ||
builder.Register<MacroController>(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
LenovoLegionToolkit.Lib.Macro/LenovoLegionToolkit.Lib.Macro.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>net8.0-windows</TargetFramework> | ||
<RuntimeIdentifier>win-x64</RuntimeIdentifier> | ||
<Platforms>x64</Platforms> | ||
<Nullable>enable</Nullable> | ||
<Copyright>© 2024 Bartosz Cichecki</Copyright> | ||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
<NeutralLanguage>en</NeutralLanguage> | ||
<UseWindowsForms>true</UseWindowsForms> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<ProjectReference Include="..\LenovoLegionToolkit.Lib\LenovoLegionToolkit.Lib.csproj" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Runtime.CompilerServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using LenovoLegionToolkit.Lib.Macro.Utils; | ||
using Windows.Win32; | ||
using Windows.Win32.Foundation; | ||
using Windows.Win32.UI.WindowsAndMessaging; | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro; | ||
|
||
public class MacroController | ||
{ | ||
public class RecorderReceivedEventArgs : EventArgs | ||
{ | ||
public MacroEvent MacroEvent { get; init; } | ||
} | ||
|
||
private static readonly uint[] AllowedKeys = [0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69]; | ||
public static readonly int[] AllowedRepeatCounts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; | ||
|
||
private readonly MacroRecorder _recorder = new(); | ||
private readonly MacroPlayer _player = new(); | ||
|
||
private readonly HOOKPROC _kbProc; | ||
private readonly MacroSettings _settings; | ||
|
||
private HHOOK _kbHook; | ||
private CancellationTokenSource? _cancellationTokenSource; | ||
|
||
public event EventHandler<RecorderReceivedEventArgs>? RecorderReceived; | ||
|
||
public MacroController(MacroSettings settings) | ||
{ | ||
_settings = settings; | ||
|
||
_kbProc = LowLevelKeyboardProc; | ||
|
||
_recorder.Received += Recorder_Received; | ||
} | ||
|
||
private void Recorder_Received(object? sender, MacroRecorder.ReceivedEventArgs e) => RecorderReceived?.Invoke(this, new() { MacroEvent = e.MacroEvent }); | ||
|
||
public bool IsEnabled => _settings.Store.IsEnabled; | ||
|
||
public void SetEnabled(bool enabled) | ||
{ | ||
_settings.Store.IsEnabled = enabled; | ||
_settings.SynchronizeStore(); | ||
} | ||
|
||
public Dictionary<ulong, MacroSequence> GetSequences() => _settings.Store.Sequences; | ||
|
||
public void SetSequences(Dictionary<ulong, MacroSequence> sequences) | ||
{ | ||
_settings.Store.Sequences = sequences; | ||
_settings.SynchronizeStore(); | ||
} | ||
|
||
public void Start() | ||
{ | ||
if (_kbHook != default) | ||
return; | ||
|
||
_kbHook = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _kbProc, HINSTANCE.Null, 0); | ||
} | ||
|
||
public void StartRecording() => _recorder.StartRecording(); | ||
|
||
public void StopRecording() => _recorder.StopRecording(); | ||
|
||
private unsafe LRESULT LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) | ||
{ | ||
if (nCode != PInvoke.HC_ACTION) | ||
return PInvoke.CallNextHookEx(HHOOK.Null, nCode, wParam, lParam); | ||
|
||
if (!IsEnabled) | ||
return PInvoke.CallNextHookEx(HHOOK.Null, nCode, wParam, lParam); | ||
|
||
ref var kbStruct = ref Unsafe.AsRef<KBDLLHOOKSTRUCT>((void*)lParam.Value); | ||
|
||
var shouldRun = !_recorder.IsRecording; | ||
shouldRun &= kbStruct.flags == 0; | ||
shouldRun &= AllowedKeys.Contains(kbStruct.vkCode); | ||
shouldRun &= _settings.Store.Sequences.ContainsKey(kbStruct.vkCode); | ||
|
||
if (!shouldRun) | ||
return PInvoke.CallNextHookEx(HHOOK.Null, nCode, wParam, lParam); | ||
|
||
var sequence = _settings.Store.Sequences[kbStruct.vkCode]; | ||
|
||
_cancellationTokenSource?.Cancel(); | ||
_cancellationTokenSource = new CancellationTokenSource(); | ||
var token = _cancellationTokenSource.Token; | ||
|
||
Play(sequence, token); | ||
|
||
// Returning a value greater than zero to prevent other hooks from handling the keypress | ||
return new LRESULT(96); | ||
|
||
} | ||
|
||
private void Play(MacroSequence sequence, CancellationToken token) => Task.Run(() => _player.PlayAsync(sequence, token), token); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
using System; | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro; | ||
|
||
public readonly struct MacroSequence | ||
{ | ||
public bool IgnoreDelays { get; init; } | ||
public int RepeatCount { get; init; } | ||
public MacroEvent[]? Events { get; init; } | ||
} | ||
|
||
public readonly struct MacroEvent | ||
{ | ||
public MacroDirection Direction { get; init; } | ||
public ulong Key { get; init; } | ||
public TimeSpan Delay { get; init; } | ||
|
||
public bool IsUndefined() => Direction == MacroDirection.Unknown || Key < 1; | ||
|
||
public override string ToString() => $"{nameof(Direction)}: {Direction}, {nameof(Key)}: {Key}, {nameof(Delay)}: {Delay}"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
using System.Runtime.InteropServices; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Windows.Win32; | ||
using Windows.Win32.UI.Input.KeyboardAndMouse; | ||
|
||
// ReSharper disable once MemberCanBeMadeStatic.Global | ||
#pragma warning disable CA1822 // Mark members as static | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro.Utils; | ||
|
||
internal class MacroPlayer | ||
{ | ||
public async Task PlayAsync(MacroSequence sequence, CancellationToken token) | ||
{ | ||
for (var i = 0; i < sequence.RepeatCount; i++) | ||
{ | ||
foreach (var macroEvent in sequence.Events ?? []) | ||
{ | ||
if (!sequence.IgnoreDelays) | ||
await Task.Delay(macroEvent.Delay, token).ConfigureAwait(false); | ||
|
||
token.ThrowIfCancellationRequested(); | ||
|
||
var input = ToInput(macroEvent); | ||
PInvoke.SendInput(MemoryMarshal.CreateSpan(ref input, 1), Marshal.SizeOf<INPUT>()); | ||
} | ||
|
||
token.ThrowIfCancellationRequested(); | ||
} | ||
} | ||
|
||
private static INPUT ToInput(MacroEvent macroEvent) => new() | ||
{ | ||
type = INPUT_TYPE.INPUT_KEYBOARD, | ||
Anonymous = new INPUT._Anonymous_e__Union | ||
{ | ||
ki = new KEYBDINPUT | ||
{ | ||
wVk = (VIRTUAL_KEY)macroEvent.Key, | ||
dwFlags = macroEvent.Direction switch | ||
{ | ||
MacroDirection.Up => KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP, | ||
_ => 0 | ||
} | ||
} | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Runtime.CompilerServices; | ||
using Windows.Win32; | ||
using Windows.Win32.Foundation; | ||
using Windows.Win32.UI.WindowsAndMessaging; | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro.Utils; | ||
|
||
internal class MacroRecorder | ||
{ | ||
public class ReceivedEventArgs : EventArgs | ||
{ | ||
public MacroEvent MacroEvent { get; init; } | ||
} | ||
|
||
private class MacroEventEqualityComparer : IEqualityComparer<MacroEvent> | ||
{ | ||
public bool Equals(MacroEvent x, MacroEvent y) => x.Key == y.Key; | ||
|
||
public int GetHashCode(MacroEvent obj) => HashCode.Combine(obj.Key); | ||
} | ||
|
||
private readonly HashSet<MacroEvent> _rolloverCache = new(new MacroEventEqualityComparer()); | ||
|
||
private readonly HOOKPROC _kbProc; | ||
|
||
private HHOOK _kbHook; | ||
private TimeSpan _timeFromLastEvent; | ||
|
||
public bool IsRecording => _kbHook != HHOOK.Null; | ||
|
||
public event EventHandler<ReceivedEventArgs>? Received; | ||
|
||
public MacroRecorder() | ||
{ | ||
_kbProc = LowLevelKeyboardProc; | ||
} | ||
|
||
public void StartRecording() | ||
{ | ||
if (_kbHook != HHOOK.Null) | ||
return; | ||
|
||
_timeFromLastEvent = TimeSpan.Zero; | ||
|
||
_kbHook = PInvoke.SetWindowsHookEx(WINDOWS_HOOK_ID.WH_KEYBOARD_LL, _kbProc, HINSTANCE.Null, 0); | ||
} | ||
|
||
public void StopRecording() | ||
{ | ||
PInvoke.UnhookWindowsHookEx(_kbHook); | ||
_kbHook = default; | ||
|
||
_timeFromLastEvent = TimeSpan.Zero; | ||
} | ||
|
||
private unsafe LRESULT LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) | ||
{ | ||
if (nCode < 0) | ||
return PInvoke.CallNextHookEx(HHOOK.Null, nCode, wParam, lParam); | ||
|
||
// Returning a value greater than zero to prevent other hooks from handling the keypress | ||
var result = new LRESULT(69); | ||
|
||
ref var kbStruct = ref Unsafe.AsRef<KBDLLHOOKSTRUCT>((void*)lParam.Value); | ||
|
||
var macroEvent = ConvertToMacroEvent(wParam, kbStruct, _timeFromLastEvent); | ||
|
||
if (!macroEvent.HasValue) | ||
return result; | ||
|
||
if (macroEvent.Value.IsUndefined()) | ||
return result; | ||
|
||
if (macroEvent.Value.Direction == MacroDirection.Down && _rolloverCache.Contains(macroEvent.Value)) | ||
return result; | ||
|
||
Received?.Invoke(this, new ReceivedEventArgs { MacroEvent = macroEvent.Value }); | ||
|
||
_timeFromLastEvent = TimeSpan.FromMilliseconds(kbStruct.time); | ||
|
||
if (macroEvent.Value.Direction == MacroDirection.Down) | ||
_rolloverCache.Add(macroEvent.Value); | ||
else | ||
_rolloverCache.Remove(macroEvent.Value); | ||
|
||
return result; | ||
} | ||
|
||
private static MacroEvent? ConvertToMacroEvent(WPARAM wParam, KBDLLHOOKSTRUCT kbStruct, TimeSpan timeFromLastEvent) | ||
{ | ||
if (timeFromLastEvent == TimeSpan.Zero) | ||
timeFromLastEvent = TimeSpan.FromMilliseconds(kbStruct.time); | ||
|
||
var delay = TimeSpan.FromMilliseconds(kbStruct.time) - timeFromLastEvent; | ||
|
||
var macroEvent = new MacroEvent | ||
{ | ||
Direction = (uint)wParam switch | ||
{ | ||
PInvoke.WM_KEYUP or PInvoke.WM_SYSKEYUP => MacroDirection.Up, | ||
PInvoke.WM_KEYDOWN or PInvoke.WM_SYSKEYDOWN => MacroDirection.Down, | ||
_ => MacroDirection.Unknown | ||
}, | ||
Key = kbStruct.vkCode, | ||
Delay = delay | ||
}; | ||
|
||
return macroEvent; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
using System.Collections.Generic; | ||
using LenovoLegionToolkit.Lib.Settings; | ||
|
||
namespace LenovoLegionToolkit.Lib.Macro.Utils; | ||
|
||
public class MacroSettings() : AbstractSettings<MacroSettings.MacroSettingsStore>("macro.json") | ||
{ | ||
public class MacroSettingsStore | ||
{ | ||
public bool IsEnabled { get; set; } | ||
|
||
public Dictionary<ulong, MacroSequence> Sequences { get; set; } = []; | ||
} | ||
|
||
protected override MacroSettingsStore Default => new(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -143,6 +143,7 @@ GetDpiForMonitor | |
|
||
RegNotifyChangeKeyValue | ||
|
||
SendInput | ||
SendMessage | ||
|
||
CallNtPowerInformation | ||
|
Oops, something went wrong.