Skip to content

Commit

Permalink
Macro (#1289)
Browse files Browse the repository at this point in the history
  • Loading branch information
BartoszCichecki authored May 15, 2024
1 parent f156e89 commit 0ec62b9
Show file tree
Hide file tree
Showing 23 changed files with 1,000 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Platforms>x64</Platforms>
<Nullable>enable</Nullable>
<Copyright2023 Bartosz Cichecki</Copyright>
<Copyright2024 Bartosz Cichecki</Copyright>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NeutralLanguage>en</NeutralLanguage>
</PropertyGroup>
Expand Down
8 changes: 8 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/Enums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace LenovoLegionToolkit.Lib.Macro;

public enum MacroDirection
{
Unknown,
Down,
Up
}
14 changes: 14 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/IoCModule.cs
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 LenovoLegionToolkit.Lib.Macro/LenovoLegionToolkit.Lib.Macro.csproj
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>
106 changes: 106 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/MacroController.cs
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);
}
21 changes: 21 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/Structs.cs
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}";
}
49 changes: 49 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/Utils/MacroPlayer.cs
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
}
}
}
};
}
112 changes: 112 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/Utils/MacroRecorder.cs
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;
}
}
16 changes: 16 additions & 0 deletions LenovoLegionToolkit.Lib.Macro/Utils/MacroSettings.cs
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();
}
2 changes: 1 addition & 1 deletion LenovoLegionToolkit.Lib/LenovoLegionToolkit.Lib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Platforms>x64</Platforms>
<Nullable>enable</Nullable>
<Copyright2023 Bartosz Cichecki</Copyright>
<Copyright2024 Bartosz Cichecki</Copyright>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<NeutralLanguage>en</NeutralLanguage>
<UseWindowsForms>true</UseWindowsForms>
Expand Down
1 change: 1 addition & 0 deletions LenovoLegionToolkit.Lib/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ GetDpiForMonitor

RegNotifyChangeKeyValue

SendInput
SendMessage

CallNtPowerInformation
Expand Down
Loading

0 comments on commit 0ec62b9

Please sign in to comment.