added configurable keyboard shortcuts

This commit is contained in:
Liam Galvin 2018-11-25 14:25:19 +00:00
parent 3f5260eff5
commit 707df1a3a1
10 changed files with 345 additions and 77 deletions

View File

@ -76,19 +76,19 @@ make install
As long as you have your `GOBIN` environment variable set up properly (and in `PATH`), you should be able to run `aminal`.
## Keyboard Shortcuts
## Keyboard/Mouse Shortcuts
| Operation | Key(s) |
| -------------------- | -------------------- |
| Select text | click + drag |
| Select word | double click |
| Select line | triple click |
| Copy | ctrl + shift + c |
| Toggle debug display | ctrl + shift + d |
| Paste | ctrl + shift + v |
| Google selected text | ctrl + shift + g |
| Report bug in aminal | ctrl + shift + r |
| Toggle slomo | ctrl + shift + ; |
| Copy | `ctrl + shift + c` (Mac: `super + c`) |
| Paste | `ctrl + shift + v` (Mac: `super + v`) |
| Google selected text | `ctrl + shift + g` (Mac: `super + g`) |
| Toggle debug display | `ctrl + shift + d` (Mac: `super + d`) |
| Toggle slomo | `ctrl + shift + ;` (Mac: `super + ;`) |
| Report bug in aminal | `ctrl + shift + r` (Mac: `super + r`) |
## Configuration
@ -96,11 +96,49 @@ Aminal looks for a config file in `~/.aminal.toml`, and will write one there the
You can ignore the config and use defaults by specifying `--ignore-config` as a CLI flag.
### Config Options/CLI Flags
### Config File
| CLI Flag | Config Section | Config Name | Type | Default | Description |
| --------------- | -------------- | ----------- | ------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| --debug | _root_ | debug | boolean | false | Enable debug mode, with debug logging and debug info terminal overlay.
| --slomo | _root_ | slomo | boolean | false | Enable slomo mode, delay the handling of each incoming byte (or escape sequence) from the pty by 100ms. Useful for debugging.
| --shell [shell] | _root_ | shell | string | User's shell | Use the specified shell program instead of the user's usual one.
| --version | n/a | n/a | boolean | n/a | Show the version of aminal.
```toml
debug = false # Enable debug logging to stdout. Defaults to false.
slomo = false # Enable slow motion output mode, useful for debugging shells/terminal GUI apps etc. Defaults to false.
shell = "/bin/bash" # The shell to run for the terminal session. Defaults to the users shell.
[colours]
cursor = "#e8dfd6"
foreground = "#e8dfd6"
background = "#021b21"
black = "#032c36"
red = "#c2454e"
green = "#7cbf9e"
yellow = "#8a7a63"
blue = "#065f73"
magenta = "#ff5879"
cyan = "#44b5b1"
light_grey = "#f2f1b9"
dark_grey = "#3e4360"
light_red = "#ef5847"
light_green = "#a2db91"
light_yellow = "#beb090"
light_blue = "#61778d"
light_magenta = "#ff99a1"
light_cyan = "#9ed9d8"
white = "#f6f6c9"
selection = "#333366" # Mouse selection background colour
[keys]
copy = "ctrl + shift + c" # Copy highlighted text to system clipboard
paste = "ctrl + shift + v" # Paste text from system clipboard
debug = "ctrl + shift + d" # Toggle debug panel overlay
google = "ctrl + shift + g" # Google selected text
report = "ctrl + shift + r" # Send bug report
slomo = "ctrl + shift + ;" # Toggle slow motion output mode (useful for debugging)
```
### CLI Flags
| Flag | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `--debug` | Enable debug mode, with debug logging and debug info terminal overlay.
| `--slomo` | Enable slomo mode, delay the handling of each incoming byte (or escape sequence) from the pty by 100ms. Useful for debugging.
| `--shell [shell]` | Use the specified shell program instead of the user's usual one.
| `--version` | Show the version of aminal and exit.

12
config/actions.go Normal file
View File

@ -0,0 +1,12 @@
package config
type UserAction string
const (
ActionCopy UserAction = "copy"
ActionPaste UserAction = "paste"
ActionGoogle UserAction = "google"
ActionReportBug UserAction = "report"
ActionToggleDebug UserAction = "debug"
ActionToggleSlomo UserAction = "slomo"
)

View File

@ -7,20 +7,21 @@ import (
)
type Config struct {
DebugMode bool `toml:"debug"`
Rendering RenderingConfig `toml:"rendering"`
Slomo bool `toml:"slomo"`
ColourScheme ColourScheme `toml:"colours"`
Shell string `toml:"shell"`
DebugMode bool `toml:"debug"`
Slomo bool `toml:"slomo"`
ColourScheme ColourScheme `toml:"colours"`
Shell string `toml:"shell"`
KeyMapping KeyMappingConfig `toml:"keys"`
}
type RenderingConfig struct {
AlwaysRepaint bool `toml:"always_repaint"`
}
type KeyMappingConfig map[string]string
func Parse(data []byte) (*Config, error) {
c := DefaultConfig
err := toml.Unmarshal(data, &c)
if c.KeyMapping == nil {
c.KeyMapping = KeyMappingConfig(map[string]string{})
}
return &c, err
}

View File

@ -1,5 +1,7 @@
package config
import "runtime"
var DefaultConfig = Config{
DebugMode: false,
ColourScheme: ColourScheme{
@ -24,4 +26,24 @@ var DefaultConfig = Config{
White: strToColourNoErr("#f6f6c9"),
Selection: strToColourNoErr("#333366"),
},
KeyMapping: KeyMappingConfig(map[string]string{}),
}
func init() {
DefaultConfig.KeyMapping[string(ActionCopy)] = addMod("c")
DefaultConfig.KeyMapping[string(ActionPaste)] = addMod("v")
DefaultConfig.KeyMapping[string(ActionGoogle)] = addMod("g")
DefaultConfig.KeyMapping[string(ActionToggleDebug)] = addMod("d")
DefaultConfig.KeyMapping[string(ActionToggleSlomo)] = addMod(";")
DefaultConfig.KeyMapping[string(ActionReportBug)] = addMod("r")
}
func addMod(keys string) string {
standardMod := "ctrl + shift + "
if runtime.GOOS == "darwin" {
standardMod = "super + "
}
return standardMod + keys
}

116
config/keys.go Normal file
View File

@ -0,0 +1,116 @@
package config
import (
"fmt"
"strings"
"github.com/go-gl/glfw/v3.2/glfw"
)
type KeyCombination struct {
mods glfw.ModifierKey
key glfw.Key
}
type KeyMod string
const (
ctrl KeyMod = "ctrl"
alt KeyMod = "alt"
shift KeyMod = "shift"
super KeyMod = "super"
)
var modMap = map[KeyMod]glfw.ModifierKey{
ctrl: glfw.ModControl,
alt: glfw.ModAlt,
shift: glfw.ModShift,
super: glfw.ModSuper,
}
var keyMap = map[string]glfw.Key{
"a": glfw.KeyA,
"b": glfw.KeyB,
"c": glfw.KeyC,
"d": glfw.KeyD,
"e": glfw.KeyE,
"f": glfw.KeyF,
"g": glfw.KeyG,
"h": glfw.KeyH,
"i": glfw.KeyI,
"j": glfw.KeyJ,
"k": glfw.KeyK,
"l": glfw.KeyL,
"m": glfw.KeyM,
"n": glfw.KeyN,
"o": glfw.KeyO,
"p": glfw.KeyP,
"q": glfw.KeyQ,
"r": glfw.KeyR,
"s": glfw.KeyS,
"t": glfw.KeyT,
"u": glfw.KeyU,
"v": glfw.KeyV,
"w": glfw.KeyW,
"x": glfw.KeyX,
"y": glfw.KeyY,
"z": glfw.KeyZ,
";": glfw.KeySemicolon,
}
// keyStr e.g. "ctrl + alt + a"
func parseKeyCombination(keyStr string) (*KeyCombination, error) {
var mods glfw.ModifierKey
var key *glfw.Key
keys := strings.Split(keyStr, "+")
for _, k := range keys {
k = strings.ToLower(strings.TrimSpace(k))
mod, ok := modMap[KeyMod(k)]
if ok {
mods = mods + mod
continue
}
mappedKey, ok := keyMap[k]
if ok {
if key != nil {
return nil, fmt.Errorf("Multiple non-modifier keys specified in keyboard shortcut")
}
key = &mappedKey
continue
}
return nil, fmt.Errorf("Unknown key '%s' in configured keyboard shortcut", k)
}
if key == nil {
return nil, fmt.Errorf("No non-modifier key specified in keyboard shortcut")
}
if mods == 0 {
return nil, fmt.Errorf("No modifier key specified in keyboard shortcut")
}
return &KeyCombination{
mods: mods,
key: *key,
}, nil
}
func (combi KeyCombination) Match(pressedMods glfw.ModifierKey, pressedKey glfw.Key) bool {
return pressedKey == combi.key && pressedMods == combi.mods
}
func (keyMapConfig KeyMappingConfig) GenerateActionMap() (map[UserAction]*KeyCombination, error) {
m := map[UserAction]*KeyCombination{}
for actionStr, keyStr := range keyMapConfig {
combi, err := parseKeyCombination(keyStr)
if err != nil {
return nil, err
}
m[UserAction(actionStr)] = combi
}
return m, nil
}

27
config/keys_test.go Normal file
View File

@ -0,0 +1,27 @@
package config
import (
"testing"
"github.com/go-gl/glfw/v3.2/glfw"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKeyCombinations(t *testing.T) {
combi, err := parseKeyCombination("ctrl + alt + a")
require.Nil(t, err)
require.NotNil(t, combi)
assert.Equal(t, glfw.KeyA, combi.key)
assert.Equal(t, glfw.ModControl+glfw.ModAlt, combi.mods)
assert.True(t, combi.Match(glfw.ModControl^glfw.ModAlt, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt, glfw.KeyB))
assert.False(t, combi.Match(glfw.ModControl, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModAlt, glfw.KeyA))
assert.False(t, combi.Match(0, glfw.KeyA))
assert.False(t, combi.Match(glfw.ModControl^glfw.ModAlt^glfw.ModShift, glfw.KeyA))
}

47
gui/actions.go Normal file
View File

@ -0,0 +1,47 @@
package gui
import (
"fmt"
"net/url"
"github.com/liamg/aminal/config"
)
var actionMap = map[config.UserAction]func(gui *GUI){
config.ActionCopy: actionCopy,
config.ActionPaste: actionPaste,
config.ActionToggleDebug: actionToggleDebug,
config.ActionGoogle: actionGoogleSelection,
config.ActionToggleSlomo: actionToggleSlomo,
config.ActionReportBug: actionReportBug,
}
func actionCopy(gui *GUI) {
gui.window.SetClipboardString(gui.terminal.ActiveBuffer().GetSelectedText())
}
func actionPaste(gui *GUI) {
if s, err := gui.window.GetClipboardString(); err == nil {
_ = gui.terminal.Paste([]byte(s))
}
}
func actionToggleDebug(gui *GUI) {
gui.showDebugInfo = !gui.showDebugInfo
gui.terminal.SetDirty()
}
func actionGoogleSelection(gui *GUI) {
keywords := gui.terminal.ActiveBuffer().GetSelectedText()
if keywords != "" {
gui.launchTarget(fmt.Sprintf("https://www.google.com/search?q=%s", url.QueryEscape(keywords)))
}
}
func actionToggleSlomo(gui *GUI) {
gui.config.Slomo = !gui.config.Slomo
}
func actionReportBug(gui *GUI) {
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
}

View File

@ -15,33 +15,40 @@ import (
)
type GUI struct {
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
fontMap *FontMap
fontScale float32
renderer *OpenGLRenderer
colourAttr uint32
mouseDown bool
overlay overlay
terminalAlpha float32
showDebugInfo bool
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
fontMap *FontMap
fontScale float32
renderer *OpenGLRenderer
colourAttr uint32
mouseDown bool
overlay overlay
terminalAlpha float32
showDebugInfo bool
keyboardShortcuts map[config.UserAction]*config.KeyCombination
}
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) (*GUI, error) {
shortcuts, err := config.KeyMapping.GenerateActionMap()
if err != nil {
return nil, err
}
return &GUI{
config: config,
logger: logger,
width: 800,
height: 600,
terminal: terminal,
fontScale: 14.0,
terminalAlpha: 1,
}
config: config,
logger: logger,
width: 800,
height: 600,
terminal: terminal,
fontScale: 14.0,
terminalAlpha: 1,
keyboardShortcuts: shortcuts,
}, nil
}
// inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl
@ -254,9 +261,7 @@ func (gui *GUI) Render() error {
gui.renderOverlay()
if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Debug Panel
===========
Cursor: %d,%d
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
View Size: %d,%d
Buffer Size: %d lines
`,
@ -266,8 +271,8 @@ Buffer Size: %d lines
gui.terminal.ActiveBuffer().ViewHeight(),
gui.terminal.ActiveBuffer().Height(),
),
[3]float32{0, 1, 0},
[3]float32{0, 0, 0},
[3]float32{1, 1, 1},
[3]float32{0.8, 0, 0},
)
}

View File

@ -2,7 +2,6 @@ package gui
import (
"fmt"
"net/url"
"github.com/go-gl/glfw/v3.2/glfw"
)
@ -32,10 +31,31 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
}
}
gui.logger.Debugf("KEY PRESS: key=0x%X scan=0x%X", key, scancode)
for userAction, shortcut := range gui.keyboardShortcuts {
if shortcut.Match(mods, key) {
f, ok := actionMap[userAction]
if ok {
f(gui)
break
}
switch key {
case glfw.KeyD:
case glfw.KeyG:
case glfw.KeyR:
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
case glfw.KeySemicolon:
gui.config.Slomo = !gui.config.Slomo
return
}
}
}
modStr := ""
switch true {
case modsPressed(mods, glfw.ModControl, glfw.ModShift, glfw.ModAlt):
modStr = "8"
@ -43,29 +63,6 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
modStr = "7"
case modsPressed(mods, glfw.ModControl, glfw.ModShift):
modStr = "6"
switch key {
case glfw.KeyC:
gui.window.SetClipboardString(gui.terminal.ActiveBuffer().GetSelectedText())
return
case glfw.KeyD:
gui.showDebugInfo = !gui.showDebugInfo
gui.terminal.SetDirty()
case glfw.KeyG:
keywords := gui.terminal.ActiveBuffer().GetSelectedText()
if keywords != "" {
gui.launchTarget(fmt.Sprintf("https://www.google.com/search?q=%s", url.QueryEscape(keywords)))
}
case glfw.KeyR:
gui.launchTarget("https://github.com/liamg/aminal/issues/new/choose")
case glfw.KeyV:
if s, err := gui.window.GetClipboardString(); err == nil {
_ = gui.terminal.Paste([]byte(s))
}
return
case glfw.KeySemicolon:
gui.config.Slomo = !gui.config.Slomo
return
}
case modsPressed(mods, glfw.ModControl):
modStr = "5"
switch key {

View File

@ -53,7 +53,10 @@ func main() {
logger.Infof("Creating terminal...")
terminal := terminal.New(pty, logger, conf)
g := gui.New(conf, terminal, logger)
g, err := gui.New(conf, terminal, logger)
if err != nil {
logger.Fatalf("Cannot start: %s", err)
}
if err := g.Render(); err != nil {
logger.Fatalf("Render error: %s", err)
}