Merge pull request #67 from liamg/keyboard-shortcuts

added configurable keyboard shortcuts
This commit is contained in:
Liam Galvin 2018-11-25 14:27:41 +00:00 committed by GitHub
commit 7da69c342c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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`. 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) | | Operation | Key(s) |
| -------------------- | -------------------- | | -------------------- | -------------------- |
| Select text | click + drag | | Select text | click + drag |
| Select word | double click | | Select word | double click |
| Select line | triple click | | Select line | triple click |
| Copy | ctrl + shift + c | | Copy | `ctrl + shift + c` (Mac: `super + c`) |
| Toggle debug display | ctrl + shift + d | | Paste | `ctrl + shift + v` (Mac: `super + v`) |
| Paste | ctrl + shift + v | | Google selected text | `ctrl + shift + g` (Mac: `super + g`) |
| Google selected text | ctrl + shift + g | | Toggle debug display | `ctrl + shift + d` (Mac: `super + d`) |
| Report bug in aminal | ctrl + shift + r | | Toggle slomo | `ctrl + shift + ;` (Mac: `super + ;`) |
| Toggle slomo | ctrl + shift + ; | | Report bug in aminal | `ctrl + shift + r` (Mac: `super + r`) |
## Configuration ## 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. 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 | ```toml
| --------------- | -------------- | ----------- | ------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | debug = false # Enable debug logging to stdout. Defaults to false.
| --debug | _root_ | debug | boolean | false | Enable debug mode, with debug logging and debug info terminal overlay. slomo = false # Enable slow motion output mode, useful for debugging shells/terminal GUI apps etc. Defaults to false.
| --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 = "/bin/bash" # The shell to run for the terminal session. Defaults to the users shell.
| --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. [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 { type Config struct {
DebugMode bool `toml:"debug"` DebugMode bool `toml:"debug"`
Rendering RenderingConfig `toml:"rendering"` Slomo bool `toml:"slomo"`
Slomo bool `toml:"slomo"` ColourScheme ColourScheme `toml:"colours"`
ColourScheme ColourScheme `toml:"colours"` Shell string `toml:"shell"`
Shell string `toml:"shell"` KeyMapping KeyMappingConfig `toml:"keys"`
} }
type RenderingConfig struct { type KeyMappingConfig map[string]string
AlwaysRepaint bool `toml:"always_repaint"`
}
func Parse(data []byte) (*Config, error) { func Parse(data []byte) (*Config, error) {
c := DefaultConfig c := DefaultConfig
err := toml.Unmarshal(data, &c) err := toml.Unmarshal(data, &c)
if c.KeyMapping == nil {
c.KeyMapping = KeyMappingConfig(map[string]string{})
}
return &c, err return &c, err
} }

View File

@ -1,5 +1,7 @@
package config package config
import "runtime"
var DefaultConfig = Config{ var DefaultConfig = Config{
DebugMode: false, DebugMode: false,
ColourScheme: ColourScheme{ ColourScheme: ColourScheme{
@ -24,4 +26,24 @@ var DefaultConfig = Config{
White: strToColourNoErr("#f6f6c9"), White: strToColourNoErr("#f6f6c9"),
Selection: strToColourNoErr("#333366"), 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 { type GUI struct {
window *glfw.Window window *glfw.Window
logger *zap.SugaredLogger logger *zap.SugaredLogger
config *config.Config config *config.Config
terminal *terminal.Terminal terminal *terminal.Terminal
width int //window width in pixels width int //window width in pixels
height int //window height in pixels height int //window height in pixels
fontMap *FontMap fontMap *FontMap
fontScale float32 fontScale float32
renderer *OpenGLRenderer renderer *OpenGLRenderer
colourAttr uint32 colourAttr uint32
mouseDown bool mouseDown bool
overlay overlay overlay overlay
terminalAlpha float32 terminalAlpha float32
showDebugInfo bool 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{ return &GUI{
config: config, config: config,
logger: logger, logger: logger,
width: 800, width: 800,
height: 600, height: 600,
terminal: terminal, terminal: terminal,
fontScale: 14.0, fontScale: 14.0,
terminalAlpha: 1, terminalAlpha: 1,
} keyboardShortcuts: shortcuts,
}, nil
} }
// inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl // 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() gui.renderOverlay()
if gui.showDebugInfo { if gui.showDebugInfo {
gui.textbox(2, 2, fmt.Sprintf(`Debug Panel gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
===========
Cursor: %d,%d
View Size: %d,%d View Size: %d,%d
Buffer Size: %d lines Buffer Size: %d lines
`, `,
@ -266,8 +271,8 @@ Buffer Size: %d lines
gui.terminal.ActiveBuffer().ViewHeight(), gui.terminal.ActiveBuffer().ViewHeight(),
gui.terminal.ActiveBuffer().Height(), gui.terminal.ActiveBuffer().Height(),
), ),
[3]float32{0, 1, 0}, [3]float32{1, 1, 1},
[3]float32{0, 0, 0}, [3]float32{0.8, 0, 0},
) )
} }

View File

@ -2,7 +2,6 @@ package gui
import ( import (
"fmt" "fmt"
"net/url"
"github.com/go-gl/glfw/v3.2/glfw" "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 := "" modStr := ""
switch true { switch true {
case modsPressed(mods, glfw.ModControl, glfw.ModShift, glfw.ModAlt): case modsPressed(mods, glfw.ModControl, glfw.ModShift, glfw.ModAlt):
modStr = "8" modStr = "8"
@ -43,29 +63,6 @@ func (gui *GUI) key(w *glfw.Window, key glfw.Key, scancode int, action glfw.Acti
modStr = "7" modStr = "7"
case modsPressed(mods, glfw.ModControl, glfw.ModShift): case modsPressed(mods, glfw.ModControl, glfw.ModShift):
modStr = "6" 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): case modsPressed(mods, glfw.ModControl):
modStr = "5" modStr = "5"
switch key { switch key {

View File

@ -53,7 +53,10 @@ func main() {
logger.Infof("Creating terminal...") logger.Infof("Creating terminal...")
terminal := terminal.New(pty, logger, conf) 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 { if err := g.Render(); err != nil {
logger.Fatalf("Render error: %s", err) logger.Fatalf("Render error: %s", err)
} }