mirror of https://github.com/liamg/aminal.git
added configurable keyboard shortcuts
This commit is contained in:
parent
3f5260eff5
commit
707df1a3a1
66
README.md
66
README.md
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
61
gui/gui.go
61
gui/gui.go
|
@ -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},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
49
gui/input.go
49
gui/input.go
|
@ -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 {
|
||||
|
|
5
main.go
5
main.go
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue