Compare commits

..

No commits in common. "master" and "0.2.1" have entirely different histories.

17 changed files with 74 additions and 376 deletions

View file

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

View file

@ -1,6 +0,0 @@
<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

@ -1,6 +0,0 @@
<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>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,32 +0,0 @@
#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

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

View file

@ -1,49 +0,0 @@
#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

View file

@ -1,17 +1,15 @@
#if JoyCon using System.Text;
using System.Text;
using HidSharp; using HidSharp;
using HidSharp.Reports;
using wtf.cluster.JoyCon; using wtf.cluster.JoyCon;
using wtf.cluster.JoyCon.ExtraData; using wtf.cluster.JoyCon.ExtraData;
using wtf.cluster.JoyCon.InputData; using wtf.cluster.JoyCon.InputData;
using wtf.cluster.JoyCon.InputReports; using wtf.cluster.JoyCon.InputReports;
namespace ControllerSlidePresenter.GamepadReader; namespace SwitchSlidePresenter;
public class JoyConRead : IGamepadReader { public class JoyConRead : IGamepadReader {
public event Action? NextSlide; public event Action NextSlide;
public event Action? PrevSlide; public event Action PrevSlide;
public async Task Read() { public async Task Read() {
Console.OutputEncoding = Encoding.UTF8; Console.OutputEncoding = Encoding.UTF8;
@ -64,44 +62,10 @@ public class JoyConRead : IGamepadReader {
} }
private static HidDevice? GetHidDevice() { private static HidDevice? GetHidDevice() {
return OperatingSystem.IsWindows()
? GetWindowsHidDevice()
: GetNonWindowsHidDevice();
}
private static HidDevice? GetWindowsHidDevice() {
DeviceList list = DeviceList.Local; DeviceList list = DeviceList.Local;
IEnumerable<HidDevice>? nintendos = list.GetHidDevices(0x057e); IEnumerable<HidDevice>? nintendos = list.GetHidDevices(0x057e);
HidDevice? device = nintendos.FirstOrDefault();
return device;
}
private static HidDevice? GetNonWindowsHidDevice() { return nintendos.FirstOrDefault();
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) { private Task OnJoyConOnReportReceived(JoyCon _, IJoyConReport input) {
@ -125,5 +89,4 @@ public class JoyConRead : IGamepadReader {
private static bool NextPressed(ButtonsSimple input) { private static bool NextPressed(ButtonsSimple input) {
return input.ZLorZR || input.Down; return input.ZLorZR || input.Down;
} }
} }
#endif

View file

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

View file

@ -1,33 +1,65 @@
using ControllerSlidePresenter.GamepadReader; using System.Runtime.InteropServices;
using ControllerSlidePresenter.InputSender; using Win32Api;
namespace ControllerSlidePresenter; namespace SwitchSlidePresenter;
public class SlideSwitcher : IDisposable { public class SlideSwitcher : IDisposable {
private readonly IGamepadReader? _reader; private readonly IGamepadReader? _reader;
#if OS_WINDOWS private const uint INPUT_KEYBOARD = 1;
private readonly IInputSender _inputSender = new WindowsInputSender(); private const ushort VK_NEXT = 0x22;
#elif OS_MAC private const ushort VK_PRIOR = 0x21;
private readonly IInputSender _inputSender = new MacInputSender(); private const uint KEYEVENTF_KEYDOWN = 0x0000;
#elif OS_LINUX private const uint KEYEVENTF_KEYUP = 0x0002;
private readonly IInputSender _inputSender = new LinuxInputSender();
#endif
public SlideSwitcher(IGamepadReader? reader) { public SlideSwitcher(IGamepadReader? reader) {
_reader = reader; _reader = reader;
if (_reader == null) return;
_reader.NextSlide += NextSlide; _reader.NextSlide += NextSlide;
_reader.PrevSlide += PreviousSlide; _reader.PrevSlide += PreviousSlide;
} }
public void Dispose() { public void Dispose() {
if (_reader == null) return;
_reader.NextSlide -= NextSlide; _reader.NextSlide -= NextSlide;
_reader.PrevSlide -= PreviousSlide; _reader.PrevSlide -= PreviousSlide;
} }
private void NextSlide() => _inputSender.NextSlide(); private static void NextSlide() {
private void PreviousSlide() => _inputSender.PreviousSlide(); 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)));
}
} }

View file

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