12 Commits
24 changed files with 945 additions and 238 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
+20
View File
@@ -0,0 +1,20 @@
# 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 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
```
+9
View File
@@ -0,0 +1,9 @@
# 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
buffer_info_message: "File: %f Cursor: (%x, %y, %p) Chars: %c"
tab_indentation: 4 # Length of tab characters
+40
View File
@@ -0,0 +1,40 @@
keybindings:
- keybinding: "Ctrl-Q"
cursor_modes: ["buffer"]
command: "quit"
- 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-R"
cursor_modes: ["buffer"]
command: "reload"
- 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"
+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

+94 -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
@@ -63,6 +126,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 ""
@@ -145,8 +220,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 +245,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 +259,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
}
+25 -19
View File
@@ -3,8 +3,8 @@ package main
import (
"fmt"
"log"
"maps"
"slices"
"strconv"
"strings"
)
@@ -140,15 +140,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 +159,50 @@ 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.")
},
}
+61
View File
@@ -0,0 +1,61 @@
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"`
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,
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
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ func drawLineIndex(window *Window) {
screen := window.screen
buffer := window.CurrentBuffer
lineIndexStyle := tcell.StyleDefault.Foreground(tcell.ColorDimGray).Background(tcell.Color235)
lineIndexStyle := tcell.StyleDefault.Background(CurrentStyle.LineIndexBg).Foreground(CurrentStyle.LineIndexFg)
_, sizeY := screen.Size()
+12 -6
View File
@@ -6,28 +6,34 @@ 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:]
}
}
}
+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()
+187
View File
@@ -0,0 +1,187 @@
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 AvailableStyles = make(map[string]TyperStyle)
var CurrentStyle TyperStyle
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) {
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[Config.SelectedStyle]; ok && slices.Index(availableTypes, style.StyleType) != -1 {
CurrentStyle = style
} else if style, ok := AvailableStyles[Config.FallbackStyle]; ok {
CurrentStyle = style
} else {
CurrentStyle = 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,
}
}
}
+56 -22
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,7 +50,13 @@ 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{"Copy", "Paste"}, 0, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "copy")
@@ -63,28 +74,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 +107,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 +122,38 @@ func drawTopMenu(window *Window) {
}
// Draw buffer info
bufferInfoMsg := getBufferInfoMsg(window)
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:]...)
}
}
+294 -90
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
@@ -28,11 +39,12 @@ type Window struct {
}
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 +53,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
@@ -56,7 +71,8 @@ func CreateWindow() (*Window, error) {
}
// Set screen style
screen.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234))
SetCurrentStyle(screen)
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
// Enable mouse
screen.EnableMouse()
@@ -70,58 +86,6 @@ func CreateWindow() (*Window, error) {
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 +102,7 @@ func (window *Window) Draw() {
// Draw current buffer
if window.CurrentBuffer != nil {
window.drawCurrentBuffer()
drawBuffer(window)
}
// Draw input bar
@@ -181,16 +145,47 @@ func (window *Window) Draw() {
func (window *Window) input(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 +194,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 +305,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 +371,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 +384,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 +412,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 +439,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 +476,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) {
@@ -426,29 +517,103 @@ func (window *Window) input(ev *tcell.EventKey) {
func (window *Window) mouseInput(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 +621,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 {
@@ -531,12 +699,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
}