Compare commits

..

17 commits

Author SHA1 Message Date
Gerard Gascón
9513da302c
Create README.md 2024-07-09 00:30:59 +02:00
Gerard Gascón
ec459f846f
update actions to v4 2024-06-30 00:52:40 +02:00
Gerard Gascón
0ea469cb2b Merge remote-tracking branch 'origin/master' 2024-06-30 00:48:30 +02:00
Gerard Gascón
86eb2137a3 fix: build warnings 2024-06-30 00:46:16 +02:00
Gerard Gascón
9e50cfbde7
Update build.yml 2024-06-30 00:39:50 +02:00
Gerard Gascón
61124fefd7 feat: single file windows build config 2024-06-30 00:37:35 +02:00
Gerard Gascón
c329754f32 feat: added runtime identifiers to project 2024-06-30 00:30:42 +02:00
Gerard Gascón
1df15a8ba3
Update build.yml 2024-06-30 00:25:26 +02:00
Gerard Gascón
168ec1ab6a
Update build.yml 2024-06-30 00:19:40 +02:00
Gerard Gascón
0c34d1839c
Update build.yml 2024-06-30 00:18:12 +02:00
Gerard Gascón
e876fef15f
Create build.yml 2024-06-30 00:13:21 +02:00
d91916d785 feat: rider publish templates added 2024-06-30 00:00:44 +02:00
d996ca0efb feat: linux support 2024-06-29 23:50:26 +02:00
Gerard Gascón
04ebe3f606 feat: added back non-windows joycon detection support 2024-06-28 15:31:23 +02:00
Gerard Gascón
72750c3388 refactor: preprocessors for wiimote and joycon compilation 2024-06-28 15:18:56 +02:00
Gerard Gascón
65d562ee74 feat: added preprocessor directive for platform dependant compilation 2024-06-28 14:39:52 +02:00
Gerard Gascón
1e6f8c0b9f refactor: extract input sender to an interface for multiple platform support 2024-06-28 14:23:17 +02:00
17 changed files with 378 additions and 76 deletions

41
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: .NET Build
on:
workflow_dispatch:
jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
include:
- os: windows-latest
vs-version: 'latest'
runtime: 'win-x64'
- os: ubuntu-latest
vs-version: 'latest'
runtime: 'linux-x64'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: Restore dependencies
run: dotnet restore
- name: Build self-contained application
run: dotnet publish SlidePresenter/ControllerSlidePresenter.csproj --no-restore --configuration Release --self-contained -r ${{ matrix.runtime }} /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.runtime }}
path: |
**/bin/Release/net8.0/${{ matrix.runtime }}/publish/

View file

@ -37,11 +37,15 @@
<setting file="file://$PROJECT_DIR$/SlidePresenter/Program.cs" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
<component name="ProblemsViewState">
<option name="selectedTabId" value="Toolset" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="2htCW7BWCLo4IJC4dn1HIEGkg1I" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true">
<OptionsSetting value="false" id="Update" />
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Publish Linux-x64" type="DotNetFolderPublish" factoryName="Publish to folder">
<riderPublish configuration="Release" platform="Any CPU" runtime="linux-x64" self_contained="true" target_folder="$PROJECT_DIR$/SlidePresenter/bin/Release/net8.0/linux-x64/publish" target_framework="net8.0" uuid_high="-467478054054508763" uuid_low="-8110965388391740835" />
<method v="2" />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Publish Windows-x64" type="DotNetFolderPublish" factoryName="Publish to folder">
<riderPublish configuration="Release" include_native_libs_for_self_extract="true" platform="Any CPU" produce_single_file="true" runtime="win-x64" self_contained="true" target_folder="$PROJECT_DIR$/SlidePresenter/bin/Release/net8.0/win-x64/publish" target_framework="net8.0" uuid_high="-467478054054508763" uuid_low="-8110965388391740835" />
<method v="2" />
</configuration>
</component>

41
README.md Normal file
View file

@ -0,0 +1,41 @@
# Controller Slide Presenter
A small tool to emulate a Slide Presenter using a Wiimote or a Joy-Con.
## Controls
### Wiimote
- A or Right - Next Slide
- B or Left - Previous Slide
### Joy-Con
- A or ZL/ZR - Next Slide
- B or L/R - Previous Slide
## Platform Support
### Windows
For the Joy-Con you just need to connect it via Bluetooth and then run the program.
For the Wiimote you may need to connect it using Dolphin Emulator and then just run the program.
**WARNING:** It's possible that Steam tries reading the Joy-Con in the background and keeps it from working.
### Linux
**Prerequisite:** You need [xdotool](https://github.com/jordansissel/xdotool) installed in order to redirect the input.
At the moment there's only Joy-Con support, so connect it via Bluetooth and run the program.
### MacOS
At the moment there's no MacOS support as I don't have a computer to test, but feel free to submit a Pull Request adding that feature, the Joy-Con reading should work just fine, only the input redirection is needed.
## Packages used
**JoyCon.NET** - ([GitHub](https://github.com/ClusterM/joycon)) ([NuGet](https://www.nuget.org/packages/JoyCon.NET))
**WiimoteLib.NetCore** - ([GitHub](https://github.com/BrianPeek/WiimoteLib)) ([NuGet](https://www.nuget.org/packages/WiimoteLib.NetCore))

View file

@ -1,11 +1,33 @@
namespace SwitchSlidePresenter;
using ControllerSlidePresenter.GamepadReader;
namespace ControllerSlidePresenter;
public static class ControllerSelector {
public static IGamepadReader? GetReader() {
Console.WriteLine("Write a number to select controller type:");
Console.WriteLine("[1] - JoyCon");
Console.WriteLine("[2] - Wiimote");
private static readonly List<(string name, IGamepadReader reader)> Readers = [
#if JoyCon
("JoyCon", new JoyConRead()),
#endif
#if Wiimote
("Wiimote", new WiimoteRead())
#endif
];
public static IGamepadReader? GetReader() {
if (Readers.Count == 1)
return Readers[0].reader;
Console.WriteLine("Write a number to select controller type:");
for (int i = 0; i < Readers.Count; i++)
Console.WriteLine($"[{i+1}] - {Readers[i].name}");
int? id = GetReaderIndex();
if (id == null)
return null;
return Readers[id.Value].reader;
}
private static int? GetReaderIndex() {
string? line = Console.ReadLine();
if (line == null) {
Console.WriteLine("Invalid input.");
@ -15,13 +37,11 @@ public static class ControllerSelector {
Console.WriteLine("Invalid number.");
return null;
}
return GetReader(id);
if (id <= 0 || id >= Readers.Count) {
Console.WriteLine("Invalid number");
return null;
}
private static IGamepadReader? GetReader(int id) => id switch {
1 => new JoyConRead(),
2 => new WiimoteRead(),
_ => null
};
return id - 1;
}
}

View file

@ -5,16 +5,31 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>SwitchSlidePresenter</RootNamespace>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Windows'))">
<DefineConstants>$(DefineConstants);OS_WINDOWS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('Linux'))">
<DefineConstants>$(DefineConstants);OS_LINUX</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('OSX'))">
<DefineConstants>$(DefineConstants);OS_MAC</DefineConstants>
</PropertyGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);JoyCon</DefineConstants>
<DefineConstants>$(DefineConstants);Wiimote</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JoyCon.NET" Version="1.0.1" />
<PackageReference Include="WiimoteLib.NetCore" Version="1.0.0" />
<PackageReference Include="JoyCon.NET" Version="1.0.1" Condition="$(DefineConstants.Contains(JoyCon))" />
<PackageReference Include="WiimoteLib.NetCore" Version="1.0.0" Condition="$(DefineConstants.Contains(Wiimote))" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Win32Api\Win32Api.csproj" />
<ProjectReference Include="..\Win32Api\Win32Api.csproj" Condition="$([MSBuild]::IsOSPlatform('Windows'))"/>
</ItemGroup>
</Project>

View file

@ -1,4 +1,4 @@
namespace SwitchSlidePresenter;
namespace ControllerSlidePresenter.GamepadReader;
public interface IGamepadReader {
public event Action NextSlide;

View file

@ -1,15 +1,17 @@
using System.Text;
#if JoyCon
using System.Text;
using HidSharp;
using HidSharp.Reports;
using wtf.cluster.JoyCon;
using wtf.cluster.JoyCon.ExtraData;
using wtf.cluster.JoyCon.InputData;
using wtf.cluster.JoyCon.InputReports;
namespace SwitchSlidePresenter;
namespace ControllerSlidePresenter.GamepadReader;
public class JoyConRead : IGamepadReader {
public event Action NextSlide;
public event Action PrevSlide;
public event Action? NextSlide;
public event Action? PrevSlide;
public async Task Read() {
Console.OutputEncoding = Encoding.UTF8;
@ -62,10 +64,44 @@ public class JoyConRead : IGamepadReader {
}
private static HidDevice? GetHidDevice() {
return OperatingSystem.IsWindows()
? GetWindowsHidDevice()
: GetNonWindowsHidDevice();
}
private static HidDevice? GetWindowsHidDevice() {
DeviceList list = DeviceList.Local;
IEnumerable<HidDevice>? nintendos = list.GetHidDevices(0x057e);
HidDevice? device = nintendos.FirstOrDefault();
return device;
}
return nintendos.FirstOrDefault();
private static HidDevice? GetNonWindowsHidDevice() {
HidDevice? device = null;
DeviceList list = DeviceList.Local;
IEnumerable<HidDevice>? hidDevices = list.GetHidDevices();
foreach (HidDevice d in hidDevices)
{
ReportDescriptor? rd = d.GetReportDescriptor();
if (rd == null) continue;
if (rd.OutputReports.Count() != 4
|| rd.OutputReports.Count(r => r.ReportID == 0x01) != 1
|| rd.OutputReports.Count(r => r.ReportID == 0x10) != 1
|| rd.OutputReports.Count(r => r.ReportID == 0x11) != 1
|| rd.OutputReports.Count(r => r.ReportID == 0x12) != 1
|| rd.InputReports.Count() != 6
|| rd.InputReports.Count(r => r.ReportID == 0x21) != 1
|| rd.InputReports.Count(r => r.ReportID == 0x30) != 1
|| rd.InputReports.Count(r => r.ReportID == 0x31) != 1
|| rd.InputReports.Count(r => r.ReportID == 0x32) != 1
|| rd.InputReports.Count(r => r.ReportID == 0x33) != 1
|| rd.InputReports.Count(r => r.ReportID == 0x3F) != 1) continue;
device = d;
break;
}
return device;
}
private Task OnJoyConOnReportReceived(JoyCon _, IJoyConReport input) {
@ -90,3 +126,4 @@ public class JoyConRead : IGamepadReader {
return input.ZLorZR || input.Down;
}
}
#endif

View file

@ -1,10 +1,11 @@
using WiimoteLib.NetCore;
#if Wiimote
using WiimoteLib.NetCore;
namespace SwitchSlidePresenter;
namespace ControllerSlidePresenter.GamepadReader;
public class WiimoteRead : IGamepadReader {
public event Action NextSlide;
public event Action PrevSlide;
public event Action? NextSlide;
public event Action? PrevSlide;
public async Task Read() {
Wiimote wiimote = new();
@ -47,3 +48,4 @@ public class WiimoteRead : IGamepadReader {
return input.A || input.Right;
}
}
#endif

View file

@ -0,0 +1,6 @@
namespace ControllerSlidePresenter.InputSender;
public interface IInputSender {
void NextSlide();
void PreviousSlide();
}

View file

@ -0,0 +1,32 @@
#if OS_LINUX
using System.Diagnostics;
namespace ControllerSlidePresenter.InputSender;
public class LinuxInputSender : IInputSender {
private const string PageUp = "0xff55";
private const string PageDown = "0xff56";
public void NextSlide() {
SendKeys(PageDown);
}
public void PreviousSlide() {
SendKeys(PageUp);
}
private static void SendKeys(string keycode) {
Process proc = new() {
StartInfo = {
FileName = "xdotool",
Arguments = $"key {keycode}",
UseShellExecute = false,
RedirectStandardError = false,
RedirectStandardInput = false,
RedirectStandardOutput = false
}
};
proc.Start();
}
}
#endif

View file

@ -0,0 +1,13 @@
#if OS_MAC
namespace ControllerSlidePresenter.InputSender;
public class MacInputSender : IInputSender {
public void NextSlide() {
}
public void PreviousSlide() {
}
}
#endif

View file

@ -0,0 +1,49 @@
#if OS_WINDOWS
using System.Runtime.InteropServices;
using Win32Api;
namespace ControllerSlidePresenter.InputSender;
public class WindowsInputSender : IInputSender {
private const uint INPUT_KEYBOARD = 1;
private const ushort VK_NEXT = 0x22;
private const ushort VK_PRIOR = 0x21;
private const uint KEYEVENTF_KEYDOWN = 0x0000;
private const uint KEYEVENTF_KEYUP = 0x0002;
public void NextSlide() => SimulateKeyPress(VK_NEXT);
public void PreviousSlide() => SimulateKeyPress(VK_PRIOR);
private static void SimulateKeyPress(ushort keyCode) {
Input[] inputs = new Input[2];
inputs[0] = new Input {
type = INPUT_KEYBOARD,
u = new InputUnion {
ki = new KeyboardInput {
wVk = keyCode,
wScan = 0,
dwFlags = KEYEVENTF_KEYDOWN,
time = 0,
dwExtraInfo = IntPtr.Zero
}
}
};
inputs[1] = new Input {
type = INPUT_KEYBOARD,
u = new InputUnion {
ki = new KeyboardInput {
wVk = keyCode,
wScan = 0,
dwFlags = KEYEVENTF_KEYUP,
time = 0,
dwExtraInfo = IntPtr.Zero
}
}
};
Win32Api.Win32Api.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(Input)));
}
}
#endif

55
SlidePresenter/Linux.cs Normal file
View file

@ -0,0 +1,55 @@
#if OS_LINUX
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace ControllerSlidePresenter;
public static class Linux {
public static bool CanRun() {
if (getuid() != 0) {
Console.WriteLine("On Linux you need tu run as 'sudo'");
return false;
}
if (!IsXdoToolInstalled())
return false;
return true;
}
private static bool IsXdoToolInstalled() {
string result = RunShellCommand("which xdotool");
if (!string.IsNullOrEmpty(result))
return true;
Console.WriteLine("xdotool is not installed.");
return false;
}
private static string RunShellCommand(string command) {
try {
ProcessStartInfo procStartInfo = new("bash", "-c \"" + command + "\"") {
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using Process process = new();
process.StartInfo = procStartInfo;
process.Start();
string result = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return result.Trim();
}
catch (Exception ex) {
Console.WriteLine("Error: " + ex.Message);
return string.Empty;
}
}
[DllImport("libc")]
private static extern uint getuid();
}
#endif

View file

@ -1,6 +1,13 @@
namespace SwitchSlidePresenter {
using ControllerSlidePresenter.GamepadReader;
namespace ControllerSlidePresenter {
internal abstract class Program {
private static async Task Main() {
#if OS_LINUX
if (!Linux.CanRun())
return;
#endif
IGamepadReader? reader = ControllerSelector.GetReader();
if (reader == null) {
Console.WriteLine("Invalid Controller Selected.");

View file

@ -1,65 +1,33 @@
using System.Runtime.InteropServices;
using Win32Api;
using ControllerSlidePresenter.GamepadReader;
using ControllerSlidePresenter.InputSender;
namespace SwitchSlidePresenter;
namespace ControllerSlidePresenter;
public class SlideSwitcher : IDisposable {
private readonly IGamepadReader? _reader;
private const uint INPUT_KEYBOARD = 1;
private const ushort VK_NEXT = 0x22;
private const ushort VK_PRIOR = 0x21;
private const uint KEYEVENTF_KEYDOWN = 0x0000;
private const uint KEYEVENTF_KEYUP = 0x0002;
#if OS_WINDOWS
private readonly IInputSender _inputSender = new WindowsInputSender();
#elif OS_MAC
private readonly IInputSender _inputSender = new MacInputSender();
#elif OS_LINUX
private readonly IInputSender _inputSender = new LinuxInputSender();
#endif
public SlideSwitcher(IGamepadReader? reader) {
_reader = reader;
if (_reader == null) return;
_reader.NextSlide += NextSlide;
_reader.PrevSlide += PreviousSlide;
}
public void Dispose() {
if (_reader == null) return;
_reader.NextSlide -= NextSlide;
_reader.PrevSlide -= PreviousSlide;
}
private static void NextSlide() {
SimulateKeyPress(VK_NEXT);
}
private static void PreviousSlide() {
SimulateKeyPress(VK_PRIOR);
}
private static void SimulateKeyPress(ushort keyCode) {
Input[] inputs = new Input[2];
inputs[0] = new Input {
type = INPUT_KEYBOARD,
u = new InputUnion {
ki = new KeyboardInput {
wVk = keyCode,
wScan = 0,
dwFlags = KEYEVENTF_KEYDOWN,
time = 0,
dwExtraInfo = IntPtr.Zero
}
}
};
inputs[1] = new Input {
type = INPUT_KEYBOARD,
u = new InputUnion {
ki = new KeyboardInput {
wVk = keyCode,
wScan = 0,
dwFlags = KEYEVENTF_KEYUP,
time = 0,
dwExtraInfo = IntPtr.Zero
}
}
};
Win32Api.Win32Api.SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(Input)));
}
private void NextSlide() => _inputSender.NextSlide();
private void PreviousSlide() => _inputSender.PreviousSlide();
}