24 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
23 changed files with 835 additions and 294 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
+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
+2 -1
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"
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'
+122 -68
View File
@@ -4,15 +4,17 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"typer/runestring"
"github.com/gdamore/tcell/v2"
)
type Buffer struct {
Name string
Contents []string
Contents []runestring.RuneString
CursorPos Position
Offset Position
@@ -20,6 +22,8 @@ type Buffer struct {
Selection *Selection
canSave bool
canEdit bool
filetype string
filename string
}
@@ -54,14 +58,51 @@ func drawBuffer(window *Window) {
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
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)
}
i := -1
for lineIndex, line := range buffer.Contents {
for runeIndex, r := range line + " " {
for runeIndex, r := range append(line, ' ') {
i++
drawPosition := Position{runeIndex, lineIndex}
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)
@@ -116,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 = strings.Split(string(content), "\n")
return nil
}
@@ -144,10 +208,10 @@ func (buffer *Buffer) Save() error {
// Add newline at the end of buffer Contents
line := buffer.Contents[len(buffer.Contents)-1]
if len(line) != 0 {
buffer.Contents = append(buffer.Contents, "")
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
}
err := os.WriteFile(buffer.filename, []byte(buffer.GetContentsAsString()), 0644)
err := os.WriteFile(buffer.filename, []byte(string(buffer.GetContentsAsString())), 0644)
if err != nil {
return err
}
@@ -155,28 +219,26 @@ func (buffer *Buffer) Save() error {
return nil
}
func (buffer *Buffer) GetContentsAsString() string {
finalText := strings.Builder{}
func (buffer *Buffer) GetContentsAsString() runestring.RuneString {
finalText := make(runestring.RuneString, 0)
for i, line := range buffer.Contents {
for _, rune := range line {
finalText.WriteRune(rune)
}
finalText = append(finalText, line...)
if i != len(buffer.Contents)-1 {
finalText.WriteRune('\n')
finalText = append(finalText, '\n')
}
}
return finalText.String()
return finalText
}
func (buffer *Buffer) PositionToAbsolutePosition(position Position) int {
i := 0
for lineIndex, line := range buffer.Contents {
if len(line) == 0 {
line += " "
line = append(line, ' ')
}
for runeIndex, _ := range line + " " {
for runeIndex, _ := range append(line, ' ') {
if position.Equals(runeIndex, lineIndex) {
return i
}
@@ -191,7 +253,7 @@ func (buffer *Buffer) AbsolutePositionToPosition(absolutePosition int) Position
i := 0
for lineIndex, line := range buffer.Contents {
for runeIndex, _ := range line + " " {
for runeIndex, _ := range append(line, ' ') {
if i == absolutePosition {
return Position{runeIndex, lineIndex}
}
@@ -215,19 +277,19 @@ func (buffer *Buffer) GetSelectionEdges() (Position, Position) {
}
}
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)
}
edge1, edge2 := buffer.GetSelectionEdges()
selectedText := strings.Builder{}
selectedText := make(runestring.RuneString, 0)
if r := buffer.GetCharAtPosition(edge1); r != 0 {
selectedText.WriteRune(r)
selectedText = append(selectedText, r)
}
for ComparePositions(edge1, edge2) < 0 {
@@ -243,21 +305,21 @@ func (buffer *Buffer) GetSelectedText() string {
}
if r := buffer.GetCharAtPosition(edge1); r != 0 {
selectedText.WriteRune(buffer.GetCharAtPosition(edge1))
selectedText = append(selectedText, buffer.GetCharAtPosition(edge1))
}
}
return selectedText.String()
return selectedText
}
func (buffer *Buffer) CutText(window *Window) (string, int) {
func (buffer *Buffer) CutText(window *Window) (runestring.RuneString, int) {
if buffer.Selection == nil {
// Cut current line
cutText := buffer.Contents[buffer.CursorPos.Y] + "\n"
cutText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
// Remove line from buffer contents
if len(buffer.Contents) == 1 {
buffer.Contents[0] = ""
buffer.Contents[0] = make(runestring.RuneString, 0)
} else {
buffer.Contents = slices.Delete(buffer.Contents, buffer.CursorPos.Y, buffer.CursorPos.Y+1)
}
@@ -275,6 +337,8 @@ func (buffer *Buffer) CutText(window *Window) (string, int) {
// Remove selected text
_, edge2 := buffer.GetSelectionEdges()
buffer.CursorPos = edge2
buffer.MoveRight(1)
buffer.Delete(len(cutText))
// Remove selection
@@ -284,10 +348,10 @@ func (buffer *Buffer) CutText(window *Window) (string, int) {
}
}
func (buffer *Buffer) CopyText() (string, int) {
func (buffer *Buffer) CopyText() (runestring.RuneString, int) {
if buffer.Selection == nil {
// Cut current line
copiedText := buffer.Contents[buffer.CursorPos.Y] + "\n"
copiedText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
return copiedText, 0
} else {
@@ -296,29 +360,21 @@ func (buffer *Buffer) CopyText() (string, int) {
}
}
func (buffer *Buffer) PasteText(window *Window, text string) {
contents := buffer.GetContentsAsString()
func (buffer *Buffer) PasteText(window *Window, text runestring.RuneString) {
// Remove selected text
if buffer.Selection != nil {
edge1, edge2 := buffer.GetSelectionEdges()
absEdge1 := buffer.PositionToAbsolutePosition(edge1)
absEdge2 := buffer.PositionToAbsolutePosition(edge2)
_, edge2 := buffer.GetSelectionEdges()
if absEdge2 == len(buffer.Contents) {
absEdge2 = len(buffer.Contents) - 1
}
buffer.CursorPos = edge2
buffer.Delete(len(buffer.GetSelectedText()))
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 {
func (buffer *Buffer) FindSubstring(substring runestring.RuneString, afterPos Position) Position {
// Return no match if afterPos is larger than the buffer contents size
contents := buffer.GetContentsAsString()
absAfterPos := buffer.PositionToAbsolutePosition(afterPos)
@@ -327,7 +383,7 @@ func (buffer *Buffer) FindSubstring(substring string, afterPos Position) Positio
return Position{-1, -1}
}
index := strings.Index(contents[absAfterPos+1:], substring)
index := runestring.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += absAfterPos + 1
@@ -335,7 +391,7 @@ func (buffer *Buffer) FindSubstring(substring string, afterPos Position) Positio
return buffer.AbsolutePositionToPosition(index)
}
func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement string, afterPos Position) Position {
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)
@@ -344,21 +400,21 @@ func (buffer *Buffer) FindAndReplaceSubstring(substring, replacement string, aft
return Position{-1, -1}
}
index := strings.Index(contents[absAfterPos+1:], substring)
index := runestring.Index(contents[absAfterPos+1:], substring)
if index != -1 {
index += absAfterPos + 1
}
// Replace substring with replacement string
contents = contents[:index] + replacement + contents[index+len(substring):]
contents = slices.Insert(contents, index, replacement...)
buffer.Contents = strings.Split(contents, "\n")
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
position := Position{}
for position.X != -1 && position.Y != -1 {
@@ -444,26 +500,25 @@ func (buffer *Buffer) MoveRight(i int) bool {
func (buffer *Buffer) WriteRune(r rune) {
if r == '\n' {
if buffer.CursorPos.Y == len(buffer.Contents) {
buffer.Contents = append(buffer.Contents, "")
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
} else {
buffer.Contents = slices.Insert(buffer.Contents, buffer.CursorPos.Y+1, "")
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] = line[buffer.CursorPos.X:] + buffer.Contents[buffer.CursorPos.Y+1]
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 {
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X] + string(r) + line[buffer.CursorPos.X:]
buffer.Contents[buffer.CursorPos.Y] = slices.Insert(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, r)
buffer.MoveRight(1)
}
}
func (buffer *Buffer) WriteString(str string) {
func (buffer *Buffer) WriteString(str runestring.RuneString) {
for _, r := range str {
buffer.WriteRune(r)
}
@@ -485,7 +540,7 @@ func (buffer *Buffer) Delete(i int) bool {
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.Contents[buffer.CursorPos.Y] = append(buffer.Contents[buffer.CursorPos.Y], deletedLine...)
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y]) - len(deletedLine)
} else {
@@ -493,8 +548,7 @@ func (buffer *Buffer) Delete(i int) bool {
return false
}
} else {
line := buffer.Contents[buffer.CursorPos.Y]
buffer.Contents[buffer.CursorPos.Y] = line[:buffer.CursorPos.X] + line[buffer.CursorPos.X+1:]
buffer.Contents[buffer.CursorPos.Y] = slices.Delete(buffer.Contents[buffer.CursorPos.Y], buffer.CursorPos.X, buffer.CursorPos.X+1)
}
remainingSteps--
@@ -550,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()
@@ -569,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: make([]string, 1),
Contents: make([]runestring.RuneString, 1),
CursorPos: Position{0, 0},
canSave: true,
canEdit: true,
filename: abs,
}
@@ -612,14 +665,15 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
func CreateBuffer(bufferName string) (*Buffer, error) {
buffer := Buffer{
Name: bufferName,
Contents: make([]string, 1),
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)
+110 -58
View File
@@ -6,6 +6,7 @@ import (
"slices"
"strconv"
"strings"
"typer/runestring"
)
type Command struct {
@@ -28,7 +29,7 @@ func initCommands() {
selectionEnd: Position{len(window.CurrentBuffer.Contents) - 1, len(lastLine) - 1},
}
PrintMessage(window, "Selected all text.")
window.PrintMessage("Selected all text", TYPER_MESSAGE_INFO)
},
}
@@ -43,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)
}
},
}
@@ -61,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)
}
},
}
@@ -71,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)
}
},
}
@@ -82,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
}
@@ -99,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
}
@@ -107,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 {
@@ -132,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
}
}()
@@ -156,7 +162,7 @@ func initCommands() {
log.Fatalf("Could not reload buffer: %s", err)
}
PrintMessage(window, "Buffer reloaded.")
window.PrintMessage("Buffer reloaded", TYPER_MESSAGE_INFO)
},
}
@@ -164,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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
PrintMessage(window, "Match found.")
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
@@ -183,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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
PrintMessage(window, "Match found.")
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)
}
}()
},
@@ -204,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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
PrintMessage(window, "Match replaced successfully.")
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
@@ -224,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.X >= 0 && pos.Y >= 0 {
window.CurrentBuffer.CursorPos = pos
PrintMessage(window, "Match replaced successfully.")
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)
}
}()
},
@@ -247,18 +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 {
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
@@ -266,19 +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 {
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)
}
}()
},
@@ -299,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)
},
}
@@ -318,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)
},
}
@@ -334,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)
},
}
@@ -353,7 +359,7 @@ func initCommands() {
window.CurrentBuffer = Buffers[bufferIndex]
}
window.CursorMode = CursorModeBuffer
PrintMessage(window, "Buffer closed.")
window.PrintMessage("Buffer closed", TYPER_MESSAGE_INFO)
},
}
@@ -382,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
@@ -404,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
}
}
@@ -434,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
}
}
@@ -446,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
}
}
@@ -520,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
@@ -532,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
}
}
+16 -37
View File
@@ -3,8 +3,6 @@ package main
import (
"log"
"os"
"path/filepath"
"runtime"
"gopkg.in/yaml.v3"
)
@@ -14,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"`
@@ -21,7 +20,7 @@ type TyperConfig struct {
var Config TyperConfig
func readConfig() {
func readMainConfig() {
Config = TyperConfig{
SelectedStyle: "default",
FallbackStyle: "default-fallback",
@@ -32,44 +31,24 @@ func readConfig() {
TabIndentation: 4,
}
homeDir, err := os.UserHomeDir()
// 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")
}
// Read config file
data, err := os.ReadFile(mainConfigPath)
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
log.Fatalf("Could not read config.yml: %s", err)
}
execPath, err := os.Executable()
// Unmarshal contents into struct
err = yaml.Unmarshal(data, &Config)
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)
}
break
log.Fatalf("Could not unmarshal config.yml: %s", err)
}
// Validate config options
+1
View File
@@ -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
}
+15 -37
View File
@@ -3,8 +3,6 @@ package main
import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/gdamore/tcell/v2"
@@ -23,49 +21,29 @@ type Keybinding struct {
var Keybindings TyperKeybindings
func readKeybindings() {
func readKeybindingsConfig() {
Keybindings = TyperKeybindings{
Keybindings: make([]Keybinding, 0),
}
homeDir, err := os.UserHomeDir()
// 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")
}
// Read config file
data, err := os.ReadFile(keybindingsConfigPath)
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
log.Fatalf("Could not read keybindings.yml: %s", err)
}
execPath, err := os.Executable()
// Unmarshal contents into struct
err = yaml.Unmarshal(data, &Keybindings)
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)
}
break
log.Fatalf("Could not unmarshal keybindings.yml: %s", err)
}
}
+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++ {
+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
}
+31 -39
View File
@@ -6,7 +6,6 @@ import (
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
@@ -21,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"`
@@ -36,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 {
@@ -73,50 +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)
}
execPath, err := os.Executable()
if err != nil {
log.Fatalf("Could not get path to executable: %s", err)
}
// Read entries in directory
for _, entry := range entries {
entryPath := filepath.Join(stylesDirPath, entry.Name())
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)
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)
}
// Read entries in directory
for _, entry := range entries {
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
}
if _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
+41 -15
View File
@@ -5,13 +5,15 @@ import (
"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)
@@ -20,7 +22,7 @@ func initTopMenu() {
// Buttons
fileButton := TopMenuButton{
Name: "File",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -28,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")
@@ -49,7 +51,7 @@ func initTopMenu() {
}
EditButton := TopMenuButton{
Name: "Edit",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -57,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")
@@ -75,7 +77,7 @@ func initTopMenu() {
}
Buffers := TopMenuButton{
Name: "Buffers",
Action: func(window *Window) {
Action: func(window *Window, button *TopMenuButton) {
ClearDropdowns()
y := 0
@@ -84,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
},
@@ -119,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
}
@@ -134,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
}
@@ -141,20 +148,39 @@ func getBufferInfoMsg(window *Window) string {
filename = filepath.Base(window.CurrentBuffer.filename)
}
contents := window.CurrentBuffer.GetContentsAsString()
chars := len(contents)
words := len(strings.Fields(contents))
if window.CurrentBuffer.filetype != "" {
filetype = window.CurrentBuffer.filetype
}
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, "%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))
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))
// 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
+59 -15
View File
@@ -1,11 +1,12 @@
package main
import (
"fmt"
"log"
"slices"
"strconv"
"strings"
"time"
"typer/runestring"
"unicode"
"github.com/gdamore/tcell/v2"
@@ -32,7 +33,7 @@ type Window struct {
ShowLineIndex bool
CursorMode CursorMode
Clipboard string
Clipboard runestring.RuneString
CurrentBuffer *Buffer
@@ -55,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 {
@@ -85,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)
}
}
@@ -395,6 +388,11 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
// Typing
if ev.Key() == tcell.KeyBackspace || ev.Key() == tcell.KeyBackspace2 {
if window.CursorMode == CursorModeBuffer {
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 {
window.CurrentBuffer.CutText(window)
} else {
@@ -412,6 +410,11 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
}
} else if ev.Key() == tcell.KeyTab {
if window.CursorMode == CursorModeBuffer {
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 {
window.CurrentBuffer.CutText(window)
@@ -421,6 +424,11 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
}
} else if ev.Key() == tcell.KeyEnter {
if window.CursorMode == CursorModeBuffer {
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 {
window.CurrentBuffer.CutText(window)
@@ -440,6 +448,11 @@ func (window *Window) handleKeyInput(ev *tcell.EventKey) {
}
} else if ev.Key() == tcell.KeyRune {
if window.CursorMode == CursorModeBuffer {
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 {
window.CurrentBuffer.CutText(window)
@@ -469,9 +482,40 @@ 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 {
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}
@@ -482,7 +526,7 @@ func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
// Offset mouse X for each tab character in line
posInLine := make([]int, 0)
for i, r := range window.CurrentBuffer.Contents[mouseBufferPos.Y] + " " {
for i, r := range append(window.CurrentBuffer.Contents[mouseBufferPos.Y], ' ') {
if r == '\t' {
for j := 0; j < Config.TabIndentation; j++ {
posInLine = append(posInLine, i)
@@ -521,7 +565,7 @@ func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
}
} 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
cursorPos := window.CurrentBuffer.CursorPos
startOfWord := window.CurrentBuffer.CursorPos.X