Compare commits

...

20 commits
0.2 ... master

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
Gerard Gascón
184f78123a Merge remote-tracking branch 'origin/master' 2024-06-15 13:20:25 +02:00
Gerard Gascón
42f5ed78fe refactor: Moved wiimote reading to event-based calls 2024-06-15 13:20:08 +02:00
Gerard Gascón
e8996a382d refactor: Removed non-windows calls as I don't support other OS APIs 2024-06-15 13:01:51 +02:00
18 changed files with 567 additions and 159 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

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoGeneratedRunConfigurationManager">
<projectFile>SwitchSlidePresenter.csproj</projectFile>
</component>
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="756a98cd-4cfe-4a7d-9110-f232a435843b" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="DpaMonitoringSettings">
<option name="autoShow" value="false" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;GerardGascon&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/GerardGascon/Switch-Slide-Presenter.git&quot;,
&quot;accountId&quot;: &quot;0af66c52-cbb7-4844-ad24-01b5c5b9bee8&quot;
}
}</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$USER_HOME$/AppData/Local/Symbols/src/dotnet/runtime/bf5e279d9239bfef5bb1b8d6212f1b971c434606/src/coreclr/System.Private.CoreLib/src/System/Threading/Monitor.CoreCLR.cs" root0="SKIP_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/DecompilerCache/decompiler/045ef3cb636f4aaa894e8cefcc5e4b367e00/c1/d22d509a/Wiimote.cs" root0="SKIP_HIGHLIGHTING" />
<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">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;.NET Project.SwitchSlidePresenter.executor&quot;: &quot;Run&quot;,
&quot;Publish to folder.Publish ControllerSlidePresenter to folder.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
&quot;keyToStringList&quot;: {
&quot;rider.external.source.directories&quot;: [
&quot;C:\\Users\\ggasc\\AppData\\Roaming\\JetBrains\\Rider2024.1\\resharper-host\\DecompilerCache&quot;,
&quot;C:\\Users\\ggasc\\AppData\\Roaming\\JetBrains\\Rider2024.1\\resharper-host\\SourcesCache&quot;,
&quot;C:\\Users\\ggasc\\AppData\\Local\\Symbols\\src&quot;
]
}
}</component>
<component name="RunManager" selected="Publish to folder.Publish ControllerSlidePresenter to folder">
<configuration name="Publish ControllerSlidePresenter to folder" type="DotNetFolderPublish" factoryName="Publish to folder">
<riderPublish configuration="Release" delete_existing_files="true" platform="Any CPU" runtime="Portable" target_folder="$PROJECT_DIR$/SlidePresenter/bin/Release/net8.0/publish" target_framework="net8.0" uuid_high="-467478054054508763" uuid_low="-8110965388391740835" />
<method v="2" />
</configuration>
<configuration name="ControllerSlidePresenter" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/SlidePresenter/bin/Debug/net8.0/ControllerSlidePresenter.exe" />
<option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/SlidePresenter/bin/Debug/net8.0" />
<option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" />
<option name="RUNTIME_ARGUMENTS" value="" />
<option name="PROJECT_PATH" value="$PROJECT_DIR$/SlidePresenter/ControllerSlidePresenter.csproj" />
<option name="PROJECT_EXE_PATH_TRACKING" value="1" />
<option name="PROJECT_ARGUMENTS_TRACKING" value="1" />
<option name="PROJECT_WORKING_DIRECTORY_TRACKING" value="1" />
<option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net8.0" />
<method v="2">
<option name="Build" />
</method>
</configuration>
<list>
<item itemvalue=".NET Project.ControllerSlidePresenter" />
<item itemvalue="Publish to folder.Publish ControllerSlidePresenter to folder" />
</list>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="756a98cd-4cfe-4a7d-9110-f232a435843b" name="Changes" comment="" />
<created>1718404015037</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1718404015037</updated>
<workItem from="1718404016227" duration="5433000" />
</task>
<task id="LOCAL-00001" summary="feat: Added wiimote support and cleaned up the project">
<option name="closed" value="true" />
<created>1718408820801</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1718408820801</updated>
</task>
<task id="LOCAL-00002" summary="rename: switch slide project to controller slide">
<option name="closed" value="true" />
<created>1718408956240</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1718408956240</updated>
</task>
<option name="localTasksCounter" value="3" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
<component name="VcsManagerConfiguration">
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
<MESSAGE value="feat: Added wiimote support and cleaned up the project" />
<MESSAGE value="rename: switch slide project to controller slide" />
<option name="LAST_COMMIT_MESSAGE" value="rename: switch slide project to controller slide" />
</component>
</project>

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;
}
if (id <= 0 || id >= Readers.Count) {
Console.WriteLine("Invalid number");
return null;
}
return GetReader(id);
return id - 1;
}
private static IGamepadReader? GetReader(int id) => id switch {
1 => new JoyConRead(),
2 => new WiimoteRead(),
_ => null
};
}

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,22 +1,24 @@
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;
HidDevice? device = GetHidDevice();
if (device == null) {
Console.WriteLine("No controller. Please connect Joy-Con or Pro controller via Bluetooth.");
Console.WriteLine("No controller. Please connect Joy-Con via Bluetooth.");
Console.WriteLine("Press any key to exit program.");
Console.ReadKey();
return;
@ -39,11 +41,14 @@ public class JoyConRead : IGamepadReader {
Console.WriteLine("JoyCon ready for presenting.");
Console.WriteLine("Press Enter to exit program.");
while (Console.ReadKey().Key != ConsoleKey.Enter) { }
while (Console.ReadKey().Key != ConsoleKey.Enter) {
await Task.Yield();
}
joycon.Stop();
Console.WriteLine();
Console.WriteLine("Stopped.");
await Task.CompletedTask;
}
private static async Task LogDeviceInfo(JoyCon joycon) {
@ -59,36 +64,42 @@ public class JoyConRead : IGamepadReader {
}
private static HidDevice? GetHidDevice() {
DeviceList list = DeviceList.Local;
HidDevice? device = null;
return OperatingSystem.IsWindows()
? GetWindowsHidDevice()
: GetNonWindowsHidDevice();
}
if (OperatingSystem.IsWindows()) {
var nintendos = list.GetHidDevices(0x057e);
device = nintendos.FirstOrDefault();
} else {
var hidDevices = list.GetHidDevices();
foreach (var d in hidDevices) {
var rd = d.GetReportDescriptor();
if (rd != null) {
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
) {
device = d;
break;
}
}
}
private static HidDevice? GetWindowsHidDevice() {
DeviceList list = DeviceList.Local;
IEnumerable<HidDevice>? nintendos = list.GetHidDevices(0x057e);
HidDevice? device = nintendos.FirstOrDefault();
return device;
}
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;
}
@ -114,4 +125,5 @@ public class JoyConRead : IGamepadReader {
private static bool NextPressed(ButtonsSimple input) {
return input.ZLorZR || input.Down;
}
}
}
#endif

View file

@ -0,0 +1,51 @@
#if Wiimote
using WiimoteLib.NetCore;
namespace ControllerSlidePresenter.GamepadReader;
public class WiimoteRead : IGamepadReader {
public event Action? NextSlide;
public event Action? PrevSlide;
public async Task Read() {
Wiimote wiimote = new();
wiimote.Connect();
if (string.IsNullOrEmpty(wiimote.HIDDevicePath)) {
Console.WriteLine("No controller. Please connect Wiimote via Bluetooth.");
Console.WriteLine("Press any key to exit program.");
Console.ReadKey();
return;
}
wiimote.WiimoteChanged += WiimoteChanged;
Console.WriteLine("Wiimote ready for presenting.");
Console.WriteLine("Press Enter to exit program.");
while (Console.ReadKey().Key != ConsoleKey.Enter) {
await Task.Yield();
}
wiimote.Disconnect();
Console.WriteLine();
Console.WriteLine("Stopped.");
await Task.CompletedTask;
}
private void WiimoteChanged(object? sender, WiimoteChangedEventArgs e) {
if (PreviousPressed(e.WiimoteState.ButtonState)) {
PrevSlide?.Invoke();
}
if (NextPressed(e.WiimoteState.ButtonState)) {
NextSlide?.Invoke();
}
}
private static bool PreviousPressed(ButtonState input) {
return input.B || input.Left;
}
private static bool NextPressed(ButtonState input) {
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();
}

View file

@ -1,58 +0,0 @@
using WiimoteLib.NetCore;
namespace SwitchSlidePresenter;
public class WiimoteRead : IGamepadReader {
public event Action NextSlide;
public event Action PrevSlide;
private const int RetryDelay = 1000;
public async Task Read() {
Wiimote wiimote = new();
while (string.IsNullOrEmpty(wiimote.HIDDevicePath)) {
wiimote.Connect();
if (string.IsNullOrEmpty(wiimote.HIDDevicePath)) {
Console.WriteLine("Wiimote connection failed, trying again...");
await Task.Delay(RetryDelay);
} else {
Console.WriteLine("Wiimote ready for presenting!");
}
}
ButtonState previousState = wiimote.WiimoteState.ButtonState;
while (true) {
if (PreviousPressed(wiimote.WiimoteState.ButtonState, previousState)) {
PrevSlide?.Invoke();
}
if (NextPressed(wiimote.WiimoteState.ButtonState, previousState)) {
NextSlide?.Invoke();
}
previousState = wiimote.WiimoteState.ButtonState;
await Task.Yield();
if (!Console.KeyAvailable || Console.ReadKey().Key != ConsoleKey.Enter)
continue;
wiimote.Disconnect();
Console.WriteLine();
Console.WriteLine("Stopped.");
break;
}
}
private static bool PreviousPressed(ButtonState input) {
return input.B || input.Left;
}
private static bool NextPressed(ButtonState input) {
return input.A || input.Right;
}
private static bool PreviousPressed(ButtonState input, ButtonState previousState) {
return PreviousPressed(input) && !PreviousPressed(previousState);
}
private static bool NextPressed(ButtonState input, ButtonState previousState) {
return NextPressed(input) && !NextPressed(previousState);
}
}