19 Commits
Author SHA1 Message Date
EnumDev 6a701dd311 Switch from path to filepath package 2026-04-26 22:50:32 +03:00
EnumDev 5e6362074e Improve config locators for compatibility with Windows 2026-04-26 22:24:52 +03:00
EnumDev 040daebafb Update Makefile 2026-04-26 21:45:55 +03:00
EnumDev 2bf960b085 Switch buffer contents to string slice 2026-03-19 11:53:01 +02:00
EnumDev 1b5ce6b7f8 Fix cursor being at wrong position in input bar when default input is set 2025-06-22 18:46:33 +03:00
EnumDev 8d9de7b732 Fix panic when saving empty file 2025-06-22 18:44:40 +03:00
EnumDev ff087b196a Add build-time system config directory variable 2025-06-21 18:01:08 +03:00
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
16 changed files with 1057 additions and 548 deletions
+17 -7
View File
@@ -4,24 +4,34 @@ BINDIR ?= $(PREFIX)/bin
SYSCONFDIR := $(PREFIX)/etc
# Compilers and tools
GO ?= $(shell which go)
GO ?= go
# Compiler flags
GOOS ?= $(shell $(GO) env | grep '^GOOS' | cut -d'=' -f2 | tr -d "'")
GOARCH ?= $(shell $(GO) env | grep '^GOARCH' | cut -d'=' -f2 | tr -d "'")
LDFLAGS ?= -w
build:
mkdir -p build
cd src/; $(GO) build -ldflags "-w" -o ../build/typer
install -dm755 build
cd src/; GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build -ldflags "$(LDFLAGS) -X 'main.sysconfdir=$(SYSCONFDIR)'" -o ../build/
install: build/typer
# Create directories
install -dm755 $(DESTDIR)$(BINDIR)
# Install files
install -m755 build/typer* $(DESTDIR)$(BINDIR)/
install-config:
# Create directories
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
-rm -f $(DESTDIR)$(BINDIR)/typer
-rm -rf $(DESTDIR)$(SYSCONFDIR)/typer
clean:
rm -r build/
-rm -rf build/
.PHONY: build
.PHONY: build install install-config uninstall clean
+6 -2
View File
@@ -6,6 +6,10 @@
| ![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
@@ -16,5 +20,5 @@ make
```
- Run the following command **with superuser privileges** to install Typer to your system
```shell
make install SYSCONFDIR=/etc
```
make install
```
+2 -1
View File
@@ -5,5 +5,6 @@ selected_style_fallback: "default-fallback" # Style for 8-color capable terminal
# 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
tab_indentation: 4 # Length of tab characters
+14 -2
View File
@@ -2,6 +2,9 @@ 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"
@@ -14,9 +17,15 @@ keybindings:
- keybinding: "Ctrl-O"
cursor_modes: ["buffer"]
command: "open"
- keybinding: "Ctrl-R"
- 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"
@@ -37,4 +46,7 @@ keybindings:
command: "menu-edit"
- keybinding: "F3"
cursor_modes: ["buffer","dropdown"]
command: "menu-buffers"
command: "menu-buffers"
- keybinding: "Ctrl-E"
cursor_modes: ["buffer"]
command: "execute"
+415 -56
View File
@@ -2,18 +2,20 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"os"
"path/filepath"
"slices"
"strings"
"github.com/gdamore/tcell/v2"
)
type Buffer struct {
Name string
Contents string
Contents []string
CursorPos int
OffsetX, OffsetY int
CursorPos Position
Offset Position
Selection *Selection
@@ -22,8 +24,7 @@ type Buffer struct {
}
type Selection struct {
selectionStart int
selectionEnd int
selectionStart, selectionEnd Position
}
var Buffers = make([]*Buffer, 0)
@@ -53,43 +54,50 @@ func drawBuffer(window *Window) {
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)
for lineIndex, line := range buffer.Contents {
for runeIndex, r := range line + " " {
drawPosition := Position{runeIndex, lineIndex}
// Change background if under cursor
if i == buffer.CursorPos {
style = style.Background(CurrentStyle.BufferAreaSel)
}
if x-buffer.Offset.X >= bufferX && y-buffer.Offset.Y >= bufferY {
// Default style
style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg)
// Change background if selected
if buffer.Selection != nil {
if edge1, edge2 := buffer.GetSelectionEdges(); i >= edge1 && i <= edge2 {
// Change background if under cursor
if buffer.CursorPos.Equals(runeIndex, lineIndex) {
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)
// Change background if selected
if buffer.Selection != nil {
edge1, edge2 := buffer.GetSelectionEdges()
if ComparePositions(drawPosition, edge1) >= 0 && ComparePositions(drawPosition, edge2) <= 0 {
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.Offset.X, y-buffer.Offset.Y, r, nil, style)
}
}
}
}
window.screen.SetContent(x-buffer.Offset.X, y-buffer.Offset.Y, 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++
// Change position for next character
if r == '\t' {
x += int(Config.TabIndentation)
} else {
x++
}
}
// Draw new line
x = bufferX
y++
}
}
func (buffer *Buffer) Load() error {
@@ -98,12 +106,22 @@ 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
}
buffer.Contents = string(content)
buffer.Contents = strings.Split(string(content), "\n")
return nil
}
@@ -113,12 +131,23 @@ func (buffer *Buffer) Save() error {
return nil
}
// Append new line character at end of buffer contents if not present
if buffer.Contents[len(buffer.Contents)-1] != '\n' {
buffer.Contents += "\n"
// 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:])
}
err := os.WriteFile(buffer.filename, []byte(buffer.Contents), 0644)
// Add newline at the end of buffer Contents
line := buffer.Contents[len(buffer.Contents)-1]
if len(line) != 0 {
buffer.Contents = append(buffer.Contents, "")
}
err := os.WriteFile(buffer.filename, []byte(buffer.GetContentsAsString()), 0644)
if err != nil {
return err
}
@@ -126,12 +155,60 @@ func (buffer *Buffer) Save() error {
return nil
}
func (buffer *Buffer) GetSelectionEdges() (int, int) {
if buffer.Selection == nil {
return -1, -1
func (buffer *Buffer) GetContentsAsString() string {
finalText := strings.Builder{}
for i, line := range buffer.Contents {
for _, rune := range line {
finalText.WriteRune(rune)
}
if i != len(buffer.Contents)-1 {
finalText.WriteRune('\n')
}
}
if buffer.Selection.selectionStart < buffer.Selection.selectionEnd {
return finalText.String()
}
func (buffer *Buffer) PositionToAbsolutePosition(position Position) int {
i := 0
for lineIndex, line := range buffer.Contents {
if len(line) == 0 {
line += " "
}
for runeIndex, _ := range line + " " {
if position.Equals(runeIndex, lineIndex) {
return i
}
i++
}
}
return i
}
func (buffer *Buffer) AbsolutePositionToPosition(absolutePosition int) Position {
i := 0
for lineIndex, line := range buffer.Contents {
for runeIndex, _ := range line + " " {
if i == absolutePosition {
return Position{runeIndex, lineIndex}
}
i++
}
}
lastLine := buffer.Contents[len(buffer.Contents)-1]
return Position{len(buffer.Contents) - 1, len(lastLine) - 1}
}
func (buffer *Buffer) GetSelectionEdges() (Position, Position) {
if buffer.Selection == nil {
return Position{-1, -1}, Position{-1, -1}
}
if ComparePositions(buffer.Selection.selectionStart, buffer.Selection.selectionEnd) == -1 {
return buffer.Selection.selectionStart, buffer.Selection.selectionEnd
} else {
return buffer.Selection.selectionEnd, buffer.Selection.selectionStart
@@ -142,28 +219,310 @@ func (buffer *Buffer) GetSelectedText() string {
if buffer.Selection == nil {
return ""
}
if len(buffer.Contents) == 0 {
return ""
}
start := buffer.Selection.selectionStart
end := buffer.Selection.selectionEnd
edge1, edge2 := buffer.GetSelectionEdges()
if start >= len(buffer.Contents) {
start = len(buffer.Contents) - 1
}
if end >= len(buffer.Contents) {
end = len(buffer.Contents) - 1
selectedText := strings.Builder{}
if r := buffer.GetCharAtPosition(edge1); r != 0 {
selectedText.WriteRune(r)
}
if start <= end {
return buffer.Contents[start : end+1]
for ComparePositions(edge1, edge2) < 0 {
edge1.X++
if edge1.X > len(buffer.Contents[edge1.Y]) {
if edge1.Y < len(buffer.Contents) {
edge1.Y++
edge1.X = 0
} else {
edge1.X = len(buffer.Contents[edge1.Y])
break
}
}
if r := buffer.GetCharAtPosition(edge1); r != 0 {
selectedText.WriteRune(buffer.GetCharAtPosition(edge1))
}
}
return selectedText.String()
}
func (buffer *Buffer) CutText(window *Window) (string, int) {
if buffer.Selection == nil {
// Cut current line
cutText := buffer.Contents[buffer.CursorPos.Y] + "\n"
// Remove line from buffer contents
if len(buffer.Contents) == 1 {
buffer.Contents[0] = ""
} else {
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y, buffer.CursorPos.Y+1)
}
buffer.CursorPos.Y -= 1
if buffer.CursorPos.Y < 0 {
buffer.CursorPos = Position{0, 0}
}
return cutText, 0
} else {
return buffer.Contents[end : start+1]
// Cut selection
cutText := buffer.GetSelectedText()
// Remove selected text
_, edge2 := buffer.GetSelectionEdges()
buffer.CursorPos = edge2
buffer.Delete(len(cutText))
// Remove selection
buffer.Selection = nil
return cutText, 1
}
}
func (buffer *Buffer) CopyText() (string, int) {
if buffer.Selection == nil {
// Cut current line
copiedText := buffer.Contents[buffer.CursorPos.Y] + "\n"
return copiedText, 0
} else {
// Copy selection
return buffer.GetSelectedText(), 1
}
}
func (buffer *Buffer) PasteText(window *Window, text string) {
contents := buffer.GetContentsAsString()
// Remove selected text
if buffer.Selection != nil {
edge1, edge2 := buffer.GetSelectionEdges()
absEdge1 := buffer.PositionToAbsolutePosition(edge1)
absEdge2 := buffer.PositionToAbsolutePosition(edge2)
if absEdge2 == len(buffer.Contents) {
absEdge2 = len(buffer.Contents) - 1
}
contents = contents[:absEdge1] + contents[absEdge2+1:]
buffer.Contents = strings.Split(contents, "\n")
buffer.CursorPos = buffer.AbsolutePositionToPosition(absEdge1)
buffer.Selection = nil
}
buffer.WriteString(text)
}
func (buffer *Buffer) FindSubstring(substring string, afterPos Position) Position {
// Return no match if afterPos is larger than the buffer contents size
contents := buffer.GetContentsAsString()
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
if absAfterPos >= len(contents) {
return Position{-1, -1}
}
index := strings.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += absAfterPos + 1
}
return buffer.AbsolutePositionToPosition(index)
}
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement string, afterPos Position) Position {
// Return no match if afterPos is larger than the buffer contents size
contents := buffer.GetContentsAsString()
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
if absAfterPos >= len(contents) {
return Position{-1, -1}
}
index := strings.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += absAfterPos + 1
}
// Replace substring with replacement string
contents = contents[:index] + replacement + contents[index+len(substring):]
buffer.Contents = strings.Split(contents, "\n")
return buffer.AbsolutePositionToPosition(index)
}
func (buffer *Buffer) FindAndReplaceAll(substring, replacement string) int {
replacements := 0
position := Position{}
for position.X != -1 && position.Y != -1 {
position = buffer.FindAndReplaceSubstring(substring, replacement, position)
if position.X != -1 && position.Y != -1 {
replacements++
}
}
return replacements
}
func (buffer *Buffer) MoveUp(i int) bool {
buffer.CursorPos.Y -= i
if buffer.CursorPos.Y < 0 {
buffer.CursorPos.Y = 0
return false
}
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
}
return true
}
func (buffer *Buffer) MoveDown(i int) bool {
buffer.CursorPos.Y += i
if buffer.CursorPos.Y >= len(buffer.Contents) {
buffer.CursorPos.Y = len(buffer.Contents) - 1
return false
}
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
}
return true
}
func (buffer *Buffer) MoveLeft(i int) bool {
remainingSteps := i
for remainingSteps > 0 {
buffer.CursorPos.X--
if buffer.CursorPos.X < 0 {
if buffer.CursorPos.Y > 0 {
buffer.CursorPos.Y--
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
} else {
buffer.CursorPos.X = 0
return false
}
}
remainingSteps--
}
return true
}
func (buffer *Buffer) MoveRight(i int) bool {
remainingSteps := i
for remainingSteps > 0 {
buffer.CursorPos.X++
if buffer.CursorPos.X > len(buffer.Contents[buffer.CursorPos.Y]) {
if buffer.CursorPos.Y < len(buffer.Contents)-1 {
buffer.CursorPos.Y++
buffer.CursorPos.X = 0
} else {
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
return false
}
}
remainingSteps--
}
return true
}
func (buffer *Buffer) WriteRune(r rune) {
if r == '\n' {
if buffer.CursorPos.Y == len(buffer.Contents) {
buffer.Contents = append(buffer.Contents, "")
} else {
buffer.Contents = slices.Insert(buffer.Contents, buffer.CursorPos.Y+1, "")
}
// Move line content after cursor X to the new line
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y+1] = line[buffer.CursorPos.X:] + buffer.Contents[buffer.CursorPos.Y+1]
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X]
buffer.MoveDown(1)
buffer.CursorPos.X = 0
} else {
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X] + string(r) + line[buffer.CursorPos.X:]
buffer.MoveRight(1)
}
}
func (buffer *Buffer) WriteString(str string) {
for _, r := range str {
buffer.WriteRune(r)
}
}
func (buffer *Buffer) Delete(i int) bool {
remainingSteps := i
for remainingSteps > 0 {
buffer.CursorPos.X--
if buffer.CursorPos.X < 0 {
if buffer.CursorPos.Y > 0 {
// Save deleted line text
deletedLine := buffer.Contents[buffer.CursorPos.Y]
buffer.CursorPos.Y--
// Delete line
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y+1, buffer.CursorPos.Y+2)
// Append deleted line text to end of current line
buffer.Contents[buffer.CursorPos.Y] += deletedLine
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y]) - len(deletedLine)
} else {
buffer.CursorPos.X = 0
return false
}
} else {
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X] + line[buffer.CursorPos.X+1:]
}
remainingSteps--
}
return true
}
func (buffer *Buffer) GetCharAtPosition(position Position) rune {
if position.Y < 0 || position.Y >= len(buffer.Contents) {
return 0
}
line := buffer.Contents[position.Y]
if position.X == len(line) {
// Do not return newline for last line if it's empty
if position.Y == len(buffer.Contents)-1 && len(line) == 0 {
return 0
}
return '\n'
} else if position.X < 0 || position.X > len(line) {
return 0
}
return rune(line[position.X])
}
func GetOpenFileBuffer(filename string) *Buffer {
// Replace tilde with home directory
if filename != "~" && strings.HasPrefix(filename, "~/") {
@@ -230,8 +589,8 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
buffer := Buffer{
Name: filename,
Contents: "",
CursorPos: 0,
Contents: make([]string, 1),
CursorPos: Position{0, 0},
canSave: true,
filename: abs,
}
@@ -253,8 +612,8 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
func CreateBuffer(bufferName string) (*Buffer, error) {
buffer := Buffer{
Name: bufferName,
Contents: "",
CursorPos: 0,
Contents: make([]string, 1),
CursorPos: Position{0, 0},
canSave: true,
filename: "",
}
+278 -17
View File
@@ -18,18 +18,52 @@ var commands = make(map[string]*Command)
func initCommands() {
// Setup commands
selectAll := Command{
cmd: "select-all",
run: func(window *Window, args ...string) {
// Select entire buffer content
lastLine := window.CurrentBuffer.Contents[len(window.CurrentBuffer.Contents)-1]
window.CurrentBuffer.Selection = &Selection{
selectionStart: Position{0, 0},
selectionEnd: Position{len(window.CurrentBuffer.Contents) - 1, len(lastLine) - 1},
}
PrintMessage(window, "Selected all text.")
},
}
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.")
PrintMessage(window, "Copied selection to clipboard. ")
}
},
}
@@ -37,16 +71,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))
},
}
@@ -128,11 +156,134 @@ func initCommands() {
log.Fatalf("Could not reload buffer: %s", err)
}
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, "Buffer reloaded.")
},
}
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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = 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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = 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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = 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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = 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 {
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 {
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) {
@@ -206,6 +357,66 @@ func initCommands() {
},
}
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))
}
}()
},
}
menuFileCmd := Command{
cmd: "menu-file",
run: func(window *Window, args ...string) {
@@ -250,20 +461,70 @@ 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["select-all"] = &selectAll
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 {
+32 -14
View File
@@ -1,10 +1,12 @@
package main
import (
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
)
type TyperConfig struct {
@@ -12,6 +14,7 @@ type TyperConfig struct {
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"`
}
@@ -24,6 +27,7 @@ func readConfig() {
FallbackStyle: "default-fallback",
ShowTopMenu: true,
ShowLineIndex: true,
ExtendLineIndex: false,
BufferInfoMessage: "File: %f Cursor: (%x, %y, %p) Chars: %c",
TabIndentation: 4,
}
@@ -33,25 +37,39 @@ func readConfig() {
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"))
execPath, err := os.Executable()
if err != nil {
log.Fatalf("Could not get path to executable: %s", err)
}
configPaths := make([]string, 0)
if runtime.GOOS == "windows" {
configPaths = append(configPaths, filepath.Join(homeDir, "AppData/Roaming/Typer/config.yml"))
configPaths = append(configPaths, filepath.Join(filepath.Dir(execPath), "etc/typer/config.yml"))
} else {
configPaths = append(configPaths, filepath.Join(homeDir, ".config/typer/config.yml"))
configPaths = append(configPaths, filepath.Join(sysconfdir, "typer/config.yml"))
}
for _, configPath := range configPaths {
// Ensure config exists at path
if _, err := os.Stat(configPath); err != nil {
continue
}
// Read config file
data, err := os.ReadFile(configPath)
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
// Unmarshal contents into struct
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()
break
}
// Validate config options
+1 -1
View File
@@ -1,4 +1,4 @@
module Typer
module typer
go 1.24
+1 -1
View File
@@ -18,7 +18,7 @@ func RequestInput(window *Window, text string, defaultInput string) chan string
request := &TyperInputRequest{
Text: text,
input: defaultInput,
cursorPos: 0,
cursorPos: len(defaultInput),
inputChannel: make(chan string),
}
+31 -15
View File
@@ -1,12 +1,14 @@
package main
import (
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
)
type TyperKeybindings struct {
@@ -31,25 +33,39 @@ func readKeybindings() {
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"))
execPath, err := os.Executable()
if err != nil {
log.Fatalf("Could not get path to executable: %s", err)
}
configPaths := make([]string, 0)
if runtime.GOOS == "windows" {
configPaths = append(configPaths, filepath.Join(homeDir, "AppData/Roaming/Typer/keybindings.yml"))
configPaths = append(configPaths, filepath.Join(filepath.Dir(execPath), "etc/typer/keybindings.yml"))
} else {
configPaths = append(configPaths, filepath.Join(homeDir, ".config/typer/keybindings.yml"))
configPaths = append(configPaths, filepath.Join(sysconfdir, "typer/keybindings.yml"))
}
for _, configPath := range configPaths {
// Ensure config exists at path
if _, err := os.Stat(configPath); err != nil {
continue
}
// Read config file
data, err := os.ReadFile(configPath)
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
// Unmarshal contents into struct
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()
break
}
}
+19 -12
View File
@@ -1,9 +1,9 @@
package main
import (
"github.com/gdamore/tcell/v2"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
)
func drawLineIndex(window *Window) {
@@ -12,16 +12,22 @@ func drawLineIndex(window *Window) {
lineIndexStyle := tcell.StyleDefault.Background(CurrentStyle.LineIndexBg).Foreground(CurrentStyle.LineIndexFg)
_, sizeY := screen.Size()
y := 0
if window.ShowTopMenu {
y = 1
}
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.Offset.Y
for y := bufferY1; y <= bufferY2; y++ {
if lineIndex > len(buffer.Contents) {
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,12 +36,13 @@ func drawLineIndex(window *Window) {
text := strconv.Itoa(lineIndex)
drawText(screen, lineIndexSize-len(text)-1, y, lineIndexSize, y, lineIndexStyle, text)
y++
lineIndex++
}
}
func getLineIndexSize(window *Window) int {
i := strings.Count(window.CurrentBuffer.Contents, "\n") + 1
i := len(window.CurrentBuffer.Contents)
if i == 0 {
return 4
}
+6 -1
View File
@@ -5,6 +5,8 @@ import (
"os"
)
var sysconfdir = "/etc/"
func main() {
// Read config
readConfig()
@@ -38,8 +40,11 @@ func main() {
}
}
for window.screen != nil {
for !window.closed {
window.Draw()
window.ProcessEvents()
}
window.screen.Fini()
window.screen = nil
}
+24
View File
@@ -0,0 +1,24 @@
package main
type Position struct {
X int
Y int
}
func (position *Position) Equals(x, y int) bool {
return position.X == x && position.Y == y
}
func ComparePositions(pos1, pos2 Position) int {
if pos1.Y < pos2.Y {
return -1
} else if pos1.Y > pos2.Y {
return 1
} else if pos1.X < pos2.X {
return -1
} else if pos1.X > pos2.X {
return 1
} else {
return 0
}
}
+60 -48
View File
@@ -2,15 +2,17 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
)
type TyperStyle struct {
@@ -46,8 +48,29 @@ type typerStyleYaml struct {
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 TyperStyle
var CurrentStyle = FallbackStyle
func readStyles() {
homeDir, err := os.UserHomeDir()
@@ -55,36 +78,42 @@ func readStyles() {
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
}
}
execPath, err := os.Executable()
if err != nil {
log.Fatalf("Could not get path to executable: %s", err)
}
if stat, err := os.Stat("/etc/typer/styles/"); err == nil && stat.IsDir() {
entries, err := os.ReadDir("/etc/typer/styles/")
stylesPaths := make([]string, 0)
if runtime.GOOS == "windows" {
stylesPaths = append(stylesPaths, filepath.Join(homeDir, "AppData/Roaming/Typer/styles"))
stylesPaths = append(stylesPaths, filepath.Join(filepath.Dir(execPath), "etc/typer/styles"))
} else {
stylesPaths = append(stylesPaths, filepath.Join(homeDir, ".config/typer/styles"))
stylesPaths = append(stylesPaths, filepath.Join(sysconfdir, "typer/styles"))
}
for _, stylesPath := range stylesPaths {
// Ensure directory exists at path
if stat, err := os.Stat(stylesPath); err != nil || !stat.IsDir() {
fmt.Println(stylesPath)
continue
}
// Get directory entries
entries, err := os.ReadDir(stylesPath)
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
// Read entries in directory
for _, entry := range entries {
entryPath := path.Join("/etc/typer/styles/", entry.Name())
entryPath := filepath.Join(stylesPath, 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
}
@@ -145,7 +174,7 @@ func readStyleYamlFile(filepath string) (TyperStyle, error) {
return style, nil
}
func SetCurrentStyle(screen tcell.Screen) {
func SetCurrentStyle(screen tcell.Screen, styleName string) bool {
availableTypes := make([]string, 1)
availableTypes[0] = "8-color"
if screen.Colors() >= 16 {
@@ -158,30 +187,13 @@ func SetCurrentStyle(screen tcell.Screen) {
availableTypes = append(availableTypes, "true-color")
}
if style, ok := AvailableStyles[Config.SelectedStyle]; ok && slices.Index(availableTypes, style.StyleType) != -1 {
if style, ok := AvailableStyles[styleName]; 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,
}
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
screen.Sync()
return true
}
return false
}
+15 -14
View File
@@ -2,10 +2,11 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"path/filepath"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
)
type TopMenuButton struct {
@@ -56,11 +57,13 @@ func initTopMenu() {
y++
}
d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, y, 0, func(i int) {
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()
@@ -123,7 +126,9 @@ func drawTopMenu(window *Window) {
// Draw buffer info
bufferInfoMsg := getBufferInfoMsg(window)
drawText(screen, sizeX-len(bufferInfoMsg)-1, 0, sizeX-1, 0, topMenuStyle, bufferInfoMsg)
if sizeX-len(bufferInfoMsg)-1 > currentX+2 {
drawText(screen, sizeX-len(bufferInfoMsg)-1, 0, sizeX-1, 0, topMenuStyle, bufferInfoMsg)
}
}
func getBufferInfoMsg(window *Window) string {
@@ -136,22 +141,18 @@ func getBufferInfoMsg(window *Window) string {
filename = filepath.Base(window.CurrentBuffer.filename)
}
cursorPos := window.CurrentBuffer.CursorPos
cursorX, cursorY := window.GetCursorPos2D()
cursorX++
cursorY++
chars := len(window.CurrentBuffer.Contents)
words := len(strings.Fields(window.CurrentBuffer.Contents))
contents := window.CurrentBuffer.GetContentsAsString()
chars := len(contents)
words := len(strings.Fields(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, "%x", strconv.Itoa(window.CurrentBuffer.CursorPos.X+1))
ret = strings.ReplaceAll(ret, "%y", strconv.Itoa(window.CurrentBuffer.CursorPos.Y+1))
ret = strings.ReplaceAll(ret, "%p", strconv.Itoa(window.CurrentBuffer.PositionToAbsolutePosition(window.CurrentBuffer.CursorPos)+1))
ret = strings.ReplaceAll(ret, "%c", strconv.Itoa(chars))
ret = strings.ReplaceAll(ret, "%w", strconv.Itoa(words))
+136 -357
View File
@@ -1,13 +1,14 @@
package main
import (
"github.com/gdamore/tcell/v2"
"log"
"slices"
"strconv"
"strings"
"time"
"unicode"
"github.com/gdamore/tcell/v2"
)
type CursorMode uint8
@@ -36,6 +37,8 @@ type Window struct {
CurrentBuffer *Buffer
screen tcell.Screen
closed bool
}
var mouseHeld = false
@@ -70,16 +73,22 @@ func CreateWindow() (*Window, error) {
log.Fatalf("Failed to initialize screen: %s", err)
}
// Set screen style
SetCurrentStyle(screen)
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
// 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()
@@ -90,6 +99,9 @@ func (window *Window) Draw() {
// Clear screen
window.screen.Clear()
// Sync buffer offset
window.SyncBufferOffset()
// Draw top menu
if window.ShowTopMenu {
drawTopMenu(window)
@@ -126,7 +138,9 @@ func (window *Window) Draw() {
// Update screen
window.screen.Show()
}
func (window *Window) ProcessEvents() {
// Poll event
ev := window.screen.PollEvent()
@@ -136,13 +150,13 @@ 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
@@ -151,25 +165,24 @@ func (window *Window) input(ev *tcell.EventKey) {
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)
}
window.CurrentBuffer.MoveRight(1)
// Skip all spaces
for endOfWord < len(window.CurrentBuffer.Contents) && unicode.IsSpace(rune(window.CurrentBuffer.Contents[endOfWord])) {
endOfWord++
for unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
if !window.CurrentBuffer.MoveRight(1) {
break
}
}
// Find end of word
for endOfWord < len(window.CurrentBuffer.Contents) && !unicode.IsSpace(rune(window.CurrentBuffer.Contents[endOfWord])) {
endOfWord++
for !unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
if !window.CurrentBuffer.MoveRight(1) {
break
}
}
window.SetCursorPos(endOfWord)
} else {
// Move cursor one character backwards
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
// Move cursor one character forwards
window.CurrentBuffer.MoveRight(1)
}
// Add to selection
@@ -177,7 +190,7 @@ func (window *Window) input(ev *tcell.EventKey) {
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.CursorPos = pos
}
window.CurrentBuffer.Selection = &Selection{
@@ -187,10 +200,6 @@ func (window *Window) input(ev *tcell.EventKey) {
} else {
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) {
window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
}
} else if window.CurrentBuffer.Selection != nil {
// Unset selection
window.CurrentBuffer.Selection = nil
@@ -204,28 +213,33 @@ func (window *Window) input(ev *tcell.EventKey) {
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
}
window.CurrentBuffer.MoveLeft(1)
// Skip all spaces
for startOfWord >= 0 && len(window.CurrentBuffer.Contents) != 0 && unicode.IsSpace(rune(window.CurrentBuffer.Contents[startOfWord])) {
startOfWord--
for unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
if !window.CurrentBuffer.MoveLeft(1) {
break
}
}
// Find start of word
for startOfWord >= 0 && len(window.CurrentBuffer.Contents) != 0 && !unicode.IsSpace(rune(window.CurrentBuffer.Contents[startOfWord])) {
startOfWord--
// Find end of word
for {
char := window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)
if char == 0 || unicode.IsSpace(char) {
break
}
if !window.CurrentBuffer.MoveLeft(1) {
break
}
}
// Move one character to the right
startOfWord++
window.SetCursorPos(startOfWord)
// Move one character to the right if not selecting
if window.CurrentBuffer.CursorPos.X != 0 {
window.CurrentBuffer.MoveRight(1)
}
} else {
// Move cursor one character backwards
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
window.CurrentBuffer.MoveLeft(1)
}
// Add to selection
@@ -233,7 +247,7 @@ func (window *Window) input(ev *tcell.EventKey) {
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.CursorPos = pos
}
window.CurrentBuffer.Selection = &Selection{
@@ -257,11 +271,11 @@ func (window *Window) input(ev *tcell.EventKey) {
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to top of buffer
window.SetCursorPos(0)
window.CurrentBuffer.CursorPos.X = 0
window.CurrentBuffer.CursorPos.Y = 0
} else {
// Move cursor one line up
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y-1)
window.CurrentBuffer.MoveUp(1)
}
// Add to selection
@@ -308,11 +322,11 @@ func (window *Window) input(ev *tcell.EventKey) {
if ev.Modifiers()&tcell.ModCtrl != 0 {
// Move cursor to bottom of buffer
window.SetCursorPos(len(window.CurrentBuffer.Contents))
window.CurrentBuffer.CursorPos.Y = len(window.CurrentBuffer.Contents) - 1
window.CurrentBuffer.CursorPos.X = len(window.CurrentBuffer.Contents[window.CurrentBuffer.CursorPos.Y])
} else {
// Move cursor one line down
x, y := window.GetCursorPos2D()
window.SetCursorPos2D(x, y+1)
window.CurrentBuffer.MoveDown(1)
}
// Add to selection
@@ -327,9 +341,9 @@ func (window *Window) input(ev *tcell.EventKey) {
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) {
window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
}
//if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
// window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
//}
} else if window.CurrentBuffer.Selection != nil {
// Unset selection
window.CurrentBuffer.Selection = nil
@@ -379,25 +393,12 @@ func (window *Window) input(ev *tcell.EventKey) {
}
// Typing
if ev.Key() == tcell.KeyBackspace2 {
if ev.Key() == tcell.KeyBackspace || ev.Key() == tcell.KeyBackspace2 {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
index := window.CurrentBuffer.CursorPos
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)
window.CurrentBuffer.CutText(window)
} else {
window.CurrentBuffer.Delete(1)
}
} else if window.CursorMode == CursorModeInputBar {
str := currentInputRequest.input
@@ -411,57 +412,21 @@ 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
window.CurrentBuffer.CutText(window)
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
str += "\t"
} else {
str = str[:index] + "\t" + str[index:]
}
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
window.CurrentBuffer.WriteRune('\t')
}
} 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
window.CurrentBuffer.CutText(window)
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
str += "\n"
} else {
str = str[:index] + "\n" + str[index:]
}
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
window.CurrentBuffer.WriteRune('\n')
} else if window.CursorMode == CursorModeInputBar {
if currentInputRequest.input == "" && slices.Index(inputHistory, currentInputRequest.input) == -1 {
inputHistory = append(inputHistory, currentInputRequest.input)
@@ -475,30 +440,12 @@ 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
window.CurrentBuffer.CutText(window)
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
str += string(ev.Rune())
} else {
str = str[:index] + string(ev.Rune()) + str[index:]
}
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
window.CurrentBuffer.WriteRune(ev.Rune())
} else if window.CursorMode == CursorModeInputBar {
str := currentInputRequest.input
index := currentInputRequest.cursorPos
@@ -515,7 +462,7 @@ 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()
// Left click was pressed
@@ -525,14 +472,44 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
// 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)
currentPos := window.CurrentBuffer.CursorPos
mouseBufferPos := Position{mouseX + window.CurrentBuffer.Offset.X - x1, mouseY + window.CurrentBuffer.Offset.Y - y1}
// Keep mouse Y in bounds
if mouseBufferPos.Y >= len(window.CurrentBuffer.Contents) {
mouseBufferPos.Y = len(window.CurrentBuffer.Contents) - 1
}
// Offset mouse X for each tab character in line
posInLine := make([]int, 0)
for i, r := range window.CurrentBuffer.Contents[mouseBufferPos.Y] + " " {
if r == '\t' {
for j := 0; j < Config.TabIndentation; j++ {
posInLine = append(posInLine, i)
}
} else {
posInLine = append(posInLine, i)
}
}
if len(posInLine) == 0 {
mouseBufferPos.X = 0
} else if mouseBufferPos.X >= len(posInLine) {
mouseBufferPos.X = posInLine[len(posInLine)-1]
} else {
mouseBufferPos.X = posInLine[mouseBufferPos.X]
}
// Keep mouse X in bounds
if mouseBufferPos.X > len(window.CurrentBuffer.Contents[mouseBufferPos.Y]) {
mouseBufferPos.X = len(window.CurrentBuffer.Contents[mouseBufferPos.Y])
}
if mouseHeld {
// Add to selection
if window.CurrentBuffer.Selection == nil {
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionEnd: window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY),
selectionEnd: mouseBufferPos,
}
// Set last click time
@@ -540,22 +517,19 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
return
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY)
window.CurrentBuffer.Selection.selectionEnd = mouseBufferPos
}
// 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 {
} else if currentPos == mouseBufferPos && 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
cursorPos := window.CurrentBuffer.CursorPos
startOfWord := window.CurrentBuffer.CursorPos.X
endOfWord := window.CurrentBuffer.CursorPos.X
// Find end of word
for i := window.CurrentBuffer.CursorPos + 1; i < len(window.CurrentBuffer.Contents); i++ {
currentRune := rune(window.CurrentBuffer.Contents[i])
for i := cursorPos.X + 1; i < len(window.CurrentBuffer.Contents[cursorPos.Y]); i++ {
currentRune := rune(window.CurrentBuffer.Contents[cursorPos.Y][i])
if unicode.IsLetter(currentRune) || unicode.IsDigit(currentRune) || currentRune == '_' {
endOfWord++
} else {
@@ -564,8 +538,8 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
}
// Find start of word
for i := window.CurrentBuffer.CursorPos - 1; i >= 0; i-- {
currentRune := rune(window.CurrentBuffer.Contents[i])
for i := cursorPos.X - 1; i >= 0; i-- {
currentRune := rune(window.CurrentBuffer.Contents[cursorPos.Y][i])
if unicode.IsLetter(currentRune) || unicode.IsDigit(currentRune) || currentRune == '_' {
startOfWord--
} else {
@@ -575,38 +549,17 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
// Add to selection
window.CurrentBuffer.Selection = &Selection{
selectionStart: startOfWord,
selectionEnd: endOfWord,
selectionStart: Position{startOfWord, cursorPos.Y},
selectionEnd: Position{endOfWord, cursorPos.Y},
}
} 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
}
}
cursorPos := window.CurrentBuffer.CursorPos
// Add to selection
window.CurrentBuffer.Selection = &Selection{
selectionStart: startOfLine,
selectionEnd: endOfLine,
selectionStart: Position{0, cursorPos.Y},
selectionEnd: Position{len(window.CurrentBuffer.Contents[cursorPos.Y]), cursorPos.Y},
}
}
@@ -621,7 +574,7 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
}
}
// Move cursor
window.SetCursorPos2D(bufferMouseX, bufferMouseY)
window.CurrentBuffer.CursorPos = mouseBufferPos
// Set last click time
lastClick = time.Now().UnixMilli()
@@ -635,8 +588,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) {
@@ -654,196 +610,19 @@ func (window *Window) GetTextAreaDimensions() (int, int, int, int) {
return x1, y1, x2 - 1, y2 - 2
}
func (window *Window) CursorPos2DToCursorPos(x, y int) int {
// Ensure x and y are positive
x = max(x, 0)
y = max(y, 0)
// Set cursor position to 0 buffer is empty
if len(window.CurrentBuffer.Contents) == 0 {
return 0
}
// Create line slice from buffer contents
lines := make([]struct {
charIndex int
str string
}, 0)
var str string
for i, char := range window.CurrentBuffer.Contents {
str += string(char)
if char == '\n' || i == len(window.CurrentBuffer.Contents)-1 {
lines = append(lines, struct {
charIndex int
str string
}{charIndex: i - len(str) + 1, str: str})
str = ""
}
}
// Append extra character or line
if window.CurrentBuffer.Contents[len(window.CurrentBuffer.Contents)-1] == '\n' {
lines = append(lines, struct {
charIndex int
str string
}{charIndex: len(window.CurrentBuffer.Contents), str: " "})
} else {
lines[len(lines)-1].str += " "
}
// Limit x and y
y = min(y, len(lines)-1)
x = min(x, len(lines[y].str)-1)
return lines[y].charIndex + x
}
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
}
func (window *Window) GetAbsoluteCursorPos() (int, int) {
cursorX, cursorY := window.GetCursorPos2D()
x1, y1, _, _ := window.GetTextAreaDimensions()
cursorX += x1
cursorY += y1
return cursorX, cursorY
}
func (window *Window) GetCursorPos2D() (int, int) {
cursorX := 0
cursorY := 0
for i := 0; i < window.CurrentBuffer.CursorPos; i++ {
char := window.CurrentBuffer.Contents[i]
if char == '\n' {
cursorY++
cursorX = 0
} else {
cursorX++
}
}
return cursorX, cursorY
}
func (window *Window) SetCursorPos(position int) {
window.CurrentBuffer.CursorPos = position
if window.CurrentBuffer.CursorPos < 0 {
window.CurrentBuffer.CursorPos = 0
}
if window.CurrentBuffer.CursorPos > len(window.CurrentBuffer.Contents) {
window.CurrentBuffer.CursorPos = len(window.CurrentBuffer.Contents)
}
window.SyncBufferOffset()
}
func (window *Window) SetCursorPos2D(x, y int) {
// Ensure x and y are positive
x = max(x, 0)
y = max(y, 0)
// Set cursor position to 0 buffer is empty
if len(window.CurrentBuffer.Contents) == 0 {
window.SetCursorPos(0)
return
}
// Create line slice from buffer contents
lines := make([]struct {
charIndex int
str string
}, 0)
var str string
for i, char := range window.CurrentBuffer.Contents {
str += string(char)
if char == '\n' || i == len(window.CurrentBuffer.Contents)-1 {
lines = append(lines, struct {
charIndex int
str string
}{charIndex: i - len(str) + 1, str: str})
str = ""
}
}
// Append extra character or line
if window.CurrentBuffer.Contents[len(window.CurrentBuffer.Contents)-1] == '\n' {
lines = append(lines, struct {
charIndex int
str string
}{charIndex: len(window.CurrentBuffer.Contents), str: " "})
} else {
lines[len(lines)-1].str += " "
}
// Limit x and y
y = min(y, len(lines)-1)
x = min(x, len(lines[y].str)-1)
window.SetCursorPos(lines[y].charIndex + x)
}
func (window *Window) SyncBufferOffset() {
x, y := window.GetCursorPos2D()
cursorPos := window.CurrentBuffer.CursorPos
bufferX1, bufferY1, bufferX2, bufferY2 := window.GetTextAreaDimensions()
if y < window.CurrentBuffer.OffsetY {
window.CurrentBuffer.OffsetY = y
} else if y > window.CurrentBuffer.OffsetY+(bufferY2-bufferY1) {
window.CurrentBuffer.OffsetY = y - (bufferY2 - bufferY1)
if cursorPos.Y < window.CurrentBuffer.Offset.Y {
window.CurrentBuffer.Offset.Y = cursorPos.Y
} else if cursorPos.Y > window.CurrentBuffer.Offset.Y+(bufferY2-bufferY1) {
window.CurrentBuffer.Offset.Y = cursorPos.Y - (bufferY2 - bufferY1)
}
if x < window.CurrentBuffer.OffsetX {
window.CurrentBuffer.OffsetX = x
} else if x > window.CurrentBuffer.OffsetX+(bufferX2-bufferX1) {
window.CurrentBuffer.OffsetX = x - (bufferX2 - bufferX1)
if cursorPos.X < window.CurrentBuffer.Offset.X {
window.CurrentBuffer.Offset.X = cursorPos.X
} else if cursorPos.X > window.CurrentBuffer.Offset.X+(bufferX2-bufferX1) {
window.CurrentBuffer.Offset.X = cursorPos.X - (bufferX2 - bufferX1)
}
}