Compare commits
64 commits
Author | SHA1 | Date | |
---|---|---|---|
|
945a7daede | ||
|
7c0a53ef94 | ||
|
5bc814cb39 | ||
|
b5812c001d | ||
|
ba67589517 | ||
|
5404859248 | ||
|
ba251d1afd | ||
|
7688cc321c | ||
|
39689f9b34 | ||
|
a169e2e131 | ||
|
f47908ebf2 | ||
|
240298271c | ||
|
a580b0944b | ||
|
a0049f668b | ||
|
ec566eb9ff | ||
|
d5a6aeef74 | ||
|
29f30ad52b | ||
|
f0e9e7102d | ||
|
d0b10e88bc | ||
|
e939562ee4 | ||
|
3845810f7c | ||
|
d1e4b66dd1 | ||
|
ee4a1cd95d | ||
|
eb41d9d70c | ||
|
618358e136 | ||
|
41268e1ece | ||
|
1b330dd7c1 | ||
|
874650ad22 | ||
|
1b79ad39bb | ||
|
7b38f27d30 | ||
|
b12bc48ef5 | ||
|
87ec7cf475 | ||
|
abb50f8438 | ||
|
f8d6461da0 | ||
|
01fcc71667 | ||
|
9018410748 | ||
|
7287191747 | ||
|
bc16ae7fe0 | ||
|
5230969381 | ||
|
9682b408ab | ||
|
8dd7d72f35 | ||
|
3b44ef5aae | ||
|
39e0e25299 | ||
|
04227e1d0e | ||
|
c828f2d8ed | ||
|
c427ab5394 | ||
|
c778104f02 | ||
|
7c091d6654 | ||
|
41be5e5e4f | ||
|
589d8d63a0 | ||
|
18a8fd0385 | ||
|
0bd6024b2e | ||
|
a5a9086a7c | ||
|
dc12ab157c | ||
|
3a313c2cf0 | ||
|
29da15e1d4 | ||
|
68987774fd | ||
|
07d333b3eb | ||
|
dbd8a9098a | ||
|
9f57e0ccc9 | ||
|
22dd3a62ff | ||
|
08bf51c855 | ||
|
b28cb87289 | ||
|
9874f01cfb |
16 changed files with 1377 additions and 875 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/dotool
|
|
@ -1,2 +1,2 @@
|
|||
# This allows users in group input to use dotool without root permissions.
|
||||
KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"
|
||||
KERNEL=="uinput", GROUP="input", MODE="0620", OPTIONS+="static_node=uinput"
|
||||
|
|
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# Changelog
|
||||
|
||||
Notable changes to dotool will be documented in this file.
|
||||
|
||||
## [1.5](https://git.sr.ht/~geb/dotool/refs/1.5)
|
||||
|
||||
### Fixed
|
||||
|
||||
- The selecting of the fewest modifiers for simulating keys.
|
||||
|
||||
## [1.4](https://git.sr.ht/~geb/dotool/refs/1.4)
|
||||
|
||||
### Added
|
||||
|
||||
- A manpage, requiring scdoc.
|
||||
- Heuristic support for dead keys.
|
||||
- Support for altgr.
|
||||
- $DOTOOL_KEYBOARD_NAME to set the virtual keyboard's name.
|
||||
- More verbose --list-keys output.
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced ./install.sh with ./build.sh.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Now prefers the fewest modifiers for simulating keys. (UPDATE: see 1.5)
|
||||
|
||||
## [1.3](https://git.sr.ht/~geb/dotool/refs/1.3)
|
||||
|
||||
### Added
|
||||
|
||||
- Support for keyboard layouts.
|
||||
- hwheel for horizontal scrolling.
|
||||
|
||||
### Changed
|
||||
|
||||
- Now depends on the xkbcommon library.
|
||||
- XKB key names are now case-sensitive.
|
||||
- scroll -> wheel
|
||||
|
||||
## [1.2](https://git.sr.ht/~geb/dotool/refs/1.2)
|
||||
|
||||
### Added
|
||||
|
||||
- Added X11 and sway config files for the virtual keyboard's layout.
|
||||
- Added ./\_install.sh for packaging the basic files.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stopped some x: keys simulating shift.
|
||||
|
||||
## [1.1](https://git.sr.ht/~geb/dotool/refs/1.1)
|
||||
|
||||
### Added
|
||||
|
||||
- Added --version.
|
||||
- Added keyhold and typehold.
|
||||
|
||||
### Changed
|
||||
|
||||
- There is now a default keydelay, keyhold, typedelay and typehold.
|
||||
- Delays now come after keypresses not before.
|
||||
- Now aborts if there are any command-line arguments.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Added a udev rule so it shouldn't type guff if you're using a non-us
|
||||
keyboard layout on at least X11.
|
||||
- Fixed x:backspace that was simulating shift.
|
51
README.md
51
README.md
|
@ -1,34 +1,47 @@
|
|||
# dotool
|
||||
|
||||
dotool reads commands from stdin and simulates keyboard and mouse events.
|
||||
It works everywhere on Linux, including in X11, Wayland and TTYs.
|
||||
dotool reads actions from stdin and simulates keyboard/mouse input using
|
||||
Linux's uinput module. It works systemwide and supports keyboard layouts.
|
||||
|
||||
It takes about half a second to register the virtual device, but it can be kept using the daemon.
|
||||
## Install From Packages
|
||||
|
||||
Packages of dotool are available on:
|
||||
|
||||
- [Alpine](https://pkgs.alpinelinux.org/packages?name=dotool)
|
||||
- [Arch (AUR)](https://aur.archlinux.org/packages?SeB=n&K=dotool)
|
||||
- [Nix](https://search.nixos.org/packages?channel=unstable&type=packages&query=dotool)
|
||||
- [Void](https://voidlinux.org/packages/?q=dotool)
|
||||
|
||||
and potentially other platforms.
|
||||
|
||||
## Install From Source
|
||||
|
||||
With go (>=1.19) run `sudo ./install.sh`.
|
||||
With `go`, `libxkbcommon-dev` and `scdoc` installed, run:
|
||||
|
||||
./build.sh && sudo ./build.sh install
|
||||
|
||||
And to trigger the udev rule, run:
|
||||
|
||||
sudo udevadm control --reload && sudo udevadm trigger
|
||||
|
||||
## Usage
|
||||
|
||||
dotool will require root permissions unless you are in group input.
|
||||
See the [manpage](doc/dotool.1.scd).
|
||||
|
||||
This greets the world:
|
||||
`echo 'type Sup, Lads!' | dotool`
|
||||
## Numen and Contact
|
||||
|
||||
This screams for three seconds:
|
||||
`{ echo keydown A; sleep 3; echo key H shift+1; } | dotool`
|
||||
dotool was written for [Numen](https://numenvoice.org), which has a
|
||||
[chat on Matrix](https://matrix.to/#/#numen:matrix.org) you're welcome to join.
|
||||
|
||||
This drags the mouse:
|
||||
`printf %s\\n 'buttondown left' 'mousemove 0 100' 'buttonup left' | dotool`
|
||||
You can also send questions or patches by composing an email to
|
||||
[~geb/numen@lists.sr.ht](https://lists.sr.ht/~geb/numen).
|
||||
|
||||
The daemon and client, `dotoold` and `dotoolc`, can used to keep a persistent virtual device for a quicker initial response:
|
||||
```
|
||||
dotoold &
|
||||
echo 'type super' | dotoolc
|
||||
echo 'type speedy' | dotoolc
|
||||
```
|
||||
## Support My Work 👀
|
||||
|
||||
## Contact
|
||||
[Thank you!](https://liberapay.com/geb)
|
||||
|
||||
You can ask a question or send a patch by composing an email to [~geb/public-inbox@lists.sr.ht](https://lists.sr.ht/~geb/public-inbox).
|
||||
## License
|
||||
|
||||
GPLv3 only, see [LICENSE](./LICENSE).
|
||||
|
||||
Copyright (c) 2022-2023 John Gebbie
|
||||
|
|
22
build.sh
Executable file
22
build.sh
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
# ./build.sh ['install']
|
||||
: "${DOTOOL_VERSION=$(git describe --long --abbrev=12 --tags --dirty 2>/dev/null || echo 1.5)}"
|
||||
: "${DOTOOL_DESTDIR=}"
|
||||
: "${DOTOOL_BINDIR=usr/local/bin}"
|
||||
: "${DOTOOL_UDEV_RULES_DIR=etc/udev/rules.d}"
|
||||
|
||||
if [ "$*" != '' ] && [ "$*" != install ]; then
|
||||
echo bad usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ "$1" ]; then
|
||||
go build -ldflags "-X main.Version=$DOTOOL_VERSION" || exit
|
||||
echo Built Successfully.
|
||||
else
|
||||
install -Dm755 dotool dotoolc dotoold -t "$DOTOOL_DESTDIR/$DOTOOL_BINDIR" || exit
|
||||
install -Dm644 80-dotool.rules -t "$DOTOOL_DESTDIR/$DOTOOL_UDEV_RULES_DIR" || exit
|
||||
mkdir -p "$DOTOOL_DESTDIR/usr/share/man/man1" || exit
|
||||
scdoc < doc/dotool.1.scd > "$DOTOOL_DESTDIR/usr/share/man/man1/dotool.1" || exit
|
||||
echo Installed Successfully.
|
||||
fi
|
132
doc/dotool.1.scd
Normal file
132
doc/dotool.1.scd
Normal file
|
@ -0,0 +1,132 @@
|
|||
dotool(1)
|
||||
|
||||
# NAME
|
||||
|
||||
*dotool* - uinput tool
|
||||
|
||||
# SYNOPSIS
|
||||
|
||||
*dotool* < _actions_
|
||||
|
||||
# DESCRIPTION
|
||||
|
||||
*dotool* reads actions from stdin and simulates keyboard/mouse input using
|
||||
Linux's uinput module.
|
||||
|
||||
# PERMISSION
|
||||
|
||||
*dotool* requires write permission to */dev/uinput*, which is granted to
|
||||
users in group *input* by a udev rule.
|
||||
|
||||
You can test:
|
||||
|
||||
*echo type hello | dotool*
|
||||
|
||||
and if need be, you could add your user to group *input* with:
|
||||
|
||||
*groupadd -f input*++
|
||||
*usermod -a -G input $USER*
|
||||
|
||||
and then it's foolproof to reboot to make the group and rule effective.
|
||||
|
||||
# KEYBOARD LAYOUTS
|
||||
|
||||
*dotool* may type gobbledygook if it's simulating keycodes for a different
|
||||
keyboard layout than your environment is expecting.
|
||||
|
||||
You can specify the layout with the environment variables *DOTOOL_XKB_LAYOUT*
|
||||
and *DOTOOL_XKB_VARIANT*. For example:
|
||||
|
||||
*echo type azerty | DOTOOL_XKB_LAYOUT=fr dotool*
|
||||
|
||||
You can also specify the name to give the virtual keyboard with the environment
|
||||
variable *DOTOOL_KEYBOARD_NAME*, which can be useful making rules for your
|
||||
environment.
|
||||
|
||||
Currently the *type* action has only heuristic support for dead keys.
|
||||
|
||||
# OPTIONS
|
||||
|
||||
*-h*, *--help*
|
||||
Print help and exit.
|
||||
|
||||
*--list-keys*
|
||||
Print the possible Linux keys and exit.
|
||||
|
||||
*--list-x-keys*
|
||||
Print the possible XKB keys and exit.
|
||||
|
||||
*--version*
|
||||
Print the version and exit.
|
||||
|
||||
# ACTIONS
|
||||
|
||||
*key* _CHORD_...++
|
||||
*keydown* _CHORD_...++
|
||||
*keyup* _CHORD_...
|
||||
Press and/or release each _CHORD_. A _CHORD_ is a key or a key with
|
||||
modifiers, such as *a*, *shift+a* or *ctrl+shift+a*.
|
||||
|
||||
The supported modifiers are *super*, *altgr*, *ctrl*, *alt* and *shift*.
|
||||
|
||||
Keys can be specified by Linux names, XKB names prefixed with
|
||||
*x:*, or Linux keycodes prefixed with *k:*. The Linux names are
|
||||
case-insensitive, except uppercase character keys also simulate
|
||||
shift. This example types *!!!* with the *us* layout:
|
||||
|
||||
*echo key shift+1 x:exclam shift+k:2 | dotool*
|
||||
|
||||
*type* _TEXT_
|
||||
Type _TEXT_.
|
||||
|
||||
*click* *left*/*middle*/*right*++
|
||||
*buttondown* *left*/*middle*/*right*++
|
||||
*buttonup* *left*/*middle*/*right*
|
||||
Press and/or release a mouse button.
|
||||
|
||||
*wheel* _AMOUNT_++
|
||||
*hwheel* _AMOUNT_
|
||||
Scroll a vertical/horizontal mouse wheel by a positive or negative
|
||||
_AMOUNT_.
|
||||
|
||||
*mouseto* _X_ _Y_
|
||||
Jump the cursor to the position _X_ _Y_, where _X_ and _Y_ are
|
||||
percentages between 0.0 and 1.0.
|
||||
|
||||
*mousemove* _X_ _Y_
|
||||
Move the cursor relative to its current position.
|
||||
|
||||
*keydelay* _MILLISECONDS_++
|
||||
*keyhold* _MILLISECONDS_++
|
||||
*typedelay* _MILLISECONDS_++
|
||||
*typehold* _MILLISECONDS_
|
||||
Set the delay between/holding each key with the *key*\* actions/*type*
|
||||
action.
|
||||
|
||||
The default *keydelay* and *typedelay* is 2ms, and the default
|
||||
*keyhold* and *typehold* is 8ms.
|
||||
|
||||
# LONG-RUNNING INSTANCE
|
||||
|
||||
Each instance of *dotool* has an initial delay registering the virtual
|
||||
devices, but you can keep writing actions to a long-running instance. The
|
||||
daemon and client, *dotoold* and *dotoolc*, let you do this with a pipe
|
||||
behind the scenes, for example:
|
||||
|
||||
*dotoold &*++
|
||||
*echo type super | dotoolc*++
|
||||
*echo type speedy | dotoolc*
|
||||
|
||||
# EXAMPLES
|
||||
|
||||
This greets the world:
|
||||
|
||||
*echo type hi | dotool*
|
||||
|
||||
This screams for roughly three seconds:
|
||||
|
||||
*{ echo keydown A; sleep 3; echo key H shift+1; } | dotool*
|
||||
|
||||
# AUTHOR
|
||||
|
||||
John Gebbie
|
465
dotool.go
465
dotool.go
|
@ -4,8 +4,9 @@ import (
|
|||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/bendahl/uinput"
|
||||
"git.sr.ht/~geb/dotool/xkb"
|
||||
"git.sr.ht/~geb/opt"
|
||||
"github.com/bendahl/uinput"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
@ -14,15 +15,42 @@ import (
|
|||
"unicode"
|
||||
)
|
||||
|
||||
func fatal(v ...interface{}) {
|
||||
fmt.Fprint(os.Stderr, "dotool: ")
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
var Version string
|
||||
|
||||
func usage() {
|
||||
fmt.Println(`dotool reads actions from stdin and simulates input using uinput.
|
||||
|
||||
The supported actions are:
|
||||
key CHORD...
|
||||
keydown CHORD...
|
||||
keyup CHORD...
|
||||
type TEXT
|
||||
click left/middle/right
|
||||
buttondown left/middle/right
|
||||
buttonup left/middle/right
|
||||
wheel AMOUNT
|
||||
hwheel AMOUNT
|
||||
mouseto X Y (where X and Y are percentages between 0.0 and 1.0)
|
||||
mousemove X Y (where X and Y are amounts to move)
|
||||
keydelay MILLISECONDS
|
||||
keyhold MILLISECONDS
|
||||
typedelay MILLISECONDS
|
||||
typehold MILLISECONDS
|
||||
|
||||
--list-keys Print the possible Linux keys and exit.
|
||||
--list-x-keys Print the possible XKB keys and exit.
|
||||
--version Print the version and exit.
|
||||
|
||||
See 'man dotool' for the documentation.`)
|
||||
}
|
||||
|
||||
func fatal(a ...any) {
|
||||
fmt.Fprintln(os.Stderr, "dotool:", fmt.Sprint(a...))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func warn(v ...interface{}) {
|
||||
fmt.Fprint(os.Stderr, "dotool WARNING: ")
|
||||
fmt.Fprintln(os.Stderr, v...)
|
||||
func warn(a ...any) {
|
||||
fmt.Fprintln(os.Stderr, "dotool: WARNING:", fmt.Sprint(a...))
|
||||
}
|
||||
|
||||
func log(err error) {
|
||||
|
@ -32,20 +60,46 @@ func log(err error) {
|
|||
}
|
||||
|
||||
type Chord struct {
|
||||
Super bool
|
||||
Ctrl bool
|
||||
Alt bool
|
||||
Shift bool
|
||||
Super, AltGr, Ctrl, Alt, Shift bool
|
||||
Key int
|
||||
|
||||
level uint32 // just for initKeys
|
||||
}
|
||||
|
||||
func parseChord(chord string) (Chord, error) {
|
||||
func parseChord(keymap *xkb.Keymap, chord string) (Chord, error) {
|
||||
var c Chord
|
||||
keys := strings.Split(chord, "+")
|
||||
|
||||
k := keys[len(keys)-1]
|
||||
if strings.HasPrefix(k, "k:") {
|
||||
code, err := strconv.Atoi(k[2:])
|
||||
if err != nil {
|
||||
return c, errors.New("invalid keycode: " + k[2:])
|
||||
}
|
||||
c.Key = code
|
||||
} else if strings.HasPrefix(k, "x:") {
|
||||
var ok bool
|
||||
c, ok = XKeys[k[2:]]
|
||||
if !ok {
|
||||
return c, errors.New("impossible XKB key for layout: " + k[2:])
|
||||
}
|
||||
} else {
|
||||
var ok bool
|
||||
c, ok = LinuxKeys[strings.ToLower(k)]
|
||||
if !ok {
|
||||
return c, errors.New("impossible key for layout: " + k)
|
||||
}
|
||||
if len(k) == 1 && unicode.IsUpper(rune(k[0])) {
|
||||
c.Shift = true
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(keys) - 1; i++ {
|
||||
switch strings.ToLower(keys[i]) {
|
||||
case "super":
|
||||
c.Super = true
|
||||
case "altgr":
|
||||
c.AltGr = true
|
||||
case "ctrl", "control":
|
||||
c.Ctrl = true
|
||||
case "alt":
|
||||
|
@ -57,195 +111,168 @@ func parseChord(chord string) (Chord, error) {
|
|||
}
|
||||
}
|
||||
|
||||
k := keys[len(keys)-1]
|
||||
if strings.HasPrefix(k, "k:") {
|
||||
code, err := strconv.Atoi(k[2:])
|
||||
if err != nil {
|
||||
return c, errors.New("invalid keycode: " + k)
|
||||
}
|
||||
c.Key = code
|
||||
} else if strings.HasPrefix(k, "x:") {
|
||||
if code, ok := xKeysShifted[strings.ToLower(k[2:])]; ok {
|
||||
if len(k[2:]) > 1 || unicode.IsUpper(rune(k[2])) {
|
||||
c.Shift = true
|
||||
}
|
||||
c.Key = code
|
||||
} else if code, ok := xKeysNormal[strings.ToLower(k[2:])]; ok {
|
||||
c.Key = code
|
||||
} else {
|
||||
return c, errors.New("unknown X11 key: " + k)
|
||||
}
|
||||
} else {
|
||||
code, ok := linuxKeys[strings.ToLower(k)]
|
||||
if len(k) == 1 && unicode.IsUpper(rune(k[0])) {
|
||||
c.Shift = true
|
||||
}
|
||||
if !ok {
|
||||
return c, errors.New("unknown key: " + k)
|
||||
}
|
||||
c.Key = code
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *Chord) Press(kb uinput.Keyboard) {
|
||||
func (c Chord) KeyDown(kb uinput.Keyboard) {
|
||||
if c.Super {
|
||||
log(kb.KeyDown(uinput.KeyLeftmeta))
|
||||
log(kb.KeyDown(super))
|
||||
}
|
||||
if c.AltGr {
|
||||
log(kb.KeyDown(altgr))
|
||||
}
|
||||
if c.Ctrl {
|
||||
log(kb.KeyDown(uinput.KeyLeftctrl))
|
||||
log(kb.KeyDown(ctrl))
|
||||
}
|
||||
if c.Alt {
|
||||
log(kb.KeyDown(uinput.KeyLeftalt))
|
||||
log(kb.KeyDown(alt))
|
||||
}
|
||||
if c.Shift {
|
||||
log(kb.KeyDown(uinput.KeyLeftshift))
|
||||
}
|
||||
log(kb.KeyPress(c.Key))
|
||||
if c.Super {
|
||||
log(kb.KeyUp(uinput.KeyLeftmeta))
|
||||
}
|
||||
if c.Ctrl {
|
||||
log(kb.KeyUp(uinput.KeyLeftctrl))
|
||||
}
|
||||
if c.Alt {
|
||||
log(kb.KeyUp(uinput.KeyLeftalt))
|
||||
}
|
||||
if c.Shift {
|
||||
log(kb.KeyUp(uinput.KeyLeftshift))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Chord) KeyDown(kb uinput.Keyboard) {
|
||||
if c.Super {
|
||||
log(kb.KeyDown(uinput.KeyLeftmeta))
|
||||
}
|
||||
if c.Ctrl {
|
||||
log(kb.KeyDown(uinput.KeyLeftctrl))
|
||||
}
|
||||
if c.Alt {
|
||||
log(kb.KeyDown(uinput.KeyLeftalt))
|
||||
}
|
||||
if c.Shift {
|
||||
log(kb.KeyDown(uinput.KeyLeftshift))
|
||||
log(kb.KeyDown(shift))
|
||||
}
|
||||
log(kb.KeyDown(c.Key))
|
||||
}
|
||||
|
||||
func (c *Chord) KeyUp(kb uinput.Keyboard) {
|
||||
func (c Chord) KeyUp(kb uinput.Keyboard) {
|
||||
if c.Super {
|
||||
log(kb.KeyUp(uinput.KeyLeftmeta))
|
||||
log(kb.KeyUp(super))
|
||||
}
|
||||
if c.AltGr {
|
||||
log(kb.KeyUp(altgr))
|
||||
}
|
||||
if c.Ctrl {
|
||||
log(kb.KeyUp(uinput.KeyLeftctrl))
|
||||
log(kb.KeyUp(ctrl))
|
||||
}
|
||||
if c.Alt {
|
||||
log(kb.KeyUp(uinput.KeyLeftalt))
|
||||
log(kb.KeyUp(alt))
|
||||
}
|
||||
if c.Shift {
|
||||
log(kb.KeyUp(uinput.KeyLeftshift))
|
||||
log(kb.KeyUp(shift))
|
||||
}
|
||||
log(kb.KeyUp(c.Key))
|
||||
}
|
||||
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, `dotool reads commands from stdin and simulates keyboard and pointer events.
|
||||
|
||||
The commands are:
|
||||
key CHORD...
|
||||
keydown CHORD...
|
||||
keyup CHORD...
|
||||
type TEXT
|
||||
click left/middle/right
|
||||
buttondown left/middle/right
|
||||
buttonup left/middle/right
|
||||
scroll NUMBER (where NUMBER is the amount down/up if positive/negative)
|
||||
mouseto X Y (where X and Y are percentages between 0.0 and 1.0)
|
||||
mousemove X Y (where X and Y are the number of pixels to move)
|
||||
keydelay MILLISECONDS
|
||||
typedelay MILLISECONDS
|
||||
|
||||
Example: echo "key h i shift+1" | dotool
|
||||
|
||||
dotool is installed with a udev rule to allow users in group input to run
|
||||
it without root permissions. You can make it effective without rebooting by
|
||||
running: sudo udevadm trigger
|
||||
|
||||
The keys are those used by Linux, but can also be specified using X11 names
|
||||
prefixed with x: like x:exclam, as well as their Linux keycode like k:30.
|
||||
They are case insensitive, except uppercase character keys also simulate shift.
|
||||
|
||||
The modifiers are: super, ctrl, alt and shift.
|
||||
|
||||
--list-keys
|
||||
Print the supported Linux keys and their keycodes.
|
||||
|
||||
--list-x-keys
|
||||
Print the supported X11 keys and their Linux keycodes.
|
||||
`)
|
||||
func (c *Chord) String() string {
|
||||
var sb strings.Builder
|
||||
if c.Super {
|
||||
sb.WriteString("super+")
|
||||
}
|
||||
if c.AltGr {
|
||||
sb.WriteString("altgr+")
|
||||
}
|
||||
if c.Ctrl {
|
||||
sb.WriteString("ctrl+")
|
||||
}
|
||||
if c.Alt {
|
||||
sb.WriteString("alt+")
|
||||
}
|
||||
if c.Shift {
|
||||
sb.WriteString("shift+")
|
||||
}
|
||||
sb.WriteString("k:")
|
||||
sb.WriteString(strconv.Itoa(c.Key))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func cutCmd(s, cmd string) (string, bool) {
|
||||
if strings.HasPrefix(s, cmd + " ") || strings.HasPrefix(s, cmd + "\t") {
|
||||
return s[len(cmd)+1:], true
|
||||
|
||||
func listKeys(keymap *xkb.Keymap, keys map[string]Chord) {
|
||||
var margin int
|
||||
for code := 1; code < 256; code++ {
|
||||
for name := range keys {
|
||||
if len(name) > margin {
|
||||
margin = len(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
for code := 1; code < 256; code++ {
|
||||
for name, chord := range keys {
|
||||
if chord.Key == code {
|
||||
fmt.Printf("%-*s %s\n", margin, name, chord.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cutWord(s, word string) (string, bool) {
|
||||
if s == word {
|
||||
return "", true
|
||||
}
|
||||
if strings.HasPrefix(s, word + " ") || strings.HasPrefix(s, word + "\t") {
|
||||
return s[len(word)+1:], true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func main() {
|
||||
var keymap *xkb.Keymap
|
||||
{
|
||||
optset := opt.NewOptionSet()
|
||||
help := func(bool) error {
|
||||
layout := os.Getenv("DOTOOL_XKB_LAYOUT")
|
||||
if layout == "" {
|
||||
layout = os.Getenv("XKB_DEFAULT_LAYOUT")
|
||||
}
|
||||
variant := os.Getenv("DOTOOL_XKB_VARIANT")
|
||||
if variant == "" {
|
||||
variant = os.Getenv("XKB_DEFAULT_VARIANT")
|
||||
}
|
||||
if variant != "" && layout == "" {
|
||||
// Otherwise xkbcommon just ignores the variant.
|
||||
fatal("you need to set $DOTOOL_XKB_LAYOUT or $XKB_DEFAULT_LAYOUT if the variant is set")
|
||||
}
|
||||
names := xkb.RuleNames{Layout: layout, Variant: variant}
|
||||
|
||||
ctx := xkb.ContextNew(xkb.ContextNoFlags)
|
||||
defer ctx.Unref()
|
||||
keymap = ctx.KeymapNewFromNames(&names, xkb.KeymapCompileNoFlags)
|
||||
if keymap == nil {
|
||||
fatal("failed to compile keymap")
|
||||
}
|
||||
defer keymap.Unref()
|
||||
|
||||
initKeys(keymap)
|
||||
}
|
||||
|
||||
{
|
||||
o := opt.NewOptSet()
|
||||
|
||||
o.FlagFunc("h", func() error {
|
||||
usage()
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
listKeys := func(bool) error {
|
||||
for i := 1; i < 249; i++ {
|
||||
for name, code := range linuxKeys {
|
||||
if code == i {
|
||||
fmt.Printf("%s %d\n", name, code)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
o.Alias("h", "help")
|
||||
|
||||
o.FlagFunc("list-keys", func() error {
|
||||
listKeys(keymap, LinuxKeys)
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
listXKeys := func(bool) error {
|
||||
for i := 1; i < 249; i++ {
|
||||
for name, code := range xKeysNormal {
|
||||
if code == i {
|
||||
fmt.Print(name)
|
||||
for name, code := range xKeysShifted {
|
||||
if code == i {
|
||||
if len(name) == 1 {
|
||||
name = strings.ToUpper(name)
|
||||
}
|
||||
fmt.Printf(" %s", name)
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Printf(" %d\n", code)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
o.FlagFunc("list-x-keys", func() error {
|
||||
listKeys(keymap, XKeys)
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
optset.BoolFunc("h", help)
|
||||
optset.BoolFunc("help", help)
|
||||
optset.BoolFunc("list-keys", listKeys)
|
||||
optset.BoolFunc("list-x-keys", listXKeys)
|
||||
err := optset.Parse(true, os.Args[1:])
|
||||
})
|
||||
|
||||
o.FlagFunc("version", func() error {
|
||||
fmt.Println(Version)
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
err := o.Parse(true, os.Args[1:])
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
if len(o.Args()) > 0 {
|
||||
fatal("there should be no arguments, commands are read from stdin")
|
||||
}
|
||||
}
|
||||
|
||||
keyboard, err := uinput.CreateKeyboard("/dev/uinput", []byte("dotool keyboard"))
|
||||
keyboardName := []byte(os.Getenv("DOTOOL_KEYBOARD_NAME"))
|
||||
if len(keyboardName) == 0 {
|
||||
keyboardName = []byte("dotool keyboard")
|
||||
}
|
||||
keyboard, err := uinput.CreateKeyboard("/dev/uinput", keyboardName)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
@ -264,8 +291,10 @@ func main() {
|
|||
}
|
||||
defer mouse.Close()
|
||||
|
||||
keydelay := time.Duration(0)
|
||||
typedelay := time.Duration(0)
|
||||
keydelay := time.Duration(2)*time.Millisecond
|
||||
keyhold := time.Duration(8)*time.Millisecond
|
||||
typedelay := time.Duration(2)*time.Millisecond
|
||||
typehold := time.Duration(8)*time.Millisecond
|
||||
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
for sc.Scan() {
|
||||
|
@ -273,34 +302,36 @@ func main() {
|
|||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if s, ok := cutCmd(text, "key"); ok {
|
||||
if s, ok := cutWord(text, "key"); ok {
|
||||
for _, field := range strings.Fields(s) {
|
||||
time.Sleep(keydelay)
|
||||
if chord, err := parseChord(field); err == nil {
|
||||
chord.Press(keyboard)
|
||||
} else {
|
||||
warn(err.Error())
|
||||
}
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "keydown"); ok {
|
||||
for _, field := range strings.Fields(s) {
|
||||
time.Sleep(keydelay)
|
||||
if chord, err := parseChord(field); err == nil {
|
||||
if chord, err := parseChord(keymap, field); err == nil {
|
||||
chord.KeyDown(keyboard)
|
||||
} else {
|
||||
warn(err.Error())
|
||||
}
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "keyup"); ok {
|
||||
for _, field := range strings.Fields(s) {
|
||||
time.Sleep(keydelay)
|
||||
if chord, err := parseChord(field); err == nil {
|
||||
time.Sleep(keyhold)
|
||||
chord.KeyUp(keyboard)
|
||||
} else {
|
||||
warn(err.Error())
|
||||
}
|
||||
time.Sleep(keydelay)
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "keydelay"); ok {
|
||||
} else if s, ok := cutWord(text, "keydown"); ok {
|
||||
for _, field := range strings.Fields(s) {
|
||||
if chord, err := parseChord(keymap, field); err == nil {
|
||||
chord.KeyDown(keyboard)
|
||||
} else {
|
||||
warn(err.Error())
|
||||
}
|
||||
time.Sleep(keydelay)
|
||||
}
|
||||
} else if s, ok := cutWord(text, "keyup"); ok {
|
||||
for _, field := range strings.Fields(s) {
|
||||
if chord, err := parseChord(keymap, field); err == nil {
|
||||
chord.KeyUp(keyboard)
|
||||
} else {
|
||||
warn(err.Error())
|
||||
}
|
||||
time.Sleep(keydelay)
|
||||
}
|
||||
} else if s, ok := cutWord(text, "keydelay"); ok {
|
||||
var d float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
|
||||
if err == nil {
|
||||
|
@ -308,17 +339,37 @@ func main() {
|
|||
} else {
|
||||
warn("invalid delay: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "type"); ok {
|
||||
for _, r := range s {
|
||||
time.Sleep(typedelay)
|
||||
if chord, ok := runeChords[unicode.ToLower(r)]; ok {
|
||||
if unicode.IsUpper(r) {
|
||||
chord.Shift = true
|
||||
}
|
||||
chord.Press(keyboard)
|
||||
}
|
||||
} else if s, ok := cutWord(text, "keyhold"); ok {
|
||||
var d float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
|
||||
if err == nil {
|
||||
keyhold = time.Duration(d)*time.Millisecond
|
||||
} else {
|
||||
warn("invalid hold time: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "typedelay"); ok {
|
||||
} else if s, ok := cutWord(text, "type"); ok {
|
||||
for _, r := range s {
|
||||
sym := xkb.Utf32ToKeysym(uint32(r))
|
||||
if sym == 0 {
|
||||
warn("invalid character: " + string(r))
|
||||
} else if c := getChord(keymap, sym); c.Key != 0 {
|
||||
c.KeyDown(keyboard)
|
||||
time.Sleep(typehold)
|
||||
c.KeyUp(keyboard)
|
||||
} else if c1, c2 := getDeadChords(keymap, r); c1.Key != 0 {
|
||||
c1.KeyDown(keyboard)
|
||||
time.Sleep(typehold)
|
||||
c1.KeyUp(keyboard)
|
||||
time.Sleep(typedelay)
|
||||
c2.KeyDown(keyboard)
|
||||
time.Sleep(typehold)
|
||||
c2.KeyUp(keyboard)
|
||||
} else {
|
||||
warn("impossible character for layout: " + string(r))
|
||||
}
|
||||
time.Sleep(typedelay)
|
||||
}
|
||||
} else if s, ok := cutWord(text, "typedelay"); ok {
|
||||
var d float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
|
||||
if err == nil {
|
||||
|
@ -326,7 +377,15 @@ func main() {
|
|||
} else {
|
||||
warn("invalid delay: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "click"); ok {
|
||||
} else if s, ok := cutWord(text, "typehold"); ok {
|
||||
var d float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &d)
|
||||
if err == nil {
|
||||
typehold = time.Duration(d)*time.Millisecond
|
||||
} else {
|
||||
warn("invalid hold time: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutWord(text, "click"); ok {
|
||||
for _, button := range strings.Fields(s) {
|
||||
switch button {
|
||||
case "left", "1":
|
||||
|
@ -339,7 +398,7 @@ func main() {
|
|||
warn("unknown button: " + button)
|
||||
}
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "buttondown"); ok {
|
||||
} else if s, ok := cutWord(text, "buttondown"); ok {
|
||||
for _, button := range strings.Fields(s) {
|
||||
switch button {
|
||||
case "left", "1":
|
||||
|
@ -352,7 +411,7 @@ func main() {
|
|||
warn("unknown button: " + button)
|
||||
}
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "buttonup"); ok {
|
||||
} else if s, ok := cutWord(text, "buttonup"); ok {
|
||||
for _, button := range strings.Fields(s) {
|
||||
switch button {
|
||||
case "left", "1":
|
||||
|
@ -365,7 +424,23 @@ func main() {
|
|||
warn("unknown button: " + button)
|
||||
}
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "scroll"); ok {
|
||||
} else if s, ok := cutWord(text, "wheel"); ok {
|
||||
var n float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &n)
|
||||
if err == nil {
|
||||
log(mouse.Wheel(false, int32(n)))
|
||||
} else {
|
||||
warn("invalid wheel amount: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutWord(text, "hwheel"); ok {
|
||||
var n float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &n)
|
||||
if err == nil {
|
||||
log(mouse.Wheel(true, int32(n)))
|
||||
} else {
|
||||
warn("invalid hwheel amount: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutWord(text, "scroll"); ok {
|
||||
var n float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f\n", &n)
|
||||
if err == nil {
|
||||
|
@ -373,7 +448,7 @@ func main() {
|
|||
} else {
|
||||
warn("invalid scroll amount: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "mouseto"); ok {
|
||||
} else if s, ok := cutWord(text, "mouseto"); ok {
|
||||
var x, y float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
|
||||
if err == nil {
|
||||
|
@ -390,7 +465,7 @@ func main() {
|
|||
} else {
|
||||
warn("invalid coordinate: " + sc.Text())
|
||||
}
|
||||
} else if s, ok := cutCmd(text, "mousemove"); ok {
|
||||
} else if s, ok := cutWord(text, "mousemove"); ok {
|
||||
var x, y float64
|
||||
_, err := fmt.Sscanf(s + "\n", "%f %f\n", &x, &y)
|
||||
if err == nil {
|
||||
|
|
13
dotoolc
13
dotoolc
|
@ -1,9 +1,8 @@
|
|||
#!/bin/sh
|
||||
if [ $# != 0 ]; then
|
||||
echo 'dotoolc writes its stdin to the pipe being read by dotoold.
|
||||
dotoolc will exit immediately if the pipe is not being read.
|
||||
The path of the pipe is $DOTOOL_PIPE else /tmp/dotool_pipe.
|
||||
' > /dev/stderr
|
||||
echo 'dotoolc writes its stdin to the pipe being read by dotoold. dotoolc will
|
||||
exit immediately if the pipe is not being read. The path of the pipe
|
||||
is $DOTOOL_PIPE else /tmp/dotool-pipe.' >&2
|
||||
[ "$1" = -h ] || [ "$1" = --help ]; exit
|
||||
fi
|
||||
|
||||
|
@ -11,14 +10,14 @@ fifo_being_read(){
|
|||
[ -p "$1" ] && /bin/echo 1<>"$1" >"$1"
|
||||
}
|
||||
|
||||
p="${DOTOOL_PIPE:-/tmp/dotool_pipe}"
|
||||
p="${DOTOOL_PIPE:-/tmp/dotool-pipe}"
|
||||
|
||||
if [ -p "$p" ] && ! [ -w "$p" ]; then
|
||||
echo 'dotoolc: the pipe does not grant write permission'
|
||||
printf %s\\n "dotoolc: the pipe does not grant write permission: $p" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! fifo_being_read "$p"; then
|
||||
echo 'dotoolc: dotoold is not connected' > /dev/stderr
|
||||
printf %s\\n "dotoolc: no dotoold instance is reading the pipe: $p" >&2
|
||||
exit 1
|
||||
fi
|
||||
exec cat > "$p"
|
||||
|
|
30
dotoold
30
dotoold
|
@ -1,25 +1,29 @@
|
|||
#!/bin/sh
|
||||
if [ $# != 0 ]; then
|
||||
echo 'dotoold runs dotool reading from a pipe for dotoolc to write to.
|
||||
dotoold will exit immediately if the pipe is already being read.
|
||||
The path used for the pipe is $DOTOOL_PIPE else /tmp/dotool_pipe.
|
||||
' > /dev/stderr
|
||||
[ "$1" = -h ] || [ "$1" = --help ]; exit
|
||||
fi
|
||||
for a; do
|
||||
case "$a" in
|
||||
-h|--help)
|
||||
echo 'dotoold runs dotool reading from a pipe for dotoolc to write to. dotoold
|
||||
will exit immediately if the pipe is already being read. The path used
|
||||
for the pipe is $DOTOOL_PIPE else /tmp/dotool-pipe.' >&2
|
||||
exit
|
||||
;;
|
||||
--) break;;
|
||||
esac
|
||||
done
|
||||
|
||||
fifo_being_read(){
|
||||
[ -p "$1" ] && /bin/echo 1<>"$1" >"$1"
|
||||
}
|
||||
|
||||
p="${DOTOOL_PIPE:-/tmp/dotool_pipe}"
|
||||
p="${DOTOOL_PIPE:-/tmp/dotool-pipe}"
|
||||
|
||||
if fifo_being_read "$p" 2> /dev/null; then
|
||||
echo 'dotoold: another instance is already connected' > /dev/stderr
|
||||
printf %s\\n "dotoold: another instance is already reading the pipe: $p" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$p" || exit 1
|
||||
trap 'rm -rf "$p"; pkill -P $$; trap - EXIT; exit' EXIT INT TERM HUP
|
||||
rm -f -- "$p" || exit 1
|
||||
trap 'rm -f -- "$p"; pkill -P $$; trap - EXIT; exit' EXIT INT TERM HUP
|
||||
mkfifo -m 660 "$p" || exit 1
|
||||
dotool <> "$p" &
|
||||
wait
|
||||
dotool "$@" <> "$p" &
|
||||
wait $!
|
||||
|
|
4
go.mod
4
go.mod
|
@ -3,6 +3,6 @@ module git.sr.ht/~geb/dotool
|
|||
go 1.19
|
||||
|
||||
require (
|
||||
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1
|
||||
github.com/bendahl/uinput v1.6.0
|
||||
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c
|
||||
github.com/bendahl/uinput v1.7.0
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -1,4 +1,4 @@
|
|||
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1 h1:bmje0IdPzrY5nX6fAx8KuHP5G8EP4XMedMFcrssfJXc=
|
||||
git.sr.ht/~geb/opt v0.0.0-20220627180516-52214b5b84a1/go.mod h1:T5QFtG9s8i/kW5pDVCke6Mt2WmElJCIfTL1HMdpP7Rk=
|
||||
github.com/bendahl/uinput v1.6.0 h1:fM6r3OSC17rHh758mizKjSBuqi+XinhiGd4N3pWvZiI=
|
||||
github.com/bendahl/uinput v1.6.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
||||
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c h1:gIC1gnCgoasPHks1x6MB+bgDmIWMxKc5HIJPJrsV5Ck=
|
||||
git.sr.ht/~geb/opt v0.0.0-20230911153257-e72225a1933c/go.mod h1:S6h1g8P7DyG7i7YIHZ5IpYbC6lzZB9DYIEl8PyXOmsg=
|
||||
github.com/bendahl/uinput v1.7.0 h1:nA4fm8Wu8UYNOPykIZm66nkWEyvxzfmJ8YC02PM40jg=
|
||||
github.com/bendahl/uinput v1.7.0/go.mod h1:Np7w3DINc9wB83p12fTAM3DPPhFnAKP0WTXRqCQJ6Z8=
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
go build && cp -v dotool dotoolc dotoold /usr/local/bin || exit
|
||||
mkdir -p /etc/udev/rules.d && cp -v 80-dotool.rules /etc/udev/rules.d || exit
|
||||
udevadm trigger
|
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh
|
||||
rm -vf /usr/local/bin/dotool /usr/local/bin/dotoolc /usr/local/bin/dotoold
|
||||
rm -vf /etc/udev/rules.d/80-dotool.rules
|
||||
rm -vf /usr/share/man/man1/dotool.1
|
||||
|
|
144
xkb/xkb.go
Normal file
144
xkb/xkb.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package xkb
|
||||
|
||||
//#cgo pkg-config: xkbcommon
|
||||
//#cgo LDFLAGS: -ldl
|
||||
//
|
||||
//#include <stdlib.h>
|
||||
//#include <xkbcommon/xkbcommon.h>
|
||||
import "C"
|
||||
import "unsafe"
|
||||
|
||||
const (
|
||||
// ModNameShift is the name of Shift Modifier
|
||||
ModNameShift = "Shift"
|
||||
// ModNameCaps is the name of Caps Lock Modifier
|
||||
ModNameCaps = "Lock"
|
||||
// ModNameCtrl is the name of Control Modifier
|
||||
ModNameCtrl = "Control"
|
||||
// ModNameAlt is the name of Alt Modifier
|
||||
ModNameAlt = "Mod1"
|
||||
// ModNameNum is the name of Num Lock Modifier
|
||||
ModNameNum = "Mod2"
|
||||
// ModNameLogo is the name of Logo Modifier
|
||||
ModNameLogo = "Mod4"
|
||||
// LedNameCaps is the name of Caps Lock Led
|
||||
LedNameCaps = "Caps Lock"
|
||||
// LedNameNum is the name of Num Lock Led
|
||||
LedNameNum = "Num Lock"
|
||||
// LedNameScroll is the name of Scroll Lock Led
|
||||
LedNameScroll = "Scroll Lock"
|
||||
)
|
||||
|
||||
func KeysymGetName(keysym uint32) string {
|
||||
s := "................................................................"
|
||||
cs := C.CString(s)
|
||||
defer C.free(unsafe.Pointer(cs))
|
||||
_ = C.xkb_keysym_get_name(C.uint(keysym), cs, C.size_t(len(s)))
|
||||
return C.GoString(cs)
|
||||
}
|
||||
|
||||
func KeysymToUtf32(keysym uint32) uint32 {
|
||||
return uint32(C.xkb_keysym_to_utf32(C.uint(keysym)))
|
||||
}
|
||||
|
||||
func Utf32ToKeysym(ucs uint32) uint32 {
|
||||
return uint32(C.xkb_utf32_to_keysym(C.uint(ucs)))
|
||||
}
|
||||
|
||||
type KeymapCompileFlags int
|
||||
|
||||
const (
|
||||
KeymapCompileNoFlags KeymapCompileFlags = C.XKB_KEYMAP_COMPILE_NO_FLAGS
|
||||
)
|
||||
|
||||
type RuleNames struct {
|
||||
Rules, Model, Layout, Variant, Options string
|
||||
}
|
||||
|
||||
func (rn *RuleNames) toC() *C.struct_xkb_rule_names {
|
||||
if rn == nil {
|
||||
return nil
|
||||
}
|
||||
return &C.struct_xkb_rule_names{
|
||||
rules: C.CString(rn.Rules),
|
||||
model: C.CString(rn.Model),
|
||||
layout: C.CString(rn.Layout),
|
||||
variant: C.CString(rn.Variant),
|
||||
options: C.CString(rn.Options),
|
||||
}
|
||||
}
|
||||
|
||||
const ContextNoFlags = 0
|
||||
|
||||
type Context struct {
|
||||
p *C.struct_xkb_context
|
||||
}
|
||||
|
||||
func ContextNew(flags uint32) (ctx *Context) {
|
||||
ctx = new(Context)
|
||||
ctx.p = C.xkb_context_new(flags)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (ctx *Context) KeymapNewFromNames(rules *RuleNames, flags KeymapCompileFlags) *Keymap {
|
||||
km := C.xkb_keymap_new_from_names(ctx.p, rules.toC(), C.enum_xkb_keymap_compile_flags(flags))
|
||||
if km == nil {
|
||||
return nil
|
||||
}
|
||||
return &Keymap{km}
|
||||
}
|
||||
|
||||
func (ctx *Context) Unref() {
|
||||
C.xkb_context_unref(ctx.p)
|
||||
ctx.p = nil
|
||||
}
|
||||
|
||||
type Keymap struct {
|
||||
p *C.struct_xkb_keymap
|
||||
}
|
||||
|
||||
func (km *Keymap) KeyGetMod(key, layout, level uint32) uint32 {
|
||||
var mask C.uint
|
||||
C.xkb_keymap_key_get_mods_for_level(km.p, C.uint(key), C.uint(layout), C.uint(level), &mask, 1)
|
||||
return uint32(mask)
|
||||
}
|
||||
|
||||
func (km *Keymap) KeyGetSymsByLevel(key, layout, level uint32) []uint32 {
|
||||
var syms *C.xkb_keysym_t
|
||||
n := int(C.xkb_keymap_key_get_syms_by_level(km.p, C.uint(key), C.uint(layout), C.uint(level), &syms))
|
||||
if n == 0 || syms == nil {
|
||||
return nil
|
||||
}
|
||||
data := unsafe.Slice(syms, n)
|
||||
s := make([]uint32, n)
|
||||
for i := 0; i < n; i++ {
|
||||
s[i] = uint32(data[i])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (km *Keymap) MaxKeycode() uint32 {
|
||||
return uint32(C.xkb_keymap_max_keycode(km.p))
|
||||
}
|
||||
|
||||
func (km *Keymap) MinKeycode() uint32 {
|
||||
return uint32(C.xkb_keymap_min_keycode(km.p))
|
||||
}
|
||||
|
||||
func (km *Keymap) ModGetIndex(mod string) uint {
|
||||
return uint(C.xkb_keymap_mod_get_index(km.p, C.CString(mod)))
|
||||
}
|
||||
|
||||
func (km *Keymap) NumLevelsForKey(key, layout uint32) uint32 {
|
||||
return uint32(C.xkb_keymap_num_levels_for_key(km.p, C.uint(key), C.uint(layout)))
|
||||
}
|
||||
|
||||
func (km *Keymap) Unref() {
|
||||
C.xkb_keymap_unref(km.p)
|
||||
km.p = nil
|
||||
}
|
||||
|
||||
func keymapUnref(km *Keymap) {
|
||||
C.xkb_keymap_unref(km.p)
|
||||
km.p = nil
|
||||
}
|
42
xkeys.bash
42
xkeys.bash
|
@ -1,42 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
align() {
|
||||
sed '/\<KeyRo\>/ d
|
||||
/\<KeyKpjpcomma\>/ d
|
||||
/\<KeyMacro\>/ d
|
||||
/\<KeyYen\>/ d
|
||||
/\<KeySetup\>/ d
|
||||
/\<KeyDeletefile\>/ d
|
||||
/\<KeyClosecd\>/ d
|
||||
/\<KeyEjectclosecd\>/ d
|
||||
/\<KeyIso\>/ d
|
||||
/\<KeyMove\>/ d
|
||||
/\<KeyEdit\>/ d
|
||||
/\<KeyAlterase\>/ d
|
||||
/\<KeyUnknown\>/ d
|
||||
/\<KeyMicmute\>/ d'
|
||||
}
|
||||
|
||||
echo 'var xKeysNormal = map[string]int{'
|
||||
{
|
||||
paste -d ' ' <(xmodmap -pke | sed '1 d; s/.*= /"/; /.*=/ d; s/ .*/":/' | sed '/^"XF86Eject"/ { N; s/.*\n// }') \
|
||||
<(go doc uinput.keyesc | sed '/Key/ !d; s/^\s*/uinput./; s/ .*/,/' | align) |
|
||||
# Skip really non-matching section, we echo some of them below
|
||||
sed '/^"XF86Tools"/,/^"XF86AudioPreset"/ d' |
|
||||
# Remove duplicate keys
|
||||
sed '/^"XF86Mail":.*Email/ d; /^"Cancel":.*Stop/ d; /^"XF86Send":.*file/ d; /^"Print":.*Sysrq/ d'
|
||||
|
||||
echo '"XF86WebCam": uinput.KeyCamera,'
|
||||
echo '"Print": uinput.KeyPrint,'
|
||||
} | sed 's/^".*"/\L&/; s/^/\t/'
|
||||
echo '}'
|
||||
|
||||
echo ''
|
||||
echo 'var xKeysShifted = map[string]int{'
|
||||
{
|
||||
paste -d ' ' <(xmodmap -pke | sed '1 d; s/.*= /"/; /.*=/ d; s/\S* /"/; s/ .*/":/' | sed '/^"XF86Eject"/ { N; s/.*\n// }') \
|
||||
<(go doc uinput.keyesc | sed '/Key/ !d; s/^\s*/uinput./; s/ .*/,/' | align) | sed '/^"NoSymbol"/ d; /^\S*_[LR]"/ d' |
|
||||
# Remove duplicate keys
|
||||
sed '/^"KP_Decimal":.*Kpcomma/ d; /\<Key102Nd\>/ d'
|
||||
} | sed 's/^".*"/\L&/; s/^/\t/'
|
||||
echo '}'
|
Loading…
Add table
Reference in a new issue