Compare commits

...

21 Commits

Author SHA1 Message Date
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
eneller
2b6ea24d5e chore: bump version 2026-01-25 20:12:29 +01:00
eneller
02e6b033c2 chore: remove demo 2026-01-25 20:00:23 +01:00
eneller
076816bd12 doc: readme 2026-01-24 18:52:01 +01:00
eneller
1ad2e8ad4c doc: readme 2026-01-24 18:38:37 +01:00
eneller
3b5d304845 feat: working osk 2026-01-24 18:11:33 +01:00
eneller
cade0046bf doc: research 2025-12-17 20:18:51 +01:00
eneller
881a723f4e chore: update deps 2025-12-07 17:33:37 +01:00
eneller
673a71de43 Merge branch 'feature/keyboard-frontend' 2025-12-07 17:21:15 +01:00
eneller
6d16af7143 doc: README 2025-12-07 16:55:52 +01:00
lukasadrion
04a8a15f23 fix light and darkmode 2025-12-06 16:39:33 +01:00
lukasadrion
7744bcf67d remove german comments 2025-12-05 01:22:36 +01:00
lukasadrion
3512bf0fac finished circle keyboard implementation and update window size 2025-12-05 01:18:54 +01:00
lukasadrion
ef740ca9dd add first implementation of circle keyboard and better switching of keyboards 2025-12-04 18:54:10 +01:00
lukasadrion
15cdc77eea add DVORAK keyboard 2025-12-01 16:53:52 +01:00
lukasadrion
2ad8b7fffb remove initial html code 2025-12-01 16:40:31 +01:00
lukasadrion
327608cc36 add normal layout keyboard 2025-11-29 23:09:58 +01:00
eneller
6c4235a52f fix: linux-specific build 2025-11-28 14:09:55 +01:00
24 changed files with 6194 additions and 1565 deletions

3
.gitignore vendored
View File

@@ -40,3 +40,6 @@ testem.log
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
TextTestExe/
data/

View File

@@ -1,7 +1,20 @@
# Tauri + Angular # Tauri + Angular
This template should help get you started developing with Tauri and Angular. On Windows, Tauri requires the the msvc toolchain:
```
rustup default stable-msvc
```
## Recommended IDE Setup ## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template). [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) + [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=Angular.ng-template).
## Keyboard Emulation
Keyboard emulation on most systems requires workarounds for two issues:
- keeping the window with the keyboard on top while another (the input target) is in focus
- not receiving focus when clicked
- sending input
## 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.

1
doc/.Rprofile Normal file
View File

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

374
doc/.gitignore vendored Normal file
View File

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

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

962
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
}

34
doc/report.Rnw Normal file
View File

@@ -0,0 +1,34 @@
\documentclass{article}
\begin{document}
\section{Abstract}\label{abstract}
\section{Introduction}\label{introduction}
\section{Keyboard Designs}\label{keyboard-designs}
\section{Experiment}\label{experiment}
\subsection{Participants}\label{participants}
\subsection{Apparatus}\label{apparatus}
\subsection{Procedure}\label{procedure}
\section{Results}\label{results}
\subsection{Descriptive Statistics}\label{descriptive-statistics}
\subsubsection{Objective Measures}\label{objective-measures}
\subsubsection{Subjective Measures}\label{subjective-measures}
\subsection{Inferential Statistics}\label{inferential-statistics}
\subsubsection{Objective Measures}\label{objective-measures-1}
\subsubsection{Subjective Measures}\label{subjective-measures-1}
\section{Discussion}\label{discussion}
\end{document}

3982
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "keeb", "name": "keeb",
"version": "0.1.0", "version": "1.0.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
@@ -10,24 +10,26 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^17.0.0", "@angular/animations": "^18.2.14",
"@angular/common": "^17.0.0", "@angular/common": "^18.2.14",
"@angular/compiler": "^17.0.0", "@angular/compiler": "^18.2.14",
"@angular/core": "^17.0.0", "@angular/core": "^18.2.14",
"@angular/forms": "^17.0.0", "@angular/forms": "^18.2.14",
"@angular/platform-browser": "^17.0.0", "@angular/platform-browser": "^18.2.14",
"@angular/platform-browser-dynamic": "^17.0.0", "@angular/platform-browser-dynamic": "^18.2.14",
"@angular/router": "^17.0.0", "@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", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.14.2", "zone.js": "~0.14.2"
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^17.0.0", "@angular-devkit/build-angular": "^18.2.21",
"@angular/cli": "^17.0.0", "@angular/cli": "^18.2.21",
"@angular/compiler-cli": "^17.0.0", "@angular/compiler-cli": "^18.2.14",
"@tauri-apps/cli": "^2",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"jasmine-core": "~5.1.0", "jasmine-core": "~5.1.0",
"karma": "~6.4.0", "karma": "~6.4.0",
@@ -35,7 +37,6 @@
"karma-coverage": "~2.2.0", "karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0", "karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0", "karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.4.0", "typescript": "~5.4.0"
"@tauri-apps/cli": "^2"
} }
} }

195
src-tauri/Cargo.lock generated
View File

@@ -41,6 +41,56 @@ dependencies = [
"libc", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.100" version = "1.0.100"
@@ -456,6 +506,39 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@@ -840,7 +923,7 @@ dependencies = [
"objc2 0.6.3", "objc2 0.6.3",
"objc2-app-kit", "objc2-app-kit",
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
"windows", "windows 0.61.3",
"x11rb", "x11rb",
"xkbcommon", "xkbcommon",
"xkeysym", "xkeysym",
@@ -1750,6 +1833,12 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -1843,7 +1932,9 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-cli",
"tauri-plugin-opener", "tauri-plugin-opener",
"windows 0.62.2",
"x11", "x11",
] ]
@@ -2433,6 +2524,12 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "open" name = "open"
version = "5.3.2" version = "5.3.2"
@@ -3598,7 +3695,7 @@ dependencies = [
"tao-macros", "tao-macros",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
@@ -3670,7 +3767,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -3753,6 +3850,21 @@ dependencies = [
"walkdir", "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]] [[package]]
name = "tauri-plugin-opener" name = "tauri-plugin-opener"
version = "2.5.0" version = "2.5.0"
@@ -3771,7 +3883,7 @@ dependencies = [
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.17", "thiserror 2.0.17",
"url", "url",
"windows", "windows 0.61.3",
"zbus", "zbus",
] ]
@@ -3797,7 +3909,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -3823,7 +3935,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"wry", "wry",
] ]
@@ -4319,6 +4431,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.18.1" version = "1.18.1"
@@ -4550,7 +4668,7 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
@@ -4574,7 +4692,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [ dependencies = [
"thiserror 2.0.17", "thiserror 2.0.17",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@@ -4630,11 +4748,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [ dependencies = [
"windows-collections", "windows-collections 0.2.0",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-future", "windows-future 0.2.1",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-numerics", "windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
] ]
[[package]] [[package]]
@@ -4646,6 +4776,15 @@ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core 0.62.2",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -4680,7 +4819,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-link 0.1.3", "windows-link 0.1.3",
"windows-threading", "windows-threading 0.1.0",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
] ]
[[package]] [[package]]
@@ -4727,6 +4877,16 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@@ -4856,6 +5016,15 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-version" name = "windows-version"
version = "0.1.7" version = "0.1.7"
@@ -5082,7 +5251,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",

View File

@@ -23,9 +23,20 @@ tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
enigo = "0.6.1" enigo = "0.6.1"
x11 = { version = "2.21.0", features = ["xlib"] }
raw-window-handle = "0.6.2" raw-window-handle = "0.6.2"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.62.2", features = [
"Win32_UI_WindowsAndMessaging",
"Win32_Foundation"
] }
[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] [profile.dev]
incremental = true # Compile your binary in smaller steps. 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,25 +1,102 @@
use enigo::{Direction::Click, Enigo, Key, Keyboard, Settings}; use enigo::{Direction, Enigo, Key, Keyboard, Settings};
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle, WindowHandle}; #[cfg(target_os = "linux")]
use std::ptr; use std::ptr;
use tauri::Manager; use tauri::Manager;
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command] #[tauri::command]
fn greet(name: &str) -> String { fn greet(name: &str) -> String {
let mut enigo = Enigo::new(&Settings::default()).unwrap();
enigo.key(Key::Unicode('a'), Click);
format!("Hello, {}! You've been greeted from Rust!", name) 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))?;
if key.len() == 1 {
let ch = key.chars().next().unwrap();
// Check if uppercase letter
if ch.is_uppercase() && ch.is_alphabetic() {
// Send Shift + lowercase letter
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,
)
.map_err(|e| format!("Key error: {}", e))?;
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)
.map_err(|e| format!("Key error: {}", e))?;
}
} else {
// Special keys
match key.as_str() {
"Enter" => enigo
.key(Key::Return, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Space" => enigo
.key(Key::Space, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
"Backspace" => enigo
.key(Key::Backspace, Direction::Click)
.map_err(|e| format!("Key error: {}", e))?,
_ => return Err("Unknown key".to_string()),
}
}
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_cli::init())
.setup(|app| { .setup(|app| {
let window = app.get_webview_window("main").unwrap(); let window = app.get_webview_window("main").unwrap();
#[cfg(target_os = "windows")]
{
#![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() {
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
// Get current extended window style
let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE) as u32;
// Add WS_EX_NOACTIVATE flag
let new_style = ex_style | WS_EX_NOACTIVATE.0;
SetWindowLongPtrW(hwnd, GWL_EXSTYLE, new_style as isize);
// Position window as topmost without activating
/*
SetWindowPos(
hwnd,
Some(HWND_TOPMOST),
0, 0, 0, 0,
SWP_NOACTIVATE
); */
}
}
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
use raw_window_handle::{HasRawWindowHandle, RawWindowHandle};
use x11::xlib; use x11::xlib;
// raw X11 pointers here
unsafe { unsafe {
let handle = window.raw_window_handle(); let handle = window.raw_window_handle();
if let Ok(RawWindowHandle::Xlib(h)) = handle { if let Ok(RawWindowHandle::Xlib(h)) = handle {
@@ -28,7 +105,7 @@ pub fn run() {
let hints = xlib::XAllocWMHints(); let hints = xlib::XAllocWMHints();
if !hints.is_null() { if !hints.is_null() {
(*hints).flags = xlib::InputHint; (*hints).flags = xlib::InputHint;
(*hints).input = 0; // Set input hint to False (*hints).input = 0;
xlib::XSetWMHints(display, h.window, hints); xlib::XSetWMHints(display, h.window, hints);
xlib::XFree(hints as *mut _); xlib::XFree(hints as *mut _);
} }
@@ -37,10 +114,11 @@ pub fn run() {
} }
} }
} }
Ok(()) Ok(())
}) })
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![greet]) .invoke_handler(tauri::generate_handler![greet, send_key])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -14,13 +14,41 @@
{ {
"title": "keeb", "title": "keeb",
"width": 800, "width": 800,
"height": 600 "height": 700,
"minWidth": 800,
"minHeight": 700,
"resizable": true,
"center": true,
"alwaysOnTop": true
} }
], ],
"security": { "security": {
"csp": null "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": { "bundle": {
"active": true, "active": true,
"targets": "all", "targets": "all",

View File

@@ -1,6 +1,4 @@
.logo.angular:hover {
filter: drop-shadow(0 0 2em #e32727);
}
:root { :root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px; font-size: 16px;
@@ -33,10 +31,6 @@
transition: 0.75s; transition: 0.75s;
} }
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row { .row {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -109,4 +103,56 @@ button {
button:active { button:active {
background-color: #0f0f0f69; background-color: #0f0f0f69;
} }
.key-button {
background: rgb(58, 56, 56);
color: white;
}
}
.keyboard {
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
background: #f5f5f5;
border-radius: 10px;
margin: 20px auto;
}
.keyboard-row {
display: flex;
gap: 6px;
justify-content: center;
}
.key-button {
min-width: 60px;
height: 60px;
font-size: 20px;
font-weight: bold;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.1s;
}
.key-button:hover {
background: #b1b1b1;
border-color: #3d3d3d;
}
.key-button:active {
background: #d0d0d0;
transform: scale(0.95);
}
.shift-key.active {
background: #4CAF50;
color: white;
border-color: #45a049;
}
.space-key {
min-width: 300px;
} }

View File

@@ -1,19 +1,30 @@
<main class="container"> <main class="container">
<h1>Welcome to Tauri + Angular!</h1> <!-- Layout Switcher -->
<div class="layout-controls">
<div class="row"> <button (click)="switchLayout()" class="layout-button">
<a href="https://tauri.app" target="_blank"> Switch Layout: {{ currentLayout.toString() }}
<img src="/assets/tauri.svg" class="logo tauri" alt="Tauri logo" /> </button>
</a>
<a href="https://angular.io" target="_blank">
<img src="/assets/angular.svg" class="logo angular" alt="Angular logo" />
</a>
</div> </div>
<p>Click on the logos to learn more about the frameworks</p>
<form class="row" (submit)="greet($event, greetInput.value)"> <!-- Conditional Rendering basierend auf currentLayout -->
<input #greetInput id="greet-input" placeholder="Enter a name..." /> <app-qwerty-keyboard
<button type="submit">Greet</button> *ngIf="currentLayout === Keyboards.QWERTY"
</form> [shiftActive]="shiftActive"
<p>{{ greetingMessage }}</p> (keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-qwerty-keyboard>
<app-dvorak-keyboard
*ngIf="currentLayout === Keyboards.DVORAK"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-dvorak-keyboard>
<app-circle-keyboard
*ngIf="currentLayout === Keyboards.CIRCLE"
[shiftActive]="shiftActive"
(keyPressed)="handleKeyPress($event)"
(shiftToggled)="toggleShift()">
</app-circle-keyboard>
</main> </main>

View File

@@ -1,24 +1,66 @@
import { Component } from '@angular/core'; import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { invoke } from "@tauri-apps/api/core"; 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';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet], imports: [
CommonModule,
RouterOutlet,
QwertyKeyboardComponent,
DvorakKeyboardComponent,
CircleKeyboardComponent
],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.css' styleUrl: './app.component.css',
}) })
export class AppComponent { export class AppComponent implements OnInit {
greetingMessage = ""; Keyboards = Keyboards;
currentLayout: Keyboards = Keyboards.QWERTY;
shiftActive = false;
async ngOnInit() {
greet(event: SubmitEvent, name: string): void { const cli = await getMatches();
event.preventDefault(); }
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ @ViewChild('greetInput') inputElement!: ElementRef;
invoke<string>("greet", { name }).then((text) => {
this.greetingMessage = text; toggleShift(): void {
}); this.shiftActive = !this.shiftActive;
}
switchLayout(): void {
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;
} }
} }
async handleKeyPress(key: string): Promise<void> {
let finalKey = key;
if (key.length === 1) {
finalKey = this.shiftActive ? key.toUpperCase() : key.toLowerCase();
}
await invoke("send_key", { key: finalKey });
if (this.shiftActive && key.length === 1) {
this.shiftActive = false;
}
}
}
enum Keyboards{
QWERTY,
DVORAK,
CIRCLE
}

View File

@@ -0,0 +1,161 @@
.circle-keyboard-container {
position: relative;
width: 400px;
height: 400px;
margin: 50px auto;
background: #f5f5f5;
}
/* Center Button Container */
.center-button-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 140px;
height: 140px;
z-index: 1;
}
/* Center Button Sections */
.center-section {
position: absolute;
background: white;
color: #0f0f0f;
font-size: 20px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
border: 2px solid #ddd;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* Top-left: Backspace (triangle) */
.top-left{
top: 0;
left: 0;
width: 70px;
height: 70px;
border-radius: 70px 0 0 0;
}
/* Top-right: Enter*/
.top-right {
top: 0;
right: 0;
width: 70px;
height: 70px;
border-radius: 0 70px 0 0;
}
/* Bottom: Space */
.bottom-left {
bottom: 0;
left: 0;
width: 70px;
height: 70px;
border-radius: 0 0 0 70px;
}
.bottom-left.active {
background: #4CAF50 !important;
color: white;
border-color: #45a049;
}
.bottom-right {
bottom: 0;
right: 0;
width: 70px;
height: 70px;
border-radius: 0 0 70px 0;
}
.middle-left {
top: 50%;
left: 50%;
transform: translate(-100%, -50%);
width: 30px;
height: 55px;
border-radius: 35px 0 0 35px;
z-index: 15;
}
.middle-right {
top: 50%;
left: 50%;
transform: translate(0%, -50%);
width: 30px;
height: 55px;
border-radius: 0 35px 35px 0;
z-index: 15;
}
/* 8 Circle Buttons */
.circle-button {
position: absolute;
top: 50%;
left: 50%;
width: 70px;
height: 70px;
margin: -35px 0 0 -35px;
border-radius: 50%;
border: 2px solid #ddd;
background: white;
color: #0f0f0f;
font-size: 24px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
box-shadow: 0 2px 2px rgba(0,0,0,0.2);
z-index: 1;
}
.second-ring {
width: 50px;
height: 50px;
font-size: 20px;
font-weight: normal;
margin: -25px 0 0 -25px;
}
.third-ring {
width: 40px;
height: 40px;
font-size: 18px;
font-weight: normal;
margin: -20px 0 0 -20px;
}
.fourth-ring {
width: 30px;
height: 30px;
font-size: 16px;
font-weight: lighter;
margin: -20px 0 0 -20px;
}
.circle-button:hover, .center-section:hover {
background: #b1b1b1;
border-color: #3d3d3d;
}
.circle-button:active, .center-section:active {
background: #d0d0d0;
}
@media (prefers-color-scheme: dark) {
.center-section {
background: rgb(58, 56, 56);
color: white;
}
.circle-button {
background: rgb(58, 56, 56);
color: white;
}
}

View File

@@ -0,0 +1,104 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
//TODO: Second and fourth ring of keys
@Component({
selector: 'app-circle-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="circle-keyboard-container">
<!-- Center Button - Large with 3 sections -->
<div class="center-button-container">
<!-- Top half split: Backspace left, Enter right -->
<button class="center-section top-left"
(click)="keyPressed.emit('Backspace')"
title="Backspace">
</button>
<button class="center-section top-right"
(click)="keyPressed.emit('Enter')"
title="Enter">
</button>
<!-- Bottom half: Space -->
<button class="center-section bottom-right"
(click)="keyPressed.emit('Space')"
title="Space">
</button>
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="center-section bottom-left">
</button>
<button class="center-section middle-left"
(click)="keyPressed.emit('.')"
title="Period">
.
</button>
<button class="center-section middle-right"
(click)="keyPressed.emit(',')"
title="Comma">
,
</button>
</div>
<!-- 8 Surrounding Buttons in Circle -->
<button *ngFor="let key of circleKeysFirst; let i = index"
class="circle-button"
[style.transform]="getCirclePositionFromTop(i, 110)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button *ngFor="let key of circleKeysSecond; let i = index"
class="circle-button second-ring"
[style.transform]="getCirclePositionFromTop(i, 150, 22.5)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button *ngFor="let key of circleKeysThird; let i = index"
class="circle-button third-ring"
[style.transform]="getCirclePositionFromTop(i, 170)"
(click)="keyPressed.emit(key)">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
<button class="circle-button fourth-ring"
[style.transform]="getCirclePositionFromTop(0, 182, -12)"
(click)="keyPressed.emit(circleKeysFourth[0])">
{{ shiftActive ? circleKeysFourth[0] : circleKeysFourth[0].toLowerCase() }}
</button>
<button class="circle-button fourth-ring"
[style.transform]="getCirclePositionFromTop(0, 182, 14)"
(click)="keyPressed.emit(circleKeysFourth[1])">
{{ shiftActive ? circleKeysFourth[1] : circleKeysFourth[1].toLowerCase() }}
</button>
</div>
`,
styleUrl: './circle-keyboard.component.css'
})
export class CircleKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
// 8 keys arranged in circle
circleKeysFirst = ['E', 'T', 'A', 'O', 'N', 'I', 'H', 'S'];
circleKeysSecond = ['R', 'L', 'D', 'U', 'C', 'M', 'W', 'Y'];
circleKeysThird = ['F', 'G', 'P', 'B', 'V', 'K', 'J', 'X'];
circleKeysFourth = ['Q', 'Z'];
// Calculate position for each button in circle
getCirclePositionFromTop(index: number, radius: number, offset: number = 0): string {
const angle = ((index * 45) - 90) + offset; // Start from top (0°), 45° apart
const angleRad = (angle * Math.PI) / 180;
const x = Math.cos(angleRad) * radius;
const y = Math.sin(angleRad) * radius;
return `translate(${x}px, ${y}px)`;
}
}

View File

@@ -0,0 +1,68 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-dvorak-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="keyboard">
<div class="keyboard-row">
<button *ngFor="let key of row1"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row2"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row3"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row special-keys">
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="key-button shift-key">
⇧ Shift
</button>
<button (click)="keyPressed.emit('Space')"
class="key-button space-key">
Leertaste
</button>
<button (click)="keyPressed.emit('Backspace')"
class="key-button">
⌫ Delete
</button>
<button (click)="keyPressed.emit('Enter')"
class="key-button">
↵ Enter
</button>
</div>
</div>
`,
styleUrl: '../app.component.css'
})
export class DvorakKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
row1 = [',', '.', 'P', 'Y', 'F', 'G', 'C', 'R', 'L'];
row2 = ['A', 'O', 'E', 'U', 'I', 'D', 'H', 'T', 'N', 'S'];
row3 = ['Q', 'J', 'K', 'X', 'B', 'M', 'W', 'V', 'Z'];
}

View File

@@ -0,0 +1,68 @@
import { Component, Output, EventEmitter, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-qwerty-keyboard',
standalone: true,
imports: [CommonModule],
template: `
<div class="keyboard">
<div class="keyboard-row">
<button *ngFor="let key of row1"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row2"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row">
<button *ngFor="let key of row3"
(click)="keyPressed.emit(key)"
class="key-button">
{{ shiftActive ? key : key.toLowerCase() }}
</button>
</div>
<div class="keyboard-row special-keys">
<button (click)="shiftToggled.emit()"
[class.active]="shiftActive"
class="key-button shift-key">
⇧ Shift
</button>
<button (click)="keyPressed.emit('Space')"
class="key-button space-key">
Leertaste
</button>
<button (click)="keyPressed.emit('Backspace')"
class="key-button">
⌫ Delete
</button>
<button (click)="keyPressed.emit('Enter')"
class="key-button">
↵ Enter
</button>
</div>
</div>
`,
styleUrl: '../app.component.css'
})
export class QwertyKeyboardComponent {
@Input() shiftActive = false;
@Output() keyPressed = new EventEmitter<string>();
@Output() shiftToggled = new EventEmitter<void>();
row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.'];
}