diff --git a/app/app.go b/app/app.go index 080c26fe..93a29801 100644 --- a/app/app.go +++ b/app/app.go @@ -42,7 +42,8 @@ func SetupApplicationWithAccount(app *tview.Application, account string) { configuration.Token = configuration.GetAccountToken(account) } - shortcutsLoadError := shortcuts.Load() + shortcutsLoadError := shortcuts.Load(app.VimMode) + shortcuts.InjectVim(app.VimMode) if shortcutsLoadError != nil { panic(shortcutsLoadError) } @@ -128,6 +129,7 @@ func SetupApplicationWithAccount(app *tview.Application, account string) { window.RegisterCommand(serverCreateCmd) window.RegisterCommand(commandimpls.NewServerCommand(serverJoinCmd, serverLeaveCmd, serverCreateCmd)) window.RegisterCommand(commandimpls.NewNickSetCmd(discord, window)) + window.RegisterCommand(commandimpls.NewVimCmd(app.VimMode)) tfaEnableCmd := commandimpls.NewTFAEnableCommand(window, discord) tfaDisableCmd := commandimpls.NewTFADisableCommand(discord) tfaBackupGetCmd := commandimpls.NewTFABackupGetCmd(discord, window) diff --git a/appveyor.yml b/appveyor.yml index 6464606a..2e83a045 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,7 +2,6 @@ clone_folder: c:\gopath\src\github.com\Bios-Marcel\cordless environment: GOPATH: c:\gopath - GO111MODULE: on build: off stack: go 1.13 @@ -16,4 +15,4 @@ build_script: test_script: - go vet ./... - - go test ./... + - go test ./... \ No newline at end of file diff --git a/commands/commandimpls/vim.go b/commands/commandimpls/vim.go new file mode 100644 index 00000000..eaa5b138 --- /dev/null +++ b/commands/commandimpls/vim.go @@ -0,0 +1,75 @@ +package commandimpls + +import ( + "fmt" + "io" + + "github.com/Bios-Marcel/cordless/util/vim" +) + +const ( + vimOpenHelpPage = `[::b]NAME + vim-mode - minor vim mode for cordless + +[::b]SYNOPSIS + [::b]Normal Mode: Navigate around the containers with hjkl. + Perform some usual commands inside text box input, as pasting, moving cursor or such. + Press ESC anywhere to return to normal mode. + + [::b]Insert Mode: Type inside box input, perform actions inside chatview. + Insert without any key restriction inside the text box using insert mode. + Inside chat view, perform useful commands such as editing message "i" or replying to user "a" + + [::b]Visual Mode: Move around everywhere with vim keys. + This mode allows to use hjkl pretty much anywhere inside the app. Due to some restrictions, this is + the only mode that officially supports using hjkl anywhere. + Also allows using same commands as insert mode inside chat view, or selecting text inside text input. + + +[::b]DESCRIPTION + This is a minor mode for vim. See all the shorcuts with Ctrl K, and edit them inside shortcuts/shortcuts.go + Toggle vim with vim on/off/enable/disable +` +) + +type VimHelp struct { + vimMode *vim.Vim +} + +func NewVimCmd(vimMode *vim.Vim) *VimHelp { + return &VimHelp{vimMode: vimMode} +} + +// PrintHelp prints a static help page for this command +func (v VimHelp) PrintHelp(writer io.Writer) { + fmt.Fprintln(writer, vimOpenHelpPage) +} + +func (v VimHelp) Execute(writer io.Writer, parameters []string) { + if len(parameters) < 1 { + fmt.Fprintf(writer, "Usage: vim on/off") + return + } + switch parameters[0] { + case "on", "enable": + v.vimMode.CurrentMode = vim.NormalMode + fmt.Fprintf(writer, "Vim mode has been enabled.\n") + case "off", "disable": + v.vimMode.CurrentMode = vim.NormalMode + v.vimMode.CurrentMode = vim.Disabled + fmt.Fprintf(writer, "Vim mode has been disabled.\n") + default: + fmt.Fprintf(writer, "Parameter %s not recognized.", parameters[0]) + } +} + +// Name returns the primary name for this command. This name will also be +// used for listing the command in the commandlist. +func (v VimHelp) Name() string { + return "vim" +} + +// Aliases are a list of aliases for this command. There might be none. +func (v VimHelp) Aliases() []string { + return []string{"vim", "vim-mode"} +} diff --git a/config/config.go b/config/config.go index 8835b723..2dd73266 100644 --- a/config/config.go +++ b/config/config.go @@ -155,6 +155,10 @@ type Config struct { // download files to. If FileOpenSaveFilesPermanently has been set to // true, then all opened files are saved in this folder for example. FileDownloadSaveLocation string + + // This will set the global vim keybindings when enabled, effectively + // taking over the normal bindings. It is changed in the config file. + VimEnabled bool } // Account has a name and a token. The name is just for the users recognition. @@ -211,6 +215,7 @@ func createDefaultConfig() *Config { FileOpenHandlers: make(map[string]string), FileOpenSaveFilesPermanently: false, FileDownloadSaveLocation: "~/Downloads", + VimEnabled: false, } } diff --git a/discordutil/guilds.go b/discordutil/guilds.go index 401d9c4c..b9cc6d31 100644 --- a/discordutil/guilds.go +++ b/discordutil/guilds.go @@ -66,7 +66,6 @@ func FindEmojiInGuild(session *discordgo.Session, guild *discordgo.Guild, omitGW continue } - if strings.EqualFold(emoji.Name, emojiSequence) && (omitGWCheck || strings.HasPrefix(emoji.Name, "GW")) { if len(emoji.Roles) != 0 { selfMember, cacheError := session.State.Member(guild.ID, session.State.User.ID) diff --git a/femto/lineArray.go b/femto/lineArray.go index 8e01020b..b2c101ef 100644 --- a/femto/lineArray.go +++ b/femto/lineArray.go @@ -234,4 +234,3 @@ func (la *LineArray) Substr(start, end Loc) string { str += string(la.lines[end.Y].data[:endX]) return str } - diff --git a/go.sum b/go.sum index 78931bda..e9181dda 100644 --- a/go.sum +++ b/go.sum @@ -1,78 +1,76 @@ -github.com/Bios-Marcel/discordemojimap v1.0.1 h1:b3UYPO7+h1+ciStkwU/KQCerOmpUNPHsBf4a7EjMdys= github.com/Bios-Marcel/discordemojimap v1.0.1/go.mod h1:AoHIpUwf3EVCAAUmk+keXjb9khyZcFnW84/rhJd4IkU= -github.com/Bios-Marcel/discordgo v0.21.2-0.20201025194456-7046b5509389 h1:Ha9AEp9HwOZElVwWWiA/aYU4g6dOYwJIhnxYf8XsNSo= github.com/Bios-Marcel/discordgo v0.21.2-0.20201025194456-7046b5509389/go.mod h1:tHeTdL2sSGuZcRHa+lp+SteLEy/XdVQisWlsMXa33L4= -github.com/Bios-Marcel/goclipimg v0.0.0-20191117180634-d0f7b06fbe82 h1:gspJ6CW9bhboosSISmuX2iq03pUsYHzlJN0s+z4fz4E= github.com/Bios-Marcel/goclipimg v0.0.0-20191117180634-d0f7b06fbe82/go.mod h1:hiFR6fH5+uc/f2yK2syh/UfzaPfGo6F2HJSoiI4ufWU= -github.com/Bios-Marcel/shortnotforlong v1.1.1 h1:cCJIp6MODd4rwH5Y+fAMAaIuFxS1FPKfwDbRzsCU9nM= github.com/Bios-Marcel/shortnotforlong v1.1.1/go.mod h1:g6bFiwq0pq7pqENRgHiCZu7uMzeYPIXwANlaBQ47LBw= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI= github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs= github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= -github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA= github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8= -github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= -github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-github/v29 v29.0.3 h1:IktKCTwU//aFHnpA+2SLIi7Oo9uhAzgsdZNbcAqhgdc= github.com/google/go-github/v29 v29.0.3/go.mod h1:CHKiKKPHJ0REzfwc14QMklvtHwCveD0PxlMjLlzAM5E= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw= github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ= github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= -github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= -github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robertkrimen/otto v0.0.0-20200922221731-ef014fd054ac/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= +0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/main.go b/main.go index 488a14dd..c88e8ec5 100644 --- a/main.go +++ b/main.go @@ -63,7 +63,7 @@ func main() { fmt.Printf("You are running cordless version %s\nKeep in mind that this version might not be correct for manually built versions, as those can contain additional commits.\n", version.Version) } else { //App that will be reused throughout the process runtime. - tviewApp := tview.NewApplication() + tviewApp := tview.NewApplication(config.Current.VimEnabled) if accountToUse != nil && *accountToUse != "" { app.SetupApplicationWithAccount(tviewApp, *accountToUse) diff --git a/shortcuts/shortcuts.go b/shortcuts/shortcuts.go index 9b2fc4fe..023438c1 100644 --- a/shortcuts/shortcuts.go +++ b/shortcuts/shortcuts.go @@ -13,6 +13,8 @@ import ( "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" "github.com/Bios-Marcel/cordless/ui/tviewutil" + "github.com/Bios-Marcel/cordless/util/vim" + //"github.com/Bios-Marcel/cordless/util/vim" ) var ( @@ -22,139 +24,354 @@ var ( guildlist = addScope("guildlist", "Guildlist", globalScope) channeltree = addScope("channeltree", "Channeltree", globalScope) + // Normal mode will always prevail in any scope. To exit insert or visual mode press ESC. + // + // A nil VimEvent means the original non-vim key will be used. + // A NullVimEvent will ignore that mapping inside the selected mode. QuoteSelectedMessage = addShortcut("quote_selected_message", "Quote selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + // Normal Insert Visual + ) + EditSelectedMessage = addShortcut("edit_selected_message", "Edit selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'e', tcell.ModNone)) - DownloadMessageFiles = addShortcut("dowbload_message_files", "Download all files in selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'e', tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'i', tcell.ModNone), NullVimEvent), + ) + + DownloadMessageFiles = addShortcut("download_message_files", "Download all files in selected message", + chatview, tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone), + // FIXME solve delete messages keybinding conflict in visual mode + addVimEvent(NullVimEvent, NullVimEvent, NullVimEvent), + ) + ReplySelectedMessage = addShortcut("reply_selected_message", "Reply to author selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'r', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'r', tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'a', tcell.ModNone), NullVimEvent), + ) + NewDirectMessage = addShortcut("new_direct_message", "Create a new direct message channel with this user", - chatview, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone), NullVimEvent), + ) + CopySelectedMessageLink = addShortcut("copy_selected_message_link", "Copy link to selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) + CopySelectedMessage = addShortcut("copy_selected_message", "Copy content of selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'y', tcell.ModNone), NullVimEvent), + ) + ToggleSelectedMessageSpoilers = addShortcut("toggle_selected_message_spoilers", "Toggle spoilers in selected message", - chatview, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) + DeleteSelectedMessage = addShortcut("delete_selected_message", "Delete the selected message", - chatview, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'd', tcell.ModNone), NullVimEvent), + ) + ViewSelectedMessageImages = addShortcut("view_selected_message_images", "View selected message's attached files", - chatview, tcell.NewEventKey(tcell.KeyRune, 'o', tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyRune, 'o', tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) + ChatViewSelectionUp = addShortcut("selection_up", "Move selection up by one", - chatview, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'k', tcell.ModNone), NullVimEvent), + ) + ChatViewSelectionDown = addShortcut("selection_down", "Move selection down by one", - chatview, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone), NullVimEvent), + ) + ChatViewSelectionTop = addShortcut("selection_top", "Move selection to the upmost message", - chatview, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'g', tcell.ModNone), NullVimEvent), + ) + ChatViewSelectionBottom = addShortcut("selection_bottom", "Move selection to the downmost message", - chatview, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)) + chatview, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'G', tcell.ModNone), NullVimEvent), + ) + // START OF INPUT ExpandSelectionToLeft = addShortcut("expand_selection_word_to_left", "Expand selection word to left", - multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'H', tcell.ModNone)), + ) + ExpandSelectionToRight = addShortcut("expand_selection_word_to_right", "Expand selection word to right", - multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'L', tcell.ModNone)), + ) + SelectAll = addShortcut("select_all", "Select all", - multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'V', tcell.ModNone)), + ) + SelectWordLeft = addShortcut("select_word_to_left", "Select word to left", - multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl|tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl|tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'b', tcell.ModNone)), + ) + SelectWordRight = addShortcut("select_word_to_right", "Select word to right", - multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl|tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl|tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'w', tcell.ModNone)), + ) + + // TODO create a way to store keys making multi key commands like yw, di or y$ SelectToStartOfLine = addShortcut("select_to_start_of_line", "Select to start of line", - multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, '0', tcell.ModNone)), + ) + SelectToEndOfLine = addShortcut("select_to_end_of_line", "Select to end of line", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, '$', tcell.ModNone)), + ) + SelectToStartOfText = addShortcut("select_to_start_of_text", "Select to start of text", - multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl|tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl|tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'g', tcell.ModNone)), + ) + SelectToEndOfText = addShortcut("select_to_end_of_text", "Select to end of text", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl|tcell.ModShift)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl|tcell.ModShift), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'G', tcell.ModNone)), + ) MoveCursorLeft = addShortcut("move_cursor_to_left", "Move cursor to left", - multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) + MoveCursorRight = addShortcut("move_cursor_to_right", "Move cursor to right", - multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) + MoveCursorWordLeft = addShortcut("move_cursor_to_word_left", "Move cursor to word left", - multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'b', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + MoveCursorWordRight = addShortcut("move_cursor_to_word_right", "Move cursor to word right", - multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'w', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + MoveCursorStartOfLine = addShortcut("move_cursor_to_start_of_line", "Move cursor to start of line", - multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModNone), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, '0', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + MoveCursorEndOfLine = addShortcut("move_cursor_to_end_of_line", "Move cursor to end of line", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModNone), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, '$', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + MoveCursorStartOfText = addShortcut("move_cursor_to_start_of_text", "Move cursor to start of text", - multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyHome, 0, tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'g', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + MoveCursorEndOfText = addShortcut("move_cursor_to_end_of_text", "Move cursor to end of text", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnd, 0, tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'G', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + + DeleteLeft = addDeleteLeftShortcut() - DeleteLeft = addDeleteLeftShortcut() DeleteRight = addShortcut("delete_right", "Delete right", - multilineTextInput, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyDelete, 0, tcell.ModNone), addVimEvent(NullVimEvent, nil, NullVimEvent)) DeleteWordLeft = addShortcut("delete_word_left", "Delete word left", - multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlW, rune(tcell.KeyCtrlW), tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlW, rune(tcell.KeyCtrlW), tcell.ModCtrl), addVimEvent(NullVimEvent, nil, NullVimEvent)) InputNewLine = addShortcut("add_new_line_character", "Add new line character", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModAlt)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModAlt), addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'o', tcell.ModNone), NullVimEvent, NullVimEvent)) CopySelection = addShortcut("copy_selection", "Copy selected text", - multilineTextInput, tcell.NewEventKey(tcell.KeyRune, 'C', tcell.ModAlt)) - //Don't fix the typo, it'll break stuff ;) + multilineTextInput, tcell.NewEventKey(tcell.KeyRune, 'C', tcell.ModAlt), + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'y', tcell.ModNone)), + ) + //Don't fix the typo, it'll break stuff ;) ---- (glups) PasteAtSelection = addShortcut("paste_at_selectiom", "Paste clipboard content", - multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlV, rune(tcell.KeyCtrlV), tcell.ModCtrl)) + multilineTextInput, tcell.NewEventKey(tcell.KeyCtrlV, rune(tcell.KeyCtrlV), tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModNone), NullVimEvent, NullVimEvent), + ) SendMessage = addShortcut("send_message", "Sends the typed message", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone), + addVimEvent(NullVimEvent, nil, NullVimEvent), + ) AddNewLineInCodeBlock = addShortcut("add_new_line_in_code_block", "Adds a new line inside a code block", - multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone)) + multilineTextInput, tcell.NewEventKey(tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'o', tcell.ModNone), NullVimEvent, NullVimEvent), + ) ExitApplication = addShortcut("exit_application", "Exit application", - globalScope, tcell.NewEventKey(tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl)) + globalScope, tcell.NewEventKey(tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone), NullVimEvent, NullVimEvent), + ) FocusUp = addShortcut("focus_up", "Focus the next widget above", - globalScope, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'k', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusDown = addShortcut("focus_down", "Focus the next widget below", - globalScope, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusLeft = addShortcut("focus_left", "Focus the next widget to the left", - globalScope, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusRight = addShortcut("focus_right", "Focus the next widget to the right", - globalScope, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone), NullVimEvent, NullVimEvent), + ) FocusChannelContainer = addShortcut("focus_channel_container", "Focus channel container", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'c', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusUserContainer = addShortcut("focus_user_container", "Focus user container", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'u', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'u', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'u', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusGuildContainer = addShortcut("focus_guild_container", "Focus guild container", - globalScope, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 's', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusPrivateChatPage = addShortcut("focus_private_chat_page", "Focus private chat page", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'p', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'P', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + SwitchToPreviousChannel = addShortcut("switch_to_previous_channel", "Switch to previous channel", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModAlt), + // FIXME unbound + addVimEvent(NullVimEvent, NullVimEvent, NullVimEvent), + ) + FocusMessageInput = addShortcut("focus_message_input", "Focus message input", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModAlt), + // Toggle insert mode + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'I', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusMessageContainer = addShortcut("focus_message_container", "Focus message container", - globalScope, tcell.NewEventKey(tcell.KeyRune, 't', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 't', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 't', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + FocusCommandInput = addShortcut("focus_command_input", "Focus command input", - globalScope, nil) + // FIXME unbound + globalScope, nil, addVimEvent(NullVimEvent, NullVimEvent, NullVimEvent)) FocusCommandOutput = addShortcut("focus_command_output", "Focus command output", - globalScope, nil) + // FIXME unbound + globalScope, nil, addVimEvent(NullVimEvent, NullVimEvent, NullVimEvent)) ToggleUserContainer = addShortcut("toggle_user_container", "Toggle user container", - globalScope, tcell.NewEventKey(tcell.KeyRune, 'U', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, 'U', tcell.ModAlt), + addVimEvent(nil, NullVimEvent, NullVimEvent), + ) + ToggleCommandView = addShortcut("toggle_command_view", "Toggle command view", - globalScope, tcell.NewEventKey(tcell.KeyRune, '.', tcell.ModAlt)) + globalScope, tcell.NewEventKey(tcell.KeyRune, '.', tcell.ModAlt), + addVimEvent(tcell.NewEventKey(tcell.KeyRune, ':', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + ToggleBareChat = addShortcut("toggle_bare_chat", "Toggle bare chat", - globalScope, tcell.NewEventKey(tcell.KeyCtrlB, rune(tcell.KeyCtrlB), tcell.ModCtrl)) + globalScope, tcell.NewEventKey(tcell.KeyCtrlB, rune(tcell.KeyCtrlB), tcell.ModCtrl), + // FIXME unknown binding + addVimEvent(NullVimEvent, NullVimEvent, NullVimEvent), + ) GuildListMarkRead = addShortcut("guild_mark_read", "Mark server as read", - guildlist, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl)) + guildlist, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModNone), tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModNone)), + ) ChannelTreeMarkRead = addShortcut("channel_mark_read", "Mark channel as read", - channeltree, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl)) + channeltree, tcell.NewEventKey(tcell.KeyCtrlR, rune(tcell.KeyCtrlR), tcell.ModCtrl), + addVimEvent(NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModNone), tcell.NewEventKey(tcell.KeyRune, 'm', tcell.ModNone)), + ) + + VimInsertMode = addShortcut("vim_insert_mode", "Change to Vim insert mode", + globalScope, nil, + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'i', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + + VimVisualMode = addShortcut("vim_visual_mode", "Change to Vim visual mode", + globalScope, nil, + addVimEvent(tcell.NewEventKey(tcell.KeyRune, 'v', tcell.ModNone), NullVimEvent, NullVimEvent), + ) + + VimNormalMode = addShortcut("vim_normal_mode", "Return to vim normal mode", + globalScope, nil, + // FIXME escape key not working in my machine. Using hyphen instead temporarily. + addVimEvent(tcell.NewEventKey(tcell.KeyEscape, 0, tcell.ModNone), tcell.NewEventKey(tcell.KeyEscape, 0, tcell.ModNone), tcell.NewEventKey(tcell.KeyEscape, 0, tcell.ModNone)), + ) + + VimSimKeyUp = addShortcut("vim_sim_up", "Simulate an arrow key press in vim mode.", + globalScope, nil, + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'k', tcell.ModNone))) + + VimSimKeyDown = addShortcut("vim_sim_down", "Simulate an arrow key press in vim mode.", + globalScope, nil, + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'j', tcell.ModNone))) + + VimSimKeyLeft = addShortcut("vim_sim_left", "Simulate an arrow key press in vim mode.", + globalScope, nil, + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'h', tcell.ModNone))) + + VimSimKeyRight = addShortcut("vim_sim_right", "Simulate an arrow key press in vim mode.", + globalScope, nil, + addVimEvent(NullVimEvent, NullVimEvent, tcell.NewEventKey(tcell.KeyRune, 'l', tcell.ModNone))) scopes []*Scope Shortcuts []*Shortcut ) +// Vim event holds possible modifiers for keys. They can be nil. In that case, they will default +// to the original binding. If the event is associated to NullVimEvent, any events while in that mode +// will be ignored. +type VimEvent struct { + NormalEvent *tcell.EventKey + InsertEvent *tcell.EventKey + VisualEvent *tcell.EventKey +} + +// NullVimEvent is the null event for the current vim mode. Any events that have this will be ignored. +var NullVimEvent *tcell.EventKey = new(tcell.EventKey) + +func addVimEvent(events ...*tcell.EventKey) *VimEvent { + vimE := VimEvent{NormalEvent: events[0], InsertEvent: events[1], VisualEvent: events[2]} + return &vimE +} + +// func createMask(runes rune...) int { +// mask := 0 +// for _, r := range runes { +// mask = mask&tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone) +// } +// return mask +// } + func addScope(identifier, name string, parent *Scope) *Scope { scope := &Scope{ Parent: parent, @@ -167,13 +384,14 @@ func addScope(identifier, name string, parent *Scope) *Scope { return scope } -func addShortcut(identifier, name string, scope *Scope, event *tcell.EventKey) *Shortcut { +func addShortcut(identifier, name string, scope *Scope, event *tcell.EventKey, vimEvent *VimEvent) *Shortcut { shortcut := &Shortcut{ Identifier: identifier, Name: name, Scope: scope, Event: event, defaultEvent: event, + VimModifier: vimEvent, } Shortcuts = append(Shortcuts, shortcut) @@ -198,15 +416,50 @@ type Shortcut struct { //This shortcuts default, in order to be able to reset it. defaultEvent *tcell.EventKey + + // Every shortcut receives a pointer to vimMode, therefore not + // wasting memory + VimStatus *vim.Vim + + // VimModifier is the shortcut that will be used inside vim mode. + VimModifier *VimEvent } // Equals compares the given EventKey with the Shortcuts Event. +// If any vim mode is enabled, it will replace the default event. func (shortcut *Shortcut) Equals(event *tcell.EventKey) bool { - return EventsEqual(shortcut.Event, event) + if shortcut.VimModifier == nil { + return EventsEqual(shortcut.Event, event) + } + if shortcut.VimStatus == nil { + return EventsEqual(shortcut.Event, event) + } + if shortcut.VimStatus.CurrentMode == vim.NormalMode { + selectedEvent := shortcut.VimModifier.NormalEvent + if selectedEvent == nil { + selectedEvent = shortcut.Event + } + return EventsEqual(selectedEvent, event) + } else if shortcut.VimStatus.CurrentMode == vim.InsertMode { + selectedEvent := shortcut.VimModifier.InsertEvent + if selectedEvent == nil { + selectedEvent = shortcut.Event + } + return EventsEqual(selectedEvent, event) + } else if shortcut.VimStatus.CurrentMode == vim.VisualMode { + selectedEvent := shortcut.VimModifier.VisualEvent + if selectedEvent == nil { + selectedEvent = shortcut.Event + } + return EventsEqual(selectedEvent, event) + } else { + return EventsEqual(shortcut.Event, event) + } } // EventsEqual compares the given events, respecting everything except for the // When field. +// Special event NullVimEvent will always return nil, because it is empty. func EventsEqual(eventOne, eventTwo *tcell.EventKey) bool { if (eventOne == nil && eventTwo != nil) || (eventOne != nil && eventTwo == nil) { return false @@ -308,7 +561,7 @@ func getShortcutsPath() (string, error) { // Load loads the shortcuts and copies the events to the correct shortcuts // that reside inside of the memory. -func Load() error { +func Load(vimMode *vim.Vim) error { shortcutsPath, pathError := getShortcutsPath() if pathError != nil { return pathError @@ -344,6 +597,7 @@ OUTER_LOOP: if otherShortcut.Identifier == shortcut.Identifier && otherShortcut.Scope.Identifier == shortcut.Scope.Identifier { otherShortcut.Event = shortcut.Event + otherShortcut.VimStatus = vimMode continue OUTER_LOOP } } @@ -352,6 +606,12 @@ OUTER_LOOP: return nil } +func InjectVim(v *vim.Vim) { + for i := range Shortcuts { + Shortcuts[i].VimStatus = v + } +} + // Persist saves the currently shortcuts that are currently being held in // memory. func Persist() error { @@ -375,6 +635,7 @@ func Persist() error { func DirectionalFocusHandling(event *tcell.EventKey, app *tview.Application) *tcell.EventKey { focused := app.GetFocus() + if FocusUp.Equals(event) { tviewutil.FocusNextIfPossible(tview.Up, app, focused) } else if FocusDown.Equals(event) { diff --git a/shortcuts/shortcuts_nix.go b/shortcuts/shortcuts_nix.go index 7acc4389..c0fa73a7 100644 --- a/shortcuts/shortcuts_nix.go +++ b/shortcuts/shortcuts_nix.go @@ -5,5 +5,6 @@ package shortcuts import tcell "github.com/gdamore/tcell/v2" func addDeleteLeftShortcut() *Shortcut { - return addShortcut("delete_left", "Delete left", multilineTextInput, tcell.NewEventKey(tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone)) + // FIXME don't know what to do with this vim binding + return addShortcut("delete_left", "Delete left", multilineTextInput, tcell.NewEventKey(tcell.KeyBackspace2, rune(tcell.KeyBackspace2), tcell.ModNone), addVimEvent(NullVimEvent, nil, NullVimEvent)) } diff --git a/shortcuts/shortcuts_win.go b/shortcuts/shortcuts_win.go index 3d3ad45f..f4b6e888 100644 --- a/shortcuts/shortcuts_win.go +++ b/shortcuts/shortcuts_win.go @@ -5,5 +5,5 @@ package shortcuts import tcell "github.com/gdamore/tcell/v2" func addDeleteLeftShortcut() *Shortcut { - return addShortcut("delete_left", "Delete left", multilineTextInput, tcell.NewEventKey(tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone)) + return addShortcut("delete_left", "Delete left", multilineTextInput, tcell.NewEventKey(tcell.KeyBackspace, rune(tcell.KeyBackspace), tcell.ModNone), nil) } diff --git a/tools/keyevents/main.go b/tools/keyevents/main.go index bfb658d1..f59a174d 100644 --- a/tools/keyevents/main.go +++ b/tools/keyevents/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" tcell "github.com/gdamore/tcell/v2" ) @@ -14,5 +15,5 @@ func main() { return event }) - tview.NewApplication().SetRoot(input, true).Run() + tview.NewApplication(config.Current.VimEnabled).SetRoot(input, true).Run() } diff --git a/tools/layout/main.go b/tools/layout/main.go index 7658b316..7397f283 100644 --- a/tools/layout/main.go +++ b/tools/layout/main.go @@ -1,12 +1,13 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" tcell "github.com/gdamore/tcell/v2" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) err := app.Run() if err != nil { panic(err) diff --git a/tview/application.go b/tview/application.go index c32a8b47..70fdbaaf 100644 --- a/tview/application.go +++ b/tview/application.go @@ -3,6 +3,7 @@ package tview import ( "sync" + "github.com/Bios-Marcel/cordless/util/vim" tcell "github.com/gdamore/tcell/v2" ) @@ -67,17 +68,17 @@ type Application struct { // stop the application. screenReplacement chan tcell.Screen - // Defines whether a bracketed paste is currently ongoing. - pasteActive bool + VimMode *vim.Vim } // NewApplication creates and returns a new application. -func NewApplication() *Application { +func NewApplication(vimEnabled bool) *Application { return &Application{ events: make(chan tcell.Event, queueSize), updates: make(chan func(), queueSize), screenReplacement: make(chan tcell.Screen, 1), MouseEnabled: true, + VimMode: vim.NewVim(vimEnabled), } } @@ -119,7 +120,6 @@ func (a *Application) SetScreen(screen tcell.Screen) *Application { if a.MouseEnabled { a.screen.EnableMouse() } - screen.EnablePaste() a.Unlock() return a } @@ -153,7 +153,6 @@ func (a *Application) Run() error { if a.MouseEnabled { a.screen.EnableMouse() } - a.screen.EnablePaste() } // We catch panics to clean up because they mess up the terminal. @@ -220,7 +219,6 @@ func (a *Application) Run() error { // Start event loop. var keyEventHandleHierarchy []Primitive - var pastedRunes []rune EventLoop: for { //resize slice to zero without having to allocate a new array. @@ -238,14 +236,6 @@ EventLoop: a.RLock() p := a.focus inputCapture := a.inputCapture - if a.pasteActive { - if event.Rune() != 0 { - pastedRunes = append(pastedRunes, event.Rune()) - a.RUnlock() - continue - } - } - a.RUnlock() // Intercept keys. @@ -291,25 +281,6 @@ EventLoop: } screen.Clear() a.draw() - case *tcell.EventPaste: - a.RLock() - p := a.focus - - if event.Start() { - - a.pasteActive = true - a.RUnlock() - } else if event.End() { - a.pasteActive = false - a.RUnlock() - if p != nil { - p.OnPaste(pastedRunes) - a.draw() - pastedRunes = pastedRunes[0:0] - } - } else { - a.RUnlock() - } case *tcell.EventMouse: if event.Buttons() == tcell.ButtonNone { continue @@ -400,11 +371,6 @@ func getSelfIfCoordinatesMatch(primitive Primitive, x, y int) *Primitive { return nil } -// IsPasting indicates whether a bracketed paste is ongoing. -func (a *Application) IsPasting() bool { - return a.pasteActive -} - // Stop stops the application, causing Run() to return. func (a *Application) Stop() { a.Lock() @@ -449,7 +415,6 @@ func (a *Application) Suspend(f func()) bool { if a.MouseEnabled { screen.EnableMouse() } - screen.EnablePaste() a.screenReplacement <- screen // One key event will get lost, see https://github.com/gdamore/tcell/issues/194 @@ -574,8 +539,6 @@ func (a *Application) SetRoot(root Primitive, fullscreen bool) *Application { } a.Unlock() - a.SetFocus(root) - return a } diff --git a/tview/box.go b/tview/box.go index dfa96af7..eb21fec1 100644 --- a/tview/box.go +++ b/tview/box.go @@ -84,8 +84,6 @@ type Box struct { nextFocusableComponents map[FocusDirection][]Primitive parent Primitive - - onPaste func([]rune) } // NewBox returns a Box without a border. @@ -615,15 +613,3 @@ func (b *Box) drawOverflow(screen tcell.Screen, showTop, showBottom bool) { } } } - -// SetOnPaste defines the function that's called in OnPaste. -func (b *Box) SetOnPaste(onPaste func([]rune)) { - b.onPaste = onPaste -} - -// OnPaste is called when a bracketed paste is finished. -func (b *Box) OnPaste(runes []rune) { - if b.onPaste != nil { - b.onPaste(runes) - } -} diff --git a/tview/demos/box/main.go b/tview/demos/box/main.go index d19f8bfe..05929405 100644 --- a/tview/demos/box/main.go +++ b/tview/demos/box/main.go @@ -2,6 +2,7 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) @@ -9,7 +10,7 @@ func main() { box := tview.NewBox(). SetBorder(true). SetTitle("A [red]c[yellow]o[green]l[darkcyan]o[blue]r[darkmagenta]f[red]u[yellow]l[white] [black:red]c[:yellow]o[:green]l[:darkcyan]o[:blue]r[:darkmagenta]f[:red]u[:yellow]l[white:] [::bu]title") - if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil { + if err := tview.NewApplication(config.Current.VimEnabled).SetRoot(box, true).Run(); err != nil { panic(err) } } diff --git a/tview/demos/button/main.go b/tview/demos/button/main.go index e35f1ef8..b0a53eb8 100644 --- a/tview/demos/button/main.go +++ b/tview/demos/button/main.go @@ -1,10 +1,13 @@ // Demo code for the Button primitive. package main -import "github.com/Bios-Marcel/cordless/tview" +import ( + "github.com/Bios-Marcel/cordless/config" + "github.com/Bios-Marcel/cordless/tview" +) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) button := tview.NewButton("Hit Enter to close").SetSelectedFunc(func() { app.Stop() }) diff --git a/tview/demos/checkbox/main.go b/tview/demos/checkbox/main.go index 764fe75b..13f39573 100644 --- a/tview/demos/checkbox/main.go +++ b/tview/demos/checkbox/main.go @@ -1,10 +1,13 @@ // Demo code for the Checkbox primitive. package main -import "github.com/Bios-Marcel/cordless/tview" +import ( + "github.com/Bios-Marcel/cordless/config" + "github.com/Bios-Marcel/cordless/tview" +) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) checkbox := tview.NewCheckbox().SetLabel("Hit Enter to check box: ") if err := app.SetRoot(checkbox, true).Run(); err != nil { panic(err) diff --git a/tview/demos/dropdown/main.go b/tview/demos/dropdown/main.go index e20be4ae..60fcd9f8 100644 --- a/tview/demos/dropdown/main.go +++ b/tview/demos/dropdown/main.go @@ -1,10 +1,13 @@ // Demo code for the DropDown primitive. package main -import "github.com/Bios-Marcel/cordless/tview" +import ( + "github.com/Bios-Marcel/cordless/config" + "github.com/Bios-Marcel/cordless/tview" +) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) dropdown := tview.NewDropDown(). SetLabel("Select an option (hit Enter): "). SetOptions([]string{"First", "Second", "Third", "Fourth", "Fifth"}, nil) diff --git a/tview/demos/flex/main.go b/tview/demos/flex/main.go index 0df05b17..b607863a 100644 --- a/tview/demos/flex/main.go +++ b/tview/demos/flex/main.go @@ -2,11 +2,12 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) flex := tview.NewFlex(). AddItem(tview.NewBox().SetBorder(true).SetTitle("Left (1/2 x width of Top)"), 0, 1, false). AddItem(tview.NewFlex().SetDirection(tview.FlexRow). diff --git a/tview/demos/form/main.go b/tview/demos/form/main.go index b589e603..d74caf12 100644 --- a/tview/demos/form/main.go +++ b/tview/demos/form/main.go @@ -2,11 +2,12 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) form := tview.NewForm(). AddDropDown("Title", []string{"Mr.", "Ms.", "Mrs.", "Dr.", "Prof."}, 0, nil). AddInputField("First name", "", 20, nil, nil). diff --git a/tview/demos/frame/main.go b/tview/demos/frame/main.go index f6037436..9d2dbf60 100644 --- a/tview/demos/frame/main.go +++ b/tview/demos/frame/main.go @@ -2,12 +2,13 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) frame := tview.NewFrame(tview.NewBox().SetBackgroundColor(tcell.ColorBlue)). SetBorders(2, 2, 2, 2, 4, 4). AddText("Header left", true, tview.AlignLeft, tcell.ColorWhite). diff --git a/tview/demos/grid/main.go b/tview/demos/grid/main.go index c6438acc..b1b06106 100644 --- a/tview/demos/grid/main.go +++ b/tview/demos/grid/main.go @@ -2,6 +2,7 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) @@ -32,7 +33,7 @@ func main() { AddItem(main, 1, 1, 1, 1, 0, 100, false). AddItem(sideBar, 1, 2, 1, 1, 0, 100, false) - if err := tview.NewApplication().SetRoot(grid, true).Run(); err != nil { + if err := tview.NewApplication(config.Current.VimEnabled).SetRoot(grid, true).Run(); err != nil { panic(err) } } diff --git a/tview/demos/inputfield-paste/main.go b/tview/demos/inputfield-paste/main.go index e9116769..7c61fab3 100644 --- a/tview/demos/inputfield-paste/main.go +++ b/tview/demos/inputfield-paste/main.go @@ -1,6 +1,7 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" tcell "github.com/gdamore/tcell/v2" ) @@ -15,6 +16,6 @@ func main() { return event }) - app := tview.NewApplication().SetRoot(field,true) + app := tview.NewApplication(config.Current.VimEnabled).SetRoot(field, true) app.Run() -} \ No newline at end of file +} diff --git a/tview/demos/inputfield/main.go b/tview/demos/inputfield/main.go index 3eecd04e..17e10aa8 100644 --- a/tview/demos/inputfield/main.go +++ b/tview/demos/inputfield/main.go @@ -2,12 +2,13 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) inputField := tview.NewInputField(). SetLabel("Enter a number: "). SetPlaceholder("E.g. 1234"). diff --git a/tview/demos/list/main.go b/tview/demos/list/main.go index 5e4ba348..851e6a61 100644 --- a/tview/demos/list/main.go +++ b/tview/demos/list/main.go @@ -2,11 +2,12 @@ package main import ( + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) list := tview.NewList(). AddItem("List item 1", "Some explanatory text", 'a', nil). AddItem("List item 2", "Some explanatory text", 'b', nil). diff --git a/tview/demos/modal/main.go b/tview/demos/modal/main.go index 0eb61a30..2c752bae 100644 --- a/tview/demos/modal/main.go +++ b/tview/demos/modal/main.go @@ -6,11 +6,12 @@ import ( tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) // Returns a new primitive which puts the provided primitive in the center and // sets its size to the given width and height. diff --git a/tview/demos/overflowindicators/main.go b/tview/demos/overflowindicators/main.go index e38d503d..0695ebd7 100644 --- a/tview/demos/overflowindicators/main.go +++ b/tview/demos/overflowindicators/main.go @@ -1,12 +1,14 @@ package main import ( - "github.com/Bios-Marcel/cordless/tview" "strings" + + "github.com/Bios-Marcel/cordless/config" + "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) textView := tview.NewTextView(). SetWordWrap(true). SetChangedFunc(func() { diff --git a/tview/demos/pages/main.go b/tview/demos/pages/main.go index 87373eff..ee6469b1 100644 --- a/tview/demos/pages/main.go +++ b/tview/demos/pages/main.go @@ -4,13 +4,14 @@ package main import ( "fmt" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) const pageCount = 5 func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) pages := tview.NewPages() for page := 0; page < pageCount; page++ { func(page int) { diff --git a/tview/demos/presentation/colors.go b/tview/demos/presentation/colors.go index 2aa70058..901225dd 100644 --- a/tview/demos/presentation/colors.go +++ b/tview/demos/presentation/colors.go @@ -3,8 +3,8 @@ package main import ( "strings" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const colorsText = `You can use color tags almost everywhere to partially change the color of a string. Simply put a color name or hex string in square brackets to change the following characters' color. H[green]er[white]e i[yellow]s a[darkcyan]n ex[red]amp[white]le. The [black:red]tags [black:green]look [black:yellow]like [::u]this: [blue:yellow:u[] [#00ff00[]` diff --git a/tview/demos/presentation/cover.go b/tview/demos/presentation/cover.go index e8a59e74..1ab5f3ac 100644 --- a/tview/demos/presentation/cover.go +++ b/tview/demos/presentation/cover.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const logo = ` diff --git a/tview/demos/presentation/end.go b/tview/demos/presentation/end.go index 7fb9b605..85551659 100644 --- a/tview/demos/presentation/end.go +++ b/tview/demos/presentation/end.go @@ -3,8 +3,8 @@ package main import ( "fmt" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) // End shows the final slide. diff --git a/tview/demos/presentation/flex.go b/tview/demos/presentation/flex.go index 2118d616..8879c107 100644 --- a/tview/demos/presentation/flex.go +++ b/tview/demos/presentation/flex.go @@ -1,8 +1,8 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) // Flex demonstrates flexbox layout. diff --git a/tview/demos/presentation/grid.go b/tview/demos/presentation/grid.go index 9c155326..4f64c58d 100644 --- a/tview/demos/presentation/grid.go +++ b/tview/demos/presentation/grid.go @@ -1,8 +1,8 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) // Grid demonstrates the grid layout. diff --git a/tview/demos/presentation/helloworld.go b/tview/demos/presentation/helloworld.go index 764cada4..a661ed58 100644 --- a/tview/demos/presentation/helloworld.go +++ b/tview/demos/presentation/helloworld.go @@ -1,8 +1,8 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const helloWorld = `[green]package[white] main diff --git a/tview/demos/presentation/inputfield.go b/tview/demos/presentation/inputfield.go index ee25caa0..3adf2e16 100644 --- a/tview/demos/presentation/inputfield.go +++ b/tview/demos/presentation/inputfield.go @@ -1,8 +1,8 @@ package main import ( - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const inputField = `[green]package[white] main diff --git a/tview/demos/presentation/main.go b/tview/demos/presentation/main.go index a23c5715..46143690 100644 --- a/tview/demos/presentation/main.go +++ b/tview/demos/presentation/main.go @@ -16,8 +16,9 @@ import ( "fmt" "strconv" - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) // Slide is a function which returns the slide's main primitive and its title. @@ -26,7 +27,7 @@ import ( type Slide func(nextSlide func()) (title string, content tview.Primitive) // The application. -var app = tview.NewApplication() +var app = tview.NewApplication(config.Current.VimEnabled) // Starting point for the presentation. func main() { diff --git a/tview/demos/presentation/table.go b/tview/demos/presentation/table.go index 5c0ab572..4d97248a 100644 --- a/tview/demos/presentation/table.go +++ b/tview/demos/presentation/table.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const tableData = `OrderDate|Region|Rep|Item|Units|UnitCost|Total diff --git a/tview/demos/presentation/textview.go b/tview/demos/presentation/textview.go index 507eb2f2..0e6c6a78 100644 --- a/tview/demos/presentation/textview.go +++ b/tview/demos/presentation/textview.go @@ -5,8 +5,8 @@ import ( "strconv" "time" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const textView1 = `[green]func[white] [yellow]main[white]() { diff --git a/tview/demos/presentation/treeview.go b/tview/demos/presentation/treeview.go index 145b5904..7de10f45 100644 --- a/tview/demos/presentation/treeview.go +++ b/tview/demos/presentation/treeview.go @@ -3,8 +3,8 @@ package main import ( "strings" - tcell "github.com/gdamore/tcell/v2" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const treeAllCode = `[green]package[white] main diff --git a/tview/demos/primitive/main.go b/tview/demos/primitive/main.go index 6c6da877..a306dd70 100644 --- a/tview/demos/primitive/main.go +++ b/tview/demos/primitive/main.go @@ -4,6 +4,7 @@ package main import ( "fmt" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" tcell "github.com/gdamore/tcell/v2" ) @@ -74,7 +75,7 @@ func main() { radioButtons.SetBorder(true). SetTitle("Radio Button Demo"). SetRect(0, 0, 30, 5) - if err := tview.NewApplication().SetRoot(radioButtons, false).Run(); err != nil { + if err := tview.NewApplication(config.Current.VimEnabled).SetRoot(radioButtons, false).Run(); err != nil { panic(err) } } diff --git a/tview/demos/table/main.go b/tview/demos/table/main.go index 3910eeb4..e5b6cf57 100644 --- a/tview/demos/table/main.go +++ b/tview/demos/table/main.go @@ -4,12 +4,13 @@ package main import ( "strings" - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) table := tview.NewTable(). SetBorders(true) lorem := strings.Split("Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", " ") diff --git a/tview/demos/textview/main.go b/tview/demos/textview/main.go index 226323b2..55b557f4 100644 --- a/tview/demos/textview/main.go +++ b/tview/demos/textview/main.go @@ -7,8 +7,9 @@ import ( "strings" "time" - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) const corporate = `Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment. @@ -20,7 +21,7 @@ Capitalize on low hanging fruit to identify a ballpark value added activity to b [yellow]Press Enter, then Tab/Backtab for word selections` func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) textView := tview.NewTextView(). SetDynamicColors(true). SetRegions(true). diff --git a/tview/demos/treeview/main.go b/tview/demos/treeview/main.go index 14995aa8..dc165b94 100644 --- a/tview/demos/treeview/main.go +++ b/tview/demos/treeview/main.go @@ -5,8 +5,9 @@ import ( "io/ioutil" "path/filepath" - tcell "github.com/gdamore/tcell/v2" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" + tcell "github.com/gdamore/tcell/v2" ) // Show a navigable tree view of the current directory. @@ -56,7 +57,7 @@ func main() { } }) - if err := tview.NewApplication().SetRoot(tree, true).Run(); err != nil { + if err := tview.NewApplication(config.Current.VimEnabled).SetRoot(tree, true).Run(); err != nil { panic(err) } } diff --git a/tview/demos/unicode/main.go b/tview/demos/unicode/main.go index 9d912e5e..d5ba5a35 100644 --- a/tview/demos/unicode/main.go +++ b/tview/demos/unicode/main.go @@ -4,11 +4,12 @@ package main import ( "fmt" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/tview" ) func main() { - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) pages := tview.NewPages() form := tview.NewForm() diff --git a/tview/primitive.go b/tview/primitive.go index 5ab3fb9b..1a9ab646 100644 --- a/tview/primitive.go +++ b/tview/primitive.go @@ -48,9 +48,6 @@ type Primitive interface { // Box.WrapInputHandler() so you inherit that functionality. InputHandler() InputHandlerFunc - // OnPaste is called when a bracketed paste is finished. - OnPaste([]rune) - // Focus is called by the application when the primitive receives focus. // Implementers may call delegate() to pass the focus on to another primitive. Focus(delegate func(p Primitive)) diff --git a/ui/commandview.go b/ui/commandview.go index 0f3f8720..aba0a378 100644 --- a/ui/commandview.go +++ b/ui/commandview.go @@ -27,7 +27,7 @@ type CommandView struct { // NewCommandView creates a new struct containing the components necessary // for a command view. It also contains the state for those components. -func NewCommandView(app *tview.Application, onExecuteCommand func(command string)) *CommandView { +func NewCommandView(vimMode *int, onExecuteCommand func(command string)) *CommandView { commandOutput := tview.NewTextView() commandOutput.SetDynamicColors(true). SetWordWrap(true). @@ -36,7 +36,7 @@ func NewCommandView(app *tview.Application, onExecuteCommand func(command string SetBorder(true). SetIndicateOverflow(true) - commandInput := NewEditor(app) + commandInput := NewEditor(vimMode) commandInput.internalTextView. SetWrap(false). SetWordWrap(false) diff --git a/ui/components/bottombar.go b/ui/components/bottombar.go index e65c07af..17e50801 100644 --- a/ui/components/bottombar.go +++ b/ui/components/bottombar.go @@ -15,11 +15,13 @@ import ( type BottomBar struct { *sync.Mutex *tview.Box - items []*bottomBarItem + items []*BottomBarItem } -type bottomBarItem struct { - content string +// BottomBarItem represents an entry in a BottomBar. It simply holds a static +// text that can technically be changed after adding the item. +type BottomBarItem struct { + Content string } // Draw draws this primitive onto the screen. Implementers can call the @@ -45,7 +47,7 @@ func (b *BottomBar) Draw(screen tcell.Screen) bool { xPos, yPos, _, _ := b.GetInnerRect() for _, item := range b.items { - gr := uniseg.NewGraphemes(item.content) + gr := uniseg.NewGraphemes(item.Content) for gr.Next() { r := gr.Runes() width := runewidth.StringWidth(gr.Str()) @@ -69,7 +71,7 @@ func (b *BottomBar) Draw(screen tcell.Screen) bool { func (b *BottomBar) AddItem(text string) { b.Lock() defer b.Unlock() - b.items = append(b.items, &bottomBarItem{text}) + b.items = append(b.items, &BottomBarItem{text}) } // NewBottomBar creates a new bar to be put at the bottom aplication. @@ -83,3 +85,13 @@ func NewBottomBar() *BottomBar { return bottomBar } + +// AddDynamicItem adds and item without content that can be filled from outside +// by assigning a new value to the Content field of the returned item. +func (b *BottomBar) AddDynamicItem() *BottomBarItem { + newItem := &BottomBarItem{} + b.Lock() + defer b.Unlock() + b.items = append(b.items, newItem) + return newItem +} diff --git a/ui/editor.go b/ui/editor.go index ec2ab09c..f9750b1a 100644 --- a/ui/editor.go +++ b/ui/editor.go @@ -4,6 +4,7 @@ import ( "unicode" "github.com/Bios-Marcel/cordless/tview" + "github.com/Bios-Marcel/cordless/util/vim" "github.com/atotto/clipboard" tcell "github.com/gdamore/tcell/v2" @@ -28,9 +29,7 @@ type Editor struct { requestedHeight int autocompleteFrom *femto.Loc - // App is the tview Application this editor is used in. The reference is - // required to query the current bracketed paste state. - App *tview.Application + vimMode *int } func (editor *Editor) applyBuffer() { @@ -378,17 +377,6 @@ func (editor *Editor) Paste(event *tcell.EventKey) { } } -func (editor *Editor) insertCharacterWithoutApply(character rune) { - selectionEnd := editor.buffer.Cursor.CurSelection[1] - selectionStart := editor.buffer.Cursor.CurSelection[0] - if editor.buffer.Cursor.HasSelection() { - editor.buffer.Replace(selectionStart, selectionEnd, string(character)) - } else { - editor.buffer.Insert(editor.buffer.Cursor.Loc, string(character)) - } - editor.buffer.Cursor.ResetSelection() -} - func (editor *Editor) InsertCharacter(character rune) { selectionEnd := editor.buffer.Cursor.CurSelection[1] selectionStart := editor.buffer.Cursor.CurSelection[0] @@ -402,13 +390,13 @@ func (editor *Editor) InsertCharacter(character rune) { } // NewEditor instantiates a ready to use text editor. -func NewEditor(app *tview.Application) *Editor { +func NewEditor(vimMode *int) *Editor { editor := Editor{ internalTextView: tview.NewTextView(), requestedHeight: 3, buffer: femto.NewBufferFromString("", ""), tempBuffer: femto.NewBufferFromString("", ""), - App: app, + vimMode: vimMode, } editor.internalTextView.SetWrap(true) @@ -422,28 +410,6 @@ func NewEditor(app *tview.Application) *Editor { editor.buffer.Cursor.SetSelectionStart(editor.buffer.Start()) editor.buffer.Cursor.SetSelectionEnd(editor.buffer.End()) - if editor.App != nil { - editor.internalTextView.SetOnPaste(func(pastedRunes []rune) { - var wasLastCharacterSlashR bool - for _, r := range pastedRunes { - //Workaround! Sometimes no \n is received, but only \r. Why? Unsure. - if r == '\r' { - wasLastCharacterSlashR = true - editor.insertCharacterWithoutApply('\n') - } else { - if wasLastCharacterSlashR { - wasLastCharacterSlashR = false - } - if r != '\n' { - editor.insertCharacterWithoutApply(r) - } - } - } - editor.applyBuffer() - editor.TriggerHeightRequestIfNecessary() - editor.internalTextView.ScrollToHighlight() - }) - } editor.internalTextView.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { inputCapture := editor.inputCapture if inputCapture != nil { @@ -455,6 +421,7 @@ func NewEditor(app *tview.Application) *Editor { if shortcuts.MoveCursorLeft.Equals(event) { editor.MoveCursorLeft() + } else if shortcuts.ExpandSelectionToLeft.Equals(event) { editor.SelectionToLeft() } else if shortcuts.MoveCursorRight.Equals(event) { @@ -504,11 +471,11 @@ func NewEditor(app *tview.Application) *Editor { return nil } else if shortcuts.InputNewLine.Equals(event) { editor.InsertCharacter('\n') - } else { - mappedRune := mapInputToRune(event) - if mappedRune != 0 { - editor.InsertCharacter(mappedRune) + } else if event.Rune() != 0 { + if *editor.vimMode != vim.Disabled && *editor.vimMode != vim.InsertMode { + return nil } + editor.InsertCharacter(event.Rune()) } editor.TriggerHeightRequestIfNecessary() @@ -518,34 +485,6 @@ func NewEditor(app *tview.Application) *Editor { return &editor } -// mapInputToRune makes sure no invalid characters get inserted into the -// editor. All exceptions that aren't specifically declared as -// Key type 'tcell.KeyRune' have to be defined here. A prominent example -// is the newline character. This was initially added to avoid inserting -// the characters produces by Ctrl+Backspace, Alt+Backspace, Ctrl+W and so -// on and so forth. These are all control characters which we don't always -// react to, since the shortcuts are configurably from within the app. -// A return value of 0 is to be treated as 'no rune'. -func mapInputToRune(event *tcell.EventKey) rune { - //If something is specifically defined as a rune, we won't question whether - //it's valid input, as it's generally not deemed a control character. - if event.Key() == tcell.KeyRune { - return event.Rune() - } - - //While '\n' is treated as newline, we'll ignore '\r', as it's useless. - if event.Rune() == '\n' { - return '\n' - } - - //Handles Tab + Ctrl+H - if event.Rune() == '\t' { - return '\t' - } - - return 0 -} - func (editor *Editor) GetTextLeftOfSelection() string { var to femto.Loc if editor.buffer.Cursor.HasSelection() { @@ -579,7 +518,9 @@ func (editor *Editor) TriggerHeightRequestIfNecessary() { return } - newRequestedHeight := editor.countRows(editor.GetText()) + rowAmount := editor.countRows(editor.GetText()) + + newRequestedHeight := rowAmount if editor.internalTextView.IsBorderTop() { newRequestedHeight++ } diff --git a/ui/shortcutdialog/dialog.go b/ui/shortcutdialog/dialog.go index 4f9162fb..5c7dee76 100644 --- a/ui/shortcutdialog/dialog.go +++ b/ui/shortcutdialog/dialog.go @@ -3,6 +3,7 @@ package shortcutdialog import ( "log" + "github.com/Bios-Marcel/cordless/config" "github.com/Bios-Marcel/cordless/shortcuts" "github.com/Bios-Marcel/cordless/tview" "github.com/Bios-Marcel/cordless/ui/components" @@ -15,7 +16,7 @@ func ShowShortcutsDialog(app *tview.Application, onClose func()) { var exitButton *tview.Button var resetButton *tview.Button - table = NewShortcutTable() + table = NewShortcutTable(&app.VimMode.CurrentMode) table.SetShortcuts(shortcuts.Shortcuts) exitButton = tview.NewButton("Go back") @@ -94,11 +95,11 @@ func ShowShortcutsDialog(app *tview.Application, onClose func()) { } func RunShortcutsDialogStandalone() { - loadError := shortcuts.Load() + loadError := shortcuts.Load(nil) if loadError != nil { log.Fatalf("Error loading shortcuts: %s\n", loadError) } - app := tview.NewApplication() + app := tview.NewApplication(config.Current.VimEnabled) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if shortcuts.ExitApplication.Equals(event) { app.Stop() diff --git a/ui/shortcutdialog/table.go b/ui/shortcutdialog/table.go index fffd592c..f1034d10 100644 --- a/ui/shortcutdialog/table.go +++ b/ui/shortcutdialog/table.go @@ -6,6 +6,7 @@ import ( "github.com/Bios-Marcel/cordless/shortcuts" "github.com/Bios-Marcel/cordless/tview" + "github.com/Bios-Marcel/cordless/util/vim" tcell "github.com/gdamore/tcell/v2" ) @@ -13,6 +14,9 @@ const ( actionCellIndex int = iota scopeCellIndex shortcutCellIndex + vimNormalCellIndex + vimInsertCellIndex + vimVisualCellIndex ) // ShortcutTable is a component that displays shortcuts and allows changing @@ -20,17 +24,21 @@ const ( type ShortcutTable struct { table *tview.Table shortcuts []*shortcuts.Shortcut + vimBindings []*shortcuts.VimEvent selection int focusNext func() focusPrevious func() + + vimMode *int } // NewShortcutTable creates a new shortcut table that doesn't contain any data. -func NewShortcutTable() *ShortcutTable { +func NewShortcutTable(vimEnabled *int) *ShortcutTable { table := tview.NewTable() shortcutsTable := &ShortcutTable{ table: table, selection: -1, + vimMode: vimEnabled, } table.SetSelectable(true, false) @@ -42,6 +50,11 @@ func NewShortcutTable() *ShortcutTable { table.SetCell(0, actionCellIndex, createHeaderCell("Action")) table.SetCell(0, scopeCellIndex, createHeaderCell("Scope")) table.SetCell(0, shortcutCellIndex, createHeaderCell("Shortcut")) + if *vimEnabled != vim.Disabled { + table.SetCell(0, vimNormalCellIndex, createHeaderCell("Vim: N")) + table.SetCell(0, vimInsertCellIndex, createHeaderCell("Vim: I")) + table.SetCell(0, vimVisualCellIndex, createHeaderCell("Vim: V")) + } table.SetInputCapture(shortcutsTable.handleInput) @@ -90,6 +103,56 @@ func (shortcutTable *ShortcutTable) SetShortcuts(shortcuts []*shortcuts.Shortcut SetMaxWidth(1) shortcutTable.table.SetCell(row, shortcutCellIndex, eventCell) + if *shortcutTable.vimMode != vim.Disabled { + var normalKey string + var insertKey string + var visualKey string + + // HACK + // Empty tcell EventKey corresponds to Ctrl-Space + if shortcut.VimModifier.NormalEvent == nil { + normalKey = EventToString(shortcut.Event) + } else if shortcut.VimModifier.NormalEvent.Rune() == rune(tcell.KeyCtrlSpace) { + normalKey = "" + } else { + normalKey = EventToString(shortcut.VimModifier.NormalEvent) + } + + if shortcut.VimModifier.InsertEvent == nil { + insertKey = EventToString(shortcut.Event) + } else if shortcut.VimModifier.InsertEvent.Rune() == rune(tcell.KeyCtrlSpace) { + insertKey = "" + } else { + insertKey = EventToString(shortcut.VimModifier.InsertEvent) + } + + if shortcut.VimModifier.VisualEvent == nil { + visualKey = EventToString(shortcut.Event) + } else if shortcut.VimModifier.VisualEvent.Rune() == rune(tcell.KeyCtrlSpace) { + visualKey = "" + } else { + visualKey = EventToString(shortcut.VimModifier.VisualEvent) + } + + vimNormalCell := tview.NewTableCell(fmt.Sprintf("%s", + normalKey)). + SetExpansion(1). + SetMaxWidth(1) + shortcutTable.table.SetCell(row, vimNormalCellIndex, vimNormalCell) + + vimInsertCell := tview.NewTableCell(fmt.Sprintf("%s", + insertKey)). + SetExpansion(1). + SetMaxWidth(1) + shortcutTable.table.SetCell(row, vimInsertCellIndex, vimInsertCell) + + vimVisualCell := tview.NewTableCell(fmt.Sprintf("%s", + visualKey)). + SetExpansion(1). + SetMaxWidth(1) + shortcutTable.table.SetCell(row, vimVisualCellIndex, vimVisualCell) + } + row++ } } diff --git a/ui/window.go b/ui/window.go index 0ce977bb..9d3d319e 100644 --- a/ui/window.go +++ b/ui/window.go @@ -18,6 +18,7 @@ import ( "github.com/Bios-Marcel/cordless/util/files" "github.com/Bios-Marcel/cordless/util/fuzzy" "github.com/Bios-Marcel/cordless/util/text" + "github.com/Bios-Marcel/cordless/util/vim" "github.com/Bios-Marcel/cordless/version" "github.com/Bios-Marcel/discordemojimap" @@ -91,6 +92,8 @@ type Window struct { bareChat bool activeView ActiveView + + vimStatus *components.BottomBarItem } type ActiveView bool @@ -120,7 +123,7 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d }() } - window.commandView = NewCommandView(window.app, window.ExecuteCommand) + window.commandView = NewCommandView(&window.app.VimMode.CurrentMode, window.ExecuteCommand) logging.SetAdditionalOutput(window.commandView) for _, engine := range window.extensionEngines { @@ -397,7 +400,7 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d }) window.messageContainer = window.chatView.GetPrimitive() - window.messageInput = NewEditor(window.app) + window.messageInput = NewEditor(&window.app.VimMode.CurrentMode) window.messageInput.internalTextView.SetIndicateOverflow(true) window.messageInput.SetOnHeightChangeRequest(func(height int) { _, _, _, chatViewHeight := window.chatView.internalTextView.GetRect() @@ -745,7 +748,12 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d window.userList = NewUserTree(window.session.State) - if config.Current.OnTypeInListBehaviour == config.SearchOnTypeInList { + if window.app.VimMode.CurrentMode != vim.Disabled { + guildList.SetSearchOnTypeEnabled(false) + channelTree.SetSearchOnTypeEnabled(false) + window.userList.internalTreeView.SetSearchOnTypeEnabled(false) + window.privateList.internalTreeView.SetSearchOnTypeEnabled(false) + } else if config.Current.OnTypeInListBehaviour == config.SearchOnTypeInList { guildList.SetSearchOnTypeEnabled(true) channelTree.SetSearchOnTypeEnabled(true) window.userList.internalTreeView.SetSearchOnTypeEnabled(true) @@ -761,6 +769,7 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d } newGuildHandler := func(event *tcell.EventKey) *tcell.EventKey { + if shortcuts.GuildListMarkRead.Equals(event) { selectedGuildNode := guildList.GetCurrentNode() if selectedGuildNode != nil && !readstate.HasGuildBeenRead(selectedGuildNode.GetReference().(string)) { @@ -790,6 +799,7 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d } newChannelListHandler := func(event *tcell.EventKey) *tcell.EventKey { + if shortcuts.ChannelTreeMarkRead.Equals(event) { selectedChannelNode := channelTree.GetCurrentNode() if selectedChannelNode != nil { @@ -865,6 +875,8 @@ func NewWindow(app *tview.Application, session *discordgo.Session, readyEvent *d bottomBar := components.NewBottomBar() bottomBar.AddItem(fmt.Sprintf("Logged in as: '%s'", tviewutil.Escape(session.State.User.Username))) bottomBar.AddItem(fmt.Sprintf("View / Change shortcuts: %s", shortcutdialog.EventToString(shortcutsDialogShortcut))) + window.vimStatus = bottomBar.AddDynamicItem() + window.vimStatus.Content = fmt.Sprintf("Vim: %s", window.app.VimMode.CurrentModeString()) window.rootContainer.AddItem(bottomBar, 1, 0, false) } @@ -956,8 +968,10 @@ important changes of the last two versions officially released. [::b]THIS VERSION - Features - - Changes - - Bugfixes +[::b]2020-11-16 + - Features + - Vim mode + - Vim-like movement and bindings [::b]2020-10-24 - Features - DM people via "p" in the chatview or use the dm-open command @@ -2117,6 +2131,35 @@ func (window *Window) handleGlobalShortcuts(event *tcell.EventKey) *tcell.EventK return nil } + if window.app.VimMode.CurrentMode != vim.Disabled { + if shortcuts.VimInsertMode.Equals(event) { + window.app.VimMode.SetInsert() + window.vimStatus.Content = fmt.Sprintf("Vim: %s", window.app.VimMode.CurrentModeString()) + return nil + } else if shortcuts.VimVisualMode.Equals(event) { + window.app.VimMode.SetVisual() + window.vimStatus.Content = fmt.Sprintf("Vim: %s", window.app.VimMode.CurrentModeString()) + return nil + } else if shortcuts.VimNormalMode.Equals(event) && window.app.GetRoot() == window.rootContainer { + window.app.VimMode.SetNormal() + window.vimStatus.Content = fmt.Sprintf("Vim: %s", window.app.VimMode.CurrentModeString()) + return nil + } + + if shortcuts.VimSimKeyDown.Equals(event) && window.app.VimMode.CurrentMode == vim.VisualMode { + return tcell.NewEventKey(tcell.KeyDown, rune(tcell.KeyDown), tcell.ModNone) + } else if shortcuts.VimSimKeyUp.Equals(event) && window.app.VimMode.CurrentMode == vim.VisualMode { + return tcell.NewEventKey(tcell.KeyUp, rune(tcell.KeyUp), tcell.ModNone) + } else if shortcuts.VimSimKeyLeft.Equals(event) && window.app.VimMode.CurrentMode == vim.VisualMode { + return tcell.NewEventKey(tcell.KeyLeft, rune(tcell.KeyLeft), tcell.ModNone) + } else if shortcuts.VimSimKeyRight.Equals(event) && window.app.VimMode.CurrentMode == vim.VisualMode { + return tcell.NewEventKey(tcell.KeyRight, rune(tcell.KeyRight), tcell.ModNone) + } + } + + window.app.QueueUpdateDraw(func() { + }) + // Maybe compare directly to table? if config.Current.DesktopNotificationsUserInactivityThreshold > 0 { window.userActive = true @@ -2147,6 +2190,7 @@ func (window *Window) handleChatWindowShortcuts(event *tcell.EventKey) *tcell.Ev } else if shortcuts.EventsEqual(event, shortcutsDialogShortcut) { shortcutdialog.ShowShortcutsDialog(window.app, func() { window.app.SetRoot(window.rootContainer, true) + window.app.SetFocus(window.chatView.GetPrimitive()) }) } else if shortcuts.ToggleCommandView.Equals(event) { window.SetCommandModeEnabled(!window.commandMode) diff --git a/util/vim/vim.go b/util/vim/vim.go new file mode 100644 index 00000000..e9c2e359 --- /dev/null +++ b/util/vim/vim.go @@ -0,0 +1,85 @@ +package vim + +type VimMode int + +const ( + // Disabled : Default mode. Vim disabled. + Disabled int = iota + // NormalMode : every key is interpreted as focus movement, + // let it be from containers or text lines. + NormalMode + // InsertMode : keys inside text box are all sent to the input handler. + // It also allows between-message movement inside chatview. + InsertMode + // Global movement with hjkl. Text selection in text box. + VisualMode +) + +// Vim stores information about vim-mode, such as +// current mode. +type Vim struct { + // CurrentMode holds the integer value of the current vim mode. + CurrentMode int +} + +func NewVim(enabled bool) *Vim { + if enabled { + return &Vim{CurrentMode: NormalMode} + } else { + return &Vim{CurrentMode: Disabled} + } +} + +// SetMode sets new vim mode. If provided mode falls out +// the defined range 0-2 it will fall back to normal mode. +func (v *Vim) SetMode(mode int) { + if mode < 0 || mode > 2 { + // fallback + v.CurrentMode = NormalMode + return + } + v.CurrentMode = mode +} + +// Quick mode switch + +// SetNormal quickly switches to normal mode. +func (v *Vim) SetNormal() { + v.CurrentMode = NormalMode +} + +// SetInsert quickly switches to insert mode. +func (v *Vim) SetInsert() { + v.CurrentMode = InsertMode +} + +// SetVisual quickly switches to visual mode. +func (v *Vim) SetVisual() { + v.CurrentMode = VisualMode +} + +// CurrentModeString returns a stringified version +// of the current mode. (Useful for visual feedback to user) +func (v *Vim) CurrentModeString() string { + switch v.CurrentMode { + case NormalMode: + return "Normal" + case InsertMode: + return "Insert" + case VisualMode: + return "Visual" + case Disabled: + return "Disabled" + // Should not be needed, but better to always add a default case. + default: + return "Disabled" + } +} + +func (v *Vim) EnabledString() string { + if v.CurrentMode == Disabled { + return "Disabled" + } else { + return "Enabled" + } +}