28 Commits
Author SHA1 Message Date
EnumDev ee29fae6af Only replace placeholders if found for expensive calls 2026-06-16 14:54:31 +03:00
EnumDev ae8e702a77 Fix panic when attempting to open directory 2026-05-19 18:24:03 +03:00
EnumDev 01bc6702a9 Color message bar messages 2026-05-19 17:12:54 +03:00
EnumDev 5af962c6b2 Select active buffer in buffers dropdown 2026-05-19 17:12:52 +03:00
EnumDev 5b4723d7d4 Add click support to top menu 2026-05-19 17:12:16 +03:00
EnumDev cc92ba51c5 Set typer logs as first buffer 2026-05-12 22:04:33 +03:00
EnumDev 69178e42b1 Replace 'path' module usage with 'filepath' 2026-05-12 21:36:49 +03:00
EnumDev e62f4bf41c Fix paste 2026-05-06 10:27:47 +03:00
EnumDev b784bc0208 Show top menu buttons at the correct X position 2026-05-04 23:00:28 +03:00
EnumDev 999f1953f9 Add logs buffer 2026-05-04 22:46:21 +03:00
EnumDev e4462f9674 Add simple syntax highlighting 2026-05-04 21:33:35 +03:00
EnumDev ef401b5aaa Update README.md 2026-05-03 23:27:49 +03:00
EnumDev 612c0ba80c Update nightly workflow 2026-05-03 17:35:27 +03:00
EnumDev d82d53d659 Update nightly workflow 2026-05-03 17:33:55 +03:00
EnumDev abafc12b47 Add github workflow for nightly releases 2026-05-03 17:32:01 +03:00
EnumDev 811a8f7ee9 Standardize config fetching 2026-05-03 14:55:06 +03:00
EnumDev 18462d1df9 Update README.md 2026-04-28 21:36:51 +03:00
EnumDev b40d89dc43 Update .gitignore 2026-04-28 21:35:30 +03:00
EnumDev 2f1978e6da Turn PrintMessage into receiver function 2026-04-28 21:34:58 +03:00
EnumDev fcde25c28a Fix missing final newline from end of file on load 2026-04-28 19:28:32 +03:00
EnumDev 0e18083929 Fix cursor position issues 2026-04-28 19:27:53 +03:00
EnumDev c2eacac313 Remove debug message 2026-04-28 17:18:35 +03:00
EnumDev b564e0ec2e Add config paths for MacOS systems 2026-04-27 20:07:48 +03:00
EnumDev a1e8d6c19a Use rune arrays instead of go strings for UTF-8 support 2026-04-27 15:45:19 +03:00
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
26 changed files with 1322 additions and 758 deletions
+69
View File
@@ -0,0 +1,69 @@
name: Publish nightly
on:
push:
branches:
- "master"
paths:
- "**"
- "!*.md"
- "!.github/**"
- ".github/workflows/publish_nightly.yml"
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
os: [linux, darwin, freebsd, windows]
arch: [amd64, arm64]
steps:
- name: Setup environment
run: echo "FILENAME=typer-nightly-$(date --iso-8601)-${{ matrix.os }}-${{ matrix.arch }}" >> $GITHUB_ENV
- name: Fetch repository
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 1.24
- name: Build using make
run: make GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }}
- name: Setup archive contents
run: |
mkdir -p staging/$FILENAME
cp build/* -t staging/$FILENAME/
cp -r config staging/$FILENAME/config
- name: Create tarball
if: matrix.os != 'windows'
run: |
cd staging
tar -czvf $FILENAME.tar.gz $FILENAME
- name: Create zip
if: matrix.os == 'windows'
run: |
cd staging/$FILENAME
zip -r ../$FILENAME.zip *
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ matrix.os }}-${{ matrix.arch }}
path: staging/${{ env.FILENAME }}.*
publish:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Update nightly release
uses: andelf/nightly-release@main
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: nightly
name: Nightly
body: |
**This is the nightly release of Typer. Expect bugs and unfinished features**
files: artifacts/*
+1 -1
View File
@@ -2,4 +2,4 @@
/build/
# IDE files
.idea
/.idea
+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 -X 'main.sysconfdir=$(SYSCONFDIR)'" -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
+7
View File
@@ -10,6 +10,13 @@
| Distribution | Package name |
|:----------------------:|:---------------------|
| Arch Linux/Artix Linux | `typer` from the AUR |
| Tide Linux | `typer` from the main repository |
#### From the releases section:
- Go to the [releases section](https://github.com/EnumeratedDev/typer/releases)
- Choose either the latest stable release (**Recommended**) or nightly pre-release
- Download the archive that corresponds to your operating system and architecture
- Optional: Add the extracted directory to your PATH so that typer can be launched from anywhere
- Optional: On Unix-based systems you can move the `typer` executable into `/usr/local/bin/typer` and `config` directory into `/usr/local/etc/typer` for a system-wide installation
#### From source:
- Download `go` from your package manager or from the go website
- Downlaod `which` from your package manager
+3 -2
View File
@@ -6,5 +6,6 @@ selected_style_fallback: "default-fallback" # Style for 8-color capable terminal
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
color_message_bar: true # Add color to message bar messages
buffer_info_message: "File: %f, Filetype: %t, Cursor: (%x, %y, %p), Chars: %c"
tab_indentation: 4 # Length of tab characters
+12
View File
@@ -19,3 +19,15 @@ colors:
message_bar_fg: "black" # Message bar text color
input_bar_bg: "245" # Input bar background color
input_bar_fg: "black" # Input bar text color
# Syntax highlighting
syntax_comment: "darkgray"
syntax_keyword: "lightgreen"
syntax_identifier: "yellow"
syntax_constant: "purple"
syntax_variable: "yellow"
syntax_string: "purple"
# Typer logs highlighting
syntax_info: "lightgreen"
syntax_warning: "gold"
syntax_error: "red"
+4
View File
@@ -19,3 +19,7 @@ colors:
message_bar_fg: "black" # Message bar text color
input_bar_bg: "white" # Input bar background color
input_bar_fg: "black" # Input bar text color
# Typer logs highlighting
syntax_info: "green"
syntax_warning: "yellow"
syntax_error: "red"
+14 -1
View File
@@ -5,6 +5,7 @@ style_type: "256-color"
# Colors
colors:
# Main 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
@@ -18,4 +19,16 @@ colors:
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
input_bar_fg: "white" # Input bar text color
# Syntax highlighting
syntax_comment: "gray"
syntax_keyword: "gold"
syntax_identifier: "purple"
syntax_constant: "teal"
syntax_variable: "purple"
syntax_string: "blue"
# Typer logs highlighting
syntax_info: "green"
syntax_warning: "yellow"
syntax_error: "red"
+12
View File
@@ -0,0 +1,12 @@
filetype: typer_logs
rules:
- type: info
regex: '^\[INFO\].*$'
multiline: true
- type: warning
regex: '^\[WARNING\].*$'
multiline: true
- type: error
regex: '^\[ERROR\].*$'
multiline: true
+15
View File
@@ -0,0 +1,15 @@
filetype: yaml
filenames: ".y[a]?ml$"
rules:
- type: string # Strings
regex: '(["''])(.*?)(["''])'
- type: comment
regex: "#.*"
- type: identifier # Keys
regex: '^[[:blank:]]*(-.)?([a-z0-9\._\-])+:'
multiline: true
- type: constant # Number values
regex: '\d+(\.\d+)?'
- type: constant # True/false values
regex: '\b(YES|yes|Y|y|ON|on|TRUE|True|true|NO|no|N|n|OFF|off|FALSE|False|false)\b'
+414 -186
View File
@@ -2,28 +2,33 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"typer/runestring"
"github.com/gdamore/tcell/v2"
)
type Buffer struct {
Name string
Contents string
Contents []runestring.RuneString
CursorPos int
OffsetX, OffsetY int
CursorPos Position
Offset Position
Selection *Selection
canSave bool
canEdit bool
filetype string
filename string
}
type Selection struct {
selectionStart int
selectionEnd int
selectionStart, selectionEnd Position
}
var Buffers = make([]*Buffer, 0)
@@ -53,43 +58,87 @@ 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)
parsedSyntaxes, err := HighlightString(string(buffer.GetContentsAsString()), buffer.filetype)
if err != nil {
window.PrintMessage(fmt.Sprintf("Could not parse regular expression in '%s' syntax: %s", buffer.filetype, err), TYPER_MESSAGE_ERROR)
}
// Change background if under cursor
if i == buffer.CursorPos {
style = style.Background(CurrentStyle.BufferAreaSel)
}
i := -1
for lineIndex, line := range buffer.Contents {
for runeIndex, r := range append(line, ' ') {
i++
drawPosition := Position{runeIndex, lineIndex}
// Change background if selected
if buffer.Selection != nil {
if edge1, edge2 := buffer.GetSelectionEdges(); i >= edge1 && i <= edge2 {
if x-buffer.Offset.X >= bufferX && y-buffer.Offset.Y >= bufferY {
// Default style
style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg)
// Check for syntax highlighting
for _, parsedSyntax := range parsedSyntaxes {
if i >= parsedSyntax.StartIndex && i < parsedSyntax.EndIndex {
switch parsedSyntax.Type {
case "comment":
style = style.Foreground(CurrentStyle.SyntaxComment)
case "keyword":
style = style.Foreground(CurrentStyle.SyntaxKeyword)
case "identifier":
style = style.Foreground(CurrentStyle.SyntaxIdentifier)
case "constant":
style = style.Foreground(CurrentStyle.SyntaxConstant)
case "variable":
style = style.Foreground(CurrentStyle.SyntaxVariable)
case "string":
style = style.Foreground(CurrentStyle.SyntaxString)
// Special types for typer logs
case "info":
style = style.Foreground(CurrentStyle.SyntaxInfo)
case "warning":
style = style.Foreground(CurrentStyle.SyntaxWarning)
case "error":
style = style.Foreground(CurrentStyle.SyntaxError)
}
break
}
}
// 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 {
@@ -108,12 +157,35 @@ func (buffer *Buffer) Load() error {
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
}
content, err := os.ReadFile(buffer.filename)
contentBytes, err := os.ReadFile(buffer.filename)
if err != nil {
return err
}
content := runestring.RuneString(string(contentBytes))
if len(content) != 0 {
buffer.Contents = runestring.Split(content, '\n')
// Add empty line at end of buffer for last newline
if content[len(content)-1] == '\n' {
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
}
buffer.CursorPos.Y = len(buffer.Contents) - 1
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
}
// Set buffer filetype
for _, syntax := range AvailableSyntaxes {
if syntax.Filenames == "" {
continue
}
if ok, _ := regexp.MatchString(syntax.Filenames, buffer.filename); ok {
buffer.filetype = syntax.Filetype
}
}
buffer.Contents = string(content)
return nil
}
@@ -133,12 +205,13 @@ func (buffer *Buffer) Save() error {
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
}
// Append new line character at end of buffer contents if not present
if buffer.Contents == "" || buffer.Contents[len(buffer.Contents)-1] != '\n' {
buffer.Contents += "\n"
// Add newline at the end of buffer Contents
line := buffer.Contents[len(buffer.Contents)-1]
if len(line) != 0 {
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
}
err := os.WriteFile(buffer.filename, []byte(buffer.Contents), 0644)
err := os.WriteFile(buffer.filename, []byte(string(buffer.GetContentsAsString())), 0644)
if err != nil {
return err
}
@@ -146,129 +219,139 @@ func (buffer *Buffer) Save() error {
return nil
}
func (buffer *Buffer) GetSelectionEdges() (int, int) {
if buffer.Selection == nil {
return -1, -1
func (buffer *Buffer) GetContentsAsString() runestring.RuneString {
finalText := make(runestring.RuneString, 0)
for i, line := range buffer.Contents {
finalText = append(finalText, line...)
if i != len(buffer.Contents)-1 {
finalText = append(finalText, '\n')
}
}
if buffer.Selection.selectionStart < buffer.Selection.selectionEnd {
return finalText
}
func (buffer *Buffer) PositionToAbsolutePosition(position Position) int {
i := 0
for lineIndex, line := range buffer.Contents {
if len(line) == 0 {
line = append(line, ' ')
}
for runeIndex, _ := range append(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 append(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
}
}
func (buffer *Buffer) GetSelectedText() string {
func (buffer *Buffer) GetSelectedText() runestring.RuneString {
if buffer.Selection == nil {
return ""
return make(runestring.RuneString, 0)
}
if len(buffer.Contents) == 0 {
return ""
return make(runestring.RuneString, 0)
}
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 := make(runestring.RuneString, 0)
if r := buffer.GetCharAtPosition(edge1); r != 0 {
selectedText = append(selectedText, r)
}
if start <= end {
return buffer.Contents[start : end+1]
} else {
return buffer.Contents[end : start+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 = append(selectedText, buffer.GetCharAtPosition(edge1))
}
}
return selectedText
}
func (buffer *Buffer) CutText(window *Window) (string, int) {
func (buffer *Buffer) CutText(window *Window) (runestring.RuneString, int) {
if buffer.Selection == nil {
// Copy line
copiedText := ""
startOfLine := window.CurrentBuffer.CursorPos
endOfLine := window.CurrentBuffer.CursorPos
// Add current letter to copied text
if buffer.CursorPos < len(buffer.Contents) {
copiedText = string(buffer.Contents[buffer.CursorPos])
}
// Find end of line
for i := buffer.CursorPos + 1; i < len(buffer.Contents); i++ {
currentLetter := buffer.Contents[i]
endOfLine++
copiedText += string(currentLetter)
if currentLetter == '\n' {
break
}
}
// Find start of line
for i := buffer.CursorPos - 1; i >= 0; i-- {
currentLetter := buffer.Contents[i]
if currentLetter != '\n' {
startOfLine--
copiedText = string(currentLetter) + copiedText
} else {
break
}
}
// Cut current line
cutText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
// Remove line from buffer contents
buffer.Contents = buffer.Contents[:startOfLine] + buffer.Contents[endOfLine+1:]
return copiedText, 0
} else {
// Copy selection
copiedText := buffer.GetSelectedText()
// Remove selected text
edge1, edge2 := buffer.GetSelectionEdges()
if edge2 == len(buffer.Contents) {
edge2 = len(buffer.Contents) - 1
if len(buffer.Contents) == 1 {
buffer.Contents[0] = make(runestring.RuneString, 0)
} else {
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y, buffer.CursorPos.Y+1)
}
buffer.Contents = buffer.Contents[:edge1] + buffer.Contents[edge2+1:]
window.SetCursorPos(edge1)
buffer.CursorPos.Y -= 1
if buffer.CursorPos.Y < 0 {
buffer.CursorPos = Position{0, 0}
}
return cutText, 0
} else {
// Cut selection
cutText := buffer.GetSelectedText()
// Remove selected text
_, edge2 := buffer.GetSelectionEdges()
buffer.CursorPos = edge2
buffer.MoveRight(1)
buffer.Delete(len(cutText))
// Remove selection
buffer.Selection = nil
return copiedText, 1
return cutText, 1
}
}
func (buffer *Buffer) CopyText() (string, int) {
func (buffer *Buffer) CopyText() (runestring.RuneString, int) {
if buffer.Selection == nil {
// Copy line
copiedText := ""
// Add current letter to copied text
if buffer.CursorPos < len(buffer.Contents) {
copiedText = string(buffer.Contents[buffer.CursorPos])
}
// Find end of line
for i := buffer.CursorPos + 1; i < len(buffer.Contents); i++ {
currentLetter := buffer.Contents[i]
copiedText += string(currentLetter)
if currentLetter == '\n' {
break
}
}
// Find start of line
for i := buffer.CursorPos - 1; i >= 0; i-- {
currentLetter := buffer.Contents[i]
if currentLetter != '\n' {
copiedText = string(currentLetter) + copiedText
} else {
break
}
}
// Cut current line
copiedText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
return copiedText, 0
} else {
@@ -277,78 +360,223 @@ func (buffer *Buffer) CopyText() (string, int) {
}
}
func (buffer *Buffer) PasteText(window *Window, text string) {
str := buffer.Contents
func (buffer *Buffer) PasteText(window *Window, text runestring.RuneString) {
// Remove selected text
if buffer.Selection != nil {
edge1, edge2 := buffer.GetSelectionEdges()
if edge2 == len(buffer.Contents) {
edge2 = len(buffer.Contents) - 1
}
_, edge2 := buffer.GetSelectionEdges()
buffer.CursorPos = edge2
buffer.Delete(len(buffer.GetSelectedText()))
str = str[:edge1] + str[edge2+1:]
buffer.Contents = str
window.SetCursorPos(edge1)
buffer.Selection = nil
}
index := buffer.CursorPos
if index == len(str) {
str += text
} else {
str = str[:index] + text + str[index:]
}
buffer.Contents = str
window.SetCursorPos(buffer.CursorPos + len(text))
buffer.WriteString(text)
}
func (buffer *Buffer) FindSubstring(substring string, afterPos int) int {
func (buffer *Buffer) FindSubstring(substring runestring.RuneString, afterPos Position) Position {
// Return no match if afterPos is larger than the buffer contents size
if afterPos >= len(buffer.Contents) {
return -1
contents := buffer.GetContentsAsString()
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
if absAfterPos >= len(contents) {
return Position{-1, -1}
}
index := strings.Index(buffer.Contents[afterPos+1:], substring)
index := runestring.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += afterPos + 1
index += absAfterPos + 1
}
return index
return buffer.AbsolutePositionToPosition(index)
}
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement string, afterPos int) int {
index := buffer.FindSubstring(substring, afterPos)
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement runestring.RuneString, afterPos Position) Position {
// Return no match if afterPos is larger than the buffer contents size
contents := buffer.GetContentsAsString()
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
// Return if substring isn't found
if index == -1 {
return -1
if absAfterPos >= len(contents) {
return Position{-1, -1}
}
index := runestring.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += absAfterPos + 1
}
// Replace substring with replacement string
buffer.Contents = buffer.Contents[:index] + replacement + buffer.Contents[index+len(substring):]
contents = slices.Insert(contents, index, replacement...)
return index
buffer.Contents = runestring.Split(contents, '\n')
return buffer.AbsolutePositionToPosition(index)
}
func (buffer *Buffer) FindAndReplaceAll(substring, replacement string) int {
func (buffer *Buffer) FindAndReplaceAll(substring, replacement runestring.RuneString) int {
replacements := 0
index := 0
for index != -1 {
index = buffer.FindAndReplaceSubstring(substring, replacement, index)
if index != -1 {
position := Position{}
for position.X != -1 && position.Y != -1 {
position = buffer.FindAndReplaceSubstring(substring, replacement, position)
if position.X != -1 && position.Y != -1 {
replacements++
}
if index == 0 {
index++
}
}
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, make(runestring.RuneString, 0))
} else {
buffer.Contents = slices.Insert(buffer.Contents, buffer.CursorPos.Y+1, make(runestring.RuneString, 0))
}
// Move line content after cursor X to the new line
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y+1] = slices.Insert(buffer.Contents[buffer.CursorPos.Y+1], 0, line[buffer.CursorPos.X:]...)
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X]
buffer.MoveDown(1)
buffer.CursorPos.X = 0
} else {
buffer.Contents[buffer.CursorPos.Y] = slices.Insert(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, r)
buffer.MoveRight(1)
}
}
func (buffer *Buffer) WriteString(str runestring.RuneString) {
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] = append(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 {
buffer.Contents[buffer.CursorPos.Y] = slices.Delete(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, 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, "~/") {
@@ -376,7 +604,7 @@ func GetOpenFileBuffer(filename string) *Buffer {
return nil
}
func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error) {
func CreateFileBuffer(filename string) (*Buffer, error) {
// Replace tilde with home directory
if filename != "~" && strings.HasPrefix(filename, "~/") {
homedir, err := os.UserHomeDir()
@@ -395,29 +623,28 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
}
stat, err := os.Stat(abs)
if !openNonExistentFile {
if err != nil {
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if !stat.Mode().IsRegular() {
return nil, fmt.Errorf("%s is not a regular file", filename)
}
} else if !stat.Mode().IsRegular() {
return nil, fmt.Errorf("not a regular file")
}
if GetBufferByName(filename) != nil {
return nil, fmt.Errorf("a buffer with the name (%s) is already open", filename)
return nil, fmt.Errorf("a buffer with the same name is already open")
}
if GetBufferByFilename(abs) != nil {
return nil, fmt.Errorf("%s is already open in another buffer", filename)
return nil, fmt.Errorf("file is already open in another buffer")
}
buffer := Buffer{
Name: filename,
Contents: "",
CursorPos: 0,
Contents: make([]runestring.RuneString, 1),
CursorPos: Position{0, 0},
canSave: true,
canEdit: true,
filename: abs,
}
@@ -438,14 +665,15 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
func CreateBuffer(bufferName string) (*Buffer, error) {
buffer := Buffer{
Name: bufferName,
Contents: "",
CursorPos: 0,
Contents: make([]runestring.RuneString, 1),
CursorPos: Position{0, 0},
canSave: true,
canEdit: true,
filename: "",
}
if GetBufferByName(bufferName) != nil {
return nil, fmt.Errorf("a buffer with the name (%s) is already open", bufferName)
return nil, fmt.Errorf("a buffer with the same name is already open")
}
Buffers = append(Buffers, &buffer)
+132 -68
View File
@@ -6,6 +6,7 @@ import (
"slices"
"strconv"
"strings"
"typer/runestring"
)
type Command struct {
@@ -18,6 +19,20 @@ 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},
}
window.PrintMessage("Selected all text", TYPER_MESSAGE_INFO)
},
}
cutCmd := Command{
cmd: "cut",
run: func(window *Window, args ...string) {
@@ -29,9 +44,9 @@ func initCommands() {
// Send appropriate message and remove text depending on copying method
if copyingMethod == 0 {
PrintMessage(window, "Copied line to clipboard.")
window.PrintMessage("Copied line to clipboard", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, "Copied selection to clipboard.")
window.PrintMessage("Copied selection to clipboard", TYPER_MESSAGE_INFO)
}
},
}
@@ -47,9 +62,9 @@ func initCommands() {
// Send appropriate message depending on copying method
if copyingMethod == 0 {
PrintMessage(window, "Copied line to clipboard.")
window.PrintMessage("Copied line to clipboard", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, "Copied selection to clipboard.")
window.PrintMessage("Copied selection to clipboard", TYPER_MESSAGE_INFO)
}
},
}
@@ -57,9 +72,14 @@ func initCommands() {
pasteCmd := Command{
cmd: "paste",
run: func(window *Window, args ...string) {
if window.Clipboard != "" {
if !window.CurrentBuffer.canEdit {
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
return
}
if len(window.Clipboard) != 0 {
window.CurrentBuffer.PasteText(window, window.Clipboard)
PrintMessage(window, "Pasted text to buffer.")
window.PrintMessage("Pasted text to buffer", TYPER_MESSAGE_INFO)
}
},
}
@@ -68,7 +88,7 @@ func initCommands() {
cmd: "save",
run: func(window *Window, args ...string) {
if !window.CurrentBuffer.canSave {
PrintMessage(window, "Cannot save buffer!")
window.PrintMessage("Cannot save buffer", TYPER_MESSAGE_ERROR)
return
}
@@ -85,7 +105,7 @@ func initCommands() {
input = <-inputChannel
if strings.TrimSpace(input) == "" {
PrintMessage(window, "No save location was given!")
window.PrintMessage("No save location was given", TYPER_MESSAGE_ERROR)
return
}
@@ -93,12 +113,12 @@ func initCommands() {
err := window.CurrentBuffer.Save()
if err != nil {
PrintMessage(window, fmt.Sprintf("Could not save file: %s", err))
window.PrintMessage(fmt.Sprintf("Could not save file: %s", err), TYPER_MESSAGE_ERROR)
window.CurrentBuffer.filename = ""
return
}
PrintMessage(window, "File saved.")
window.PrintMessage("File saved", TYPER_MESSAGE_INFO)
}()
},
autocomplete: func(window *Window, args ...string) []string {
@@ -118,16 +138,16 @@ func initCommands() {
}
if openBuffer := GetOpenFileBuffer(input); openBuffer != nil {
PrintMessage(window, fmt.Sprintf("File already open! Switching to buffer: %s", openBuffer.Name))
window.PrintMessage(fmt.Sprintf("File already open! Switching to buffer: %s", openBuffer.Name), TYPER_MESSAGE_INFO)
window.CurrentBuffer = openBuffer
} else {
newBuffer, err := CreateFileBuffer(input, false)
newBuffer, err := CreateFileBuffer(input)
if err != nil {
PrintMessage(window, fmt.Sprintf("Could not open file: %s", err.Error()))
window.PrintMessage(fmt.Sprintf("Could not open file %s: %s", input, err), TYPER_MESSAGE_ERROR)
return
}
PrintMessage(window, fmt.Sprintf("Opening file at: %s", newBuffer.filename))
window.PrintMessage(fmt.Sprintf("Opening file at: %s", newBuffer.filename), TYPER_MESSAGE_INFO)
window.CurrentBuffer = newBuffer
}
}()
@@ -142,8 +162,7 @@ func initCommands() {
log.Fatalf("Could not reload buffer: %s", err)
}
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, "Buffer reloaded.")
window.PrintMessage("Buffer reloaded", TYPER_MESSAGE_INFO)
},
}
@@ -151,18 +170,18 @@ func initCommands() {
cmd: "find",
run: func(window *Window, args ...string) {
if len(args) >= 1 {
input := args[0]
input := runestring.RuneString(args[0])
if input == "" {
if len(input) == 0 {
return
}
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match found.")
if pos.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
window.PrintMessage("Match found", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", input))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(input)), TYPER_MESSAGE_WARNING)
}
return
@@ -170,18 +189,18 @@ func initCommands() {
inputChannel := RequestInput(window, "Substring to search for:", "")
go func() {
input := <-inputChannel
input := runestring.RuneString(<-inputChannel)
if input == "" {
if len(input) == 0 {
return
}
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match found.")
if pos.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
window.PrintMessage("Match found", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", input))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(input)), TYPER_MESSAGE_WARNING)
}
}()
},
@@ -191,19 +210,19 @@ func initCommands() {
cmd: "replace",
run: func(window *Window, args ...string) {
if len(args) >= 2 {
findStr := args[0]
replaceStr := args[1]
findStr := runestring.RuneString(args[0])
replaceStr := runestring.RuneString(args[1])
if findStr == "" {
if len(findStr) == 0 {
return
}
pos := window.CurrentBuffer.FindAndReplaceSubstring(findStr, replaceStr, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match replaced successfully.")
if pos.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
window.PrintMessage("Match replaced successfully", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
}
return
@@ -211,20 +230,20 @@ func initCommands() {
go func() {
inputChannel := RequestInput(window, "Substring to search for:", "")
findStr := <-inputChannel
if findStr == "" {
findStr := runestring.RuneString(<-inputChannel)
if len(findStr) == 0 {
return
}
inputChannel = RequestInput(window, "String to replace with:", "")
replaceStr := <-inputChannel
replaceStr := runestring.RuneString(<-inputChannel)
pos := window.CurrentBuffer.FindAndReplaceSubstring(findStr, replaceStr, window.CurrentBuffer.CursorPos)
if pos >= 0 {
window.SetCursorPos(pos)
PrintMessage(window, "Match replaced successfully.")
if pos.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
window.PrintMessage("Match replaced successfully", TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
}
}()
},
@@ -234,19 +253,18 @@ func initCommands() {
cmd: "replace-all",
run: func(window *Window, args ...string) {
if len(args) >= 2 {
findStr := args[0]
replaceStr := args[1]
findStr := runestring.RuneString(args[0])
replaceStr := runestring.RuneString(args[1])
if findStr == "" {
if len(findStr) == 0 {
return
}
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
if replacements > 0 {
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, fmt.Sprintf("Replaced all %d matches successfully.", replacements))
window.PrintMessage(fmt.Sprintf("Replaced all %d matches successfully", replacements), TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
}
return
@@ -254,20 +272,19 @@ func initCommands() {
go func() {
inputChannel := RequestInput(window, "Substring to search for:", "")
findStr := <-inputChannel
if findStr == "" {
findStr := runestring.RuneString(<-inputChannel)
if len(findStr) == 0 {
return
}
inputChannel = RequestInput(window, "String to replace with:", "")
replaceStr := <-inputChannel
replaceStr := runestring.RuneString(<-inputChannel)
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
if replacements > 0 {
window.SetCursorPos(window.CurrentBuffer.CursorPos)
PrintMessage(window, fmt.Sprintf("Replaced all %d matches successfully.", replacements))
window.PrintMessage(fmt.Sprintf("Replaced all %d matches successfully", replacements), TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("'%s' not found in buffer!", findStr))
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
}
}()
},
@@ -288,7 +305,7 @@ func initCommands() {
}
window.CurrentBuffer = Buffers[index]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
window.PrintMessage(fmt.Sprintf("Set current buffer to '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
},
}
@@ -307,7 +324,7 @@ func initCommands() {
}
window.CurrentBuffer = Buffers[index]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
window.PrintMessage(fmt.Sprintf("Set current buffer to '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
},
}
@@ -323,7 +340,7 @@ func initCommands() {
}
window.CursorMode = CursorModeBuffer
PrintMessage(window, fmt.Sprintf("New buffer created with the name '%s'.", window.CurrentBuffer.Name))
window.PrintMessage(fmt.Sprintf("New buffer created with the name '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
},
}
@@ -342,7 +359,7 @@ func initCommands() {
window.CurrentBuffer = Buffers[bufferIndex]
}
window.CursorMode = CursorModeBuffer
PrintMessage(window, "Buffer closed.")
window.PrintMessage("Buffer closed", TYPER_MESSAGE_INFO)
},
}
@@ -371,14 +388,14 @@ func initCommands() {
}
if _, ok := AvailableStyles[input]; !ok {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
return
}
if ok := SetCurrentStyle(window.screen, input); ok {
PrintMessage(window, fmt.Sprintf("Setting style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Setting style to '%s'", input), TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
}
return
@@ -393,25 +410,70 @@ func initCommands() {
}
if _, ok := AvailableStyles[input]; !ok {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
return
}
if ok := SetCurrentStyle(window.screen, input); ok {
PrintMessage(window, fmt.Sprintf("Setting style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Setting style to '%s'", input), TYPER_MESSAGE_INFO)
} else {
PrintMessage(window, fmt.Sprintf("Could not set style to '%s'", input))
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
}
}()
},
}
setFiletypeCmd := Command{
cmd: "set-filetype",
run: func(window *Window, args ...string) {
if len(args) >= 1 {
input := args[0]
if input == "" {
return
}
if strings.ToLower(input) == "none" {
window.CurrentBuffer.filetype = ""
window.PrintMessage("Setting filetype to 'none'", TYPER_MESSAGE_INFO)
return
} else if _, ok := AvailableSyntaxes[input]; !ok {
window.PrintMessage(fmt.Sprintf("Could not set filetype to '%s'", input), TYPER_MESSAGE_ERROR)
return
}
window.CurrentBuffer.filetype = input
window.PrintMessage(fmt.Sprintf("Setting filetype to '%s'", input), TYPER_MESSAGE_INFO)
return
}
inputChannel := RequestInput(window, "Filetype to switch to:", "")
go func() {
input := <-inputChannel
if input == "" {
return
}
if _, ok := AvailableSyntaxes[input]; !ok {
window.PrintMessage(fmt.Sprintf("Could not set filetype to '%s'", input), TYPER_MESSAGE_ERROR)
return
}
window.CurrentBuffer.filetype = input
window.PrintMessage(fmt.Sprintf("Setting filetype to '%s'", input), TYPER_MESSAGE_INFO)
}()
},
}
menuFileCmd := Command{
cmd: "menu-file",
run: func(window *Window, args ...string) {
for _, button := range TopMenuButtons {
if button.Name == "File" {
button.Action(window)
button.Action(window, &button)
break
}
}
@@ -423,7 +485,7 @@ func initCommands() {
run: func(window *Window, args ...string) {
for _, button := range TopMenuButtons {
if button.Name == "Edit" {
button.Action(window)
button.Action(window, &button)
break
}
}
@@ -435,7 +497,7 @@ func initCommands() {
run: func(window *Window, args ...string) {
for _, button := range TopMenuButtons {
if button.Name == "Buffers" {
button.Action(window)
button.Action(window, &button)
break
}
}
@@ -492,6 +554,7 @@ func initCommands() {
}
// Register commands
commands["select-all"] = &selectAll
commands["cut"] = &cutCmd
commands["copy"] = &copyCmd
commands["paste"] = &pasteCmd
@@ -508,6 +571,7 @@ func initCommands() {
commands["toggle-top-bar"] = &toggleTopBar
commands["toggle-line-index"] = &toggleLineIndex
commands["set-style"] = &setStyleCmd
commands["set-filetype"] = &setFiletypeCmd
commands["menu-file"] = &menuFileCmd
commands["menu-edit"] = &menuEditCmd
commands["menu-buffers"] = &menuBuffersCmd
@@ -520,7 +584,7 @@ func RunCommand(window *Window, cmd string, args ...string) bool {
command.run(window, args...)
return true
} else {
PrintMessage(window, fmt.Sprintf("Could not find command '%s'!", cmd))
window.PrintMessage(fmt.Sprintf("Could not find command '%s'", cmd), TYPER_MESSAGE_ERROR)
return false
}
}
+20 -25
View File
@@ -1,10 +1,10 @@
package main
import (
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"gopkg.in/yaml.v3"
)
type TyperConfig struct {
@@ -12,6 +12,7 @@ type TyperConfig struct {
FallbackStyle string `yaml:"fallback_style,omitempty"`
ShowTopMenu bool `yaml:"show_top_menu,omitempty"`
ShowLineIndex bool `yaml:"show_line_index,omitempty"`
ColorMessageBar bool `yaml:"color_message_bar"`
ExtendLineIndex bool `yaml:"extend_line_index,omitempty"`
BufferInfoMessage string `yaml:"buffer_info_message,omitempty"`
TabIndentation int `yaml:"tab_indentation,omitempty"`
@@ -19,7 +20,7 @@ type TyperConfig struct {
var Config TyperConfig
func readConfig() {
func readMainConfig() {
Config = TyperConfig{
SelectedStyle: "default",
FallbackStyle: "default-fallback",
@@ -30,30 +31,24 @@ func readConfig() {
TabIndentation: 4,
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
// Get main config path
mainConfigPath := GetConfigPath("config.yml")
// Ensure config exists at path
if mainConfigPath == "" {
log.Fatalf("config.yml not found in any config directory")
}
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(path.Join(sysconfdir, "typer/config.yml")); err == nil {
reader, err := os.Open(path.Join(sysconfdir, "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()
// Read config file
data, err := os.ReadFile(mainConfigPath)
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)
}
// Validate config options
+2 -1
View File
@@ -1,4 +1,4 @@
module Typer
module typer
go 1.24
@@ -9,6 +9,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.10 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
+2
View File
@@ -11,6 +11,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+109
View File
@@ -0,0 +1,109 @@
package main
import (
"log"
"os"
"path/filepath"
"regexp"
"gopkg.in/yaml.v3"
)
type SyntaxRule struct {
Type string `yaml:"type"`
Regex string `yaml:"regex"`
Multiline bool `yaml:"multiline"`
}
type Syntax struct {
Filetype string `yaml:"filetype"`
Filenames string `yaml:"filenames"`
Rules []SyntaxRule `yaml:"rules"`
}
type ParsedSyntax struct {
StartIndex int
EndIndex int
Type string
}
var AvailableSyntaxes map[string]Syntax = make(map[string]Syntax)
func ReadSyntaxHighlighters() {
// Get syntax directory path
syntaxDirPath := GetConfigPath("syntax")
// Ensure directory exists at path
if stat, err := os.Stat(syntaxDirPath); syntaxDirPath == "" || err != nil || !stat.IsDir() {
return
}
// Get directory entries
entries, err := os.ReadDir(syntaxDirPath)
if err != nil {
log.Fatalf("Could not read syntax directory: %s", err)
}
// Read entries in directory
for _, entry := range entries {
entryPath := filepath.Join(syntaxDirPath, entry.Name())
data, err := os.ReadFile(entryPath)
if err != nil {
log.Fatalf("Could not read syntax file (%s): %s", entryPath, err)
}
syntax := Syntax{}
err = yaml.Unmarshal(data, &syntax)
if err != nil {
log.Fatalf("Could not read syntax file (%s): %s", entryPath, err)
}
if _, ok := AvailableSyntaxes[syntax.Filetype]; !ok {
AvailableSyntaxes[syntax.Filetype] = syntax
}
}
}
func HighlightString(s string, filetype string) (parsedSyntaxes []ParsedSyntax, err error) {
// Get syntax for filetype
syntax, ok := AvailableSyntaxes[filetype]
if !ok {
return nil, nil
}
for _, rule := range syntax.Rules {
regex := rule.Regex
if rule.Multiline {
regex = "(?m)" + rule.Regex
}
r, err := regexp.Compile(regex)
if err != nil {
return nil, err
}
matches := r.FindAllStringIndex(s, -1)
for _, match := range matches {
skip := false
for _, parsedSyntax := range parsedSyntaxes {
if (match[0] >= parsedSyntax.StartIndex && match[0] < parsedSyntax.EndIndex) || (match[1] >= parsedSyntax.StartIndex && match[1] < parsedSyntax.EndIndex) {
skip = true
}
}
if skip {
continue
}
parsedSyntax := ParsedSyntax{
StartIndex: match[0],
EndIndex: match[1],
Type: rule.Type,
}
parsedSyntaxes = append(parsedSyntaxes, parsedSyntax)
}
}
return parsedSyntaxes, nil
}
+20 -26
View File
@@ -1,12 +1,12 @@
package main
import (
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"strings"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
)
type TyperKeybindings struct {
@@ -21,35 +21,29 @@ type Keybinding struct {
var Keybindings TyperKeybindings
func readKeybindings() {
func readKeybindingsConfig() {
Keybindings = TyperKeybindings{
Keybindings: make([]Keybinding, 0),
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
// Get keybindings config path
keybindingsConfigPath := GetConfigPath("keybindings.yml")
// Ensure config exists at path
if keybindingsConfigPath == "" {
log.Fatalf("keybindings.yml not found in any config directory")
}
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(path.Join(sysconfdir, "typer/keybindings.yml")); err == nil {
reader, err := os.Open(path.Join(sysconfdir, "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()
// Read config file
data, err := os.ReadFile(keybindingsConfigPath)
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)
}
}
+5 -5
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) {
@@ -16,9 +16,9 @@ func drawLineIndex(window *Window) {
_, bufferY1, _, bufferY2 := window.GetTextAreaDimensions()
lineIndex := 1 + buffer.OffsetY
lineIndex := 1 + buffer.Offset.Y
for y := bufferY1; y <= bufferY2; y++ {
if lineIndex > strings.Count(buffer.Contents, "\n")+1 {
if lineIndex > len(buffer.Contents) {
if Config.ExtendLineIndex {
for x := 0; x < lineIndexSize; x++ {
screen.SetContent(x, y, ' ', nil, lineIndexStyle)
@@ -42,7 +42,7 @@ func drawLineIndex(window *Window) {
}
func getLineIndexSize(window *Window) int {
i := strings.Count(window.CurrentBuffer.Contents, "\n") + 1
i := len(window.CurrentBuffer.Contents)
if i == 0 {
return 4
}
+42 -13
View File
@@ -1,22 +1,32 @@
package main
import (
"fmt"
"log"
"os"
flag "github.com/spf13/pflag"
)
var sysconfdir = "/etc/"
var configDirFlag = flag.StringP("config", "c", "", "Path to config directory")
func main() {
// Read config
readConfig()
// Read flags
readFlags()
// Read key bindings
readKeybindings()
// Read main config
readMainConfig()
// Read styles
// Read keybindings config
readKeybindingsConfig()
// Read styles directory
readStyles()
// Read syntax directory
ReadSyntaxHighlighters()
// Initialize commands
initCommands()
@@ -25,21 +35,36 @@ func main() {
log.Fatalf("Failed to create window: %v", err)
}
if len(os.Args) > 1 {
for i, file := range os.Args[1:] {
b, err := CreateFileBuffer(file, true)
// Create logs buffer
logsBuffer, err := CreateBuffer("Typer Logs")
if err != nil {
log.Fatalf("Could not create logs buffer")
}
logsBuffer.filetype = "typer_logs"
logsBuffer.canEdit = false
// Open paths passed as arguments
if flag.NArg() > 0 {
for _, file := range flag.Args() {
buffer, err := CreateFileBuffer(file)
if err != nil {
PrintMessage(window, "Could not open file: "+file)
window.PrintMessage(fmt.Sprintf("Could not open file %s: %s", file, err), TYPER_MESSAGE_ERROR)
continue
}
if i == 0 {
window.CurrentBuffer = b
Buffers = Buffers[1:]
if window.CurrentBuffer == nil {
window.CurrentBuffer = buffer
}
}
}
if window.CurrentBuffer == nil {
buffer, err := CreateBuffer("New Buffer 1")
if err == nil {
window.CurrentBuffer = buffer
}
}
for !window.closed {
window.Draw()
window.ProcessEvents()
@@ -48,3 +73,7 @@ func main() {
window.screen.Fini()
window.screen = nil
}
func readFlags() {
flag.Parse()
}
+68 -8
View File
@@ -1,19 +1,60 @@
package main
import (
"github.com/gdamore/tcell/v2"
"slices"
"time"
"typer/runestring"
"github.com/gdamore/tcell/v2"
)
type TyperMessageUrgency uint
const (
TYPER_MESSAGE_INFO TyperMessageUrgency = iota
TYPER_MESSAGE_WARNING
TYPER_MESSAGE_ERROR
)
type TyperMessage struct {
timestamp int64
message string
Timestamp int64
Urgency TyperMessageUrgency
Message string
}
var messageLog = make([]TyperMessage, 0)
var lastMessage *TyperMessage
func PrintMessage(window *Window, message string) {
messageLog = append(messageLog, TyperMessage{timestamp: time.Now().UnixMilli(), message: message})
func (window *Window) PrintMessage(message string, urgency TyperMessageUrgency) {
lastMessage = &TyperMessage{Timestamp: time.Now().UnixMilli(), Message: message, Urgency: urgency}
logsBuffer := GetBufferByName("Typer Logs")
if logsBuffer != nil {
messageToPrint := ""
switch lastMessage.Urgency {
case TYPER_MESSAGE_INFO:
messageToPrint = "[INFO] "
case TYPER_MESSAGE_WARNING:
messageToPrint = "[WARNING] "
case TYPER_MESSAGE_ERROR:
messageToPrint = "[ERROR] "
default:
messageToPrint = "[???] "
}
messageToPrint += "[" + time.UnixMilli(lastMessage.Timestamp).Format("15:04:05") + "] "
messageToPrint += lastMessage.Message + "\n"
if len(logsBuffer.Contents) >= 1000 {
logsBuffer.Contents = slices.Delete(logsBuffer.Contents, 0, len(logsBuffer.Contents)-999)
}
logsBuffer.CursorPos = Position{
X: len(logsBuffer.Contents[len(logsBuffer.Contents)-1]),
Y: len(logsBuffer.Contents) - 1,
}
logsBuffer.WriteString(runestring.RuneString(messageToPrint))
}
err := window.screen.PostEvent(tcell.NewEventInterrupt(nil))
if err != nil {
@@ -38,8 +79,27 @@ func drawMessageBar(window *Window) {
sizeX, sizeY := screen.Size()
messageToPrint := ""
if len(messageLog) > 0 && time.Since(time.UnixMilli(messageLog[len(messageLog)-1].timestamp)).Seconds() < 5 {
messageToPrint = messageLog[len(messageLog)-1].message
if lastMessage != nil && time.Since(time.UnixMilli(lastMessage.Timestamp)).Seconds() < 5 {
switch lastMessage.Urgency {
case TYPER_MESSAGE_INFO:
if Config.ColorMessageBar {
messageBarStyle = messageBarStyle.Foreground(CurrentStyle.SyntaxInfo)
}
messageToPrint = "[INFO] "
case TYPER_MESSAGE_WARNING:
if Config.ColorMessageBar {
messageBarStyle = messageBarStyle.Foreground(CurrentStyle.SyntaxWarning)
}
messageToPrint = "[WARNING] "
case TYPER_MESSAGE_ERROR:
if Config.ColorMessageBar {
messageBarStyle = messageBarStyle.Foreground(CurrentStyle.SyntaxError)
}
messageToPrint = "[ERROR] "
default:
messageToPrint = "[???] "
}
messageToPrint += lastMessage.Message
}
for x := 0; x < sizeX; x++ {
+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
}
}
+35
View File
@@ -0,0 +1,35 @@
package runestring
type RuneString []rune
func Split(s RuneString, sep rune) []RuneString {
ret := make([]RuneString, 0)
currentStr := make(RuneString, 0)
for _, r := range s {
if r == sep {
ret = append(ret, currentStr)
currentStr = make(RuneString, 0)
continue
}
currentStr = append(currentStr, r)
}
return ret
}
func Index(s, substr RuneString) int {
substrIndex := 0
for i, r := range s {
if r == substr[substrIndex] {
if substrIndex == len(substr) {
return i - substrIndex
}
substrIndex++
} else {
substrIndex = 0
}
}
return -1
}
+36 -36
View File
@@ -2,15 +2,16 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
)
type TyperStyle struct {
@@ -19,7 +20,7 @@ type TyperStyle struct {
Description string
StyleType string
// Colors
// Main Colors
BufferAreaBg tcell.Color `name:"buffer_area_bg"`
BufferAreaFg tcell.Color `name:"buffer_area_fg"`
BufferAreaSel tcell.Color `name:"buffer_area_sel"`
@@ -34,6 +35,18 @@ type TyperStyle struct {
MessageBarFg tcell.Color `name:"message_bar_fg"`
InputBarBg tcell.Color `name:"input_bar_bg"`
InputBarFg tcell.Color `name:"input_bar_fg"`
// Syntax highlighting
SyntaxComment tcell.Color `name:"syntax_comment"`
SyntaxKeyword tcell.Color `name:"syntax_keyword"`
SyntaxIdentifier tcell.Color `name:"syntax_identifier"`
SyntaxConstant tcell.Color `name:"syntax_constant"`
SyntaxVariable tcell.Color `name:"syntax_variable"`
SyntaxString tcell.Color `name:"syntax_string"`
// Typer logs highlighting
SyntaxInfo tcell.Color `name:"syntax_info"`
SyntaxWarning tcell.Color `name:"syntax_warning"`
SyntaxError tcell.Color `name:"syntax_error"`
}
type typerStyleYaml struct {
@@ -71,44 +84,31 @@ var AvailableStyles = make(map[string]TyperStyle)
var CurrentStyle = FallbackStyle
func readStyles() {
homeDir, err := os.UserHomeDir()
// Get styles directory path
stylesDirPath := GetConfigPath("styles")
// Ensure directory exists at path
if stat, err := os.Stat(stylesDirPath); stylesDirPath == "" || err != nil || !stat.IsDir() {
return
}
// Get directory entries
entries, err := os.ReadDir(stylesDirPath)
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
log.Fatalf("Could not read user style 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/"))
// Read entries in directory
for _, entry := range entries {
entryPath := filepath.Join(stylesDirPath, entry.Name())
style, err := readStyleYamlFile(entryPath)
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
log.Fatalf("Could not read style file (%s): %s", entryPath, 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(path.Join(sysconfdir, "typer/styles/")); err == nil && stat.IsDir() {
entries, err := os.ReadDir(path.Join(sysconfdir, "typer/styles/"))
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
for _, entry := range entries {
entryPath := path.Join(path.Join(sysconfdir, "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 _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
+44 -21
View File
@@ -2,15 +2,18 @@ package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"path/filepath"
"strconv"
"strings"
"typer/runestring"
"github.com/gdamore/tcell/v2"
)
type TopMenuButton struct {
Name string
Action func(w *Window)
PosX int
Action func(w *Window, b *TopMenuButton)
}
var TopMenuButtons = make([]TopMenuButton, 0)
@@ -19,7 +22,7 @@ func initTopMenu() {
// Buttons
fileButton := TopMenuButton{
Name: "File",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -27,7 +30,7 @@ func initTopMenu() {
y++
}
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, y, 0, func(i int) {
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, button.PosX, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "new-buffer")
@@ -48,7 +51,7 @@ func initTopMenu() {
}
EditButton := TopMenuButton{
Name: "Edit",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -56,7 +59,7 @@ func initTopMenu() {
y++
}
d := CreateDropdownMenu([]string{"Cut", "Copy", "Paste"}, 0, y, 0, func(i int) {
d := CreateDropdownMenu([]string{"Cut", "Copy", "Paste"}, button.PosX, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "cut")
@@ -74,7 +77,7 @@ func initTopMenu() {
}
Buffers := TopMenuButton{
Name: "Buffers",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -83,20 +86,23 @@ func initTopMenu() {
}
buffersSlice := make([]string, 0)
selected := 0
for i, buffer := range Buffers {
if window.CurrentBuffer == buffer {
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", i+1, buffer.Name))
selected = i
} else {
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", i+1, buffer.Name))
}
}
d := CreateDropdownMenu(buffersSlice, 0, y, 0, func(i int) {
d := CreateDropdownMenu(buffersSlice, button.PosX, y, 0, func(i int) {
window.CurrentBuffer = Buffers[i]
PrintMessage(window, fmt.Sprintf("Set current buffer to '%s'.", window.CurrentBuffer.Name))
window.PrintMessage(fmt.Sprintf("Set current buffer to '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
ClearDropdowns()
window.CursorMode = CursorModeBuffer
})
d.Selected = selected
ActiveDropdown = d
window.CursorMode = CursorModeDropdown
},
@@ -118,8 +124,9 @@ func drawTopMenu(window *Window) {
}
currentX := 1
for _, button := range TopMenuButtons {
for i, button := range TopMenuButtons {
drawText(screen, currentX, 0, currentX+len(button.Name), 0, topMenuStyle, button.Name)
TopMenuButtons[i].PosX = currentX
currentX += len(button.Name) + 1
}
@@ -133,6 +140,7 @@ func drawTopMenu(window *Window) {
func getBufferInfoMsg(window *Window) string {
pathToFile := "Not set"
filename := "Not set"
filetype := "Not set"
if window.CurrentBuffer.filename != "" {
pathToFile = window.CurrentBuffer.filename
}
@@ -140,24 +148,39 @@ func getBufferInfoMsg(window *Window) string {
filename = filepath.Base(window.CurrentBuffer.filename)
}
cursorPos := window.CurrentBuffer.CursorPos
cursorX, cursorY := window.GetCursorPos2D()
cursorX++
cursorY++
if window.CurrentBuffer.filetype != "" {
filetype = window.CurrentBuffer.filetype
}
chars := len(window.CurrentBuffer.Contents)
words := len(strings.Fields(window.CurrentBuffer.Contents))
var contents runestring.RuneString = nil
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))
ret = strings.ReplaceAll(ret, "%t", filetype)
ret = strings.ReplaceAll(ret, "%x", strconv.Itoa(window.CurrentBuffer.CursorPos.X+1))
ret = strings.ReplaceAll(ret, "%y", strconv.Itoa(window.CurrentBuffer.CursorPos.Y+1))
// Only replace if found for expensive calls
if strings.Contains(ret, "%p") {
ret = strings.ReplaceAll(ret, "%p", strconv.Itoa(window.CurrentBuffer.PositionToAbsolutePosition(window.CurrentBuffer.CursorPos)+1))
}
if strings.Contains(ret, "%c") {
contents = window.CurrentBuffer.GetContentsAsString()
chars := len(contents)
ret = strings.ReplaceAll(ret, "%c", strconv.Itoa(chars))
}
if strings.Contains(ret, "%w") {
if contents == nil {
contents = window.CurrentBuffer.GetContentsAsString()
}
words := len(strings.Fields(string(contents)))
ret = strings.ReplaceAll(ret, "%w", strconv.Itoa(words))
}
return ret
}
+48 -1
View File
@@ -1,6 +1,53 @@
package main
import "github.com/gdamore/tcell/v2"
import (
"log"
"os"
"path/filepath"
"runtime"
"github.com/gdamore/tcell/v2"
)
func GetConfigPath(relativeConfigPath string) string {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
execPath, err := os.Executable()
if err != nil {
log.Fatalf("Could not get path to executable: %s", err)
}
paths := make([]string, 0)
if *configDirFlag != "" {
paths = append(paths, filepath.Join(*configDirFlag, relativeConfigPath))
}
switch runtime.GOOS {
case "windows":
paths = append(paths, filepath.Join(homeDir, "AppData/Roaming/Typer", relativeConfigPath))
paths = append(paths, "C:/ProgramData/Typer", relativeConfigPath)
case "darwin":
paths = append(paths, filepath.Join(homeDir, "Library/Typer", relativeConfigPath))
paths = append(paths, filepath.Join(homeDir, "Library/typer", relativeConfigPath))
paths = append(paths, filepath.Join(sysconfdir, "Typer", relativeConfigPath))
paths = append(paths, filepath.Join(sysconfdir, "typer", relativeConfigPath))
default:
paths = append(paths, filepath.Join(homeDir, ".config/typer", relativeConfigPath))
paths = append(paths, filepath.Join(sysconfdir, "typer", relativeConfigPath))
}
paths = append(paths, filepath.Join(filepath.Dir(execPath), "config", relativeConfigPath))
for _, p := range paths {
// Return true if path exists
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
func drawText(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style, text string) {
row := y1
+167 -357
View File
@@ -1,13 +1,15 @@
package main
import (
"github.com/gdamore/tcell/v2"
"fmt"
"log"
"slices"
"strconv"
"strings"
"time"
"typer/runestring"
"unicode"
"github.com/gdamore/tcell/v2"
)
type CursorMode uint8
@@ -31,7 +33,7 @@ type Window struct {
ShowLineIndex bool
CursorMode CursorMode
Clipboard string
Clipboard runestring.RuneString
CurrentBuffer *Buffer
@@ -54,14 +56,6 @@ func CreateWindow() (*Window, error) {
screen: nil,
}
// Create empty buffer if nil
for i := 1; window.CurrentBuffer == nil; i++ {
buffer, err := CreateBuffer("New Buffer " + strconv.Itoa(i))
if err == nil {
window.CurrentBuffer = buffer
}
}
// Create tcell screen
screen, err := tcell.NewScreen()
if err != nil {
@@ -84,7 +78,7 @@ func CreateWindow() (*Window, error) {
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!")
window.PrintMessage("Could not set style either to selected one nor to fallback one", TYPER_MESSAGE_ERROR)
}
}
@@ -98,6 +92,9 @@ func (window *Window) Draw() {
// Clear screen
window.screen.Clear()
// Sync buffer offset
window.SyncBufferOffset()
// Draw top menu
if window.ShowTopMenu {
drawTopMenu(window)
@@ -161,25 +158,24 @@ func (window *Window) handleKeyInput(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
@@ -187,7 +183,7 @@ func (window *Window) handleKeyInput(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{
@@ -197,10 +193,6 @@ func (window *Window) handleKeyInput(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
@@ -214,28 +206,33 @@ func (window *Window) handleKeyInput(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
@@ -243,7 +240,7 @@ func (window *Window) handleKeyInput(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{
@@ -267,11 +264,11 @@ func (window *Window) handleKeyInput(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
@@ -318,11 +315,11 @@ func (window *Window) handleKeyInput(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
@@ -337,9 +334,9 @@ func (window *Window) handleKeyInput(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
@@ -389,25 +386,17 @@ func (window *Window) handleKeyInput(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.canEdit {
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
return
}
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
@@ -421,57 +410,31 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
}
} else if ev.Key() == tcell.KeyTab {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
if !window.CurrentBuffer.canEdit {
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
return
}
// 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
if !window.CurrentBuffer.canEdit {
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
return
}
// 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)
@@ -485,30 +448,17 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
}
} else if ev.Key() == tcell.KeyRune {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
if !window.CurrentBuffer.canEdit {
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
return
}
// 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
@@ -532,17 +482,78 @@ func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
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 mouseY == 0 && Config.ShowTopMenu && !mouseHeld {
// Mouse is in top menu
// Find clicked button
buttonFound := false
for _, button := range TopMenuButtons {
if mouseX >= button.PosX && mouseX <= button.PosX+len(button.Name) {
buttonFound = true
button.Action(window, &button)
}
}
if !buttonFound {
// Exit top menu
ClearDropdowns()
window.CursorMode = CursorModeBuffer
}
} else if window.CursorMode == CursorModeDropdown && !mouseHeld {
// Mouse is in top menu
if mouseX >= ActiveDropdown.PosX && mouseX <= ActiveDropdown.PosX+ActiveDropdown.Width+1 && mouseY > ActiveDropdown.PosY && mouseY <= ActiveDropdown.PosY+len(ActiveDropdown.Options) {
// Dropdown button clicked
ActiveDropdown.Selected = mouseY - ActiveDropdown.PosY - 1
ActiveDropdown.Action(ActiveDropdown.Selected)
} else {
// Exit top menu
ClearDropdowns()
window.CursorMode = CursorModeBuffer
}
} else if mouseX >= x1 && mouseY >= y1 && mouseX <= x2 && mouseY <= y2 && window.CursorMode == CursorModeBuffer {
// Mouse is in buffer area
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 append(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
@@ -550,22 +561,19 @@ func (window *Window) handleMouseInput(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") {
if window.CurrentBuffer.Selection == nil || strings.HasSuffix(string(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 {
@@ -574,8 +582,8 @@ func (window *Window) handleMouseInput(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 {
@@ -585,38 +593,17 @@ func (window *Window) handleMouseInput(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},
}
}
@@ -631,7 +618,7 @@ func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
}
}
// Move cursor
window.SetCursorPos2D(bufferMouseX, bufferMouseY)
window.CurrentBuffer.CursorPos = mouseBufferPos
// Set last click time
lastClick = time.Now().UnixMilli()
@@ -667,196 +654,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)
}
}