24 Commits
Author SHA1 Message Date
EnumDev 3025fee2bb Add 'cut' command and key binding 2025-06-16 14:14:18 +03:00
EnumDev fe16b5c067 Add 'find' 'replace' and 'replace-all' commands and key bindings 2025-06-16 13:55:02 +03:00
EnumDev d69aba5c1a Update README.md 2025-06-15 21:52:04 +03:00
EnumDev 8d24988057 Hide buffer info message when screen is too small 2025-06-15 21:00:12 +03:00
EnumDev 6cbe0e8ab2 Add 'toggle-top-bar' and 'toggle-line-index' commands 2025-06-15 18:01:46 +03:00
EnumDev f31650e67a Add 'set-style' command 2025-06-15 17:59:02 +03:00
EnumDev a3138a9e86 Add key binding and command to run any other command through input bar 2025-06-15 15:43:12 +03:00
EnumDev e25916228c Replace tilde characters with home directory when saving and loading files 2025-06-15 09:31:36 +03:00
EnumDev de19696b35 Improve copy and pasting 2025-06-15 09:11:51 +03:00
EnumDev f30eb3c46f Rename 'input' and 'mouseInput' functions to 'handleKeyInput' and 'handleMouseInput' 2025-06-14 17:58:26 +03:00
EnumDev ab19981179 Split drawing and processing events into different functions 2025-06-14 17:54:16 +03:00
EnumDev 7af696cc20 Add 'extend_line_index' config option 2025-06-14 17:15:53 +03:00
EnumDev 2d49f84d6f Add README.md 2025-06-14 13:08:56 +03:00
EnumDev ffd8cd54c0 Add helpful messages for different actions 2025-06-14 12:32:27 +03:00
EnumDev 12495d4bd9 Change how buffers are handled, fix PgDn key binding running prev-buffer command, fix next/prev-buffer not working correctly 2025-06-14 11:38:02 +03:00
EnumDev 51ec45cd14 Move and rename drawCurrentBuffer function 2025-06-13 21:32:45 +03:00
EnumDev cc3b4ecf18 Add ctrl+arrow key functionality 2025-06-13 18:10:54 +03:00
EnumDev 1b6b45aaea Allow selecting word and entire line using sequential mouse clicking 2025-06-13 13:43:16 +03:00
EnumDev e2ac216d6e Allow custom buffer info message 2025-06-13 12:07:53 +03:00
EnumDev cd823c9d9f Remove unused function 2025-06-12 21:17:17 +03:00
EnumDev dd0cc2a293 Add config options for showing and hiding the top menu and line index 2025-06-12 21:16:33 +03:00
EnumDev 444c355117 Add configuration files for styles, keybindings and other options 2025-06-12 21:01:37 +03:00
EnumDev 12649af8e3 Allow deleting or replacing selections 2025-06-12 14:03:25 +03:00
EnumDev 5d64673519 Render tab characters as 4 spaces 2025-06-11 16:44:52 +03:00
24 changed files with 1463 additions and 272 deletions
+2
View File
@@ -13,8 +13,10 @@ build:
install: build/typer
# Create directories
install -dm755 $(DESTDIR)$(BINDIR)
install -dm755 $(DESTDIR)$(SYSCONFDIR)
# Install files
install -Dm755 build/typer $(DESTDIR)$(BINDIR)/typer
cp -r config -T $(DESTDIR)$(SYSCONFDIR)/typer
uninstall:
rm $(DESTDIR)$(BINDIR)/typer
+24
View File
@@ -0,0 +1,24 @@
# Typer Text Editor
### A simple and easy to use text editor written in Go
| Default Style | Classic Style |
|:----------------------------------------------------------------:|:----------------------------------------------------------------:|
| ![Example of the Typer's default style](media/default-style.png) | ![Example of the Typer's classic style](media/classic-style.png) |
### Installation
#### From a package manager:
| Distribution | Package name |
|:----------------------:|:---------------------|
| Arch Linux/Artix Linux | `typer` from the AUR |
#### From source:
- Download `go` from your package manager or from the go website
- Downlaod `which` from your package manager
- Download `make` from your package manager
- Run the following command to compile Typer
```shell
make
```
- Run the following command **with superuser privileges** to install Typer to your system
```shell
make install SYSCONFDIR=/etc
```
+10
View File
@@ -0,0 +1,10 @@
# Editor style option
selected_style: "default" # Style for 256-color and true-color capable terminals
selected_style_fallback: "default-fallback" # Style for 8-color capable terminals (Like TTYs)
# Other
show_top_menu: true
show_line_index: true
extend_line_index: false # Extend line index to the bottom of the screen
buffer_info_message: "File: %f Cursor: (%x, %y, %p) Chars: %c"
tab_indentation: 4 # Length of tab characters
+52
View File
@@ -0,0 +1,52 @@
keybindings:
- keybinding: "Ctrl-Q"
cursor_modes: ["buffer"]
command: "quit"
- keybinding: "Ctrl-X"
cursor_modes: ["buffer"]
command: "cut"
- keybinding: "Ctrl-C"
cursor_modes: ["buffer"]
command: "copy"
- keybinding: "Ctrl-V"
cursor_modes: ["buffer"]
command: "paste"
- keybinding: "Ctrl-S"
cursor_modes: ["buffer"]
command: "save"
- keybinding: "Ctrl-O"
cursor_modes: ["buffer"]
command: "open"
- keybinding: "Ctrl-L"
cursor_modes: ["buffer"]
command: "reload"
- keybinding: "Ctrl-F"
cursor_modes: [ "buffer" ]
command: "find"
- keybinding: "Ctrl-R"
cursor_modes: [ "buffer" ]
command: "replace"
- keybinding: "PgUp"
cursor_modes: ["buffer"]
command: "prev-buffer"
- keybinding: "PgDn"
cursor_modes: ["buffer"]
command: "next-buffer"
- keybinding: "Ctrl-N"
cursor_modes: ["buffer"]
command: "new-buffer"
- keybinding: "Delete"
cursor_modes: ["buffer"]
command: "close-buffer"
- keybinding: "F1"
cursor_modes: ["buffer","dropdown"]
command: "menu-file"
- keybinding: "F2"
cursor_modes: ["buffer","dropdown"]
command: "menu-edit"
- keybinding: "F3"
cursor_modes: ["buffer","dropdown"]
command: "menu-buffers"
- keybinding: "Ctrl-E"
cursor_modes: ["buffer"]
command: "execute"
+21
View File
@@ -0,0 +1,21 @@
# Metadata
name: "classic"
description: "Style imitating the look of classic text editors and IDEs from the and 90s"
style_type: "256-color"
# Colors
colors:
buffer_area_bg: "darkblue" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "blue" # Buffer area selected text and cursor background color
top_menu_bg: "245" # Top menu background color
top_menu_fg: "black" # Top menu text color
dropdown_bg: "lightgray" # Dropdown background color
dropdown_fg: "black" # Dropdown text color
dropdown_sel: "blue" # Dropdown selected option background color
line_index_bg: "247" # Line index background color
line_index_fg: "black" # Line index text color
message_bar_bg: "245" # Message bar background color
message_bar_fg: "black" # Message bar text color
input_bar_bg: "245" # Input bar background color
input_bar_fg: "black" # Input bar text color
+21
View File
@@ -0,0 +1,21 @@
# Metadata
name: "default-fallback"
description: "The default look of Typer - Fallback style"
style_type: "8-color"
# Colors
colors:
buffer_area_bg: "black" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "navy" # Buffer area selected text and cursor background color
top_menu_bg: "white" # Top menu background color
top_menu_fg: "black" # Top -menu text color
dropdown_bg: "white" # Dropdown background color
dropdown_fg: "black" # Dropdown text color
dropdown_sel: "navy" # Dropdown selected option background color
line_index_bg: "white" # Line index background color
line_index_fg: "black" # Line index text color
message_bar_bg: "white" # Message bar background color
message_bar_fg: "black" # Message bar text color
input_bar_bg: "white" # Input bar background color
input_bar_fg: "black" # Input bar text color
+21
View File
@@ -0,0 +1,21 @@
# Metadata
name: "default"
description: "The default look of Typer"
style_type: "256-color"
# Colors
colors:
buffer_area_bg: "234" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "243" # Buffer area selected text and cursor background color
top_menu_bg: "236" # Top menu background color
top_menu_fg: "white" # Top menu text color
dropdown_bg: "236" # Dropdown background color
dropdown_fg: "white" # Dropdown text color
dropdown_sel: "240" # Dropdown selected option background color
line_index_bg: "235" # Line index background color
line_index_fg: "dimgray" # Line index text color
message_bar_bg: "236" # Message bar background color
message_bar_fg: "white" # Message bar text color
input_bar_bg: "236" # Input bar background color
input_bar_fg: "white" # Input bar text color
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+279 -11
View File
@@ -2,13 +2,13 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"os"
"path/filepath"
"strings"
)
type Buffer struct {
Id int
Name string
Contents string
@@ -26,8 +26,71 @@ type Selection struct {
selectionEnd int
}
var Buffers = make(map[int]*Buffer)
var LastBufferId int
var Buffers = make([]*Buffer, 0)
func GetBufferByName(name string) *Buffer {
for _, buffer := range Buffers {
if buffer.Name == name {
return buffer
}
}
return nil
}
func GetBufferByFilename(filename string) *Buffer {
for _, buffer := range Buffers {
if buffer.filename == filename {
return buffer
}
}
return nil
}
func drawBuffer(window *Window) {
buffer := window.CurrentBuffer
x, y, _, _ := window.GetTextAreaDimensions()
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
for i, r := range buffer.Contents + " " {
if x-buffer.OffsetX >= bufferX && y-buffer.OffsetY >= bufferY {
// Default style
style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg)
// Change background if under cursor
if i == buffer.CursorPos {
style = style.Background(CurrentStyle.BufferAreaSel)
}
// Change background if selected
if buffer.Selection != nil {
if edge1, edge2 := buffer.GetSelectionEdges(); i >= edge1 && i <= edge2 {
style = style.Background(CurrentStyle.BufferAreaSel)
// Show selection on entire tab space
if r == '\t' {
for j := 0; j < int(Config.TabIndentation); j++ {
window.screen.SetContent(x+j-buffer.OffsetX, y-buffer.OffsetY, r, nil, style)
}
}
}
}
window.screen.SetContent(x-buffer.OffsetX, y-buffer.OffsetY, r, nil, style)
}
// Change position for next character
if r == '\n' {
x = bufferX
y++
} else if r == '\t' {
x += int(Config.TabIndentation)
} else {
x++
}
}
}
func (buffer *Buffer) Load() error {
// Do not load if canSave is false or filename is not set
@@ -35,6 +98,16 @@ func (buffer *Buffer) Load() error {
return nil
}
// Replace tilde with home directory
if strings.HasPrefix(buffer.filename, "~/") {
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
}
content, err := os.ReadFile(buffer.filename)
if err != nil {
return err
@@ -50,6 +123,16 @@ func (buffer *Buffer) Save() error {
return nil
}
// Replace tilde with home directory
if strings.HasPrefix(buffer.filename, "~/") {
homedir, err := os.UserHomeDir()
if err != nil {
return err
}
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
}
// Append new line character at end of buffer contents if not present
if buffer.Contents[len(buffer.Contents)-1] != '\n' {
buffer.Contents += "\n"
@@ -63,6 +146,18 @@ func (buffer *Buffer) Save() error {
return nil
}
func (buffer *Buffer) GetSelectionEdges() (int, int) {
if buffer.Selection == nil {
return -1, -1
}
if buffer.Selection.selectionStart < buffer.Selection.selectionEnd {
return buffer.Selection.selectionStart, buffer.Selection.selectionEnd
} else {
return buffer.Selection.selectionEnd, buffer.Selection.selectionStart
}
}
func (buffer *Buffer) GetSelectedText() string {
if buffer.Selection == nil {
return ""
@@ -89,6 +184,171 @@ func (buffer *Buffer) GetSelectedText() string {
}
}
func (buffer *Buffer) CutText(window *Window) (string, int) {
if buffer.Selection == nil {
// Copy line
copiedText := ""
startOfLine := window.CurrentBuffer.CursorPos
endOfLine := window.CurrentBuffer.CursorPos
// Add current letter to copied text
if buffer.CursorPos < len(buffer.Contents) {
copiedText = string(buffer.Contents[buffer.CursorPos])
}
// Find end of line
for i := buffer.CursorPos + 1; i < len(buffer.Contents); i++ {
currentLetter := buffer.Contents[i]
endOfLine++
copiedText += string(currentLetter)
if currentLetter == '\n' {
break
}
}
// Find start of line
for i := buffer.CursorPos - 1; i >= 0; i-- {
currentLetter := buffer.Contents[i]
if currentLetter != '\n' {
startOfLine--
copiedText = string(currentLetter) + copiedText
} else {
break
}
}
// Remove line from buffer contents
buffer.Contents = buffer.Contents[:startOfLine] + buffer.Contents[endOfLine+1:]
return copiedText, 0
} else {
// Copy selection
copiedText := buffer.GetSelectedText()
// Remove selected text
edge1, edge2 := buffer.GetSelectionEdges()
if edge2 == len(buffer.Contents) {
edge2 = len(buffer.Contents) - 1
}
buffer.Contents = buffer.Contents[:edge1] + buffer.Contents[edge2+1:]
window.SetCursorPos(edge1)
buffer.Selection = nil
return copiedText, 1
}
}
func (buffer *Buffer) CopyText() (string, int) {
if buffer.Selection == nil {
// Copy line
copiedText := ""
// Add current letter to copied text
if buffer.CursorPos < len(buffer.Contents) {
copiedText = string(buffer.Contents[buffer.CursorPos])
}
// Find end of line
for i := buffer.CursorPos + 1; i < len(buffer.Contents); i++ {
currentLetter := buffer.Contents[i]
copiedText += string(currentLetter)
if currentLetter == '\n' {
break
}
}
// Find start of line
for i := buffer.CursorPos - 1; i >= 0; i-- {
currentLetter := buffer.Contents[i]
if currentLetter != '\n' {
copiedText = string(currentLetter) + copiedText
} else {
break
}
}
return copiedText, 0
} else {
// Copy selection
return buffer.GetSelectedText(), 1
}
}
func (buffer *Buffer) PasteText(window *Window, text string) {
str := buffer.Contents
// Remove selected text
if buffer.Selection != nil {
edge1, edge2 := buffer.GetSelectionEdges()
if edge2 == len(buffer.Contents) {
edge2 = len(buffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
buffer.Contents = str
window.SetCursorPos(edge1)
buffer.Selection = nil
}
index := buffer.CursorPos
if index == len(str) {
str += text
} else {
str = str[:index] + text + str[index:]
}
buffer.Contents = str
window.SetCursorPos(buffer.CursorPos + len(text))
}
func (buffer *Buffer) FindSubstring(substring string, afterPos int) int {
// Return no match if afterPos is larger than the buffer contents size
if afterPos >= len(buffer.Contents) {
return -1
}
index := strings.Index(buffer.Contents[afterPos+1:], substring)
if index != -1 {
index += afterPos + 1
}
return index
}
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement string, afterPos int) int {
index := buffer.FindSubstring(substring, afterPos)
// Return if substring isn't found
if index == -1 {
return -1
}
// Replace substring with replacement string
buffer.Contents = buffer.Contents[:index] + replacement + buffer.Contents[index+len(substring):]
return index
}
func (buffer *Buffer) FindAndReplaceAll(substring, replacement string) int {
replacements := 0
index := 0
for index != -1 {
index = buffer.FindAndReplaceSubstring(substring, replacement, index)
if index != -1 {
replacements++
}
if index == 0 {
index++
}
}
return replacements
}
func GetOpenFileBuffer(filename string) *Buffer {
// Replace tilde with home directory
if filename != "~" && strings.HasPrefix(filename, "~/") {
@@ -145,8 +405,15 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
}
}
if GetBufferByName(filename) != nil {
return nil, fmt.Errorf("a buffer with the name (%s) is already open", filename)
}
if GetBufferByFilename(abs) != nil {
return nil, fmt.Errorf("%s is already open in another buffer", filename)
}
buffer := Buffer{
Id: LastBufferId + 1,
Name: filename,
Contents: "",
CursorPos: 0,
@@ -163,15 +430,13 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
}
}
Buffers[buffer.Id] = &buffer
LastBufferId++
Buffers = append(Buffers, &buffer)
return &buffer, nil
}
func CreateBuffer(bufferName string) *Buffer {
func CreateBuffer(bufferName string) (*Buffer, error) {
buffer := Buffer{
Id: LastBufferId + 1,
Name: bufferName,
Contents: "",
CursorPos: 0,
@@ -179,8 +444,11 @@ func CreateBuffer(bufferName string) *Buffer {
filename: "",
}
Buffers[buffer.Id] = &buffer
LastBufferId++
if GetBufferByName(bufferName) != nil {
return nil, fmt.Errorf("a buffer with the name (%s) is already open", bufferName)
}
return &buffer
Buffers = append(Buffers, &buffer)
return &buffer, nil
}
+289 -34
View File
@@ -3,8 +3,8 @@ package main
import (
"fmt"
"log"
"maps"
"slices"
"strconv"
"strings"
)
@@ -18,17 +18,37 @@ var commands = make(map[string]*Command)
func initCommands() {
// Setup commands
cutCmd := Command{
cmd: "cut",
run: func(window *Window, args ...string) {
// Cut text from buffer
copiedText, copyingMethod := window.CurrentBuffer.CutText(window)
// Put cut text to clipboard
window.Clipboard = copiedText
// Send appropriate message and remove text depending on copying method
if copyingMethod == 0 {
PrintMessage(window, "Copied line to clipboard.")
} else {
PrintMessage(window, "Copied selection to clipboard.")
}
},
}
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]
// Copy text from buffer
copiedText, copyingMethod := window.CurrentBuffer.CopyText()
// Put copied text to clipboard
window.Clipboard = copiedText
// Send appropriate message depending on copying method
if copyingMethod == 0 {
PrintMessage(window, "Copied line to clipboard.")
} else {
// Copy selection
window.Clipboard = window.CurrentBuffer.GetSelectedText()
PrintMessage(window, "Copied selection to clipboard.")
}
},
@@ -37,16 +57,10 @@ func initCommands() {
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:]
if window.Clipboard != "" {
window.CurrentBuffer.PasteText(window, window.Clipboard)
PrintMessage(window, "Pasted text to buffer.")
}
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos + len(window.Clipboard))
},
}
@@ -133,6 +147,132 @@ func initCommands() {
},
}
findCmd := Command{
cmd: "find",
run: func(window *Window, args ...string) {
if len(args) >= 1 {
input := args[0]
if input == "" {
return
}
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match found.")
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", input))
}
return
}
inputChannel := RequestInput(window, "Substring to search for:", "")
go func() {
input := <-inputChannel
if input == "" {
return
}
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match found.")
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", input))
}
}()
},
}
replaceCmd := Command{
cmd: "replace",
run: func(window *Window, args ...string) {
if len(args) >= 2 {
findStr := args[0]
replaceStr := args[1]
if findStr == "" {
return
}
pos := window.CurrentBuffer.FindAndReplaceSubstring(findStr, replaceStr, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match replaced successfully.")
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
}
return
}
go func() {
inputChannel := RequestInput(window, "Substring to search for:", "")
findStr := <-inputChannel
if findStr == "" {
return
}
inputChannel = RequestInput(window, "String to replace with:", "")
replaceStr := <-inputChannel
pos := window.CurrentBuffer.FindAndReplaceSubstring(findStr, replaceStr, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match replaced successfully.")
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
}
}()
},
}
replaceAllCmd := Command{
cmd: "replace-all",
run: func(window *Window, args ...string) {
if len(args) >= 2 {
findStr := args[0]
replaceStr := args[1]
if findStr == "" {
return
}
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
if replacements > 0 {
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, fmt.Sprintf("Replaced all %d matches successfully.", replacements))
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
}
return
}
go func() {
inputChannel := RequestInput(window, "Substring to search for:", "")
findStr := <-inputChannel
if findStr == "" {
return
}
inputChannel = RequestInput(window, "String to replace with:", "")
replaceStr := <-inputChannel
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
if replacements > 0 {
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, fmt.Sprintf("Replaced all %d matches successfully.", replacements))
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
}
}()
},
}
prevBufferCmd := Command{
cmd: "prev-buffer",
run: func(window *Window, args ...string) {
@@ -140,15 +280,15 @@ func initCommands() {
return
}
buffers := slices.Collect(maps.Values(Buffers))
index := slices.Index(buffers, window.CurrentBuffer)
index := slices.Index(Buffers, window.CurrentBuffer)
index--
if index < 0 {
index = 0
}
window.CurrentBuffer = buffers[index]
window.CurrentBuffer = Buffers[index]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
},
}
@@ -159,44 +299,110 @@ func initCommands() {
return
}
buffers := slices.Collect(maps.Values(Buffers))
index := slices.Index(buffers, window.CurrentBuffer)
index := slices.Index(Buffers, window.CurrentBuffer)
index++
if index >= len(buffers) {
index = len(buffers) - 1
if index >= len(Buffers) {
index = len(Buffers) - 1
}
window.CurrentBuffer = buffers[index]
window.CurrentBuffer = Buffers[index]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
},
}
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++
for i := 1; true; i++ {
buffer, err := CreateBuffer("New Buffer " + strconv.Itoa(i))
if err == nil {
window.CurrentBuffer = buffer
break
}
}
buffer := CreateBuffer(fmt.Sprintf("New File %d", number))
window.CurrentBuffer = buffer
window.CursorMode = CursorModeBuffer
PrintMessage(window, fmt.Sprintf("New buffer created with the name '%s'.", window.CurrentBuffer.Name))
},
}
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 {
bufferIndex := slices.Index(Buffers, window.CurrentBuffer)
Buffers = DeleteFromSlice(Buffers, bufferIndex)
if len(Buffers) == 0 {
window.Close()
return
}
window.CurrentBuffer = buffersSlice[0]
if bufferIndex >= len(Buffers) {
window.CurrentBuffer = Buffers[bufferIndex-1]
} else {
window.CurrentBuffer = Buffers[bufferIndex]
}
window.CursorMode = CursorModeBuffer
PrintMessage(window, "Buffer closed.")
},
}
toggleTopBar := Command{
cmd: "toggle-top-bar",
run: func(window *Window, args ...string) {
window.ShowTopMenu = !window.ShowTopMenu
},
}
toggleLineIndex := Command{
cmd: "toggle-line-index",
run: func(window *Window, args ...string) {
window.ShowLineIndex = !window.ShowLineIndex
},
}
setStyleCmd := Command{
cmd: "set-style",
run: func(window *Window, args ...string) {
if len(args) >= 1 {
input := args[0]
if input == "" {
return
}
if _, ok := AvailableStyles[input]; !ok {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
return
}
if ok := SetCurrentStyle(window.screen, input); ok {
PrintMessage(window, fmt.Sprintf("Setting style to '%s'", input))
} else {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
}
return
}
inputChannel := RequestInput(window, "Style to switch to:", "")
go func() {
input := <-inputChannel
if input == "" {
return
}
if _, ok := AvailableStyles[input]; !ok {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
return
}
if ok := SetCurrentStyle(window.screen, input); ok {
PrintMessage(window, fmt.Sprintf("Setting style to '%s'", input))
} else {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
}
}()
},
}
@@ -244,20 +450,69 @@ func initCommands() {
},
}
executeCmd := Command{
cmd: "execute",
run: func(window *Window, args ...string) {
inputChannel := RequestInput(window, "Run:", "")
go func() {
input := strings.TrimSpace(<-inputChannel)
if input == "" {
return
}
var arguments []string
builder := &strings.Builder{}
quoted := false
for _, r := range input {
if r == '"' {
quoted = !quoted
} else if !quoted && r == ' ' {
arguments = append(arguments, builder.String())
builder.Reset()
} else {
builder.WriteRune(r)
}
}
if builder.Len() > 0 {
arguments = append(arguments, builder.String())
}
window.CursorMode = CursorModeBuffer
if len(arguments) == 1 {
RunCommand(window, arguments[0])
} else {
RunCommand(window, arguments[0], arguments[1:]...)
}
}()
},
}
// Register commands
commands["cut"] = &cutCmd
commands["copy"] = &copyCmd
commands["paste"] = &pasteCmd
commands["save"] = &saveCmd
commands["open"] = &openCmd
commands["reload"] = &reloadCmd
commands["find"] = &findCmd
commands["replace"] = &replaceCmd
commands["replace-all"] = &replaceAllCmd
commands["prev-buffer"] = &prevBufferCmd
commands["next-buffer"] = &nextBufferCmd
commands["new-buffer"] = &newBufferCmd
commands["close-buffer"] = &closeBufferCmd
commands["toggle-top-bar"] = &toggleTopBar
commands["toggle-line-index"] = &toggleLineIndex
commands["set-style"] = &setStyleCmd
commands["menu-file"] = &menuFileCmd
commands["menu-edit"] = &menuEditCmd
commands["menu-buffers"] = &menuBuffersCmd
commands["quit"] = &quitCmd
commands["execute"] = &executeCmd
}
func RunCommand(window *Window, cmd string, args ...string) bool {
+63
View File
@@ -0,0 +1,63 @@
package main
import (
"gopkg.in/yaml.v3"
"log"
"os"
"path"
)
type TyperConfig struct {
SelectedStyle string `yaml:"selected_style,omitempty"`
FallbackStyle string `yaml:"fallback_style,omitempty"`
ShowTopMenu bool `yaml:"show_top_menu,omitempty"`
ShowLineIndex bool `yaml:"show_line_index,omitempty"`
ExtendLineIndex bool `yaml:"extend_line_index,omitempty"`
BufferInfoMessage string `yaml:"buffer_info_message,omitempty"`
TabIndentation int `yaml:"tab_indentation,omitempty"`
}
var Config TyperConfig
func readConfig() {
Config = TyperConfig{
SelectedStyle: "default",
FallbackStyle: "default-fallback",
ShowTopMenu: true,
ShowLineIndex: true,
ExtendLineIndex: false,
BufferInfoMessage: "File: %f Cursor: (%x, %y, %p) Chars: %c",
TabIndentation: 4,
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if _, err := os.Stat(path.Join(homeDir, ".config/typer/config.yml")); err == nil {
data, err := os.ReadFile(path.Join(homeDir, ".config/typer/config.yml"))
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
err = yaml.Unmarshal(data, &Config)
if err != nil {
log.Fatalf("Could not unmarshal config.yml: %s", err)
}
} else if _, err := os.Stat("/etc/typer/config.yml"); err == nil {
reader, err := os.Open("/etc/typer/config.yml")
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
err = yaml.NewDecoder(reader).Decode(&Config)
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
reader.Close()
}
// Validate config options
if Config.TabIndentation < 1 {
Config.TabIndentation = 1
}
}
+3 -3
View File
@@ -50,13 +50,13 @@ func ClearDropdowns() {
}
func drawDropdowns(window *Window) {
dropdownStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
dropdownStyle := tcell.StyleDefault.Background(CurrentStyle.DropdownBg).Foreground(CurrentStyle.DropdownFg)
for _, d := range dropdowns {
drawBox(window.screen, d.PosX, d.PosY, d.PosX+d.Width+1, d.PosY+len(d.Options)+1, dropdownStyle)
line := d.PosY
line := 1
for i, option := range d.Options {
if d.Selected == i {
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(tcell.Color240), option)
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(CurrentStyle.DropdownSel), option)
} else {
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle, option)
}
+1
View File
@@ -12,4 +12,5 @@ require (
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+3
View File
@@ -79,3 +79,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+1 -5
View File
@@ -31,10 +31,6 @@ func RequestInput(window *Window, text string, defaultInput string) chan string
return request.inputChannel
}
func IsRequestingInput() bool {
return currentInputRequest != nil
}
func drawInputBar(window *Window) {
if currentInputRequest == nil {
return
@@ -42,7 +38,7 @@ func drawInputBar(window *Window) {
screen := window.screen
inputBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
inputBarStyle := tcell.StyleDefault.Background(CurrentStyle.InputBarBg).Foreground(CurrentStyle.InputBarFg)
sizeX, sizeY := screen.Size()
+60 -80
View File
@@ -2,106 +2,86 @@ package main
import (
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"strings"
)
type TyperKeybindings struct {
Keybindings []Keybinding `yaml:"keybindings"`
}
type Keybinding struct {
keybind string
cursorModes []CursorMode
command string
Keybinding string `yaml:"keybinding"`
CursorModes []string `yaml:"cursor_modes"`
Command string `yaml:"command"`
}
var Keybinds = make([]Keybinding, 0)
var Keybindings TyperKeybindings
func initKeybindings() {
// Add key bindings
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-Q",
cursorModes: []CursorMode{CursorModeBuffer},
command: "quit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-C",
cursorModes: []CursorMode{CursorModeBuffer},
command: "copy",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-V",
cursorModes: []CursorMode{CursorModeBuffer},
command: "paste",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-S",
cursorModes: []CursorMode{CursorModeBuffer},
command: "save",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-O",
cursorModes: []CursorMode{CursorModeBuffer},
command: "open",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-R",
cursorModes: []CursorMode{CursorModeBuffer},
command: "reload",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "PgUp",
cursorModes: []CursorMode{CursorModeBuffer},
command: "prev-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "PgDn",
cursorModes: []CursorMode{CursorModeBuffer},
command: "next-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-N",
cursorModes: []CursorMode{CursorModeBuffer},
command: "new-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Delete",
cursorModes: []CursorMode{CursorModeBuffer},
command: "close-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-Q",
cursorModes: []CursorMode{CursorModeBuffer},
command: "quit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F1",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-file",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F2",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-edit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F3",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-buffers",
})
func readKeybindings() {
Keybindings = TyperKeybindings{
Keybindings: make([]Keybinding, 0),
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if _, err := os.Stat(path.Join(homeDir, ".config/typer/keybindings.yml")); err == nil {
data, err := os.ReadFile(path.Join(homeDir, ".config/typer/keybindings.yml"))
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
err = yaml.Unmarshal(data, &Keybindings)
if err != nil {
log.Fatalf("Could not unmarshal keybindings.yml: %s", err)
}
} else if _, err := os.Stat("/etc/typer/keybindings.yml"); err == nil {
reader, err := os.Open("/etc/typer/keybindings.yml")
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
err = yaml.NewDecoder(reader).Decode(&Keybindings)
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
reader.Close()
}
}
func (keybind *Keybinding) IsPressed(ev *tcell.EventKey) bool {
keys := strings.SplitN(keybind.keybind, "+", 2)
func (keybinding *Keybinding) GetCursorModes() []CursorMode {
ret := make([]CursorMode, 0)
for _, cursorModeStr := range keybinding.CursorModes {
for key, value := range CursorModeNames {
if cursorModeStr == value {
ret = append(ret, key)
}
}
}
return ret
}
func (keybinding *Keybinding) IsPressed(ev *tcell.EventKey) bool {
keys := strings.SplitN(keybinding.Keybinding, "+", 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 keybinding.Keybinding == v {
if ev.Key() == k {
return true
}
}
} else {
if keybind.keybind == string(ev.Rune()) {
if keybinding.Keybinding == string(ev.Rune()) {
return true
}
}
+17 -10
View File
@@ -10,18 +10,24 @@ func drawLineIndex(window *Window) {
screen := window.screen
buffer := window.CurrentBuffer
lineIndexStyle := tcell.StyleDefault.Foreground(tcell.ColorDimGray).Background(tcell.Color235)
_, sizeY := screen.Size()
y := 0
if window.ShowTopMenu {
y = 1
}
lineIndexStyle := tcell.StyleDefault.Background(CurrentStyle.LineIndexBg).Foreground(CurrentStyle.LineIndexFg)
lineIndexSize := getLineIndexSize(window)
for lineIndex := 1 + buffer.OffsetY; lineIndex <= strings.Count(buffer.Contents, "\n")+1 && lineIndex < sizeY+buffer.OffsetY; lineIndex++ {
_, bufferY1, _, bufferY2 := window.GetTextAreaDimensions()
lineIndex := 1 + buffer.OffsetY
for y := bufferY1; y <= bufferY2; y++ {
if lineIndex > strings.Count(buffer.Contents, "\n")+1 {
if Config.ExtendLineIndex {
for x := 0; x < lineIndexSize; x++ {
screen.SetContent(x, y, ' ', nil, lineIndexStyle)
}
continue
} else {
break
}
}
for x := 0; x < lineIndexSize; x++ {
screen.SetContent(x, y, ' ', nil, lineIndexStyle)
@@ -30,7 +36,8 @@ func drawLineIndex(window *Window) {
text := strconv.Itoa(lineIndex)
drawText(screen, lineIndexSize-len(text)-1, y, lineIndexSize, y, lineIndexStyle, text)
y++
lineIndex++
}
}
+16 -7
View File
@@ -6,34 +6,43 @@ import (
)
func main() {
// Read config
readConfig()
// Read key bindings
readKeybindings()
// Read styles
readStyles()
// Initialize commands
initCommands()
// Initialize key bindings
initKeybindings()
window, err := CreateWindow()
if err != nil {
log.Fatalf("Failed to create window: %v", err)
}
if len(os.Args) > 1 {
for _, file := range os.Args[1:] {
for i, file := range os.Args[1:] {
b, err := CreateFileBuffer(file, true)
if err != nil {
PrintMessage(window, "Could not open file: "+file)
continue
}
if window.CurrentBuffer.Name == "New File 1" {
delete(Buffers, window.CurrentBuffer.Id)
if i == 0 {
window.CurrentBuffer = b
Buffers = Buffers[1:]
}
}
}
for window.screen != nil {
for !window.closed {
window.Draw()
window.ProcessEvents()
}
window.screen.Fini()
window.screen = nil
}
+1 -1
View File
@@ -33,7 +33,7 @@ func PrintMessage(window *Window, message string) {
func drawMessageBar(window *Window) {
screen := window.screen
messageBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
messageBarStyle := tcell.StyleDefault.Background(CurrentStyle.MessageBarBg).Foreground(CurrentStyle.MessageBarFg)
sizeX, sizeY := screen.Size()
+191
View File
@@ -0,0 +1,191 @@
package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"reflect"
"slices"
"strconv"
"strings"
)
type TyperStyle struct {
// Metadata
Name string
Description string
StyleType string
// Colors
BufferAreaBg tcell.Color `name:"buffer_area_bg"`
BufferAreaFg tcell.Color `name:"buffer_area_fg"`
BufferAreaSel tcell.Color `name:"buffer_area_sel"`
TopMenuBg tcell.Color `name:"top_menu_bg"`
TopMenuFg tcell.Color `name:"top_menu_fg"`
DropdownBg tcell.Color `name:"dropdown_bg"`
DropdownFg tcell.Color `name:"dropdown_fg"`
DropdownSel tcell.Color `name:"dropdown_sel"`
LineIndexBg tcell.Color `name:"line_index_bg"`
LineIndexFg tcell.Color `name:"line_index_fg"`
MessageBarBg tcell.Color `name:"message_bar_bg"`
MessageBarFg tcell.Color `name:"message_bar_fg"`
InputBarBg tcell.Color `name:"input_bar_bg"`
InputBarFg tcell.Color `name:"input_bar_fg"`
}
type typerStyleYaml struct {
// Metadata
Name string `yaml:"name"`
Description string `yaml:"description"`
StyleType string `yaml:"style_type"`
// Colors
Colors map[string]string `yaml:"colors"`
}
var FallbackStyle = TyperStyle{
Name: "fallback",
Description: "Fallback style",
StyleType: "8-color",
BufferAreaBg: tcell.ColorBlack,
BufferAreaFg: tcell.ColorWhite,
BufferAreaSel: tcell.ColorNavy,
TopMenuBg: tcell.ColorWhite,
TopMenuFg: tcell.ColorBlack,
DropdownBg: tcell.ColorWhite,
DropdownFg: tcell.ColorBlack,
DropdownSel: tcell.ColorNavy,
LineIndexBg: tcell.ColorWhite,
LineIndexFg: tcell.ColorBlack,
MessageBarBg: tcell.ColorWhite,
MessageBarFg: tcell.ColorBlack,
InputBarBg: tcell.ColorWhite,
InputBarFg: tcell.ColorBlack,
}
var AvailableStyles = make(map[string]TyperStyle)
var CurrentStyle = FallbackStyle
func readStyles() {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if stat, err := os.Stat(path.Join(homeDir, ".config/typer/styles/")); err == nil && stat.IsDir() {
entries, err := os.ReadDir(path.Join(homeDir, ".config/typer/styles/"))
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
for _, entry := range entries {
entryPath := path.Join(homeDir, ".config/typer/styles/", entry.Name())
style, err := readStyleYamlFile(entryPath)
if err != nil {
log.Fatalf("Could not read style file (%s): %s", entryPath, err)
}
if _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
if stat, err := os.Stat("/etc/typer/styles/"); err == nil && stat.IsDir() {
entries, err := os.ReadDir("/etc/typer/styles/")
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
for _, entry := range entries {
entryPath := path.Join("/etc/typer/styles/", entry.Name())
style, err := readStyleYamlFile(entryPath)
if err != nil {
log.Fatalf("Could not read style file (%s): %s", entryPath, err)
}
if _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
}
func readStyleYamlFile(filepath string) (TyperStyle, error) {
styleYaml := typerStyleYaml{}
data, err := os.ReadFile(filepath)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not read file: %s", err)
}
err = yaml.Unmarshal(data, &styleYaml)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not unmarshal style: %s", err)
}
style := TyperStyle{
Name: styleYaml.Name,
Description: styleYaml.Description,
StyleType: styleYaml.StyleType,
}
for name, colorStr := range styleYaml.Colors {
var color tcell.Color
if n, err := strconv.Atoi(colorStr); err == nil && n >= 0 && n < 256 {
color = tcell.ColorValid + tcell.Color(n)
} else if strings.HasPrefix(colorStr, "#") && len(colorStr) == 7 {
n, err := strconv.ParseInt(colorStr[1:], 16, 32)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
}
color = tcell.NewHexColor(int32(n))
} else if c, ok := tcell.ColorNames[colorStr]; ok {
color = c
} else {
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
}
pt := reflect.TypeOf(&style)
t := pt.Elem()
pv := reflect.ValueOf(&style)
v := pv.Elem()
for i := 0; i < t.NumField(); i++ {
field := v.Field(i)
if tag, ok := t.Field(i).Tag.Lookup("name"); ok && tag == name {
field.Set(reflect.ValueOf(color))
}
}
}
return style, nil
}
func SetCurrentStyle(screen tcell.Screen, styleName string) bool {
availableTypes := make([]string, 1)
availableTypes[0] = "8-color"
if screen.Colors() >= 16 {
availableTypes = append(availableTypes, "16-color")
}
if screen.Colors() >= 256 {
availableTypes = append(availableTypes, "256-color")
}
if screen.Colors() >= 16777216 {
availableTypes = append(availableTypes, "true-color")
}
if style, ok := AvailableStyles[styleName]; ok && slices.Index(availableTypes, style.StyleType) != -1 {
CurrentStyle = style
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
screen.Sync()
return true
}
return false
}
+61 -23
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"github.com/gdamore/tcell/v2"
"path/filepath"
"slices"
"strconv"
"strings"
)
@@ -22,7 +21,13 @@ func initTopMenu() {
Name: "File",
Action: func(window *Window) {
ClearDropdowns()
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, 1, 0, func(i int) {
y := 0
if window.ShowTopMenu {
y++
}
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "new-buffer")
@@ -45,11 +50,19 @@ func initTopMenu() {
Name: "Edit",
Action: func(window *Window) {
ClearDropdowns()
d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, 1, 0, func(i int) {
y := 0
if window.ShowTopMenu {
y++
}
d := CreateDropdownMenu([]string{"Cut", "Copy", "Paste"}, 0, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "copy")
RunCommand(window, "cut")
case 1:
RunCommand(window, "copy")
case 2:
RunCommand(window, "paste")
}
ClearDropdowns()
@@ -63,28 +76,24 @@ func initTopMenu() {
Name: "Buffers",
Action: func(window *Window) {
ClearDropdowns()
y := 0
if window.ShowTopMenu {
y++
}
buffersSlice := make([]string, 0)
for _, buffer := range Buffers {
for i, buffer := range Buffers {
if window.CurrentBuffer == buffer {
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", buffer.Id, buffer.Name))
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", i+1, buffer.Name))
} else {
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", buffer.Id, buffer.Name))
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", i+1, buffer.Name))
}
}
slices.Sort(buffersSlice)
d := CreateDropdownMenu(buffersSlice, 0, 1, 0, func(i int) {
start := strings.Index(buffersSlice[i], "[")
end := strings.Index(buffersSlice[i], "]")
id, err := strconv.Atoi(buffersSlice[i][start+1 : end])
if err != nil {
PrintMessage(window, fmt.Sprintf("Cannot convert buffer id '%s' to int", buffersSlice[i][start:end]))
return
}
window.CurrentBuffer = Buffers[id]
d := CreateDropdownMenu(buffersSlice, 0, y, 0, func(i int) {
window.CurrentBuffer = Buffers[i]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
ClearDropdowns()
window.CursorMode = CursorModeBuffer
})
@@ -100,7 +109,7 @@ func initTopMenu() {
func drawTopMenu(window *Window) {
screen := window.screen
topMenuStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
topMenuStyle := tcell.StyleDefault.Background(CurrentStyle.TopMenuBg).Foreground(CurrentStyle.TopMenuFg)
sizeX, _ := screen.Size()
@@ -115,11 +124,40 @@ func drawTopMenu(window *Window) {
}
// Draw buffer info
bufferInfoMsg := getBufferInfoMsg(window)
if sizeX-len(bufferInfoMsg)-1 > currentX+2 {
drawText(screen, sizeX-len(bufferInfoMsg)-1, 0, sizeX-1, 0, topMenuStyle, bufferInfoMsg)
}
}
func getBufferInfoMsg(window *Window) string {
pathToFile := "Not set"
filename := "Not set"
if window.CurrentBuffer.filename != "" {
pathToFile = window.CurrentBuffer.filename
}
if filepath.Base(window.CurrentBuffer.filename) != "." {
filename = filepath.Base(window.CurrentBuffer.filename)
}
cursorPos := window.CurrentBuffer.CursorPos
cursorX, cursorY := window.GetCursorPos2D()
cursorInfo := fmt.Sprintf("File: %s Cursor: (%d,%d,%d) Words: %d", filename, cursorX+1, cursorY+1, window.CurrentBuffer.CursorPos+1, len(strings.Fields(window.CurrentBuffer.Contents)))
drawText(screen, sizeX-len(cursorInfo)-1, 0, sizeX-1, 0, topMenuStyle, cursorInfo)
cursorX++
cursorY++
chars := len(window.CurrentBuffer.Contents)
words := len(strings.Fields(window.CurrentBuffer.Contents))
ret := Config.BufferInfoMessage
ret = strings.ReplaceAll(ret, "\n", " ")
ret = strings.ReplaceAll(ret, "%F", pathToFile)
ret = strings.ReplaceAll(ret, "%f", filename)
ret = strings.ReplaceAll(ret, "%x", strconv.Itoa(cursorX))
ret = strings.ReplaceAll(ret, "%y", strconv.Itoa(cursorY))
ret = strings.ReplaceAll(ret, "%p", strconv.Itoa(cursorPos))
ret = strings.ReplaceAll(ret, "%c", strconv.Itoa(chars))
ret = strings.ReplaceAll(ret, "%w", strconv.Itoa(words))
return ret
}
+12
View File
@@ -53,3 +53,15 @@ func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style) {
drawText(s, x1+1, y1+1, x2-1, y2-1, style, " ")
}
func DeleteFromSlice[T any](slice []T, i int) []T {
if i >= len(slice) {
return slice
} else if i < 0 {
return slice
} else if i == len(slice)-1 {
return slice[:len(slice)-1]
} else {
return append(slice[:i], slice[i+1:]...)
}
}
+315 -98
View File
@@ -4,6 +4,10 @@ import (
"github.com/gdamore/tcell/v2"
"log"
"slices"
"strconv"
"strings"
"time"
"unicode"
)
type CursorMode uint8
@@ -15,6 +19,13 @@ const (
CursorModeInputBar
)
var CursorModeNames = map[CursorMode]string{
CursorModeDisabled: "disabled",
CursorModeBuffer: "buffer",
CursorModeDropdown: "dropdown",
CursorModeInputBar: "input_bar",
}
type Window struct {
ShowTopMenu bool
ShowLineIndex bool
@@ -25,14 +36,17 @@ type Window struct {
CurrentBuffer *Buffer
screen tcell.Screen
closed bool
}
var mouseHeld = false
var lastClick int64 = 0
func CreateWindow() (*Window, error) {
window := Window{
ShowTopMenu: true,
ShowLineIndex: true,
ShowTopMenu: Config.ShowTopMenu,
ShowLineIndex: Config.ShowLineIndex,
CursorMode: CursorModeBuffer,
CurrentBuffer: nil,
@@ -41,8 +55,11 @@ func CreateWindow() (*Window, error) {
}
// Create empty buffer if nil
if window.CurrentBuffer == nil {
window.CurrentBuffer = CreateBuffer("New File 1")
for i := 1; window.CurrentBuffer == nil; i++ {
buffer, err := CreateBuffer("New Buffer " + strconv.Itoa(i))
if err == nil {
window.CurrentBuffer = buffer
}
}
// Create tcell screen
@@ -55,73 +72,28 @@ func CreateWindow() (*Window, error) {
log.Fatalf("Failed to initialize screen: %s", err)
}
// Set screen style
screen.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234))
// Enable mouse
screen.EnableMouse()
// Set window screen field
window.screen = screen
// Try to set screen style to selected one
if ok := SetCurrentStyle(screen, Config.SelectedStyle); !ok {
// Try to set screen style to selected fallback one
if ok := SetCurrentStyle(screen, Config.FallbackStyle); !ok {
// Use hard-coded fallback style
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
PrintMessage(&window, "Could not set style either to selected one nor to fallback one!")
}
}
// Initialize top menu
initTopMenu()
return &window, nil
}
func (window *Window) drawCurrentBuffer() {
buffer := window.CurrentBuffer
x, y, _, _ := window.GetTextAreaDimensions()
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
normalStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234)
selectedStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color243)
for i, r := range buffer.Contents {
if r == '\n' {
x = 0
if window.ShowLineIndex {
x += bufferX
}
y++
}
if r != '\n' {
x++
}
if x-buffer.OffsetX-1 < bufferX {
continue
}
if y-buffer.OffsetY < bufferY {
continue
}
if buffer.Selection != nil && buffer.Selection.selectionEnd >= buffer.Selection.selectionStart && i >= buffer.Selection.selectionStart && i <= buffer.Selection.selectionEnd {
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, selectedStyle)
} else if buffer.Selection != nil && i <= buffer.Selection.selectionStart && i >= buffer.Selection.selectionEnd {
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, selectedStyle)
} else {
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, normalStyle)
}
}
// Draw cursor
cursorX, cursorY := window.GetCursorPos2D()
cursorX += bufferX
cursorY += bufferY
cursorX -= window.CurrentBuffer.OffsetX
cursorY -= window.CurrentBuffer.OffsetY
r, _, _, _ := window.screen.GetContent(cursorX, cursorY)
window.screen.SetContent(cursorX, cursorY, r, nil, selectedStyle)
}
func (window *Window) Draw() {
// Clear screen
window.screen.Clear()
@@ -138,7 +110,7 @@ func (window *Window) Draw() {
// Draw current buffer
if window.CurrentBuffer != nil {
window.drawCurrentBuffer()
drawBuffer(window)
}
// Draw input bar
@@ -162,7 +134,9 @@ func (window *Window) Draw() {
// Update screen
window.screen.Show()
}
func (window *Window) ProcessEvents() {
// Poll event
ev := window.screen.PollEvent()
@@ -172,25 +146,56 @@ func (window *Window) Draw() {
window.screen.Sync()
window.SyncBufferOffset()
case *tcell.EventMouse:
window.mouseInput(ev)
window.handleMouseInput(ev)
case *tcell.EventKey:
window.input(ev)
window.handleKeyInput(ev)
}
}
func (window *Window) input(ev *tcell.EventKey) {
func (window *Window) handleKeyInput(ev *tcell.EventKey) {
if ev.Key() == tcell.KeyRight { // Navigation Keys
if window.CursorMode == CursorModeBuffer {
// Get original cursor position
pos := window.CurrentBuffer.CursorPos
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to start of word
// Set variable to one character right of current position
endOfWord := pos + 1
if endOfWord >= len(window.CurrentBuffer.Contents) {
endOfWord = len(window.CurrentBuffer.Contents)
}
// Skip all spaces
for endOfWord < len(window.CurrentBuffer.Contents) && unicode.IsSpace(rune(window.CurrentBuffer.Contents[endOfWord])) {
endOfWord++
}
// Find end of word
for endOfWord < len(window.CurrentBuffer.Contents) && !unicode.IsSpace(rune(window.CurrentBuffer.Contents[endOfWord])) {
endOfWord++
}
window.SetCursorPos(endOfWord)
} else {
// Move cursor one character backwards
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
}
// Add to selection
if ev.Modifiers() == tcell.ModShift {
if ev.Modifiers()&tcell.ModShift != 0 {
if window.CurrentBuffer.Selection == nil {
// Cancel cursor movement when creating selection without holding ctrl
if ev.Modifiers()&tcell.ModCtrl == 0 {
window.SetCursorPos(pos)
}
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionStart: pos,
selectionEnd: window.CurrentBuffer.CursorPos,
}
return
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos + 1
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
}
// Prevent selecting dummy character at the end of the buffer
if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
@@ -199,46 +204,83 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if window.CurrentBuffer.Selection != nil {
// Unset selection
window.CurrentBuffer.Selection = nil
return
}
// Move cursor
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
}
} else if ev.Key() == tcell.KeyLeft {
if window.CursorMode == CursorModeBuffer {
// Get original cursor position
pos := window.CurrentBuffer.CursorPos
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to start of word
// Set variable to one character left of current position
startOfWord := pos - 1
if startOfWord < 0 {
startOfWord = 0
}
// Skip all spaces
for startOfWord >= 0 && len(window.CurrentBuffer.Contents) != 0 && unicode.IsSpace(rune(window.CurrentBuffer.Contents[startOfWord])) {
startOfWord--
}
// Find start of word
for startOfWord >= 0 && len(window.CurrentBuffer.Contents) != 0 && !unicode.IsSpace(rune(window.CurrentBuffer.Contents[startOfWord])) {
startOfWord--
}
// Move one character to the right
startOfWord++
window.SetCursorPos(startOfWord)
} else {
// Move cursor one character backwards
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
}
// Add to selection
if ev.Modifiers() == tcell.ModShift {
if ev.Modifiers()&tcell.ModShift != 0 {
if window.CurrentBuffer.Selection == nil {
// Cancel cursor movement when creating selection without holding ctrl
if ev.Modifiers()&tcell.ModCtrl == 0 {
window.SetCursorPos(pos)
}
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionStart: pos,
selectionEnd: window.CurrentBuffer.CursorPos,
}
return
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos - 1
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
}
} else if window.CurrentBuffer.Selection != nil {
// Unset selection
window.CurrentBuffer.Selection = nil
return
}
// Move cursor
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
}
} else if ev.Key() == tcell.KeyUp {
if window.CursorMode == CursorModeBuffer {
// Get original cursor position
pos := window.CurrentBuffer.CursorPos
// Move cursor
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y-1)
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to top of buffer
window.SetCursorPos(0)
} else {
// Move cursor one line up
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y-1)
}
// Add to selection
if ev.Modifiers() == tcell.ModShift {
if ev.Modifiers()&tcell.ModShift != 0 {
// Add to selection
if window.CurrentBuffer.Selection == nil {
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionEnd: pos,
selectionStart: pos,
selectionEnd: window.CurrentBuffer.CursorPos,
}
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
@@ -273,11 +315,18 @@ func (window *Window) input(ev *tcell.EventKey) {
if window.CursorMode == CursorModeBuffer {
// Get original cursor position
pos := window.CurrentBuffer.CursorPos
// Move cursor
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y+1)
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to bottom of buffer
window.SetCursorPos(len(window.CurrentBuffer.Contents))
} else {
// Move cursor one line down
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y+1)
}
// Add to selection
if ev.Modifiers() == tcell.ModShift {
if ev.Modifiers()&tcell.ModShift != 0 {
// Add to selection
if window.CurrentBuffer.Selection == nil {
window.CurrentBuffer.Selection = &Selection{
@@ -332,9 +381,9 @@ func (window *Window) input(ev *tcell.EventKey) {
}
// Check key bindings
for _, keybinding := range Keybinds {
if keybinding.IsPressed(ev) && slices.Index(keybinding.cursorModes, window.CursorMode) != -1 {
RunCommand(window, keybinding.command)
for _, keybinding := range Keybindings.Keybindings {
if keybinding.IsPressed(ev) && slices.Index(keybinding.GetCursorModes(), window.CursorMode) != -1 {
RunCommand(window, keybinding.Command)
return
}
}
@@ -345,7 +394,17 @@ func (window *Window) input(ev *tcell.EventKey) {
str := window.CurrentBuffer.Contents
index := window.CurrentBuffer.CursorPos
if index != 0 {
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
} else if index != 0 {
str = str[:index-1] + str[index:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
@@ -363,6 +422,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyTab {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@@ -376,6 +449,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyEnter {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@@ -399,6 +486,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyRune {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@@ -424,31 +525,105 @@ func (window *Window) input(ev *tcell.EventKey) {
}
}
func (window *Window) mouseInput(ev *tcell.EventMouse) {
func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
mouseX, mouseY := ev.Position()
bufferMouseX, bufferMouseY := window.AbsolutePosToBufferArea(mouseX, mouseY)
// Left click was pressed
if ev.Buttons() == tcell.Button1 {
// Get last click time
lastClickTime := time.UnixMilli(lastClick)
// Ensure click was in buffer area
x1, y1, x2, y2 := window.GetTextAreaDimensions()
if mouseX >= x1 && mouseY >= y1 && mouseX <= x2 && mouseY <= y2 {
currentX, currentY := window.GetCursorPos2D()
bufferMouseX, bufferMouseY := window.AbsolutePosToCursorPos2D(mouseX, mouseY)
if mouseHeld {
// Add to selection
if window.CurrentBuffer.Selection == nil {
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionEnd: window.CursorPos2DToCursorPos(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY),
selectionEnd: window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY),
}
// Set last click time
lastClick = time.Now().UnixMilli()
return
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY)
}
// Prevent selecting dummy character at the end of the buffer
if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
}
} else if currentX == bufferMouseX && currentY == bufferMouseY && window.CurrentBuffer.CursorPos < len(window.CurrentBuffer.Contents) && time.Since(lastClickTime).Milliseconds() < 300 {
selectedText := window.CurrentBuffer.GetSelectedText()
if window.CurrentBuffer.Selection == nil || strings.HasSuffix(selectedText, "\n") {
// Select word
startOfWord := window.CurrentBuffer.CursorPos
endOfWord := window.CurrentBuffer.CursorPos
// Find end of word
for i := window.CurrentBuffer.CursorPos + 1; i < len(window.CurrentBuffer.Contents); i++ {
currentRune := rune(window.CurrentBuffer.Contents[i])
if unicode.IsLetter(currentRune) || unicode.IsDigit(currentRune) || currentRune == '_' {
endOfWord++
} else {
break
}
}
// Find start of word
for i := window.CurrentBuffer.CursorPos - 1; i >= 0; i-- {
currentRune := rune(window.CurrentBuffer.Contents[i])
if unicode.IsLetter(currentRune) || unicode.IsDigit(currentRune) || currentRune == '_' {
startOfWord--
} else {
break
}
}
// Add to selection
window.CurrentBuffer.Selection = &Selection{
selectionStart: startOfWord,
selectionEnd: endOfWord,
}
} else {
// Select line
startOfLine := window.CurrentBuffer.CursorPos
endOfLine := window.CurrentBuffer.CursorPos
// Find end of line
for i := window.CurrentBuffer.CursorPos + 1; i < len(window.CurrentBuffer.Contents); i++ {
currentLetter := window.CurrentBuffer.Contents[i]
endOfLine++
if currentLetter == '\n' {
break
}
}
// Find start of line
for i := window.CurrentBuffer.CursorPos - 1; i >= 0; i-- {
currentLetter := window.CurrentBuffer.Contents[i]
if currentLetter != '\n' {
startOfLine--
} else {
break
}
}
// Add to selection
window.CurrentBuffer.Selection = &Selection{
selectionStart: startOfLine,
selectionEnd: endOfLine,
}
}
// Set last click time
lastClick = time.Now().UnixMilli()
return
} else {
// Clear selection
if window.CurrentBuffer.Selection != nil {
@@ -456,7 +631,10 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
}
}
// Move cursor
window.SetCursorPos2D(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
window.SetCursorPos2D(bufferMouseX, bufferMouseY)
// Set last click time
lastClick = time.Now().UnixMilli()
}
mouseHeld = true
} else if ev.Buttons() == tcell.ButtonNone {
@@ -467,8 +645,11 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
}
func (window *Window) Close() {
window.screen.Fini()
window.screen = nil
window.closed = true
err := window.screen.PostEvent(tcell.NewEventInterrupt(nil))
if err != nil {
return
}
}
func (window *Window) GetTextAreaDimensions() (int, int, int, int) {
@@ -531,12 +712,48 @@ func (window *Window) CursorPos2DToCursorPos(x, y int) int {
return lines[y].charIndex + x
}
func (window *Window) AbsolutePosToBufferArea(x, y int) (int, int) {
func (window *Window) AbsolutePosToCursorPos2D(x, y int) (int, int) {
x1, y1, _, _ := window.GetTextAreaDimensions()
x -= x1
y -= y1
x += window.CurrentBuffer.OffsetX
y += window.CurrentBuffer.OffsetY
if x < 0 {
x = 0
}
if y < 0 {
y = 0
}
split := strings.SplitAfter(window.CurrentBuffer.Contents+" ", "\n")
if y >= len(split) {
y = len(split) - 1
}
line := split[y]
posInLine := make([]int, 0)
for i, char := range []rune(line) {
if char == '\t' {
for j := 0; j < Config.TabIndentation; j++ {
posInLine = append(posInLine, i)
}
} else {
posInLine = append(posInLine, i)
}
}
if len(posInLine) == 0 {
x = 0
} else if x >= len(posInLine) {
x = posInLine[len(posInLine)-1]
} else {
x = posInLine[x]
}
return x, y
}