From 2b0f190f15f1e3fb1e8c9143fdc10b20a1331f59 Mon Sep 17 00:00:00 2001 From: EnumDev Date: Mon, 9 Jun 2025 18:31:07 +0300 Subject: [PATCH] Add commands and key bindings --- src/command.go | 271 +++++++++++++++++++++++++++++++++++++++++++++ src/keybindings.go | 133 ++++++++++++++++++++++ src/main.go | 6 + src/top_menu.go | 105 ++---------------- src/window.go | 46 ++------ 5 files changed, 428 insertions(+), 133 deletions(-) create mode 100644 src/command.go create mode 100644 src/keybindings.go diff --git a/src/command.go b/src/command.go new file mode 100644 index 0000000..afdacb5 --- /dev/null +++ b/src/command.go @@ -0,0 +1,271 @@ +package main + +import ( + "fmt" + "log" + "maps" + "slices" + "strings" +) + +type Command struct { + cmd string + run func(window *Window, args ...string) + autocomplete func(window *Window, args ...string) []string +} + +var commands = make(map[string]*Command) + +func initCommands() { + // Setup commands + copyCmd := Command{ + cmd: "copy", + run: func(window *Window, args ...string) { + if window.CurrentBuffer.Selection == nil { + // Copy line + _, line := window.GetCursorPos2D() + window.Clipboard = strings.SplitAfter(window.CurrentBuffer.Contents, "\n")[line] + PrintMessage(window, "Copied line to clipboard.") + } else { + // Copy selection + window.Clipboard = window.CurrentBuffer.GetSelectedText() + PrintMessage(window, "Copied selection to clipboard.") + } + }, + } + + pasteCmd := Command{ + cmd: "paste", + run: func(window *Window, args ...string) { + str := window.CurrentBuffer.Contents + index := window.CurrentBuffer.CursorPos + + if index == len(str) { + str += window.Clipboard + } else { + str = str[:index] + window.Clipboard + str[index:] + } + window.CurrentBuffer.CursorPos += len(window.Clipboard) + window.CurrentBuffer.Contents = str + }, + } + + saveCmd := Command{ + cmd: "save", + run: func(window *Window, args ...string) { + if !window.CurrentBuffer.canSave { + PrintMessage(window, "Cannot save buffer!") + return + } + + inputChannel := RequestInput(window, "Save file [y\\N]:", "") + go func() { + input := <-inputChannel + + if strings.ToLower(input) != "y" && strings.ToLower(input) != "yes" { + return + } + + inputChannel = RequestInput(window, "Save buffer to:", window.CurrentBuffer.filename) + + input = <-inputChannel + + if strings.TrimSpace(input) == "" { + PrintMessage(window, "No save location was given!") + return + } + + window.CurrentBuffer.filename = strings.TrimSpace(input) + err := window.CurrentBuffer.Save() + if err != nil { + + PrintMessage(window, fmt.Sprintf("Could not save file: %s", err)) + window.CurrentBuffer.filename = "" + return + } + + PrintMessage(window, "File saved.") + }() + }, + autocomplete: func(window *Window, args ...string) []string { + return nil + }, + } + + openCmd := Command{ + cmd: "open", + run: func(window *Window, args ...string) { + inputChannel := RequestInput(window, "File to open:", "") + go func() { + input := <-inputChannel + + if input == "" { + return + } + + if openBuffer := GetOpenFileBuffer(input); openBuffer != nil { + PrintMessage(window, fmt.Sprintf("File already open! Switching to buffer: %s", openBuffer.Name)) + window.CurrentBuffer = openBuffer + } else { + newBuffer, err := CreateFileBuffer(input, false) + if err != nil { + PrintMessage(window, fmt.Sprintf("Could not open file: %s", err.Error())) + return + } + + PrintMessage(window, fmt.Sprintf("Opening file at: %s", newBuffer.filename)) + window.CurrentBuffer = newBuffer + } + }() + }, + } + + reloadCmd := Command{ + cmd: "reload", + run: func(window *Window, args ...string) { + err := window.CurrentBuffer.Load() + if err != nil { + log.Fatalf("Could not reload buffer: %s", err) + } + + window.SetCursorPos(window.CurrentBuffer.CursorPos) + PrintMessage(window, "Buffer reloaded.") + }, + } + + prevBufferCmd := Command{ + cmd: "prev-buffer", + run: func(window *Window, args ...string) { + if window.CursorMode != CursorModeBuffer { + return + } + + buffers := slices.Collect(maps.Values(Buffers)) + index := slices.Index(buffers, window.CurrentBuffer) + + index-- + if index < 0 { + index = 0 + } + + window.CurrentBuffer = buffers[index] + }, + } + + nextBufferCmd := Command{ + cmd: "next-buffer", + run: func(window *Window, args ...string) { + if window.CursorMode != CursorModeBuffer { + return + } + + buffers := slices.Collect(maps.Values(Buffers)) + index := slices.Index(buffers, window.CurrentBuffer) + + index++ + if index >= len(buffers) { + index = len(buffers) - 1 + } + + window.CurrentBuffer = buffers[index] + }, + } + + newBufferCmd := Command{ + cmd: "new-buffer", + run: func(window *Window, args ...string) { + number := 1 + for _, buffer := range Buffers { + if strings.HasPrefix(buffer.Name, "New File ") { + number++ + } + } + buffer := CreateBuffer(fmt.Sprintf("New File %d", number)) + window.CurrentBuffer = buffer + window.CursorMode = CursorModeBuffer + }, + } + + closeBufferCmd := Command{ + cmd: "close-buffer", + run: func(window *Window, args ...string) { + delete(Buffers, window.CurrentBuffer.Id) + buffersSlice := slices.Collect(maps.Values(Buffers)) + if len(buffersSlice) == 0 { + window.Close() + return + } + window.CurrentBuffer = buffersSlice[0] + window.CursorMode = CursorModeBuffer + }, + } + + menuFileCmd := Command{ + cmd: "menu-file", + run: func(window *Window, args ...string) { + for _, button := range TopMenuButtons { + if button.Name == "File" { + button.Action(window) + break + } + } + }, + } + + menuEditCmd := Command{ + cmd: "menu-edit", + run: func(window *Window, args ...string) { + for _, button := range TopMenuButtons { + if button.Name == "Edit" { + button.Action(window) + break + } + } + }, + } + + menuBuffersCmd := Command{ + cmd: "menu-buffers", + run: func(window *Window, args ...string) { + for _, button := range TopMenuButtons { + if button.Name == "Buffers" { + button.Action(window) + break + } + } + }, + } + + quitCmd := Command{ + cmd: "quit", + run: func(window *Window, args ...string) { + window.Close() + window.CursorMode = CursorModeBuffer + }, + } + + // Register commands + commands["copy"] = ©Cmd + commands["paste"] = &pasteCmd + commands["save"] = &saveCmd + commands["open"] = &openCmd + commands["reload"] = &reloadCmd + commands["prev-buffer"] = &prevBufferCmd + commands["next-buffer"] = &nextBufferCmd + commands["new-buffer"] = &newBufferCmd + commands["close-buffer"] = &closeBufferCmd + commands["menu-file"] = &menuFileCmd + commands["menu-edit"] = &menuEditCmd + commands["menu-buffers"] = &menuBuffersCmd + commands["quit"] = &quitCmd +} + +func RunCommand(window *Window, cmd string, args ...string) bool { + if command, ok := commands[cmd]; ok { + command.run(window, args...) + return true + } else { + PrintMessage(window, fmt.Sprintf("Could not find command '%s'!", cmd)) + return false + } +} diff --git a/src/keybindings.go b/src/keybindings.go new file mode 100644 index 0000000..44c6535 --- /dev/null +++ b/src/keybindings.go @@ -0,0 +1,133 @@ +package main + +import ( + "github.com/gdamore/tcell/v2" + "strings" +) + +type Keybinding struct { + keybind string + command string +} + +var Keybinds = make([]Keybinding, 0) + +func initKeybindings() { + // Add key bindings + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-Q", + command: "quit", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-C", + command: "copy", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-V", + command: "paste", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-S", + command: "save", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-O", + command: "open", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-R", + command: "reload", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "PgUp", + command: "prev-buffer", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "PgDn", + command: "next-buffer", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-N", + command: "new-buffer", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Delete", + command: "close-buffer", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "Ctrl-Q", + command: "quit", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "F1", + command: "menu-file", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "F2", + command: "menu-edit", + }) + Keybinds = append(Keybinds, Keybinding{ + keybind: "F3", + command: "menu-buffers", + }) +} + +func (keybind *Keybinding) IsPressed(ev *tcell.EventKey) bool { + keys := strings.SplitN(keybind.keybind, "+", 2) + + if len(keys) == 0 { + return false + } else if len(keys) == 1 { + for k, v := range tcell.KeyNames { + if k != tcell.KeyRune { + if keybind.keybind == v { + if ev.Key() == k { + return true + } + } + } else { + if keybind.keybind == string(ev.Rune()) { + return true + } + } + } + } else { + modKey := keys[0] + key := keys[1] + + switch modKey { + case "Shift": + if ev.Modifiers() != tcell.ModShift { + return false + } + case "Alt": + if ev.Modifiers() != tcell.ModAlt { + return false + } + case "Ctrl": + if ev.Modifiers() != tcell.ModCtrl { + return false + } + case "Meta": + if ev.Modifiers() != tcell.ModMeta { + return false + } + } + + for k, v := range tcell.KeyNames { + if k != tcell.KeyRune { + if key == v { + if ev.Key() == k { + return true + } + } + } + } + + if strings.ToLower(key) == string(ev.Rune()) { + return true + } + } + + return false +} diff --git a/src/main.go b/src/main.go index 331f66f..e687d08 100644 --- a/src/main.go +++ b/src/main.go @@ -6,6 +6,12 @@ import ( ) func main() { + // Initialize commands + initCommands() + + // Initialize key bindings + initKeybindings() + window, err := CreateWindow() if err != nil { log.Fatalf("Failed to create window: %v", err) diff --git a/src/top_menu.go b/src/top_menu.go index a959a13..00d17b3 100644 --- a/src/top_menu.go +++ b/src/top_menu.go @@ -3,7 +3,6 @@ package main import ( "fmt" "github.com/gdamore/tcell/v2" - "maps" "slices" "strconv" "strings" @@ -11,7 +10,6 @@ import ( type TopMenuButton struct { Name string - Key rune Action func(w *Window) } @@ -21,89 +19,20 @@ func initTopMenu() { // Buttons fileButton := TopMenuButton{ Name: "File", - Key: 'f', Action: func(window *Window) { ClearDropdowns() d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, 1, 0, func(i int) { switch i { case 0: - number := 1 - for _, buffer := range Buffers { - if strings.HasPrefix(buffer.Name, "New File ") { - number++ - } - } - buffer := CreateBuffer(fmt.Sprintf("New File %d", number)) - window.CurrentBuffer = buffer - window.CursorMode = CursorModeBuffer + RunCommand(window, "new-buffer") case 1: - if !window.CurrentBuffer.canSave { - PrintMessage(window, "Cannot save this buffer!") - return - } - - inputChannel := RequestInput(window, "Save file [y\\N]:", "") - go func() { - input := <-inputChannel - - if strings.ToLower(input) != "y" && strings.ToLower(input) != "yes" { - return - } - - inputChannel = RequestInput(window, "Save buffer to:", window.CurrentBuffer.filename) - - input = <-inputChannel - - if strings.TrimSpace(input) == "" { - PrintMessage(window, "No save location was given!") - return - } - - window.CurrentBuffer.filename = strings.TrimSpace(input) - err := window.CurrentBuffer.Save() - if err != nil { - PrintMessage(window, fmt.Sprintf("Could not save file: %s", err)) - window.CurrentBuffer.filename = "" - return - } - - PrintMessage(window, "File saved.") - }() + RunCommand(window, "save") case 2: - inputChannel := RequestInput(window, "File to open:", "") - go func() { - input := <-inputChannel - - if input == "" { - return - } - - if openBuffer := GetOpenFileBuffer(input); openBuffer != nil { - PrintMessage(window, fmt.Sprintf("File already open! Switching to buffer: %s", openBuffer.Name)) - window.CurrentBuffer = openBuffer - } else { - newBuffer, err := CreateFileBuffer(input, false) - if err != nil { - PrintMessage(window, fmt.Sprintf("Could not open file: %s", err.Error())) - return - } - - PrintMessage(window, fmt.Sprintf("Opening file at: %s", newBuffer.filename)) - window.CurrentBuffer = newBuffer - } - }() + RunCommand(window, "open") case 3: - delete(Buffers, window.CurrentBuffer.Id) - buffersSlice := slices.Collect(maps.Values(Buffers)) - if len(buffersSlice) == 0 { - window.Close() - return - } - window.CurrentBuffer = buffersSlice[0] - window.CursorMode = CursorModeBuffer + RunCommand(window, "close-buffer") case 4: - window.Close() - window.CursorMode = CursorModeBuffer + RunCommand(window, "quit") } ClearDropdowns() }) @@ -113,33 +42,14 @@ func initTopMenu() { } EditButton := TopMenuButton{ Name: "Edit", - Key: 'e', Action: func(window *Window) { ClearDropdowns() d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, 1, 0, func(i int) { switch i { case 0: - if window.CurrentBuffer.Selection == nil { - // Copy line - _, line := window.GetCursorPos2D() - window.Clipboard = strings.SplitAfter(window.CurrentBuffer.Contents, "\n")[line] - PrintMessage(window, "Copied line to clipboard.") - } else { - // Copy selection - window.Clipboard = window.CurrentBuffer.GetSelectedText() - PrintMessage(window, "Copied selection to clipboard.") - } + RunCommand(window, "copy") case 1: - str := window.CurrentBuffer.Contents - index := window.CurrentBuffer.CursorPos - - if index == len(str) { - str += window.Clipboard - } else { - str = str[:index] + window.Clipboard + str[index:] - } - window.CurrentBuffer.CursorPos += len(window.Clipboard) - window.CurrentBuffer.Contents = str + RunCommand(window, "paste") } ClearDropdowns() window.CursorMode = CursorModeBuffer @@ -150,7 +60,6 @@ func initTopMenu() { } Buffers := TopMenuButton{ Name: "Buffers", - Key: 'b', Action: func(window *Window) { ClearDropdowns() buffersSlice := make([]string, 0) diff --git a/src/window.go b/src/window.go index 6bfce6f..28dbf3d 100644 --- a/src/window.go +++ b/src/window.go @@ -4,7 +4,6 @@ import ( "github.com/gdamore/tcell/v2" "log" "slices" - "strings" ) type CursorMode uint8 @@ -180,7 +179,6 @@ func (window *Window) Draw() { func (window *Window) input(ev *tcell.EventKey) { if ev.Key() == tcell.KeyRight { // Navigation Keys - if window.CursorMode == CursorModeBuffer { // Add to selection if ev.Modifiers() == tcell.ModShift { @@ -330,42 +328,20 @@ func (window *Window) input(ev *tcell.EventKey) { ClearDropdowns() window.CursorMode = CursorModeBuffer } - } else if ev.Key() == tcell.KeyCtrlC { // Copy to clipboard key - if window.CursorMode == CursorModeBuffer { - if window.CurrentBuffer.Selection == nil { - // Copy line - _, line := window.GetCursorPos2D() - window.Clipboard = strings.SplitAfter(window.CurrentBuffer.Contents, "\n")[line] - PrintMessage(window, "Copied line to clipboard.") - } else { - // Copy selection - window.Clipboard = window.CurrentBuffer.GetSelectedText() - PrintMessage(window, "Copied selection to clipboard.") - } - } - } else if ev.Key() == tcell.KeyCtrlV { // Paste from clipboard - if window.CursorMode == CursorModeBuffer { - str := window.CurrentBuffer.Contents - index := window.CurrentBuffer.CursorPos + } - if index == len(str) { - str += window.Clipboard - } else { - str = str[:index] + window.Clipboard + str[index:] - } - window.CurrentBuffer.CursorPos += len(window.Clipboard) - window.CurrentBuffer.Contents = str - } - } else if ev.Key() == tcell.KeyCtrlQ { // Exit key - window.Close() - } else if ev.Modifiers()&tcell.ModAlt != 0 { // Menu Bar - for _, button := range TopMenuButtons { - if ev.Rune() == button.Key { - button.Action(window) - break + // Check key bindings + if window.CursorMode == CursorModeBuffer { + for _, keybinding := range Keybinds { + if keybinding.IsPressed(ev) { + RunCommand(window, keybinding.command) + return } } - } else if ev.Key() == tcell.KeyBackspace2 { // Typing + } + + // Typing + if ev.Key() == tcell.KeyBackspace2 { if window.CursorMode == CursorModeBuffer { str := window.CurrentBuffer.Contents index := window.CurrentBuffer.CursorPos