diff --git a/PublishBin.cmd b/PublishBin.cmd
new file mode 100644
index 0000000..0e57229
--- /dev/null
+++ b/PublishBin.cmd
@@ -0,0 +1,2 @@
+IF NOT EXIST bin\ mkdir bin
+dotnet publish src/FrontEnd/MapToKeyboardInput/MapToKeyboardInput.csproj -c Release -f netcoreapp2.0 -r win10-x64 -o ../../../bin/
diff --git a/README.md b/README.md
index 2c16e5c..ae5161e 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,23 @@
On Screen Keyboard
==================
+Quick Start
+-
+This solution is built on dot net core 2.0. It requires the dot net core 2.0 SDK to be installed to build. This is installed automatically with the latest version of Visual Studio 2017.
+
+1. Run `PublishBin.cmd` to build the solution to the `bin` folder.
+2. Run `RunTest.cmd` to pipe the data in the file `data\TestData.txt` through the `MapToKeyboardInput` utility and write the output to `stdout`.
+
+The Solution
+-
+`MapToKeyboardInput.exe` is a utility that reads lines from `stdin` and translates the text to cursor instructions, which are written to `stdout`. This allows for easy use in scripts, the most likely use for a utility like this. To read the test data and write it to a file called `output.txt` the command would be:
+
+ cat data\TestData.txt | bin\MapToKeyboardInput.exe > output.txt
+
+In the solution, the core logic is broken out into it's own assembly that could be referenced by other projects that need this functionality. The MapToKeyboardInput project is a thin command line interface to the OnScreenKeyboard.Logic API. The solution also contains some simple tests of the API.
+
+---
+
The Problem
-----------
On screen keyboards are the bane of DVR users. To help alleviate the pain, one local company is asking you to implement part of a voice to text search for their DVR by developing an algorithm to script the on screen keyboard.
diff --git a/RunTest.cmd b/RunTest.cmd
new file mode 100644
index 0000000..0ce2bb3
--- /dev/null
+++ b/RunTest.cmd
@@ -0,0 +1,17 @@
+@echo OFF
+echo.
+IF NOT EXIST bin\ (
+ echo Please run PublishBin.cmd before running the test.
+) else (
+ echo Input:
+ echo.
+ cat data\TestData.txt
+ echo.
+ echo.
+ echo Output:
+ echo.
+ cat data\TestData.txt | bin\MapToKeyboardInput.exe
+)
+echo.
+
+
diff --git a/data/TestData.txt b/data/TestData.txt
new file mode 100644
index 0000000..802aab4
--- /dev/null
+++ b/data/TestData.txt
@@ -0,0 +1,3 @@
+ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
+TEST TEST
+Test \"Test\" :';,.<>/?\\
\ No newline at end of file
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..9c455c3
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1,322 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+**/Properties/launchSettings.json
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# TypeScript v1 declaration files
+typings/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
diff --git a/src/BackEnd/OnScreenKeyboard.Logic/IKeyboardLayout.cs b/src/BackEnd/OnScreenKeyboard.Logic/IKeyboardLayout.cs
new file mode 100644
index 0000000..2f5eb43
--- /dev/null
+++ b/src/BackEnd/OnScreenKeyboard.Logic/IKeyboardLayout.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OnScreenKeyboard.Logic
+{
+ public interface IKeyboardLayout
+ {
+ KeyLocation? GetKeyLocation(char key);
+ }
+}
diff --git a/src/BackEnd/OnScreenKeyboard.Logic/InputProcessor.cs b/src/BackEnd/OnScreenKeyboard.Logic/InputProcessor.cs
new file mode 100644
index 0000000..2c4dbd1
--- /dev/null
+++ b/src/BackEnd/OnScreenKeyboard.Logic/InputProcessor.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OnScreenKeyboard.Logic
+{
+ public class InputProcessor
+ {
+ public InputProcessor(IKeyboardLayout keyboardLayout)
+ {
+ _keyboardLayout = keyboardLayout ?? throw new ArgumentNullException(nameof(keyboardLayout));
+ }
+
+ private IKeyboardLayout _keyboardLayout;
+
+ ///
+ /// Convert all characters to upper case, all punctuation to spaces and remove any repeated
+ /// spaces.
+ ///
+ public string ScrubInput(string input)
+ {
+ if (input == null)
+ return null;
+
+ bool lastCharWasSpace = false;
+ var sb = new StringBuilder();
+
+ foreach(char c in input.Trim().ToUpper())
+ {
+ char cc = (char.IsPunctuation(c) || char.IsSymbol(c)) ? ' ' : c;
+ if (cc == ' ')
+ {
+ if (lastCharWasSpace) continue;
+ else lastCharWasSpace = true;
+ }
+ else
+ lastCharWasSpace = false;
+ sb.Append(cc);
+ }
+
+ return sb.ToString().Trim();
+ }
+
+ ///
+ /// Convert an input string into instructions to move the cursor around the on screen keyboard.
+ ///
+ public string ToCursorInstructions(string input)
+ {
+ var keySequences = ToKeyLocationSequences(input);
+ if (keySequences == null)
+ return null;
+
+ var currentLocation = new KeyLocation { Row = 0, Column = 0 };
+ return
+ string.Join(",S,",
+ keySequences.Select(seq => string.Join(",",
+ seq.Select(
+ keyLoc =>
+ {
+ var result = ToRelativeCursorInstructions(currentLocation, keyLoc);
+ currentLocation = keyLoc;
+ return result;
+ }))));
+
+ }
+
+ ///
+ /// Convert an input string into a sequence of key locations seperated by spaces.
+ ///
+ private List> ToKeyLocationSequences(string input)
+ => ScrubInput(input)?.Split(' ')
+ ?.Select(word =>
+ word.Select(c => _keyboardLayout.GetKeyLocation(c))
+ .Where(kl => kl != null)
+ .Cast()
+ .ToList())
+ ?.ToList();
+
+ ///
+ /// Generate the instructions to move from one key to another.
+ ///
+ private string ToRelativeCursorInstructions(KeyLocation currentLocation, KeyLocation targetLocation)
+ {
+ var diff = targetLocation - currentLocation;
+ var sb = new StringBuilder();
+
+ for (int i = 0; i < diff.Column; i++)
+ sb.Append("R,");
+ for (int i = 0; i > diff.Column; i--)
+ sb.Append("L,");
+ for (int i = 0; i < diff.Row; i++)
+ sb.Append("D,");
+ for (int i = 0; i > diff.Row; i--)
+ sb.Append("U,");
+
+ sb.Append("#");
+
+
+ return sb.ToString();
+ }
+
+ }
+}
diff --git a/src/BackEnd/OnScreenKeyboard.Logic/KeyLocation.cs b/src/BackEnd/OnScreenKeyboard.Logic/KeyLocation.cs
new file mode 100644
index 0000000..f958f71
--- /dev/null
+++ b/src/BackEnd/OnScreenKeyboard.Logic/KeyLocation.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OnScreenKeyboard.Logic
+{
+ public struct KeyLocation
+ {
+ public int Row;
+ public int Column;
+
+ public static KeyLocation operator +(KeyLocation a, KeyLocation b)
+ => new KeyLocation { Row = a.Row + b.Row, Column = a.Column + b.Column };
+ public static KeyLocation operator -(KeyLocation a, KeyLocation b)
+ => new KeyLocation { Row = a.Row - b.Row, Column = a.Column - b.Column };
+ }
+}
diff --git a/src/BackEnd/OnScreenKeyboard.Logic/OnScreenKeyboard.Logic.csproj b/src/BackEnd/OnScreenKeyboard.Logic/OnScreenKeyboard.Logic.csproj
new file mode 100644
index 0000000..9f5c4f4
--- /dev/null
+++ b/src/BackEnd/OnScreenKeyboard.Logic/OnScreenKeyboard.Logic.csproj
@@ -0,0 +1,7 @@
+
+
+
+ netstandard2.0
+
+
+
diff --git a/src/BackEnd/OnScreenKeyboard.Logic/SixBySixKeyboardLayout.cs b/src/BackEnd/OnScreenKeyboard.Logic/SixBySixKeyboardLayout.cs
new file mode 100644
index 0000000..b405431
--- /dev/null
+++ b/src/BackEnd/OnScreenKeyboard.Logic/SixBySixKeyboardLayout.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace OnScreenKeyboard.Logic
+{
+ public class SixBySixKeyboardLayout : IKeyboardLayout
+ {
+ private static char[] _keys =
+ {
+ 'A', 'B', 'C', 'D', 'E', 'F',
+ 'G', 'H', 'I', 'J', 'K', 'L',
+ 'M', 'N', 'O', 'P', 'Q', 'R',
+ 'S', 'T', 'U', 'V', 'W', 'X',
+ 'Y', 'Z', '1', '2', '3', '4',
+ '5', '6', '7', '8', '9', '0',
+ };
+
+ public KeyLocation? GetKeyLocation(char key)
+ {
+ int keyIndex = Array.IndexOf(_keys, char.ToUpper(key));
+ return keyIndex < 0 ? (KeyLocation?)null
+ : new KeyLocation { Row = keyIndex / 6, Column = keyIndex % 6 };
+ }
+ }
+}
diff --git a/src/FrontEnd/MapToKeyboardInput/MapToKeyboardInput.csproj b/src/FrontEnd/MapToKeyboardInput/MapToKeyboardInput.csproj
new file mode 100644
index 0000000..f9967f7
--- /dev/null
+++ b/src/FrontEnd/MapToKeyboardInput/MapToKeyboardInput.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+ Exe
+ netcoreapp2.0
+ win10-x64
+ 7.1
+
+
+
diff --git a/src/FrontEnd/MapToKeyboardInput/Program.cs b/src/FrontEnd/MapToKeyboardInput/Program.cs
new file mode 100644
index 0000000..ce2ae1c
--- /dev/null
+++ b/src/FrontEnd/MapToKeyboardInput/Program.cs
@@ -0,0 +1,33 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace MapToKeyboardInput
+{
+ public class Program
+ {
+ public static async Task Main(string[] args)
+ {
+ using (var stdin = Console.OpenStandardInput())
+ using (var stdout = Console.OpenStandardOutput())
+ await ProcessAsync(stdin, stdout);
+ }
+
+ ///
+ /// Read lines from the input stream and write the cursor instructions to the output stream.
+ ///
+ private static async Task ProcessAsync(Stream stdin, Stream stdout)
+ {
+ var processor = new OnScreenKeyboard.Logic.InputProcessor(new OnScreenKeyboard.Logic.SixBySixKeyboardLayout());
+
+ using (var reader = new StreamReader(stdin))
+ using (var writer = new StreamWriter(stdout))
+ {
+ string input = null;
+ while ((input = await reader.ReadLineAsync()) != null)
+ writer.WriteLine(processor.ToCursorInstructions(input));
+ }
+
+ }
+ }
+}
diff --git a/src/OnScreenKeyboard.sln b/src/OnScreenKeyboard.sln
new file mode 100644
index 0000000..75d991d
--- /dev/null
+++ b/src/OnScreenKeyboard.sln
@@ -0,0 +1,75 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.27004.2005
+MinimumVisualStudioVersion = 15.0.26124.0
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BackEnd", "BackEnd", "{987EA8CC-B8B2-4A04-97EB-F0F29DD096BC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OnScreenKeyboard.Logic", "BackEnd\OnScreenKeyboard.Logic\OnScreenKeyboard.Logic.csproj", "{83812D7D-871C-4786-B9F2-91DB315721E0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "FrontEnd", "FrontEnd", "{9C3F1701-A97C-4BDA-8DCA-354E391F635A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MapToKeyboardInput", "FrontEnd\MapToKeyboardInput\MapToKeyboardInput.csproj", "{7565402E-00FD-468B-9DD1-4AFE8101DECF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{798031D8-B876-4BDA-B7C1-A4760B18CC20}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogicTests", "Tests\LogicTests\LogicTests.csproj", "{64B5B8B4-E012-4918-AFEE-5BA484C9920F}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|x64.Build.0 = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Debug|x86.Build.0 = Debug|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|x64.ActiveCfg = Release|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|x64.Build.0 = Release|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|x86.ActiveCfg = Release|Any CPU
+ {83812D7D-871C-4786-B9F2-91DB315721E0}.Release|x86.Build.0 = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|x64.Build.0 = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Debug|x86.Build.0 = Debug|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|x64.ActiveCfg = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|x64.Build.0 = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|x86.ActiveCfg = Release|Any CPU
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF}.Release|x86.Build.0 = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|x64.Build.0 = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Debug|x86.Build.0 = Debug|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|x64.ActiveCfg = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|x64.Build.0 = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|x86.ActiveCfg = Release|Any CPU
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {83812D7D-871C-4786-B9F2-91DB315721E0} = {987EA8CC-B8B2-4A04-97EB-F0F29DD096BC}
+ {7565402E-00FD-468B-9DD1-4AFE8101DECF} = {9C3F1701-A97C-4BDA-8DCA-354E391F635A}
+ {64B5B8B4-E012-4918-AFEE-5BA484C9920F} = {798031D8-B876-4BDA-B7C1-A4760B18CC20}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {793D06F3-A316-4065-8A4B-85AEA2E31382}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Tests/LogicTests/InputProcessorTests.cs b/src/Tests/LogicTests/InputProcessorTests.cs
new file mode 100644
index 0000000..a1cdb6e
--- /dev/null
+++ b/src/Tests/LogicTests/InputProcessorTests.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using OnScreenKeyboard.Logic;
+
+namespace LogicTests
+{
+ [TestClass]
+ public class InputProcessorTests
+ {
+ [TestInitialize]
+ public void Init()
+ {
+ _processor = new InputProcessor(new SixBySixKeyboardLayout());
+ }
+
+ InputProcessor _processor;
+
+ [TestMethod]
+ public void ScrubInput()
+ {
+ Assert.AreEqual(
+ "TEST TEST 123",
+ _processor.ScrubInput("Test \"Test\" 123 :';,.<>/?\\"));
+ }
+
+ [TestMethod]
+ public void ToCursorInstructions()
+ {
+
+ Assert.AreEqual(
+ "#,R,#,R,#,R,#,R,#,R,#,L,L,L,L,L,D,#,R,#,R,#,R,#,R,#,R,#,L,L,L,L,L,D,#,R,#,R,#,R,#,R,#,R,#,L,L,L,L,L,D,#,R,#,R,#,R,#,R,#,R,#,L,L,L,L,L,D,#,R,#,R,#,R,#,R,#,R,#,L,L,L,L,L,D,#,R,#,R,#,R,#,R,#,R,#",
+ _processor.ToCursorInstructions("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"));
+
+ Assert.AreEqual(
+ "R,D,D,D,#,R,R,R,U,U,U,#,L,L,L,L,D,D,D,#,R,#,S,#,R,R,R,U,U,U,#,L,L,L,L,D,D,D,#,R,#",
+ _processor.ToCursorInstructions("TEST TEST"));
+
+ Assert.AreEqual(
+ "R,D,D,D,#,R,R,R,U,U,U,#,L,L,L,L,D,D,D,#,R,#,S,#,R,R,R,U,U,U,#,L,L,L,L,D,D,D,#,R,#,S,R,D,#,R,#,R,#",
+ _processor.ToCursorInstructions("Test \"Test\" 123 :';,.<>/?\\"));
+ }
+ }
+}
diff --git a/src/Tests/LogicTests/LogicTests.csproj b/src/Tests/LogicTests/LogicTests.csproj
new file mode 100644
index 0000000..a444bd2
--- /dev/null
+++ b/src/Tests/LogicTests/LogicTests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp2.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+