Compare commits

..

31 Commits

Author SHA1 Message Date
eneller
7ea92145ea doc 2026-02-07 16:10:27 +01:00
eneller
7777a3b71c doc: fix slide formatting 2026-02-06 14:58:04 +01:00
eneller
84f4ee55c0 doc: slides 2026-02-06 14:56:21 +01:00
lukasadrion
9e5c82d82c add introduction and discussion 2026-02-06 13:58:04 +01:00
eneller
64a0934471 doc: formatting 2026-02-06 13:54:22 +01:00
lukasadrion
4ddaf78137 update values 2026-02-06 13:46:05 +01:00
eneller
d38c62c381 add final participant 2026-02-06 13:32:46 +01:00
eneller
f88e9ed3ae doc: begin slides 2026-02-06 13:23:55 +01:00
lukasadrion
df6908995b Merge branch 'main' of github.com:eneller/hr-hci 2026-02-06 12:39:48 +01:00
lukasadrion
1fe3b37504 add results chapter 2026-02-06 12:39:27 +01:00
eneller
1ef991c90f doc: procedure 2026-02-06 11:48:38 +01:00
eneller
dc0acdd012 doc: begin experiments section 2026-02-06 11:23:00 +01:00
lukasadrion
f1c16f1222 add new nasa tlx values 2026-02-06 10:50:24 +01:00
eneller
afce143022 doc: better summary table 2026-02-06 10:24:08 +01:00
eneller
3c1cb5f9c7 minor fixes 2026-02-06 09:39:58 +01:00
lukasadrion
03c40a3a96 update latex format for r output 2026-02-06 01:25:30 +01:00
eneller
97b40d8c18 chore: stop tracking figures/ 2026-02-05 21:35:28 +01:00
lukasadrion
81d496e6ce add abstract and keyboard types chapter 2026-02-05 18:01:51 +01:00
lukasadrion
c2673f71bb add inferential statistics 2026-02-05 16:41:25 +01:00
lukasadrion
c217589fc4 add r code for new descriptive statistics 2026-02-04 00:40:14 +01:00
lukasadrion
8da2b97ab6 add missing figures for descriptive statistics 2026-02-04 00:38:31 +01:00
eneller
e2c25e5f59 more study participants 2026-02-01 21:17:19 +01:00
eneller
da4fc7c99c document structure 2026-02-01 16:12:40 +01:00
eneller
05631293be fix: csv, echo 2026-02-01 13:36:00 +01:00
lukasadrion
31a2da04fb add descriptive statistics 2026-01-31 19:23:37 +01:00
eneller
cdacf0bc4a doc: first 2 study participants 2026-01-28 20:14:55 +01:00
eneller
d1d9a782f8 doc: add procedure 2026-01-28 19:29:08 +01:00
eneller
8aca73cd9c doc: begin sweave 2026-01-26 12:59:36 +01:00
eneller
e8bfcacacb doc: init R 2026-01-26 12:29:00 +01:00
eneller
86b67f36e4 refactor: enum for keyboards 2026-01-26 11:50:43 +01:00
eneller
93041a370e begin cli args 2026-01-26 11:14:28 +01:00
29 changed files with 4065 additions and 78 deletions

3
.gitignore vendored
View File

@@ -42,3 +42,6 @@ testem.log
Thumbs.db
TextTestExe/
data/raw
.Rproj.user
figures/

View File

@@ -17,4 +17,4 @@ Keyboard emulation on most systems requires workarounds for two issues:
## Logging and Statistics
For logging, either use [TextTestExe](https://depts.washington.edu/acelab/proj/texttest/) (local program) or [TextTestPP](https://drustz.com/TextTestPP/) (online).
The theoretical framework is inspired by https://www.yorku.ca/mack/bit95.html
The theoretical framework is inspired by https://www.yorku.ca/mack/bit95.html.

37
data/nasaTLX.csv Normal file
View File

@@ -0,0 +1,37 @@
layout, mental_demand, physical_demand, performance, effort, frustration
dvorak, 19, 18, 13, 18, 18
qwerty, 0, 0, 7, 2, 0
circle, 15, 12, 12, 15, 10
dvorak, 17, 17, 3, 17, 17
qwerty, 15, 15, 3, 16, 15
circle, 17, 14, 3, 17, 17
dvorak, 14, 17, 15, 13, 14
qwerty, 3, 3, 12, 12, 11
circle, 19, 19, 9, 9, 9
dvorak, 14, 14, 12, 15, 13
qwerty, 11, 8, 5, 16, 4
circle, 7, 5, 5, 11, 9
dvorak, 14, 13, 9, 16, 19
qwerty, 4, 7, 6, 11, 3
circle, 12, 9, 6, 6, 14
dvorak, 15, 4, 4, 14, 16
qwerty, 1, 2, 1, 2, 1
circle, 19, 7, 15, 13, 18
dvorak, 7, 2, 12, 11, 11
qwerty, 1, 2, 17, 2, 1
circle, 6, 5, 13, 12, 17
dvorak, 15, 4, 8, 15, 17
qwerty, 5, 2, 15, 6, 3
circle, 13, 4, 7, 12, 13
dvorak, 9, 11, 14, 14, 14
qwerty, 0, 0, 5, 7, 2
circle, 15, 15, 11, 15, 11
dvorak, 16, 14, 6, 16, 15
qwerty, 2, 3, 8, 5, 4
circle, 14, 12, 8, 10, 12
dvorak, 14, 13, 9, 16, 19
qwerty, 3, 4, 5, 7, 3
circle, 17, 14, 7, 12, 13
dvorak, 12, 2, 10, 5, 10
qwerty, 1, 1, 3, 1, 1
circle, 4, 1, 7, 2, 2
1 layout mental_demand physical_demand performance effort frustration
2 dvorak 19 18 13 18 18
3 qwerty 0 0 7 2 0
4 circle 15 12 12 15 10
5 dvorak 17 17 3 17 17
6 qwerty 15 15 3 16 15
7 circle 17 14 3 17 17
8 dvorak 14 17 15 13 14
9 qwerty 3 3 12 12 11
10 circle 19 19 9 9 9
11 dvorak 14 14 12 15 13
12 qwerty 11 8 5 16 4
13 circle 7 5 5 11 9
14 dvorak 14 13 9 16 19
15 qwerty 4 7 6 11 3
16 circle 12 9 6 6 14
17 dvorak 15 4 4 14 16
18 qwerty 1 2 1 2 1
19 circle 19 7 15 13 18
20 dvorak 7 2 12 11 11
21 qwerty 1 2 17 2 1
22 circle 6 5 13 12 17
23 dvorak 15 4 8 15 17
24 qwerty 5 2 15 6 3
25 circle 13 4 7 12 13
26 dvorak 9 11 14 14 14
27 qwerty 0 0 5 7 2
28 circle 15 15 11 15 11
29 dvorak 16 14 6 16 15
30 qwerty 2 3 8 5 4
31 circle 14 12 8 10 12
32 dvorak 14 13 9 16 19
33 qwerty 3 4 5 7 3
34 circle 17 14 7 12 13
35 dvorak 12 2 10 5 10
36 qwerty 1 1 3 1 1
37 circle 4 1 7 2 2

13
data/results.csv Normal file
View File

@@ -0,0 +1,13 @@
id,age,sequence,qwerty_wpm, qwerty_ter, dvorak_wpm, dvorak_ter, circle_wpm, circle_ter
en, 24, qwerty-dvorak-circle, 21.6, 0.067, 9.0, 0.023, 8.5, 0.017
la, 25, qwerty-circle-dvorak, 18.5, 0.007, 8.8, 0.017, 9.8, 0.022
as, 22, dvorak-qwerty-circle, 21.1, 0.049, 10.0, 0.006, 9.0, 0.033
hb, 24, dvorak-circle-qwerty, 19.0, 0.019, 6.2, 0.018, 5.8, 0.004
gs, 21, circle-qwerty-dvorak, 18.2, 0.034, 10.0, 0.038, 11.3, 0.00
ab, 21, circle-dvorak-qwerty, 12.1, 0.034, 5.4, 0.029, 5.8, 0.105
mz, 24, qwerty-dvorak-circle, 15.8, 0.063, 9.8, 0.043, 10.3, 0.065
oa, 21, qwerty-circle-dvorak, 17.5, 0.011, 9.5, 0.011, 11.6, 0.010
dc, 22, dvorak-qwerty-circle, 15.2, 0.033, 7.0, 0.097, 5.1, 0.009
pt, 23, dvorak-circle-qwerty, 16.3, 0.027, 6.5, 0.012, 6.2, 0.005
rn, 24, circle-qwerty-dvorak, 14.8, 0.052, 8.8, 0.034, 9.5, 0.022
ss, 21, dvorak-circle-qwerty, 18.2, 0.034, 10.0, 0.038, 11.3, 0.000
1 id age sequence qwerty_wpm qwerty_ter dvorak_wpm dvorak_ter circle_wpm circle_ter
2 en 24 qwerty-dvorak-circle 21.6 0.067 9.0 0.023 8.5 0.017
3 la 25 qwerty-circle-dvorak 18.5 0.007 8.8 0.017 9.8 0.022
4 as 22 dvorak-qwerty-circle 21.1 0.049 10.0 0.006 9.0 0.033
5 hb 24 dvorak-circle-qwerty 19.0 0.019 6.2 0.018 5.8 0.004
6 gs 21 circle-qwerty-dvorak 18.2 0.034 10.0 0.038 11.3 0.00
7 ab 21 circle-dvorak-qwerty 12.1 0.034 5.4 0.029 5.8 0.105
8 mz 24 qwerty-dvorak-circle 15.8 0.063 9.8 0.043 10.3 0.065
9 oa 21 qwerty-circle-dvorak 17.5 0.011 9.5 0.011 11.6 0.010
10 dc 22 dvorak-qwerty-circle 15.2 0.033 7.0 0.097 5.1 0.009
11 pt 23 dvorak-circle-qwerty 16.3 0.027 6.5 0.012 6.2 0.005
12 rn 24 circle-qwerty-dvorak 14.8 0.052 8.8 0.034 9.5 0.022
13 ss 21 dvorak-circle-qwerty 18.2 0.034 10.0 0.038 11.3 0.000

1
doc/.Rprofile Normal file
View File

@@ -0,0 +1 @@
source("renv/activate.R")

375
doc/.gitignore vendored Normal file
View File

@@ -0,0 +1,375 @@
# Created by https://www.toptal.com/developers/gitignore/api/tex,r
# Edit at https://www.toptal.com/developers/gitignore?templates=tex,r
### R ###
# History files
.Rhistory
.Rapp.history
# Session Data files
.RData
.RDataTmp
# User-specific files
.Ruserdata
# Example code in package build process
*-Ex.R
# Output files from R CMD build
/*.tar.gz
# Output files from R CMD check
/*.Rcheck/
# RStudio files
.Rproj.user/
# produced vignettes
vignettes/*.html
vignettes/*.pdf
# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
.httr-oauth
# knitr and R markdown default cache directories
*_cache/
/cache/
# Temporary files created by R markdown
*.utf8.md
*.knit.md
# R Environment Variables
.Renviron
# pkgdown site
docs/
# translation temp files
po/*~
# RStudio Connect folder
rsconnect/
### R.Bookdown Stack ###
# R package: bookdown caching files
/*_files/
### TeX ###
## Core latex/pdflatex auxiliary files:
*.aux
*.lof
*.log
*.lot
*.fls
*.out
*.toc
*.fmt
*.fot
*.cb
*.cb2
.*.lb
## Intermediate documents:
*.dvi
*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
# *.eps
*.pdf
## Generated if empty string is given at "Please type another file name for output:"
.pdf
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
*.blg
*-blx.aux
*-blx.bib
*.run.xml
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
*.synctex(busy)
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
## Build tool directories for auxiliary files
# latexrun
latex.out/
## Auxiliary and intermediate files from other packages:
# algorithms
*.alg
*.loa
# achemso
acs-*.bib
# amsthm
*.thm
# beamer
*.nav
*.pre
*.snm
*.vrb
# changes
*.soc
# comment
*.cut
# cprotect
*.cpt
# elsarticle (documentclass of Elsevier journals)
*.spl
# endnotes
*.ent
# fixme
*.lox
# feynmf/feynmp
*.mf
*.mp
*.t[1-9]
*.t[1-9][0-9]
*.tfm
#(r)(e)ledmac/(r)(e)ledpar
*.end
*.?end
*.[1-9]
*.[1-9][0-9]
*.[1-9][0-9][0-9]
*.[1-9]R
*.[1-9][0-9]R
*.[1-9][0-9][0-9]R
*.eledsec[1-9]
*.eledsec[1-9]R
*.eledsec[1-9][0-9]
*.eledsec[1-9][0-9]R
*.eledsec[1-9][0-9][0-9]
*.eledsec[1-9][0-9][0-9]R
# glossaries
*.acn
*.acr
*.glg
*.glo
*.gls
*.glsdefs
*.lzo
*.lzs
*.slg
*.slo
*.sls
# uncomment this for glossaries-extra (will ignore makeindex's style files!)
# *.ist
# gnuplot
*.gnuplot
*.table
# gnuplottex
*-gnuplottex-*
# gregoriotex
*.gaux
*.glog
*.gtex
# htlatex
*.4ct
*.4tc
*.idv
*.lg
*.trc
*.xref
# hyperref
*.brf
# knitr
*-concordance.tex
# TODO Uncomment the next line if you use knitr and want to ignore its generated tikz files
# *.tikz
*-tikzDictionary
# listings
*.lol
# luatexja-ruby
*.ltjruby
# makeidx
*.idx
*.ilg
*.ind
# minitoc
*.maf
*.mlf
*.mlt
*.mtc[0-9]*
*.slf[0-9]*
*.slt[0-9]*
*.stc[0-9]*
# minted
_minted*
*.pyg
# morewrites
*.mw
# newpax
*.newpax
# nomencl
*.nlg
*.nlo
*.nls
# pax
*.pax
# pdfpcnotes
*.pdfpc
# sagetex
*.sagetex.sage
*.sagetex.py
*.sagetex.scmd
# scrwfile
*.wrt
# svg
svg-inkscape/
# sympy
*.sout
*.sympy
sympy-plots-for-*.tex/
# pdfcomment
*.upa
*.upb
# pythontex
*.pytxcode
pythontex-files-*/
# tcolorbox
*.listing
# thmtools
*.loe
# TikZ & PGF
*.dpth
*.md5
*.auxlock
# titletoc
*.ptc
# todonotes
*.tdo
# vhistory
*.hst
*.ver
# easy-todo
*.lod
# xcolor
*.xcp
# xmpincl
*.xmpi
# xindy
*.xdy
# xypic precompiled matrices and outlines
*.xyc
*.xyd
# endfloat
*.ttt
*.fff
# Latexian
TSWLatexianTemp*
## Editors:
# WinEdt
*.bak
*.sav
# Texpad
.texpadtmp
# LyX
*.lyx~
# Kile
*.backup
# gummi
.*.swp
# KBibTeX
*~[0-9]*
# TeXnicCenter
*.tps
# auto folder when using emacs and auctex
./auto/*
*.el
# expex forward references with \gathertags
*-tags.tex
# standalone packages
*.sta
# Makeindex log files
*.lpz
# xwatermark package
*.xwm
# REVTeX puts footnotes in the bibliography by default, unless the nofootinbib
# option is specified. Footnotes are the stored in a file with suffix Notes.bib.
# Uncomment the next line to have this generated file ignored.
#*Notes.bib
### TeX Patch ###
# LIPIcs / OASIcs
*.vtc
# glossaries
*.glstex
# End of https://www.toptal.com/developers/gitignore/api/tex,r
*.html
bib
# Sweave-specific
*.tex
*-SAVE-ERROR

29
doc/.latexmkrc Normal file
View File

@@ -0,0 +1,29 @@
@default_files = ('report.Rnw');
$latex = 'latex %O --shell-escape %S';
$pdflatex = 'pdflatex --shell-escape %S -synctex=1 %O %S';
$pdf_mode = 1;
$clean_ext = 'lol nav snm loa bbl* glo ist tex aux bbl blg log out toc Rnw.tex %R-concordance.tex %R.tex';
$bibtex_use = 2;
# only enable when compiling .Rnw or .Rtex file
if(grep(/\.(rnw|rtex)$/i, @ARGV)) {
$latex = 'internal knitrlatex ' . $latex;
$pdflatex = 'internal knitrlatex ' . $pdflatex;
my $knitr_compiled = {};
sub knitrlatex {
for (@_) {
next unless -e $_;
my $input = $_;
next unless $_ =~ s/\.(rnw|rtex)$/.tex/i;
my $tex = $_;
my $checksum = (fdb_get($input))[-1];
if (!$knitr_compiled{$input} || $knitr_compiled{$input} ne $checksum) {
my $ret = system("Rscript -e \"knitr::knit('$input')\"");
if($ret) { return $ret; }
rdb_ensure_file($rule, $tex);
$knitr_compiled{$input} = $checksum;
}
}
return system(@_);
}
}

13
doc/hci-doc.Rproj Normal file
View File

@@ -0,0 +1,13 @@
Version: 1.0
RestoreWorkspace: Default
SaveWorkspace: Default
AlwaysSaveHistory: Default
EnableCodeIndexing: Yes
UseSpacesForTab: Yes
NumSpacesForTab: 2
Encoding: UTF-8
RnwWeave: knitr
LaTeX: pdfLaTeX

BIN
doc/images/circle-pic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
doc/images/dvorak-pic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
doc/images/qwerty-pic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

BIN
doc/images/tauri.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

12
doc/procedure.md Normal file
View File

@@ -0,0 +1,12 @@
# Procedure
1. Open `TestTestExe`
2. Go to `Test > Options`:
1. Tab `Auto`: Stop test after Trial **13**, Switch to test mode after trial **3**
2. Tab `Prevention`: Prevent **Punctuation, numbers, capitals**
3. determine keyboard testing sequence
4. ask participant age
5. Repeat for each keyboard:
1. Set keyboard
2. Participant fulfills typing task
3. Note down `wpm` and `ter` in results.csv
6. Present reduced NASA-TLX

1285
doc/renv.lock Normal file

File diff suppressed because one or more lines are too long

7
doc/renv/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
library/
local/
cellar/
lock/
python/
sandbox/
staging/

1403
doc/renv/activate.R Normal file

File diff suppressed because it is too large Load Diff

20
doc/renv/settings.json Normal file
View File

@@ -0,0 +1,20 @@
{
"bioconductor.version": null,
"external.libraries": [],
"ignored.packages": [],
"package.dependency.fields": [
"Imports",
"Depends",
"LinkingTo"
],
"ppm.enabled": null,
"ppm.ignored.urls": [],
"r.version": null,
"snapshot.dev": false,
"snapshot.type": "implicit",
"use.cache": true,
"vcs.ignore.cellar": true,
"vcs.ignore.library": true,
"vcs.ignore.local": true,
"vcs.manage.ignores": true
}

476
doc/report.Rnw Normal file
View File

@@ -0,0 +1,476 @@
\documentclass[conference,a4paper]{IEEEtran}
\usepackage{graphicx} % for including figures
\usepackage{booktabs} % for nicer tables
\usepackage{float}
\usepackage[utf8x]{inputenc}
\usepackage[margin=1in]{geometry} % Adjust margins
\usepackage{caption}
\usepackage{hyperref}
\PassOptionsToPackage{hyphens}{url} % allow breaking urls
\usepackage{float}
\usepackage{wrapfig}
\usepackage{subcaption}
\usepackage{parskip}
\usepackage[style=ieee, backend=biber, maxnames=1, minnames=1]{biblatex}
\addbibresource{report.bib}
\title{On-Screen Keyboard Layout Study}
\begin{document}
\maketitle
\section{Abstract}\label{abstract}
We evaluated three on-screen keyboard layouts: QWERTY, Dvorak, and Circle. Objective performance, measured in words per minute (WPM), showed a significant main effect of layout. Post-hoc comparisons revealed that QWERTY was significantly faster than both Dvorak and Circle, while no difference was observed between Dvorak and Circle. Total error rate (TER) did not differ significantly between layouts.
Subjective workload ratings assessed via NASA-TLX were similar for Dvorak and Circle, but QWERTY was perceived as less demanding. These results indicate that QWERTY offers superior typing speed, whereas error rates and perceived workload are comparable across layouts.
\section{Introduction}\label{introduction}
On-screen keyboards have become increasingly relevant for applications where traditional physical keyboards are not practical, such as in virtual environments, accessibility tools, or experimental settings. Unlike physical keyboards, on-screen keyboards require users to select keys via a pointing device, such as a mouse, which introduces unique challenges in terms of speed, accuracy, and ergonomic efficiency.
Traditional physical keyboard layouts, such as QWERTY and Dvorak, have been extensively studied and optimized for mechanical typing. QWERTY, the most commonly used layout, was originally designed for typewriters and provides a familiar arrangement for most users. Dvorak, in contrast, aims to increase typing efficiency by placing frequently used letters on the home row to minimize finger movement. While these layouts are well understood for physical typing, their effectiveness may differ when key selection is performed via a pointing device.
In addition to established layouts, novel designs have emerged to exploit the flexibility of on-screen interfaces. For example, circular or radial layouts can optimize visual prominence and spatial reachability of keys, potentially improving selection speed and reducing cognitive effort when using a mouse. However, empirical comparisons of these alternative layouts with traditional designs remain limited, particularly with respect to both objective measures such as words per minute (WPM) and total error rate (TER), and subjective workload assessments.
The present study investigates three on-screen keyboard layouts: QWERTY, Dvorak, and a custom-designed Circle layout. Participants entered text using a mouse to select keys, allowing us to evaluate typing speed, accuracy, and perceived workload across different layouts. The results provide insights into the efficiency and usability of established and novel on-screen keyboard designs in contexts where mouse-based input is required.
\section{Keyboard Designs}\label{keyboard-designs}
Three on-screen keyboard layouts were evaluated in this study: QWERTY, Dvorak, and a custom-designed Circle layout.
1. QWERTY: The standard layout commonly used in English typing, serving as a baseline for comparison.
2. Dvorak: An alternative layout designed to increase typing efficiency for physical keyboards
by placing frequently used letters in the home row, minimizing finger movements.
3. Circle Layout: A custom layout developed for this study, in which keys were arranged in a circular pattern. Letters that occur more frequently in English were positioned closer to the center and rendered larger to facilitate faster access. Less frequently used letters were placed toward the periphery and sized smaller, aiming to optimize ergonomic reach and visual salience.
This design allowed us to investigate both established and novel layouts, comparing objective typing performance, error rates, and subjective workload.
\begin{figure}[H]
\centering
\includegraphics[width=0.45\textwidth]{images/qwerty-pic.png}
\caption{QWERTY Keyboard Layout}
\label{fig:qwerty}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.45\textwidth]{images/dvorak-pic.png}
\caption{Dvorak Keyboard Layout}
\label{fig:dvorak}
\end{figure}
\begin{figure}[H]
\centering
\includegraphics[width=0.3\textwidth]{images/circle-pic.png}
\caption{Circle Keyboard Layout}
\label{fig:circle}
\end{figure}
\section{Experiment}\label{experiment}
<<echo=FALSE, message=FALSE>>=
# load libraries here
library(knitr)
library(dplyr)
library(tidyr)
# Read the results CSV
results <- read.csv("../data/results.csv", sep=",", header=TRUE)
@
\subsection{Participants}\label{participants}
Our experiment was conducted using a small sample of \Sexpr{nrow(results)} participants.
All of our participants, predominantly male with an average age of \Sexpr{round(mean(results$age),digits=1)},
were students familiar with computers.
\subsection{Apparatus}\label{apparatus}
The main body of our experimental apparatus was our On-Screen Keyboard, implemented using Tauri + Angular.
It provides a view of exactly one of the layouts described in \autoref{keyboard-designs} at a time.
Text-entry measures were collected using TextTest \cite{texttest}, with everything running on a stationary Windows 11 computer.
\subsection{Procedure}\label{procedure}
Each participant was first provided with an overview of the three keyboard models and design rationale
They were then presented all three layouts in a counterbalanced order to mitigate common order effects
such as practice, fatigue and boredom.
Each keyboard was evaluated using only lowercase letters, space and enter to display the next sentence.
Due to our chosen time constraint of 30 minutes, each participant was given three practice sentences per keyboard,
followed by 10 recorded sentences for the experiment.
After completion of all 3 layouts, the participants were then asked to fill out the NASA Task Load Index \cite{nasatlx}.
\section{Results}\label{results}
This section presents the experiment results comparing the three keyboard layouts QWERTY, Dvorak and Circle.
Performance was evaluated using typing speed measured in words per minute (WPM) and accuracy assessed through the total error rate (TER).
In addition, subjective workload was collected using the NASA-TLX questionnaire.
\subsection{Descriptive Statistics}\label{descriptive-statistics}
This section reports descriptive statistics for the objective performance measures and the subjective workload assessments.
\subsubsection{Objective Measures}\label{objective-measures}
Objective typing performance was assessed using words per minute (WPM) and total error rate (TER) as shown in table~\ref{tab:wpm} and ~\ref{tab:ter}. Descriptive statistics show that QWERTY clearly outperformed the alternative layouts in terms of typing speed. Participants achieved a mean speed of 17.28 WPM on QWERTY, whereas both DVORAK (8.27 WPM) and CIRCLE (8.45 WPM) resulted in substantially lower average speeds. This indicates that participants typed more than twice as fast on the standard QWERTY layout compared to the other two designs.
In contrast, accuracy differences between layouts were relatively small. Mean TER values were low across all conditions, with QWERTY showing a slightly higher average error rate (0.036) than DVORAK (0.0298) and CIRCLE (0.0265). Overall, the objective results suggest that layout differences were most pronounced for speed rather than error performance.
<<echo=FALSE, message=FALSE>>=
ter_stats <- results %>%
summarise(
qwerty_min = min(qwerty_ter, na.rm = TRUE),
qwerty_q1 = quantile(qwerty_ter, 0.25, na.rm = TRUE),
qwerty_median = median(qwerty_ter, na.rm = TRUE),
qwerty_mean = mean(qwerty_ter, na.rm = TRUE),
qwerty_q3 = quantile(qwerty_ter, 0.75, na.rm = TRUE),
qwerty_max = max(qwerty_ter, na.rm = TRUE),
dvorak_min = min(dvorak_ter, na.rm = TRUE),
dvorak_q1 = quantile(dvorak_ter, 0.25, na.rm = TRUE),
dvorak_median = median(dvorak_ter, na.rm = TRUE),
dvorak_mean = mean(dvorak_ter, na.rm = TRUE),
dvorak_q3 = quantile(dvorak_ter, 0.75, na.rm = TRUE),
dvorak_max = max(dvorak_ter, na.rm = TRUE),
circle_min = min(circle_ter, na.rm = TRUE),
circle_q1 = quantile(circle_ter, 0.25, na.rm = TRUE),
circle_median = median(circle_ter, na.rm = TRUE),
circle_mean = mean(circle_ter, na.rm = TRUE),
circle_q3 = quantile(circle_ter, 0.75, na.rm = TRUE),
circle_max = max(circle_ter, na.rm = TRUE)
)
ter_tidy <- ter_stats %>%
pivot_longer(
cols = everything(),
names_to = c("layout", ".value"),
names_sep = "_"
)
ter_tidy <- ter_tidy %>%
select(layout, min, q1, median, mean, q3, max)
wpm_stats <- results %>%
summarise(
qwerty_min = min(qwerty_wpm, na.rm = TRUE),
qwerty_q1 = quantile(qwerty_wpm, 0.25, na.rm = TRUE),
qwerty_median = median(qwerty_wpm, na.rm = TRUE),
qwerty_mean = mean(qwerty_wpm, na.rm = TRUE),
qwerty_q3 = quantile(qwerty_wpm, 0.75, na.rm = TRUE),
qwerty_max = max(qwerty_wpm, na.rm = TRUE),
dvorak_min = min(dvorak_wpm, na.rm = TRUE),
dvorak_q1 = quantile(dvorak_wpm, 0.25, na.rm = TRUE),
dvorak_median = median(dvorak_wpm, na.rm = TRUE),
dvorak_mean = mean(dvorak_wpm, na.rm = TRUE),
dvorak_q3 = quantile(dvorak_wpm, 0.75, na.rm = TRUE),
dvorak_max = max(dvorak_wpm, na.rm = TRUE),
circle_min = min(circle_wpm, na.rm = TRUE),
circle_q1 = quantile(circle_wpm, 0.25, na.rm = TRUE),
circle_median = median(circle_wpm, na.rm = TRUE),
circle_mean = mean(circle_wpm, na.rm = TRUE),
circle_q3 = quantile(circle_wpm, 0.75, na.rm = TRUE),
circle_max = max(circle_wpm, na.rm = TRUE)
)
wpm_tidy <- wpm_stats %>%
pivot_longer(
cols = everything(),
names_to = c("layout", ".value"),
names_sep = "_"
)
wpm_tidy <- wpm_tidy %>%
select(layout, min, q1, median, mean, q3, max)
@
% WPM table
\begin{table}[H]
\centering
\caption{Summary of Words per Minute (WPM)}
\label{tab:wpm}
\resizebox{\columnwidth}{!}{
<<results='asis', echo=FALSE>>=
kable(wpm_tidy, format="latex", booktabs=TRUE)
@
}
\end{table}
% TER table
\begin{table}[H]
\centering
\caption{Summary of Total Error Rate (TER)}
\label{tab:ter}
\resizebox{\columnwidth}{!}{
<<results='asis', echo=FALSE>>=
kable(ter_tidy, format="latex", booktabs=TRUE)
@
}
\end{table}
<<echo=FALSE, results='hide'>>=
# Create figures directory if it doesn't exist
dir.create("../figures", showWarnings=FALSE)
# Helper functions for standard deviation and confidence intervals
mean_sd <- function(x) {
m <- mean(x)
s <- sd(x)
c(mean=m, lower=m-s, upper=m+s)
}
mean_ci <- function(x) {
m <- mean(x)
se <- sd(x)/sqrt(length(x))
ci <- qt(0.975, df=length(x)-1)*se
c(mean=m, lower=m-ci, upper=m+ci)
}
# TER stats
ter_stats <- rbind(
mean_ci(results$qwerty_ter),
mean_ci(results$dvorak_ter),
mean_ci(results$circle_ter)
)
# Save TER barplot as PDF using LaTeX-compatible fonts
suppressMessages(pdf("../figures/ter_plot.pdf"))
bar_pos <- barplot(
ter_stats[,"mean"],
names.arg=c("QWERTY","DVORAK","CIRCLE"),
ylab="Total Error Rate (TER)",
main="TER of layouts",
ylim=c(0, max(ter_stats[,"upper"])*1.1)
)
# Add confidence intervals
arrows(
x0=bar_pos, y0=ter_stats[,"lower"],
x1=bar_pos, y1=ter_stats[,"upper"],
angle=90, code=3, length=0.05
)
dev.off()
# WPM stats
wpm_stats <- rbind(
mean_sd(results$qwerty_wpm),
mean_sd(results$dvorak_wpm),
mean_sd(results$circle_wpm)
)
# Save WPM barplot as PDF using LaTeX-compatible fonts
suppressMessages(pdf("../figures/wpm_plot.pdf"))
bar_pos <- barplot(
wpm_stats[,"mean"],
names.arg=c("QWERTY","DVORAK","CIRCLE"),
ylab="Words per minute (WPM)",
main="WPM of layouts",
ylim=c(0, max(wpm_stats[,"upper"])*1.1)
)
arrows(
x0=bar_pos, y0=wpm_stats[,"lower"],
x1=bar_pos, y1=wpm_stats[,"upper"],
angle=90, code=3, length=0.05
)
dev.off()
@
%Include ter and wpm plot
\begin{figure}[H]
\centering
\includegraphics[width=0.7\columnwidth]{../figures/wpm_plot.pdf}
\vspace{-0.4cm}
\includegraphics[width=0.7\columnwidth]{../figures/ter_plot.pdf}
\caption{TER (top) and WPM (bottom) by keyboard layout.}
\end{figure}
\subsubsection{Subjective Measures}\label{subjective-measures}
Subjective workload was measured using the NASA-TLX dimensions of
mental demand, physical demand, effort, frustration, and perceived performance (as shown in \autoref{fig:nasa}).
Across all workload categories, QWERTY was consistently rated as the most favorable layout,
indicating lower perceived demand and higher user comfort.
Dvorak and Circle received generally similar subjective evaluations, with no major differences between them. However, Circle was mostly better perceived than Dvorak across all NASA-TLX dimensions, suggesting a modest subjective preference for the circular layout design. Overall, the subjective findings align with the objective performance trends, with QWERTY being clearly preferred by participants.
<<echo=FALSE, results='hide'>>=
# Read NASA-TLX data
nasa <- read.csv("../data/nasaTLX.csv")
nasa$layout <- factor(nasa$layout)
# Save boxplots as PDF using LaTeX-compatible fonts
suppressMessages(pdf("../figures/nasa_boxplots.pdf", width=10, height=20, pointsize = 24))
par(mfrow=c(3,2))
boxplot(mental_demand ~ layout, data=nasa, main="Mental Demand")
boxplot(physical_demand ~ layout, data=nasa, main="Physical Demand")
boxplot(performance ~ layout, data=nasa, main="Performance")
boxplot(effort ~ layout, data=nasa, main="Effort")
boxplot(frustration ~ layout, data=nasa, main="Frustration")
par(mfrow=c(1,1))
dev.off()
@
% Include NASA-TLX boxplots
\begin{figure}[H]
\centering
\includegraphics[width=\columnwidth]{../figures/nasa_boxplots.pdf}
\caption{NASA-TLX Scores by Keyboard Layout}
\label{fig:nasa}
\end{figure}
\subsection{Inferential Statistics}\label{inferential-statistics}
To examine the effect of keyboard layout on typing performance, repeated-measures ANOVAs were conducted separately for typing speed (WPM) and total error rate (TER).
%TODO Friedman test of Subjective
\subsubsection{Typing speed (WPM)}
The repeated-measures ANOVA revealed a significant main effect of layout on typing speed, $F(2, 22) = 120.56, p < .001$ (Table~\ref{tab:anova_wpm}). Post-hoc pairwise comparisons with Bonferroni correction indicated that QWERTY yielded significantly higher typing speeds than both DVORAK ($p < .001$) and CIRCLE ($p < .001$).
%Anova RM for WPM
<<echo=FALSE, results='hide'>>=
library(tidyr)
# Add participant ID
results$id <- 1:nrow(results)
# --- WPM Long Format ---
wpm_long <- results %>%
select(id, qwerty_wpm, dvorak_wpm, circle_wpm) %>%
pivot_longer(
cols = -id,
names_to = "layout",
values_to = "wpm"
)
wpm_long$id <- factor(wpm_long$id)
wpm_long$layout <- factor(wpm_long$layout,
levels=c("qwerty_wpm","dvorak_wpm","circle_wpm"),
labels=c("QWERTY","DVORAK","CIRCLE"))
# --- RM ANOVA for WPM ---
anova_wpm <- aov(wpm ~ layout + Error(id/layout), data=wpm_long)
@
\begin{table}[H]
\centering
\caption{Repeated-Measurements ANOVA for WPM}
\label{tab:anova_wpm}
\resizebox{\columnwidth}{!}{
<<results='asis', echo=FALSE>>=
wpm_tab <- summary(anova_wpm)[[2]][[1]]
wpm_effect <- wpm_tab["layout", , drop=FALSE]
wpm_effect$`Pr(>F)` <- "$p< .001$"
colnames(wpm_effect) <- c("Df", "Sum Sq", "Mean Sq", "F value", "p-value")
kable(wpm_effect,
format="latex",
booktabs=TRUE,
escape=FALSE)
@
}
\end{table}
\subsubsection{Total Error Rate (TER)}
In contrast, the ANOVA for total error rate did not reveal a significant effect of layout, $F(2, 22) = 0.71, p = 0.505$ (Table~\ref{tab:anova_ter}). This indicates that accuracy was comparable across QWERTY, DVORAK, and CIRCLE layouts. Although QWERTY exhibited a slightly higher mean TER than the other layouts, these differences were not statistically significant. Therefore, while QWERTY facilitated faster typing, it did not compromise accuracy.
%Anova RM for TER
<<echo=FALSE, results='hide'>>=
# --- TER Long Format ---
ter_long <- results %>%
select(id, qwerty_ter, dvorak_ter, circle_ter) %>%
pivot_longer(
cols = -id,
names_to = "layout",
values_to = "ter"
)
ter_long$id <- factor(ter_long$id)
ter_long$layout <- factor(ter_long$layout,
levels=c("qwerty_ter","dvorak_ter","circle_ter"),
labels=c("QWERTY","DVORAK","CIRCLE"))
# --- RM ANOVA for TER ---
anova_ter <- aov(ter ~ layout + Error(id/layout), data=ter_long)
@
\begin{table}[H]
\centering
\caption{Repeated-Measures ANOVA for TER}
\label{tab:anova_ter}
\resizebox{\columnwidth}{!}{
<<results='asis', echo=FALSE>>=
ter_tab <- summary(anova_ter)[[2]][[1]]
ter_effect <- ter_tab["layout", , drop=FALSE]
colnames(ter_effect) <- c("Df", "Sum Sq", "Mean Sq", "F value", "p-value")
kable(ter_effect,
format="latex",
booktabs=TRUE,
escape=FALSE)
@
}
\end{table}
\subsubsection{Post-hoc Comparison for WPM}
Post-hoc pairwise comparisons with Bonferroni adjustment were conducted to further explore differences between keyboard layouts (Table~\ref{tab:posthoc}). Results revealed that QWERTY was significantly faster than DVORAK ($p < .001$) and CIRCLE ($p < .001$), confirming the advantage of the standard layout. The difference between DVORAK and CIRCLE was not significant ($p = 1.000$), indicating comparable performance between these alternative layouts. These comparisons highlight that the observed main effect of layout on typing speed is primarily driven by the superior performance of QWERTY, while the two non-standard layouts yield similar typing speeds.
% Post-Hoc analysis with bonferroni correction for WPM
\begin{table}[H]
\centering
\caption{Post-hoc-comparison of layouts with Bonferroni correction}
\label{tab:posthoc}
\resizebox{\columnwidth}{!}{
<<echo=FALSE, results='asis'>>=
suppressMessages(library(emmeans))
suppressMessages(emm_wpm <- emmeans(anova_wpm, ~ layout))
posthoc <- pairs(emm_wpm, adjust = "bonferroni")
posthoc_df <- as.data.frame(posthoc)
posthoc_df <- posthoc_df %>%
mutate(p.value = ifelse(p.value < 0.001, "$<0.001$", sprintf("%.3f", p.value)))
kable(
posthoc_df,
format = "latex",
booktabs = TRUE,
digits = 3,
float=FALSE,
escape=FALSE
)
@
}
\end{table}
\section{Discussion}\label{discussion}
The results of this study provide clear evidence that keyboard layout significantly affects typing performance when using a mouse to select keys on an on-screen keyboard. Participants typed substantially faster on the QWERTY layout compared to both the Dvorak and Circle layouts, with mean WPM values more than twice as high. This finding indicates that prior familiarity with the QWERTY layout plays a crucial role in enabling efficient text entry, even when interaction is performed via a pointing device rather than physical key presses.
In contrast, total error rates were low across all layouts and did not differ significantly. This suggests that participants were able to maintain accuracy regardless of layout, and that the speed advantage of QWERTY did not come at the cost of increased errors. The similar TER values across layouts indicate that the Circle layout, despite its unconventional design, and Dvorak, despite its unfamiliarity, are viable for accurate text entry with a mouse, even if they do not provide a speed advantage.
Subjective workload assessments using NASA-TLX mirrored the objective performance results. QWERTY was consistently rated as less mentally demanding and effortful, reflecting participants' familiarity and confidence with this layout. Dvorak and Circle were evaluated similarly across most dimensions, with Circle slightly preferred in some cases, indicating that its design may offer modest ergonomic or cognitive benefits compared to Dvorak, although these did not translate into faster typing speeds.
Overall, the findings highlight several important points for the design of on-screen keyboards for mouse-based input. First, established layouts like QWERTY remain highly efficient due to user familiarity, even in non-traditional input modalities. Second, novel layouts such as Circle can achieve comparable accuracy and subjective comfort, suggesting potential for specialized applications where visual ergonomics or reach optimization is critical. Finally, training and experience are likely necessary for users to achieve performance gains with alternative layouts like Dvorak or Circle.
In practical terms, these results suggest that for general-purpose applications requiring rapid text entry via a mouse, QWERTY should be the default layout. However, alternative designs could be explored in contexts where visual salience, ergonomics, or user-specific customization is prioritized, provided users are given adequate practice to adapt to the new layout.
Future research should investigate longer-term adaptation effects, as performance with unfamiliar layouts may improve substantially with training. Additionally, studies could examine the impact of alternative pointing devices (e.g., trackpads, styluses) and larger participant samples to generalize findings across different user populations. Such work would help refine the design principles of on-screen keyboards for a range of input scenarios beyond traditional physical typing.
\printbibliography
\end{document}

20
doc/report.bib Normal file
View File

@@ -0,0 +1,20 @@
@article{texttest,
title={Analyzing the input stream for character-level errors in unconstrained text entry evaluations},
author={Wobbrock, Jacob O and Myers, Brad A},
journal={ACM Transactions on Computer-Human Interaction (TOCHI)},
volume={13},
number={4},
pages={458--489},
year={2006},
publisher={ACM New York, NY, USA},
url={https://depts.washington.edu/acelab/proj/texttest/},
}
@incollection{nasatlx,
title={Development of NASA-TLX (Task Load Index): Results of empirical and theoretical research},
author={Hart, Sandra G and Staveland, Lowell E},
booktitle={Advances in psychology},
volume={52},
pages={139--183},
year={1988},
publisher={Elsevier}
}

132
doc/slides.Rmd Normal file
View File

@@ -0,0 +1,132 @@
---
title: "On-Screen Keyboard Layout Study"
subtitle: "Human-Computer Interfaces"
date: "`r Sys.Date()`"
output: beamer_presentation
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE)
library(tidyr)
library(dplyr)
results <- read.csv("../data/results.csv", sep=",", header=TRUE)
```
## Implementation
![tauri](images/tauri.png)
## Keyboard Design - Qwerty
![qwerty](images/qwerty-pic.png)
## Keyboard Design - Dvorak
![dvorak](images/dvorak-pic.png)
## Keyboard Design - Circle
![circle](images/circle-pic.png)
## Descriptive Stats - Objective
![wpm](../figures/wpm_plot.pdf)
---
![ter](../figures/ter_plot.pdf)
## Descriptive Stats- Subjective
```{r, echo=FALSE}
# Read NASA-TLX data
nasa <- read.csv("../data/nasaTLX.csv")
nasa$layout <- factor(nasa$layout)
# Save boxplots as PDF using LaTeX-compatible fonts
par(mfrow=c(1,2))
boxplot(mental_demand ~ layout, data=nasa, main="Mental Demand")
boxplot(physical_demand ~ layout, data=nasa, main="Physical Demand")
```
## Descriptive Stats- Subjective
```{r, echo=FALSE}
par(mfrow=c(1,3))
boxplot(performance ~ layout, data=nasa, main="Performance")
boxplot(effort ~ layout, data=nasa, main="Effort")
boxplot(frustration ~ layout, data=nasa, main="Frustration")
par(mfrow=c(1,1))
```
## Inferential Stats
WPM:
```{r, echo=FALSE}
# Add participant ID
results$id <- 1:nrow(results)
# --- WPM Long Format ---
wpm_long <- results %>%
select(id, qwerty_wpm, dvorak_wpm, circle_wpm) %>%
pivot_longer(
cols = -id,
names_to = "layout",
values_to = "wpm"
)
wpm_long$id <- factor(wpm_long$id)
wpm_long$layout <- factor(wpm_long$layout,
levels=c("qwerty_wpm","dvorak_wpm","circle_wpm"),
labels=c("QWERTY","DVORAK","CIRCLE"))
# --- RM ANOVA for WPM ---
anova_wpm <- aov(wpm ~ layout + Error(id/layout), data=wpm_long)
wpm_tab <- summary(anova_wpm)[[2]][[1]]
wpm_effect <- wpm_tab["layout", , drop=FALSE]
wpm_effect$`Pr(>F)` <- "$p< .001$"
colnames(wpm_effect) <- c("Df", "Sum Sq", "Mean Sq", "F value", "p-value")
wpm_effect
```
TER:
```{r, echo=FALSE}
# --- TER Long Format ---
ter_long <- results %>%
select(id, qwerty_ter, dvorak_ter, circle_ter) %>%
pivot_longer(
cols = -id,
names_to = "layout",
values_to = "ter"
)
ter_long$id <- factor(ter_long$id)
ter_long$layout <- factor(ter_long$layout,
levels=c("qwerty_ter","dvorak_ter","circle_ter"),
labels=c("QWERTY","DVORAK","CIRCLE"))
# --- RM ANOVA for TER ---
anova_ter <- aov(ter ~ layout + Error(id/layout), data=ter_long)
ter_tab <- summary(anova_ter)[[2]][[1]]
ter_effect <- ter_tab["layout", , drop=FALSE]
colnames(ter_effect) <- c("Df", "Sum Sq", "Mean Sq", "F value", "p-value")
ter_effect
```
## Post-Hoc
```{r, echo=FALSE, message=FALSE}
library(emmeans)
emm_wpm <- emmeans(anova_wpm, ~ layout)
posthoc <- pairs(emm_wpm, adjust = "bonferroni")
posthoc_df <- as.data.frame(posthoc)
posthoc_df <- posthoc_df %>%
mutate(p.value = ifelse(p.value < 0.001, "$<0.001$", sprintf("%.3f", p.value)))
posthoc_df$SE <- NULL
posthoc_df
```

44
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "keeb",
"version": "0.1.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "keeb",
"version": "0.1.0",
"version": "1.0.1",
"dependencies": {
"@angular/animations": "^18.2.14",
"@angular/common": "^18.2.14",
@@ -17,6 +17,7 @@
"@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^18.2.14",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-cli": "^2.4.1",
"@tauri-apps/plugin-opener": "^2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
@@ -358,7 +359,6 @@
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.14.tgz",
"integrity": "sha512-Kp/MWShoYYO+R3lrrZbZgszbbLGVXHB+39mdJZwnIuZMDkeL3JsIBlSOzyJRTnpS1vITc+9jgHvP/6uKbMrW1Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -554,7 +554,6 @@
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.14.tgz",
"integrity": "sha512-ZPRswzaVRiqcfZoowuAM22Hr2/z10ajWOUoFDoQ9tWqz/fH/773kJv2F9VvePIekgNPCzaizqv9gF6tGNqaAwg==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -571,7 +570,6 @@
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.14.tgz",
"integrity": "sha512-Mpq3v/mztQzGAQAAFV+wAI1hlXxZ0m8eDBgaN2kD3Ue+r4S6bLm1Vlryw0iyUnt05PcFIdxPT6xkcphq5pl6lw==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -593,7 +591,6 @@
"integrity": "sha512-BmmjyrFSBSYkm0tBSqpu4cwnJX/b/XvhM36mj2k8jah3tNS5zLDDx5w6tyHmaPJa/1D95MlXx2h6u7K9D+Mhew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "7.25.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@@ -700,7 +697,6 @@
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.14.tgz",
"integrity": "sha512-BIPrCs93ZZTY9ym7yfoTgAQ5rs706yoYeAdrgc8kh/bDbM9DawxKlgeKBx2FLt09Y0YQ1bFhKVp0cV4gDEaMxQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -735,7 +731,6 @@
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.14.tgz",
"integrity": "sha512-W+JTxI25su3RiZVZT3Yrw6KNUCmOIy7OZIZ+612skPgYK2f2qil7VclnW1oCwG896h50cMJU/lnAfxZxefQgyQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
@@ -820,7 +815,6 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@@ -3118,7 +3112,6 @@
"integrity": "sha512-b2BudQY/Si4Y2a0PdZZL6BeJtl8llgeZa7U2j47aaJSCeAl1e4UI7y8a9bSkO3o/ZbZrgT5muy/34JbsjfIWxA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@inquirer/checkbox": "^2.4.7",
"@inquirer/confirm": "^3.1.22",
@@ -4581,6 +4574,15 @@
"node": ">= 10"
}
},
"node_modules/@tauri-apps/plugin-cli": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.4.1.tgz",
"integrity": "sha512-8JXofQFI5cmiGolh1PlU4hzE2YJgrgB1lyaztyBYiiMCy13luVxBXaXChYPeqMkUo46J1UadxvYdjRjj0E8zaw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@tauri-apps/api": "^2.8.0"
}
},
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.0.tgz",
@@ -4779,7 +4781,6 @@
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.14.0"
}
@@ -5120,7 +5121,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5197,7 +5197,6 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -5641,7 +5640,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -8711,8 +8709,7 @@
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz",
"integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/jest-worker": {
"version": "27.5.1",
@@ -8838,7 +8835,6 @@
"integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "1.5.0",
"body-parser": "^1.19.0",
@@ -9051,7 +9047,6 @@
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
@@ -11099,7 +11094,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
@@ -11743,7 +11737,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -11800,7 +11793,6 @@
"integrity": "sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@@ -12921,7 +12913,6 @@
"integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -13080,8 +13071,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tuf-js": {
"version": "2.2.1",
@@ -13138,7 +13128,6 @@
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13969,7 +13958,6 @@
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.5",
"@webassemblyjs/ast": "^1.12.1",
@@ -14199,7 +14187,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -14452,8 +14439,7 @@
"version": "0.14.10",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz",
"integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==",
"license": "MIT",
"peer": true
"license": "MIT"
}
}
}

View File

@@ -18,16 +18,18 @@
"@angular/platform-browser": "^18.2.14",
"@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^18.2.14",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-cli": "^2.4.1",
"@tauri-apps/plugin-opener": "^2",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.2",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
"zone.js": "~0.14.2"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.21",
"@angular/cli": "^18.2.21",
"@angular/compiler-cli": "^18.2.14",
"@tauri-apps/cli": "^2",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
@@ -35,7 +37,6 @@
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.0",
"@tauri-apps/cli": "^2"
"typescript": "~5.4.0"
}
}

117
src-tauri/Cargo.lock generated
View File

@@ -41,6 +41,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.100"
@@ -456,6 +506,39 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "clap"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
@@ -1750,6 +1833,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
@@ -1843,6 +1932,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-opener",
"windows 0.62.2",
"x11",
@@ -2434,6 +2524,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "open"
version = "5.3.2"
@@ -3754,6 +3850,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28e78fb2c09a81546bcd376d34db4bda5769270d00990daa9f0d6e7ac1107e25"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.0"
@@ -4320,6 +4431,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"

View File

@@ -34,6 +34,9 @@ windows = { version = "0.62.2", features = [
[target.'cfg(target_os = "linux")'.dependencies]
x11 = { version = "2.21.0", features = ["xlib"] }
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2"
[profile.dev]
incremental = true # Compile your binary in smaller steps.

View File

@@ -0,0 +1,14 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"windows": [
"main"
],
"permissions": [
"cli:default"
]
}

View File

@@ -1,14 +1,9 @@
use enigo::{Direction, Enigo, Key, Keyboard, Settings};
use tauri::Manager;
#[cfg(target_os = "linux")]
use std::ptr;
use tauri::Manager;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
#[tauri::command]
fn send_key(key: String) -> Result<(), String> {
let mut enigo = Enigo::new(&Settings::default()).map_err(|e| format!("Enigo error: {}", e))?;
@@ -19,25 +14,35 @@ fn send_key(key: String) -> Result<(), String> {
// Check if uppercase letter
if ch.is_uppercase() && ch.is_alphabetic() {
// Send Shift + lowercase letter
enigo.key(Key::Shift, Direction::Press)
enigo
.key(Key::Shift, Direction::Press)
.map_err(|e| format!("Shift press error: {}", e))?;
enigo.key(Key::Unicode(ch.to_lowercase().next().unwrap()), Direction::Click)
enigo
.key(
Key::Unicode(ch.to_lowercase().next().unwrap()),
Direction::Click,
)
.map_err(|e| format!("Key error: {}", e))?;
enigo.key(Key::Shift, Direction::Release)
enigo
.key(Key::Shift, Direction::Release)
.map_err(|e| format!("Shift release error: {}", e))?;
} else {
// Send character as-is (lowercase or non-letter)
enigo.key(Key::Unicode(ch), Direction::Click)
enigo
.key(Key::Unicode(ch), Direction::Click)
.map_err(|e| format!("Key error: {}", e))?;
}
} else {
// Special keys
match key.as_str() {
"Enter" => enigo.key(Key::Return, Direction::Click)
"Enter" => enigo
.key(Key::Return, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Space" => enigo.key(Key::Space, Direction::Click)
"Space" => enigo
.key(Key::Space, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Backspace" => enigo.key(Key::Backspace, Direction::Click)
"Backspace" => enigo
.key(Key::Backspace, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
_ => return Err("Unknown key".to_string()),
}
@@ -48,14 +53,16 @@ fn send_key(key: String) -> Result<(), String> {
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_cli::init())
.setup(|app| {
let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "windows")]
{
use windows::Win32::UI::WindowsAndMessaging::{SetWindowPos, HWND_TOPMOST, SWP_NOACTIVATE, GetWindowLongPtrW, SetWindowLongPtrW, GWL_EXSTYLE, WS_EX_NOACTIVATE};
use windows::Win32::Foundation::HWND;
#![allow(deprecated)]
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::WindowsAndMessaging::{GetWindowLongPtrW, SetWindowLongPtrW, GWL_EXSTYLE, WS_EX_NOACTIVATE,};
unsafe {
if let Ok(RawWindowHandle::Win32(handle)) = window.raw_window_handle() {
@@ -82,8 +89,8 @@ pub fn run() {
#[cfg(target_os = "linux")]
{
use x11::xlib;
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use x11::xlib;
unsafe {
let handle = window.raw_window_handle();
@@ -106,7 +113,7 @@ pub fn run() {
Ok(())
})
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet, send_key])
.invoke_handler(tauri::generate_handler![send_key])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -26,6 +26,29 @@
"csp": null
}
},
"plugins": {
"cli":{
"description": "On-Screen keyboard for Human-Computer Interface studies",
"args": [
{
"name": "order",
"short": "o",
"takesValue": true,
"multiple": true
},
{
"name": "limit",
"short": "l",
"takesValue": true
},
{
"name": "demo",
"short": "d",
"takesValue": true
}
]
}
},
"bundle": {
"active": true,
"targets": "all",

View File

@@ -2,27 +2,27 @@
<!-- Layout Switcher -->
<div class="layout-controls">
<button (click)="switchLayout()" class="layout-button">
Switch Layout: {{ currentLayout.toUpperCase() }}
Switch Layout: {{ currentLayout.toString() }}
</button>
</div>
<!-- Conditional Rendering basierend auf currentLayout -->
<app-qwerty-keyboard
*ngIf="currentLayout === 'qwerty'"
*ngIf="currentLayout === Keyboards.QWERTY"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-qwerty-keyboard>
<app-dvorak-keyboard
*ngIf="currentLayout === 'dvorak'"
*ngIf="currentLayout === Keyboards.DVORAK"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-dvorak-keyboard>
<app-circle-keyboard
*ngIf="currentLayout === 'circle'"
*ngIf="currentLayout === Keyboards.CIRCLE"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">

View File

@@ -1,7 +1,8 @@
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { invoke } from "@tauri-apps/api/core";
import { getMatches } from '@tauri-apps/plugin-cli';
import { QwertyKeyboardComponent } from './keyboards/qwerty-keyboard.component';
import { DvorakKeyboardComponent } from './keyboards/dvorak-keyboard.component';
import { CircleKeyboardComponent } from './keyboards/circle-keyboard.component';
@@ -17,12 +18,16 @@ import { CircleKeyboardComponent } from './keyboards/circle-keyboard.component';
CircleKeyboardComponent
],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
styleUrl: './app.component.css',
})
export class AppComponent {
greetingMessage = "";
currentLayout: 'qwerty' | 'dvorak' | 'circle' = 'qwerty';
export class AppComponent implements OnInit {
Keyboards = Keyboards;
currentLayout: Keyboards = Keyboards.QWERTY;
shiftActive = false;
async ngOnInit() {
const cli = await getMatches();
}
@ViewChild('greetInput') inputElement!: ElementRef;
@@ -31,12 +36,12 @@ export class AppComponent {
}
switchLayout(): void {
if (this.currentLayout == 'qwerty'){
this.currentLayout = 'dvorak';
} else if (this.currentLayout == 'dvorak'){
this.currentLayout = 'circle';
} else if (this.currentLayout == 'circle'){
this.currentLayout = 'qwerty';
if (this.currentLayout === Keyboards.QWERTY){
this.currentLayout = Keyboards.DVORAK;
} else if (this.currentLayout === Keyboards.DVORAK){
this.currentLayout = Keyboards.CIRCLE;
} else if (this.currentLayout === Keyboards.CIRCLE){
this.currentLayout = Keyboards.QWERTY;
}
}
@@ -54,3 +59,8 @@ export class AppComponent {
}
}
}
enum Keyboards{
QWERTY,
DVORAK,
CIRCLE
}