mirror of
https://github.com/EnumeratedDev/typer.git
synced 2026-09-26 18:06:11 +00:00
Compare commits
55
Commits
4ca6d9b75b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee29fae6af
|
||
|
|
ae8e702a77
|
||
|
|
01bc6702a9
|
||
|
|
5af962c6b2
|
||
|
|
5b4723d7d4
|
||
|
|
cc92ba51c5
|
||
|
|
69178e42b1
|
||
|
|
e62f4bf41c
|
||
|
|
b784bc0208
|
||
|
|
999f1953f9
|
||
|
|
e4462f9674
|
||
|
|
ef401b5aaa
|
||
|
|
612c0ba80c
|
||
|
|
d82d53d659
|
||
|
|
abafc12b47
|
||
|
|
811a8f7ee9
|
||
|
|
18462d1df9
|
||
|
|
b40d89dc43
|
||
|
|
2f1978e6da
|
||
|
|
fcde25c28a
|
||
|
|
0e18083929
|
||
|
|
c2eacac313
|
||
|
|
b564e0ec2e
|
||
|
|
a1e8d6c19a
|
||
|
|
6a701dd311
|
||
|
|
5e6362074e
|
||
|
|
040daebafb
|
||
|
|
2bf960b085
|
||
|
|
1b5ce6b7f8 | ||
|
|
8d9de7b732 | ||
|
|
ff087b196a | ||
|
|
3025fee2bb | ||
|
|
fe16b5c067 | ||
|
|
d69aba5c1a | ||
|
|
8d24988057 | ||
|
|
6cbe0e8ab2 | ||
|
|
f31650e67a | ||
|
|
a3138a9e86 | ||
|
|
e25916228c | ||
|
|
de19696b35 | ||
|
|
f30eb3c46f | ||
|
|
ab19981179 | ||
|
|
7af696cc20 | ||
|
|
2d49f84d6f | ||
|
|
ffd8cd54c0 | ||
|
|
12495d4bd9 | ||
|
|
51ec45cd14 | ||
|
|
cc3b4ecf18 | ||
|
|
1b6b45aaea | ||
|
|
e2ac216d6e | ||
|
|
cd823c9d9f | ||
|
|
dd0cc2a293 | ||
|
|
444c355117 | ||
|
|
12649af8e3 | ||
|
|
5d64673519 |
@@ -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
@@ -2,4 +2,4 @@
|
||||
/build/
|
||||
|
||||
# IDE files
|
||||
.idea
|
||||
/.idea
|
||||
|
||||
@@ -4,22 +4,34 @@ BINDIR ?= $(PREFIX)/bin
|
||||
SYSCONFDIR := $(PREFIX)/etc
|
||||
|
||||
# Compilers and tools
|
||||
GO ?= $(shell which go)
|
||||
GO ?= go
|
||||
|
||||
# Compiler flags
|
||||
GOOS ?= $(shell $(GO) env | grep '^GOOS' | cut -d'=' -f2 | tr -d "'")
|
||||
GOARCH ?= $(shell $(GO) env | grep '^GOARCH' | cut -d'=' -f2 | tr -d "'")
|
||||
LDFLAGS ?= -w
|
||||
|
||||
build:
|
||||
mkdir -p build
|
||||
cd src/; $(GO) build -ldflags "-w" -o ../build/typer
|
||||
install -dm755 build
|
||||
cd src/; GOOS=$(GOOS) GOARCH=$(GOARCH) $(GO) build -ldflags "$(LDFLAGS) -X 'main.sysconfdir=$(SYSCONFDIR)'" -o ../build/
|
||||
|
||||
install: build/typer
|
||||
# Create directories
|
||||
install -dm755 $(DESTDIR)$(BINDIR)
|
||||
# Install files
|
||||
install -Dm755 build/typer $(DESTDIR)$(BINDIR)/typer
|
||||
install -m755 build/typer* $(DESTDIR)$(BINDIR)/
|
||||
|
||||
install-config:
|
||||
# Create directories
|
||||
install -dm755 $(DESTDIR)$(SYSCONFDIR)
|
||||
# Install files
|
||||
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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Typer Text Editor
|
||||
### A simple and easy to use text editor written in Go
|
||||
|
||||
| Default Style | Classic Style |
|
||||
|:----------------------------------------------------------------:|:----------------------------------------------------------------:|
|
||||
|  |  |
|
||||
|
||||
### Installation
|
||||
#### From a package manager:
|
||||
| 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
|
||||
- Download `make` from your package manager
|
||||
- Run the following command to compile Typer
|
||||
```shell
|
||||
make
|
||||
```
|
||||
- Run the following command **with superuser privileges** to install Typer to your system
|
||||
```shell
|
||||
make install
|
||||
```
|
||||
@@ -0,0 +1,11 @@
|
||||
# Editor style option
|
||||
selected_style: "default" # Style for 256-color and true-color capable terminals
|
||||
selected_style_fallback: "default-fallback" # Style for 8-color capable terminals (Like TTYs)
|
||||
|
||||
# Other
|
||||
show_top_menu: true
|
||||
show_line_index: true
|
||||
extend_line_index: false # Extend line index to the bottom of the screen
|
||||
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
|
||||
@@ -0,0 +1,52 @@
|
||||
keybindings:
|
||||
- keybinding: "Ctrl-Q"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "quit"
|
||||
- keybinding: "Ctrl-X"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "cut"
|
||||
- keybinding: "Ctrl-C"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "copy"
|
||||
- keybinding: "Ctrl-V"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "paste"
|
||||
- keybinding: "Ctrl-S"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "save"
|
||||
- keybinding: "Ctrl-O"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "open"
|
||||
- keybinding: "Ctrl-L"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "reload"
|
||||
- keybinding: "Ctrl-F"
|
||||
cursor_modes: [ "buffer" ]
|
||||
command: "find"
|
||||
- keybinding: "Ctrl-R"
|
||||
cursor_modes: [ "buffer" ]
|
||||
command: "replace"
|
||||
- keybinding: "PgUp"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "prev-buffer"
|
||||
- keybinding: "PgDn"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "next-buffer"
|
||||
- keybinding: "Ctrl-N"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "new-buffer"
|
||||
- keybinding: "Delete"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "close-buffer"
|
||||
- keybinding: "F1"
|
||||
cursor_modes: ["buffer","dropdown"]
|
||||
command: "menu-file"
|
||||
- keybinding: "F2"
|
||||
cursor_modes: ["buffer","dropdown"]
|
||||
command: "menu-edit"
|
||||
- keybinding: "F3"
|
||||
cursor_modes: ["buffer","dropdown"]
|
||||
command: "menu-buffers"
|
||||
- keybinding: "Ctrl-E"
|
||||
cursor_modes: ["buffer"]
|
||||
command: "execute"
|
||||
@@ -0,0 +1,33 @@
|
||||
# Metadata
|
||||
name: "classic"
|
||||
description: "Style imitating the look of classic text editors and IDEs from the and 90s"
|
||||
style_type: "256-color"
|
||||
|
||||
# Colors
|
||||
colors:
|
||||
buffer_area_bg: "darkblue" # Buffer area background color
|
||||
buffer_area_fg: "white" # Buffer area text color
|
||||
buffer_area_sel: "blue" # Buffer area selected text and cursor background color
|
||||
top_menu_bg: "245" # Top menu background color
|
||||
top_menu_fg: "black" # Top menu text color
|
||||
dropdown_bg: "lightgray" # Dropdown background color
|
||||
dropdown_fg: "black" # Dropdown text color
|
||||
dropdown_sel: "blue" # Dropdown selected option background color
|
||||
line_index_bg: "247" # Line index background color
|
||||
line_index_fg: "black" # Line index text color
|
||||
message_bar_bg: "245" # Message bar background color
|
||||
message_bar_fg: "black" # Message bar text color
|
||||
input_bar_bg: "245" # Input bar background color
|
||||
input_bar_fg: "black" # Input bar text color
|
||||
|
||||
# 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"
|
||||
@@ -0,0 +1,25 @@
|
||||
# Metadata
|
||||
name: "default-fallback"
|
||||
description: "The default look of Typer - Fallback style"
|
||||
style_type: "8-color"
|
||||
|
||||
# Colors
|
||||
colors:
|
||||
buffer_area_bg: "black" # Buffer area background color
|
||||
buffer_area_fg: "white" # Buffer area text color
|
||||
buffer_area_sel: "navy" # Buffer area selected text and cursor background color
|
||||
top_menu_bg: "white" # Top menu background color
|
||||
top_menu_fg: "black" # Top -menu text color
|
||||
dropdown_bg: "white" # Dropdown background color
|
||||
dropdown_fg: "black" # Dropdown text color
|
||||
dropdown_sel: "navy" # Dropdown selected option background color
|
||||
line_index_bg: "white" # Line index background color
|
||||
line_index_fg: "black" # Line index text color
|
||||
message_bar_bg: "white" # Message bar background color
|
||||
message_bar_fg: "black" # Message bar text color
|
||||
input_bar_bg: "white" # Input bar background color
|
||||
input_bar_fg: "black" # Input bar text color
|
||||
# Typer logs highlighting
|
||||
syntax_info: "green"
|
||||
syntax_warning: "yellow"
|
||||
syntax_error: "red"
|
||||
@@ -0,0 +1,34 @@
|
||||
# Metadata
|
||||
name: "default"
|
||||
description: "The default look of Typer"
|
||||
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
|
||||
top_menu_bg: "236" # Top menu background color
|
||||
top_menu_fg: "white" # Top menu text color
|
||||
dropdown_bg: "236" # Dropdown background color
|
||||
dropdown_fg: "white" # Dropdown text color
|
||||
dropdown_sel: "240" # Dropdown selected option background color
|
||||
line_index_bg: "235" # Line index background color
|
||||
line_index_fg: "dimgray" # Line index text color
|
||||
message_bar_bg: "236" # Message bar background color
|
||||
message_bar_fg: "white" # Message bar text color
|
||||
input_bar_bg: "236" # Input bar background color
|
||||
input_bar_fg: "white" # Input bar text color
|
||||
|
||||
# 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"
|
||||
@@ -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
|
||||
@@ -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'
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+547
-51
@@ -4,30 +4,142 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"typer/runestring"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type Buffer struct {
|
||||
Id int
|
||||
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(map[int]*Buffer)
|
||||
var LastBufferId int
|
||||
var Buffers = make([]*Buffer, 0)
|
||||
|
||||
func GetBufferByName(name string) *Buffer {
|
||||
for _, buffer := range Buffers {
|
||||
if buffer.Name == name {
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetBufferByFilename(filename string) *Buffer {
|
||||
for _, buffer := range Buffers {
|
||||
if buffer.filename == filename {
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func drawBuffer(window *Window) {
|
||||
buffer := window.CurrentBuffer
|
||||
|
||||
x, y, _, _ := window.GetTextAreaDimensions()
|
||||
|
||||
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
|
||||
|
||||
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 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Do not load if canSave is false or filename is not set
|
||||
@@ -35,12 +147,45 @@ func (buffer *Buffer) Load() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(buffer.filename)
|
||||
// Replace tilde with home directory
|
||||
if strings.HasPrefix(buffer.filename, "~/") {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -50,12 +195,23 @@ func (buffer *Buffer) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Append new line character at end of buffer contents if not present
|
||||
if buffer.Contents[len(buffer.Contents)-1] != '\n' {
|
||||
buffer.Contents += "\n"
|
||||
// Replace tilde with home directory
|
||||
if strings.HasPrefix(buffer.filename, "~/") {
|
||||
homedir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer.filename = filepath.Join(homedir, buffer.filename[2:])
|
||||
}
|
||||
|
||||
err := os.WriteFile(buffer.filename, []byte(buffer.Contents), 0644)
|
||||
// Add newline at the end of buffer Contents
|
||||
line := buffer.Contents[len(buffer.Contents)-1]
|
||||
if len(line) != 0 {
|
||||
buffer.Contents = append(buffer.Contents, make(runestring.RuneString, 0))
|
||||
}
|
||||
|
||||
err := os.WriteFile(buffer.filename, []byte(string(buffer.GetContentsAsString())), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,32 +219,364 @@ func (buffer *Buffer) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (buffer *Buffer) GetSelectedText() string {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
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 ""
|
||||
return Position{-1, -1}, Position{-1, -1}
|
||||
}
|
||||
|
||||
if len(buffer.Contents) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
start := buffer.Selection.selectionStart
|
||||
end := buffer.Selection.selectionEnd
|
||||
|
||||
if start >= len(buffer.Contents) {
|
||||
start = len(buffer.Contents) - 1
|
||||
}
|
||||
if end >= len(buffer.Contents) {
|
||||
end = len(buffer.Contents) - 1
|
||||
}
|
||||
|
||||
if start <= end {
|
||||
return buffer.Contents[start : end+1]
|
||||
if ComparePositions(buffer.Selection.selectionStart, buffer.Selection.selectionEnd) == -1 {
|
||||
return buffer.Selection.selectionStart, buffer.Selection.selectionEnd
|
||||
} else {
|
||||
return buffer.Contents[end : start+1]
|
||||
return buffer.Selection.selectionEnd, buffer.Selection.selectionStart
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) GetSelectedText() runestring.RuneString {
|
||||
if buffer.Selection == nil {
|
||||
return make(runestring.RuneString, 0)
|
||||
}
|
||||
if len(buffer.Contents) == 0 {
|
||||
return make(runestring.RuneString, 0)
|
||||
}
|
||||
|
||||
edge1, edge2 := buffer.GetSelectionEdges()
|
||||
|
||||
selectedText := make(runestring.RuneString, 0)
|
||||
if r := buffer.GetCharAtPosition(edge1); r != 0 {
|
||||
selectedText = append(selectedText, r)
|
||||
}
|
||||
|
||||
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) (runestring.RuneString, int) {
|
||||
if buffer.Selection == nil {
|
||||
// Cut current line
|
||||
cutText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
|
||||
|
||||
// Remove line from buffer contents
|
||||
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.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 cutText, 1
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) CopyText() (runestring.RuneString, int) {
|
||||
if buffer.Selection == nil {
|
||||
// Cut current line
|
||||
copiedText := append(buffer.Contents[buffer.CursorPos.Y], '\n')
|
||||
|
||||
return copiedText, 0
|
||||
} else {
|
||||
// Copy selection
|
||||
return buffer.GetSelectedText(), 1
|
||||
}
|
||||
}
|
||||
|
||||
func (buffer *Buffer) PasteText(window *Window, text runestring.RuneString) {
|
||||
// Remove selected text
|
||||
if buffer.Selection != nil {
|
||||
_, edge2 := buffer.GetSelectionEdges()
|
||||
|
||||
buffer.CursorPos = edge2
|
||||
buffer.Delete(len(buffer.GetSelectedText()))
|
||||
|
||||
buffer.Selection = nil
|
||||
}
|
||||
|
||||
buffer.WriteString(text)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if absAfterPos >= len(contents) {
|
||||
return Position{-1, -1}
|
||||
}
|
||||
|
||||
index := runestring.Index(contents[absAfterPos+1:], substring)
|
||||
|
||||
if index != -1 {
|
||||
index += absAfterPos + 1
|
||||
}
|
||||
return buffer.AbsolutePositionToPosition(index)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
contents = slices.Insert(contents, index, replacement...)
|
||||
|
||||
buffer.Contents = runestring.Split(contents, '\n')
|
||||
|
||||
return buffer.AbsolutePositionToPosition(index)
|
||||
}
|
||||
|
||||
func (buffer *Buffer) FindAndReplaceAll(substring, replacement runestring.RuneString) int {
|
||||
replacements := 0
|
||||
position := Position{}
|
||||
for position.X != -1 && position.Y != -1 {
|
||||
position = buffer.FindAndReplaceSubstring(substring, replacement, position)
|
||||
if position.X != -1 && position.Y != -1 {
|
||||
replacements++
|
||||
}
|
||||
}
|
||||
|
||||
return replacements
|
||||
}
|
||||
|
||||
func (buffer *Buffer) MoveUp(i int) bool {
|
||||
buffer.CursorPos.Y -= i
|
||||
if buffer.CursorPos.Y < 0 {
|
||||
buffer.CursorPos.Y = 0
|
||||
return false
|
||||
}
|
||||
|
||||
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
|
||||
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) MoveDown(i int) bool {
|
||||
buffer.CursorPos.Y += i
|
||||
if buffer.CursorPos.Y >= len(buffer.Contents) {
|
||||
buffer.CursorPos.Y = len(buffer.Contents) - 1
|
||||
return false
|
||||
}
|
||||
|
||||
if buffer.CursorPos.X >= len(buffer.Contents[buffer.CursorPos.Y]) {
|
||||
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) MoveLeft(i int) bool {
|
||||
remainingSteps := i
|
||||
|
||||
for remainingSteps > 0 {
|
||||
buffer.CursorPos.X--
|
||||
if buffer.CursorPos.X < 0 {
|
||||
if buffer.CursorPos.Y > 0 {
|
||||
buffer.CursorPos.Y--
|
||||
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
||||
} else {
|
||||
buffer.CursorPos.X = 0
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
remainingSteps--
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) MoveRight(i int) bool {
|
||||
remainingSteps := i
|
||||
|
||||
for remainingSteps > 0 {
|
||||
buffer.CursorPos.X++
|
||||
if buffer.CursorPos.X > len(buffer.Contents[buffer.CursorPos.Y]) {
|
||||
if buffer.CursorPos.Y < len(buffer.Contents)-1 {
|
||||
buffer.CursorPos.Y++
|
||||
buffer.CursorPos.X = 0
|
||||
} else {
|
||||
buffer.CursorPos.X = len(buffer.Contents[buffer.CursorPos.Y])
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
remainingSteps--
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (buffer *Buffer) WriteRune(r rune) {
|
||||
if r == '\n' {
|
||||
if buffer.CursorPos.Y == len(buffer.Contents) {
|
||||
buffer.Contents = append(buffer.Contents, 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, "~/") {
|
||||
@@ -116,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()
|
||||
@@ -135,22 +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
|
||||
}
|
||||
} else if !stat.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("not a regular file")
|
||||
}
|
||||
|
||||
if !stat.Mode().IsRegular() {
|
||||
return nil, fmt.Errorf("%s is not a regular file", filename)
|
||||
}
|
||||
if GetBufferByName(filename) != nil {
|
||||
return nil, fmt.Errorf("a buffer with the same name is already open")
|
||||
}
|
||||
|
||||
if GetBufferByFilename(abs) != nil {
|
||||
return nil, fmt.Errorf("file is already open in another buffer")
|
||||
}
|
||||
|
||||
buffer := Buffer{
|
||||
Id: LastBufferId + 1,
|
||||
Name: filename,
|
||||
Contents: "",
|
||||
CursorPos: 0,
|
||||
Contents: make([]runestring.RuneString, 1),
|
||||
CursorPos: Position{0, 0},
|
||||
canSave: true,
|
||||
canEdit: true,
|
||||
filename: abs,
|
||||
}
|
||||
|
||||
@@ -163,24 +657,26 @@ func CreateFileBuffer(filename string, openNonExistentFile bool) (*Buffer, error
|
||||
}
|
||||
}
|
||||
|
||||
Buffers[buffer.Id] = &buffer
|
||||
LastBufferId++
|
||||
Buffers = append(Buffers, &buffer)
|
||||
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
func CreateBuffer(bufferName string) *Buffer {
|
||||
func CreateBuffer(bufferName string) (*Buffer, error) {
|
||||
buffer := Buffer{
|
||||
Id: LastBufferId + 1,
|
||||
Name: bufferName,
|
||||
Contents: "",
|
||||
CursorPos: 0,
|
||||
Contents: make([]runestring.RuneString, 1),
|
||||
CursorPos: Position{0, 0},
|
||||
canSave: true,
|
||||
canEdit: true,
|
||||
filename: "",
|
||||
}
|
||||
|
||||
Buffers[buffer.Id] = &buffer
|
||||
LastBufferId++
|
||||
if GetBufferByName(bufferName) != nil {
|
||||
return nil, fmt.Errorf("a buffer with the same name is already open")
|
||||
}
|
||||
|
||||
return &buffer
|
||||
Buffers = append(Buffers, &buffer)
|
||||
|
||||
return &buffer, nil
|
||||
}
|
||||
|
||||
+368
-49
@@ -3,9 +3,10 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"maps"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"typer/runestring"
|
||||
)
|
||||
|
||||
type Command struct {
|
||||
@@ -18,18 +19,52 @@ var commands = make(map[string]*Command)
|
||||
|
||||
func initCommands() {
|
||||
// Setup commands
|
||||
selectAll := Command{
|
||||
cmd: "select-all",
|
||||
run: func(window *Window, args ...string) {
|
||||
// Select entire buffer content
|
||||
lastLine := window.CurrentBuffer.Contents[len(window.CurrentBuffer.Contents)-1]
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: Position{0, 0},
|
||||
selectionEnd: Position{len(window.CurrentBuffer.Contents) - 1, len(lastLine) - 1},
|
||||
}
|
||||
|
||||
window.PrintMessage("Selected all text", TYPER_MESSAGE_INFO)
|
||||
},
|
||||
}
|
||||
|
||||
cutCmd := Command{
|
||||
cmd: "cut",
|
||||
run: func(window *Window, args ...string) {
|
||||
// Cut text from buffer
|
||||
copiedText, copyingMethod := window.CurrentBuffer.CutText(window)
|
||||
|
||||
// Put cut text to clipboard
|
||||
window.Clipboard = copiedText
|
||||
|
||||
// Send appropriate message and remove text depending on copying method
|
||||
if copyingMethod == 0 {
|
||||
window.PrintMessage("Copied line to clipboard", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage("Copied selection to clipboard", TYPER_MESSAGE_INFO)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
copyCmd := Command{
|
||||
cmd: "copy",
|
||||
run: func(window *Window, args ...string) {
|
||||
if window.CurrentBuffer.Selection == nil {
|
||||
// Copy line
|
||||
_, line := window.GetCursorPos2D()
|
||||
window.Clipboard = strings.SplitAfter(window.CurrentBuffer.Contents, "\n")[line]
|
||||
PrintMessage(window, "Copied line to clipboard.")
|
||||
// Copy text from buffer
|
||||
copiedText, copyingMethod := window.CurrentBuffer.CopyText()
|
||||
|
||||
// Put copied text to clipboard
|
||||
window.Clipboard = copiedText
|
||||
|
||||
// Send appropriate message depending on copying method
|
||||
if copyingMethod == 0 {
|
||||
window.PrintMessage("Copied line to clipboard", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
// Copy selection
|
||||
window.Clipboard = window.CurrentBuffer.GetSelectedText()
|
||||
PrintMessage(window, "Copied selection to clipboard.")
|
||||
window.PrintMessage("Copied selection to clipboard", TYPER_MESSAGE_INFO)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -37,16 +72,15 @@ func initCommands() {
|
||||
pasteCmd := Command{
|
||||
cmd: "paste",
|
||||
run: func(window *Window, args ...string) {
|
||||
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 index == len(str) {
|
||||
str += window.Clipboard
|
||||
} else {
|
||||
str = str[:index] + window.Clipboard + str[index:]
|
||||
if len(window.Clipboard) != 0 {
|
||||
window.CurrentBuffer.PasteText(window, window.Clipboard)
|
||||
window.PrintMessage("Pasted text to buffer", TYPER_MESSAGE_INFO)
|
||||
}
|
||||
window.CurrentBuffer.Contents = str
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos + len(window.Clipboard))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -54,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
|
||||
}
|
||||
|
||||
@@ -71,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
|
||||
}
|
||||
|
||||
@@ -79,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 {
|
||||
@@ -104,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
|
||||
}
|
||||
}()
|
||||
@@ -128,8 +162,131 @@ 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)
|
||||
},
|
||||
}
|
||||
|
||||
findCmd := Command{
|
||||
cmd: "find",
|
||||
run: func(window *Window, args ...string) {
|
||||
if len(args) >= 1 {
|
||||
input := runestring.RuneString(args[0])
|
||||
|
||||
if len(input) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
|
||||
if pos.X >= 0 && pos.Y >= 0 {
|
||||
window.CurrentBuffer.CursorPos = pos
|
||||
window.PrintMessage("Match found", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(input)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
inputChannel := RequestInput(window, "Substring to search for:", "")
|
||||
go func() {
|
||||
input := runestring.RuneString(<-inputChannel)
|
||||
|
||||
if len(input) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pos := window.CurrentBuffer.FindSubstring(input, window.CurrentBuffer.CursorPos)
|
||||
if pos.X >= 0 && pos.Y >= 0 {
|
||||
window.CurrentBuffer.CursorPos = pos
|
||||
window.PrintMessage("Match found", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(input)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
}()
|
||||
},
|
||||
}
|
||||
|
||||
replaceCmd := Command{
|
||||
cmd: "replace",
|
||||
run: func(window *Window, args ...string) {
|
||||
if len(args) >= 2 {
|
||||
findStr := runestring.RuneString(args[0])
|
||||
replaceStr := runestring.RuneString(args[1])
|
||||
|
||||
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
|
||||
window.PrintMessage("Match replaced successfully", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
inputChannel := RequestInput(window, "Substring to search for:", "")
|
||||
findStr := runestring.RuneString(<-inputChannel)
|
||||
if len(findStr) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
inputChannel = RequestInput(window, "String to replace with:", "")
|
||||
replaceStr := runestring.RuneString(<-inputChannel)
|
||||
|
||||
pos := window.CurrentBuffer.FindAndReplaceSubstring(findStr, replaceStr, window.CurrentBuffer.CursorPos)
|
||||
if pos.X >= 0 && pos.Y >= 0 {
|
||||
window.CurrentBuffer.CursorPos = pos
|
||||
window.PrintMessage("Match replaced successfully", TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
}()
|
||||
},
|
||||
}
|
||||
|
||||
replaceAllCmd := Command{
|
||||
cmd: "replace-all",
|
||||
run: func(window *Window, args ...string) {
|
||||
if len(args) >= 2 {
|
||||
findStr := runestring.RuneString(args[0])
|
||||
replaceStr := runestring.RuneString(args[1])
|
||||
|
||||
if len(findStr) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
|
||||
if replacements > 0 {
|
||||
window.PrintMessage(fmt.Sprintf("Replaced all %d matches successfully", replacements), TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
inputChannel := RequestInput(window, "Substring to search for:", "")
|
||||
findStr := runestring.RuneString(<-inputChannel)
|
||||
if len(findStr) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
inputChannel = RequestInput(window, "String to replace with:", "")
|
||||
replaceStr := runestring.RuneString(<-inputChannel)
|
||||
|
||||
replacements := window.CurrentBuffer.FindAndReplaceAll(findStr, replaceStr)
|
||||
if replacements > 0 {
|
||||
window.PrintMessage(fmt.Sprintf("Replaced all %d matches successfully", replacements), TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("'%s' not found in buffer", string(findStr)), TYPER_MESSAGE_WARNING)
|
||||
}
|
||||
}()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -140,15 +297,15 @@ func initCommands() {
|
||||
return
|
||||
}
|
||||
|
||||
buffers := slices.Collect(maps.Values(Buffers))
|
||||
index := slices.Index(buffers, window.CurrentBuffer)
|
||||
index := slices.Index(Buffers, window.CurrentBuffer)
|
||||
|
||||
index--
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
|
||||
window.CurrentBuffer = buffers[index]
|
||||
window.CurrentBuffer = Buffers[index]
|
||||
window.PrintMessage(fmt.Sprintf("Set current buffer to '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -159,44 +316,155 @@ func initCommands() {
|
||||
return
|
||||
}
|
||||
|
||||
buffers := slices.Collect(maps.Values(Buffers))
|
||||
index := slices.Index(buffers, window.CurrentBuffer)
|
||||
index := slices.Index(Buffers, window.CurrentBuffer)
|
||||
|
||||
index++
|
||||
if index >= len(buffers) {
|
||||
index = len(buffers) - 1
|
||||
if index >= len(Buffers) {
|
||||
index = len(Buffers) - 1
|
||||
}
|
||||
|
||||
window.CurrentBuffer = buffers[index]
|
||||
window.CurrentBuffer = Buffers[index]
|
||||
window.PrintMessage(fmt.Sprintf("Set current buffer to '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
|
||||
},
|
||||
}
|
||||
|
||||
newBufferCmd := Command{
|
||||
cmd: "new-buffer",
|
||||
run: func(window *Window, args ...string) {
|
||||
number := 1
|
||||
for _, buffer := range Buffers {
|
||||
if strings.HasPrefix(buffer.Name, "New File ") {
|
||||
number++
|
||||
for i := 1; true; i++ {
|
||||
buffer, err := CreateBuffer("New Buffer " + strconv.Itoa(i))
|
||||
if err == nil {
|
||||
window.CurrentBuffer = buffer
|
||||
break
|
||||
}
|
||||
}
|
||||
buffer := CreateBuffer(fmt.Sprintf("New File %d", number))
|
||||
window.CurrentBuffer = buffer
|
||||
|
||||
window.CursorMode = CursorModeBuffer
|
||||
window.PrintMessage(fmt.Sprintf("New buffer created with the name '%s'", window.CurrentBuffer.Name), TYPER_MESSAGE_INFO)
|
||||
},
|
||||
}
|
||||
|
||||
closeBufferCmd := Command{
|
||||
cmd: "close-buffer",
|
||||
run: func(window *Window, args ...string) {
|
||||
delete(Buffers, window.CurrentBuffer.Id)
|
||||
buffersSlice := slices.Collect(maps.Values(Buffers))
|
||||
if len(buffersSlice) == 0 {
|
||||
bufferIndex := slices.Index(Buffers, window.CurrentBuffer)
|
||||
Buffers = DeleteFromSlice(Buffers, bufferIndex)
|
||||
if len(Buffers) == 0 {
|
||||
window.Close()
|
||||
return
|
||||
}
|
||||
window.CurrentBuffer = buffersSlice[0]
|
||||
if bufferIndex >= len(Buffers) {
|
||||
window.CurrentBuffer = Buffers[bufferIndex-1]
|
||||
} else {
|
||||
window.CurrentBuffer = Buffers[bufferIndex]
|
||||
}
|
||||
window.CursorMode = CursorModeBuffer
|
||||
window.PrintMessage("Buffer closed", TYPER_MESSAGE_INFO)
|
||||
},
|
||||
}
|
||||
|
||||
toggleTopBar := Command{
|
||||
cmd: "toggle-top-bar",
|
||||
run: func(window *Window, args ...string) {
|
||||
window.ShowTopMenu = !window.ShowTopMenu
|
||||
},
|
||||
}
|
||||
|
||||
toggleLineIndex := Command{
|
||||
cmd: "toggle-line-index",
|
||||
run: func(window *Window, args ...string) {
|
||||
window.ShowLineIndex = !window.ShowLineIndex
|
||||
},
|
||||
}
|
||||
|
||||
setStyleCmd := Command{
|
||||
cmd: "set-style",
|
||||
run: func(window *Window, args ...string) {
|
||||
if len(args) >= 1 {
|
||||
input := args[0]
|
||||
|
||||
if input == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := AvailableStyles[input]; !ok {
|
||||
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
if ok := SetCurrentStyle(window.screen, input); ok {
|
||||
window.PrintMessage(fmt.Sprintf("Setting style to '%s'", input), TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
inputChannel := RequestInput(window, "Style to switch to:", "")
|
||||
go func() {
|
||||
input := <-inputChannel
|
||||
|
||||
if input == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := AvailableStyles[input]; !ok {
|
||||
window.PrintMessage(fmt.Sprintf("Could not set style to '%s'", input), TYPER_MESSAGE_ERROR)
|
||||
return
|
||||
}
|
||||
|
||||
if ok := SetCurrentStyle(window.screen, input); ok {
|
||||
window.PrintMessage(fmt.Sprintf("Setting style to '%s'", input), TYPER_MESSAGE_INFO)
|
||||
} else {
|
||||
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)
|
||||
|
||||
}()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -205,7 +473,7 @@ func initCommands() {
|
||||
run: func(window *Window, args ...string) {
|
||||
for _, button := range TopMenuButtons {
|
||||
if button.Name == "File" {
|
||||
button.Action(window)
|
||||
button.Action(window, &button)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -217,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
|
||||
}
|
||||
}
|
||||
@@ -229,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
|
||||
}
|
||||
}
|
||||
@@ -244,20 +512,71 @@ func initCommands() {
|
||||
},
|
||||
}
|
||||
|
||||
executeCmd := Command{
|
||||
cmd: "execute",
|
||||
run: func(window *Window, args ...string) {
|
||||
inputChannel := RequestInput(window, "Run:", "")
|
||||
|
||||
go func() {
|
||||
input := strings.TrimSpace(<-inputChannel)
|
||||
|
||||
if input == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var arguments []string
|
||||
|
||||
builder := &strings.Builder{}
|
||||
quoted := false
|
||||
for _, r := range input {
|
||||
if r == '"' {
|
||||
quoted = !quoted
|
||||
} else if !quoted && r == ' ' {
|
||||
arguments = append(arguments, builder.String())
|
||||
builder.Reset()
|
||||
} else {
|
||||
builder.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if builder.Len() > 0 {
|
||||
arguments = append(arguments, builder.String())
|
||||
}
|
||||
|
||||
window.CursorMode = CursorModeBuffer
|
||||
|
||||
if len(arguments) == 1 {
|
||||
RunCommand(window, arguments[0])
|
||||
} else {
|
||||
RunCommand(window, arguments[0], arguments[1:]...)
|
||||
}
|
||||
}()
|
||||
},
|
||||
}
|
||||
|
||||
// Register commands
|
||||
commands["select-all"] = &selectAll
|
||||
commands["cut"] = &cutCmd
|
||||
commands["copy"] = ©Cmd
|
||||
commands["paste"] = &pasteCmd
|
||||
commands["save"] = &saveCmd
|
||||
commands["open"] = &openCmd
|
||||
commands["reload"] = &reloadCmd
|
||||
commands["find"] = &findCmd
|
||||
commands["replace"] = &replaceCmd
|
||||
commands["replace-all"] = &replaceAllCmd
|
||||
commands["prev-buffer"] = &prevBufferCmd
|
||||
commands["next-buffer"] = &nextBufferCmd
|
||||
commands["new-buffer"] = &newBufferCmd
|
||||
commands["close-buffer"] = &closeBufferCmd
|
||||
commands["toggle-top-bar"] = &toggleTopBar
|
||||
commands["toggle-line-index"] = &toggleLineIndex
|
||||
commands["set-style"] = &setStyleCmd
|
||||
commands["set-filetype"] = &setFiletypeCmd
|
||||
commands["menu-file"] = &menuFileCmd
|
||||
commands["menu-edit"] = &menuEditCmd
|
||||
commands["menu-buffers"] = &menuBuffersCmd
|
||||
commands["quit"] = &quitCmd
|
||||
commands["execute"] = &executeCmd
|
||||
}
|
||||
|
||||
func RunCommand(window *Window, cmd string, args ...string) bool {
|
||||
@@ -265,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type TyperConfig struct {
|
||||
SelectedStyle string `yaml:"selected_style,omitempty"`
|
||||
FallbackStyle string `yaml:"fallback_style,omitempty"`
|
||||
ShowTopMenu bool `yaml:"show_top_menu,omitempty"`
|
||||
ShowLineIndex bool `yaml:"show_line_index,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
var Config TyperConfig
|
||||
|
||||
func readMainConfig() {
|
||||
Config = TyperConfig{
|
||||
SelectedStyle: "default",
|
||||
FallbackStyle: "default-fallback",
|
||||
ShowTopMenu: true,
|
||||
ShowLineIndex: true,
|
||||
ExtendLineIndex: false,
|
||||
BufferInfoMessage: "File: %f Cursor: (%x, %y, %p) Chars: %c",
|
||||
TabIndentation: 4,
|
||||
}
|
||||
|
||||
// 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 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
|
||||
if Config.TabIndentation < 1 {
|
||||
Config.TabIndentation = 1
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -50,13 +50,13 @@ func ClearDropdowns() {
|
||||
}
|
||||
|
||||
func drawDropdowns(window *Window) {
|
||||
dropdownStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
|
||||
dropdownStyle := tcell.StyleDefault.Background(CurrentStyle.DropdownBg).Foreground(CurrentStyle.DropdownFg)
|
||||
for _, d := range dropdowns {
|
||||
drawBox(window.screen, d.PosX, d.PosY, d.PosX+d.Width+1, d.PosY+len(d.Options)+1, dropdownStyle)
|
||||
line := d.PosY
|
||||
line := 1
|
||||
for i, option := range d.Options {
|
||||
if d.Selected == i {
|
||||
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(tcell.Color240), option)
|
||||
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(CurrentStyle.DropdownSel), option)
|
||||
} else {
|
||||
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle, option)
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,4 +1,4 @@
|
||||
module Typer
|
||||
module typer
|
||||
|
||||
go 1.24
|
||||
|
||||
@@ -9,7 +9,9 @@ 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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
@@ -79,3 +81,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+2
-6
@@ -18,7 +18,7 @@ func RequestInput(window *Window, text string, defaultInput string) chan string
|
||||
request := &TyperInputRequest{
|
||||
Text: text,
|
||||
input: defaultInput,
|
||||
cursorPos: 0,
|
||||
cursorPos: len(defaultInput),
|
||||
inputChannel: make(chan string),
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ func RequestInput(window *Window, text string, defaultInput string) chan string
|
||||
return request.inputChannel
|
||||
}
|
||||
|
||||
func IsRequestingInput() bool {
|
||||
return currentInputRequest != nil
|
||||
}
|
||||
|
||||
func drawInputBar(window *Window) {
|
||||
if currentInputRequest == nil {
|
||||
return
|
||||
@@ -42,7 +38,7 @@ func drawInputBar(window *Window) {
|
||||
|
||||
screen := window.screen
|
||||
|
||||
inputBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
|
||||
inputBarStyle := tcell.StyleDefault.Background(CurrentStyle.InputBarBg).Foreground(CurrentStyle.InputBarFg)
|
||||
|
||||
sizeX, sizeY := screen.Size()
|
||||
|
||||
|
||||
+55
-81
@@ -1,107 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type TyperKeybindings struct {
|
||||
Keybindings []Keybinding `yaml:"keybindings"`
|
||||
}
|
||||
|
||||
type Keybinding struct {
|
||||
keybind string
|
||||
cursorModes []CursorMode
|
||||
command string
|
||||
Keybinding string `yaml:"keybinding"`
|
||||
CursorModes []string `yaml:"cursor_modes"`
|
||||
Command string `yaml:"command"`
|
||||
}
|
||||
|
||||
var Keybinds = make([]Keybinding, 0)
|
||||
var Keybindings TyperKeybindings
|
||||
|
||||
func initKeybindings() {
|
||||
// Add key bindings
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-Q",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "quit",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-C",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "copy",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-V",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "paste",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-S",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "save",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-O",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "open",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-R",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "reload",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "PgUp",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "prev-buffer",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "PgDn",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "next-buffer",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-N",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "new-buffer",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Delete",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "close-buffer",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "Ctrl-Q",
|
||||
cursorModes: []CursorMode{CursorModeBuffer},
|
||||
command: "quit",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "F1",
|
||||
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
|
||||
command: "menu-file",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "F2",
|
||||
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
|
||||
command: "menu-edit",
|
||||
})
|
||||
Keybinds = append(Keybinds, Keybinding{
|
||||
keybind: "F3",
|
||||
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
|
||||
command: "menu-buffers",
|
||||
})
|
||||
func readKeybindingsConfig() {
|
||||
Keybindings = TyperKeybindings{
|
||||
Keybindings: make([]Keybinding, 0),
|
||||
}
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
}
|
||||
|
||||
func (keybind *Keybinding) IsPressed(ev *tcell.EventKey) bool {
|
||||
keys := strings.SplitN(keybind.keybind, "+", 2)
|
||||
func (keybinding *Keybinding) GetCursorModes() []CursorMode {
|
||||
ret := make([]CursorMode, 0)
|
||||
|
||||
for _, cursorModeStr := range keybinding.CursorModes {
|
||||
for key, value := range CursorModeNames {
|
||||
if cursorModeStr == value {
|
||||
ret = append(ret, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (keybinding *Keybinding) IsPressed(ev *tcell.EventKey) bool {
|
||||
keys := strings.SplitN(keybinding.Keybinding, "+", 2)
|
||||
|
||||
if len(keys) == 0 {
|
||||
return false
|
||||
} else if len(keys) == 1 {
|
||||
for k, v := range tcell.KeyNames {
|
||||
if k != tcell.KeyRune {
|
||||
if keybind.keybind == v {
|
||||
if keybinding.Keybinding == v {
|
||||
if ev.Key() == k {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if keybind.keybind == string(ev.Rune()) {
|
||||
if keybinding.Keybinding == string(ev.Rune()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,27 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
func drawLineIndex(window *Window) {
|
||||
screen := window.screen
|
||||
buffer := window.CurrentBuffer
|
||||
|
||||
lineIndexStyle := tcell.StyleDefault.Foreground(tcell.ColorDimGray).Background(tcell.Color235)
|
||||
|
||||
_, sizeY := screen.Size()
|
||||
|
||||
y := 0
|
||||
if window.ShowTopMenu {
|
||||
y = 1
|
||||
}
|
||||
lineIndexStyle := tcell.StyleDefault.Background(CurrentStyle.LineIndexBg).Foreground(CurrentStyle.LineIndexFg)
|
||||
|
||||
lineIndexSize := getLineIndexSize(window)
|
||||
|
||||
for lineIndex := 1 + buffer.OffsetY; lineIndex <= strings.Count(buffer.Contents, "\n")+1 && lineIndex < sizeY+buffer.OffsetY; lineIndex++ {
|
||||
_, bufferY1, _, bufferY2 := window.GetTextAreaDimensions()
|
||||
|
||||
lineIndex := 1 + buffer.Offset.Y
|
||||
for y := bufferY1; y <= bufferY2; y++ {
|
||||
if lineIndex > len(buffer.Contents) {
|
||||
if Config.ExtendLineIndex {
|
||||
for x := 0; x < lineIndexSize; x++ {
|
||||
screen.SetContent(x, y, ' ', nil, lineIndexStyle)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for x := 0; x < lineIndexSize; x++ {
|
||||
screen.SetContent(x, y, ' ', nil, lineIndexStyle)
|
||||
@@ -30,12 +36,13 @@ func drawLineIndex(window *Window) {
|
||||
text := strconv.Itoa(lineIndex)
|
||||
|
||||
drawText(screen, lineIndexSize-len(text)-1, y, lineIndexSize, y, lineIndexStyle, text)
|
||||
y++
|
||||
|
||||
lineIndex++
|
||||
}
|
||||
}
|
||||
|
||||
func getLineIndexSize(window *Window) int {
|
||||
i := strings.Count(window.CurrentBuffer.Contents, "\n") + 1
|
||||
i := len(window.CurrentBuffer.Contents)
|
||||
if i == 0 {
|
||||
return 4
|
||||
}
|
||||
|
||||
+53
-13
@@ -1,39 +1,79 @@
|
||||
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 flags
|
||||
readFlags()
|
||||
|
||||
// Read main config
|
||||
readMainConfig()
|
||||
|
||||
// Read keybindings config
|
||||
readKeybindingsConfig()
|
||||
|
||||
// Read styles directory
|
||||
readStyles()
|
||||
|
||||
// Read syntax directory
|
||||
ReadSyntaxHighlighters()
|
||||
|
||||
// Initialize commands
|
||||
initCommands()
|
||||
|
||||
// Initialize key bindings
|
||||
initKeybindings()
|
||||
|
||||
window, err := CreateWindow()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create window: %v", err)
|
||||
}
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
for _, file := range os.Args[1:] {
|
||||
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 window.CurrentBuffer.Name == "New File 1" {
|
||||
delete(Buffers, window.CurrentBuffer.Id)
|
||||
window.CurrentBuffer = b
|
||||
if window.CurrentBuffer == nil {
|
||||
window.CurrentBuffer = buffer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for window.screen != nil {
|
||||
window.Draw()
|
||||
if window.CurrentBuffer == nil {
|
||||
buffer, err := CreateBuffer("New Buffer 1")
|
||||
if err == nil {
|
||||
window.CurrentBuffer = buffer
|
||||
}
|
||||
}
|
||||
|
||||
for !window.closed {
|
||||
window.Draw()
|
||||
window.ProcessEvents()
|
||||
}
|
||||
|
||||
window.screen.Fini()
|
||||
window.screen = nil
|
||||
}
|
||||
|
||||
func readFlags() {
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
+69
-9
@@ -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 {
|
||||
@@ -33,13 +74,32 @@ func PrintMessage(window *Window, message string) {
|
||||
func drawMessageBar(window *Window) {
|
||||
screen := window.screen
|
||||
|
||||
messageBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
|
||||
messageBarStyle := tcell.StyleDefault.Background(CurrentStyle.MessageBarBg).Foreground(CurrentStyle.MessageBarFg)
|
||||
|
||||
sizeX, sizeY := screen.Size()
|
||||
|
||||
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++ {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type TyperStyle struct {
|
||||
// Metadata
|
||||
Name string
|
||||
Description string
|
||||
StyleType string
|
||||
|
||||
// Main Colors
|
||||
BufferAreaBg tcell.Color `name:"buffer_area_bg"`
|
||||
BufferAreaFg tcell.Color `name:"buffer_area_fg"`
|
||||
BufferAreaSel tcell.Color `name:"buffer_area_sel"`
|
||||
TopMenuBg tcell.Color `name:"top_menu_bg"`
|
||||
TopMenuFg tcell.Color `name:"top_menu_fg"`
|
||||
DropdownBg tcell.Color `name:"dropdown_bg"`
|
||||
DropdownFg tcell.Color `name:"dropdown_fg"`
|
||||
DropdownSel tcell.Color `name:"dropdown_sel"`
|
||||
LineIndexBg tcell.Color `name:"line_index_bg"`
|
||||
LineIndexFg tcell.Color `name:"line_index_fg"`
|
||||
MessageBarBg tcell.Color `name:"message_bar_bg"`
|
||||
MessageBarFg tcell.Color `name:"message_bar_fg"`
|
||||
InputBarBg tcell.Color `name:"input_bar_bg"`
|
||||
InputBarFg tcell.Color `name:"input_bar_fg"`
|
||||
|
||||
// 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 {
|
||||
// Metadata
|
||||
Name string `yaml:"name"`
|
||||
Description string `yaml:"description"`
|
||||
StyleType string `yaml:"style_type"`
|
||||
|
||||
// Colors
|
||||
Colors map[string]string `yaml:"colors"`
|
||||
}
|
||||
|
||||
var FallbackStyle = TyperStyle{
|
||||
Name: "fallback",
|
||||
Description: "Fallback style",
|
||||
StyleType: "8-color",
|
||||
|
||||
BufferAreaBg: tcell.ColorBlack,
|
||||
BufferAreaFg: tcell.ColorWhite,
|
||||
BufferAreaSel: tcell.ColorNavy,
|
||||
TopMenuBg: tcell.ColorWhite,
|
||||
TopMenuFg: tcell.ColorBlack,
|
||||
DropdownBg: tcell.ColorWhite,
|
||||
DropdownFg: tcell.ColorBlack,
|
||||
DropdownSel: tcell.ColorNavy,
|
||||
LineIndexBg: tcell.ColorWhite,
|
||||
LineIndexFg: tcell.ColorBlack,
|
||||
MessageBarBg: tcell.ColorWhite,
|
||||
MessageBarFg: tcell.ColorBlack,
|
||||
InputBarBg: tcell.ColorWhite,
|
||||
InputBarFg: tcell.ColorBlack,
|
||||
}
|
||||
|
||||
var AvailableStyles = make(map[string]TyperStyle)
|
||||
var CurrentStyle = FallbackStyle
|
||||
|
||||
func readStyles() {
|
||||
// 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 read user style directory: %s", err)
|
||||
}
|
||||
|
||||
// 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 style file (%s): %s", entryPath, err)
|
||||
}
|
||||
|
||||
if _, ok := AvailableStyles[style.Name]; !ok {
|
||||
AvailableStyles[style.Name] = style
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readStyleYamlFile(filepath string) (TyperStyle, error) {
|
||||
styleYaml := typerStyleYaml{}
|
||||
|
||||
data, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return TyperStyle{}, fmt.Errorf("could not read file: %s", err)
|
||||
}
|
||||
err = yaml.Unmarshal(data, &styleYaml)
|
||||
if err != nil {
|
||||
return TyperStyle{}, fmt.Errorf("could not unmarshal style: %s", err)
|
||||
}
|
||||
|
||||
style := TyperStyle{
|
||||
Name: styleYaml.Name,
|
||||
Description: styleYaml.Description,
|
||||
StyleType: styleYaml.StyleType,
|
||||
}
|
||||
|
||||
for name, colorStr := range styleYaml.Colors {
|
||||
var color tcell.Color
|
||||
|
||||
if n, err := strconv.Atoi(colorStr); err == nil && n >= 0 && n < 256 {
|
||||
color = tcell.ColorValid + tcell.Color(n)
|
||||
} else if strings.HasPrefix(colorStr, "#") && len(colorStr) == 7 {
|
||||
n, err := strconv.ParseInt(colorStr[1:], 16, 32)
|
||||
if err != nil {
|
||||
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
|
||||
}
|
||||
|
||||
color = tcell.NewHexColor(int32(n))
|
||||
} else if c, ok := tcell.ColorNames[colorStr]; ok {
|
||||
color = c
|
||||
} else {
|
||||
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
|
||||
}
|
||||
|
||||
pt := reflect.TypeOf(&style)
|
||||
t := pt.Elem()
|
||||
pv := reflect.ValueOf(&style)
|
||||
v := pv.Elem()
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
|
||||
if tag, ok := t.Field(i).Tag.Lookup("name"); ok && tag == name {
|
||||
field.Set(reflect.ValueOf(color))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style, nil
|
||||
}
|
||||
|
||||
func SetCurrentStyle(screen tcell.Screen, styleName string) bool {
|
||||
availableTypes := make([]string, 1)
|
||||
availableTypes[0] = "8-color"
|
||||
if screen.Colors() >= 16 {
|
||||
availableTypes = append(availableTypes, "16-color")
|
||||
}
|
||||
if screen.Colors() >= 256 {
|
||||
availableTypes = append(availableTypes, "256-color")
|
||||
}
|
||||
if screen.Colors() >= 16777216 {
|
||||
availableTypes = append(availableTypes, "true-color")
|
||||
}
|
||||
|
||||
if style, ok := AvailableStyles[styleName]; ok && slices.Index(availableTypes, style.StyleType) != -1 {
|
||||
CurrentStyle = style
|
||||
|
||||
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
|
||||
screen.Sync()
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
+91
-30
@@ -2,16 +2,18 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"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,9 +22,15 @@ func initTopMenu() {
|
||||
// Buttons
|
||||
fileButton := TopMenuButton{
|
||||
Name: "File",
|
||||
Action: func(window *Window) {
|
||||
Action: func(window *Window, button *TopMenuButton) {
|
||||
ClearDropdowns()
|
||||
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, 1, 0, func(i int) {
|
||||
|
||||
y := 0
|
||||
if window.ShowTopMenu {
|
||||
y++
|
||||
}
|
||||
|
||||
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, button.PosX, y, 0, func(i int) {
|
||||
switch i {
|
||||
case 0:
|
||||
RunCommand(window, "new-buffer")
|
||||
@@ -43,13 +51,21 @@ func initTopMenu() {
|
||||
}
|
||||
EditButton := TopMenuButton{
|
||||
Name: "Edit",
|
||||
Action: func(window *Window) {
|
||||
Action: func(window *Window, button *TopMenuButton) {
|
||||
ClearDropdowns()
|
||||
d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, 1, 0, func(i int) {
|
||||
|
||||
y := 0
|
||||
if window.ShowTopMenu {
|
||||
y++
|
||||
}
|
||||
|
||||
d := CreateDropdownMenu([]string{"Cut", "Copy", "Paste"}, button.PosX, y, 0, func(i int) {
|
||||
switch i {
|
||||
case 0:
|
||||
RunCommand(window, "copy")
|
||||
RunCommand(window, "cut")
|
||||
case 1:
|
||||
RunCommand(window, "copy")
|
||||
case 2:
|
||||
RunCommand(window, "paste")
|
||||
}
|
||||
ClearDropdowns()
|
||||
@@ -61,33 +77,32 @@ func initTopMenu() {
|
||||
}
|
||||
Buffers := TopMenuButton{
|
||||
Name: "Buffers",
|
||||
Action: func(window *Window) {
|
||||
Action: func(window *Window, button *TopMenuButton) {
|
||||
ClearDropdowns()
|
||||
|
||||
y := 0
|
||||
if window.ShowTopMenu {
|
||||
y++
|
||||
}
|
||||
|
||||
buffersSlice := make([]string, 0)
|
||||
for _, buffer := range Buffers {
|
||||
selected := 0
|
||||
for i, buffer := range Buffers {
|
||||
if window.CurrentBuffer == buffer {
|
||||
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", buffer.Id, buffer.Name))
|
||||
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] * %s", i+1, buffer.Name))
|
||||
selected = i
|
||||
} else {
|
||||
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", buffer.Id, buffer.Name))
|
||||
buffersSlice = append(buffersSlice, fmt.Sprintf("[%d] %s", i+1, buffer.Name))
|
||||
}
|
||||
}
|
||||
|
||||
slices.Sort(buffersSlice)
|
||||
|
||||
d := CreateDropdownMenu(buffersSlice, 0, 1, 0, func(i int) {
|
||||
start := strings.Index(buffersSlice[i], "[")
|
||||
end := strings.Index(buffersSlice[i], "]")
|
||||
|
||||
id, err := strconv.Atoi(buffersSlice[i][start+1 : end])
|
||||
if err != nil {
|
||||
PrintMessage(window, fmt.Sprintf("Cannot convert buffer id '%s' to int", buffersSlice[i][start:end]))
|
||||
return
|
||||
}
|
||||
|
||||
window.CurrentBuffer = Buffers[id]
|
||||
d := CreateDropdownMenu(buffersSlice, button.PosX, y, 0, func(i int) {
|
||||
window.CurrentBuffer = Buffers[i]
|
||||
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
|
||||
},
|
||||
@@ -100,7 +115,7 @@ func initTopMenu() {
|
||||
func drawTopMenu(window *Window) {
|
||||
screen := window.screen
|
||||
|
||||
topMenuStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
|
||||
topMenuStyle := tcell.StyleDefault.Background(CurrentStyle.TopMenuBg).Foreground(CurrentStyle.TopMenuFg)
|
||||
|
||||
sizeX, _ := screen.Size()
|
||||
|
||||
@@ -109,17 +124,63 @@ 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
|
||||
}
|
||||
|
||||
// Draw buffer info
|
||||
bufferInfoMsg := getBufferInfoMsg(window)
|
||||
if sizeX-len(bufferInfoMsg)-1 > currentX+2 {
|
||||
drawText(screen, sizeX-len(bufferInfoMsg)-1, 0, sizeX-1, 0, topMenuStyle, bufferInfoMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func getBufferInfoMsg(window *Window) string {
|
||||
pathToFile := "Not set"
|
||||
filename := "Not set"
|
||||
filetype := "Not set"
|
||||
if window.CurrentBuffer.filename != "" {
|
||||
pathToFile = window.CurrentBuffer.filename
|
||||
}
|
||||
if filepath.Base(window.CurrentBuffer.filename) != "." {
|
||||
filename = filepath.Base(window.CurrentBuffer.filename)
|
||||
}
|
||||
cursorX, cursorY := window.GetCursorPos2D()
|
||||
cursorInfo := fmt.Sprintf("File: %s Cursor: (%d,%d,%d) Words: %d", filename, cursorX+1, cursorY+1, window.CurrentBuffer.CursorPos+1, len(strings.Fields(window.CurrentBuffer.Contents)))
|
||||
drawText(screen, sizeX-len(cursorInfo)-1, 0, sizeX-1, 0, topMenuStyle, cursorInfo)
|
||||
|
||||
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))
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
+60
-1
@@ -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
|
||||
@@ -53,3 +100,15 @@ func drawBox(s tcell.Screen, x1, y1, x2, y2 int, style tcell.Style) {
|
||||
|
||||
drawText(s, x1+1, y1+1, x2-1, y2-1, style, " ")
|
||||
}
|
||||
|
||||
func DeleteFromSlice[T any](slice []T, i int) []T {
|
||||
if i >= len(slice) {
|
||||
return slice
|
||||
} else if i < 0 {
|
||||
return slice
|
||||
} else if i == len(slice)-1 {
|
||||
return slice[:len(slice)-1]
|
||||
} else {
|
||||
return append(slice[:i], slice[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
+323
-296
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
"typer/runestring"
|
||||
"unicode"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
)
|
||||
|
||||
type CursorMode uint8
|
||||
@@ -15,24 +21,34 @@ const (
|
||||
CursorModeInputBar
|
||||
)
|
||||
|
||||
var CursorModeNames = map[CursorMode]string{
|
||||
CursorModeDisabled: "disabled",
|
||||
CursorModeBuffer: "buffer",
|
||||
CursorModeDropdown: "dropdown",
|
||||
CursorModeInputBar: "input_bar",
|
||||
}
|
||||
|
||||
type Window struct {
|
||||
ShowTopMenu bool
|
||||
ShowLineIndex bool
|
||||
CursorMode CursorMode
|
||||
|
||||
Clipboard string
|
||||
Clipboard runestring.RuneString
|
||||
|
||||
CurrentBuffer *Buffer
|
||||
|
||||
screen tcell.Screen
|
||||
|
||||
closed bool
|
||||
}
|
||||
|
||||
var mouseHeld = false
|
||||
var lastClick int64 = 0
|
||||
|
||||
func CreateWindow() (*Window, error) {
|
||||
window := Window{
|
||||
ShowTopMenu: true,
|
||||
ShowLineIndex: true,
|
||||
ShowTopMenu: Config.ShowTopMenu,
|
||||
ShowLineIndex: Config.ShowLineIndex,
|
||||
CursorMode: CursorModeBuffer,
|
||||
|
||||
CurrentBuffer: nil,
|
||||
@@ -40,11 +56,6 @@ func CreateWindow() (*Window, error) {
|
||||
screen: nil,
|
||||
}
|
||||
|
||||
// Create empty buffer if nil
|
||||
if window.CurrentBuffer == nil {
|
||||
window.CurrentBuffer = CreateBuffer("New File 1")
|
||||
}
|
||||
|
||||
// Create tcell screen
|
||||
screen, err := tcell.NewScreen()
|
||||
if err != nil {
|
||||
@@ -55,77 +66,35 @@ func CreateWindow() (*Window, error) {
|
||||
log.Fatalf("Failed to initialize screen: %s", err)
|
||||
}
|
||||
|
||||
// Set screen style
|
||||
screen.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234))
|
||||
|
||||
// Enable mouse
|
||||
screen.EnableMouse()
|
||||
|
||||
// Set window screen field
|
||||
window.screen = screen
|
||||
|
||||
// Try to set screen style to selected one
|
||||
if ok := SetCurrentStyle(screen, Config.SelectedStyle); !ok {
|
||||
// Try to set screen style to selected fallback one
|
||||
if ok := SetCurrentStyle(screen, Config.FallbackStyle); !ok {
|
||||
// Use hard-coded fallback style
|
||||
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
|
||||
window.PrintMessage("Could not set style either to selected one nor to fallback one", TYPER_MESSAGE_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize top menu
|
||||
initTopMenu()
|
||||
|
||||
return &window, nil
|
||||
}
|
||||
|
||||
func (window *Window) drawCurrentBuffer() {
|
||||
buffer := window.CurrentBuffer
|
||||
|
||||
x, y, _, _ := window.GetTextAreaDimensions()
|
||||
|
||||
bufferX, bufferY, _, _ := window.GetTextAreaDimensions()
|
||||
|
||||
normalStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234)
|
||||
selectedStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color243)
|
||||
|
||||
for i, r := range buffer.Contents {
|
||||
if r == '\n' {
|
||||
x = 0
|
||||
if window.ShowLineIndex {
|
||||
x += bufferX
|
||||
}
|
||||
y++
|
||||
}
|
||||
|
||||
if r != '\n' {
|
||||
x++
|
||||
}
|
||||
|
||||
if x-buffer.OffsetX-1 < bufferX {
|
||||
continue
|
||||
}
|
||||
if y-buffer.OffsetY < bufferY {
|
||||
continue
|
||||
}
|
||||
|
||||
if buffer.Selection != nil && buffer.Selection.selectionEnd >= buffer.Selection.selectionStart && i >= buffer.Selection.selectionStart && i <= buffer.Selection.selectionEnd {
|
||||
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, selectedStyle)
|
||||
} else if buffer.Selection != nil && i <= buffer.Selection.selectionStart && i >= buffer.Selection.selectionEnd {
|
||||
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, selectedStyle)
|
||||
} else {
|
||||
window.screen.SetContent(x-buffer.OffsetX-1, y-buffer.OffsetY, r, nil, normalStyle)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw cursor
|
||||
cursorX, cursorY := window.GetCursorPos2D()
|
||||
|
||||
cursorX += bufferX
|
||||
cursorY += bufferY
|
||||
|
||||
cursorX -= window.CurrentBuffer.OffsetX
|
||||
cursorY -= window.CurrentBuffer.OffsetY
|
||||
|
||||
r, _, _, _ := window.screen.GetContent(cursorX, cursorY)
|
||||
window.screen.SetContent(cursorX, cursorY, r, nil, selectedStyle)
|
||||
}
|
||||
|
||||
func (window *Window) Draw() {
|
||||
// Clear screen
|
||||
window.screen.Clear()
|
||||
|
||||
// Sync buffer offset
|
||||
window.SyncBufferOffset()
|
||||
|
||||
// Draw top menu
|
||||
if window.ShowTopMenu {
|
||||
drawTopMenu(window)
|
||||
@@ -138,7 +107,7 @@ func (window *Window) Draw() {
|
||||
|
||||
// Draw current buffer
|
||||
if window.CurrentBuffer != nil {
|
||||
window.drawCurrentBuffer()
|
||||
drawBuffer(window)
|
||||
}
|
||||
|
||||
// Draw input bar
|
||||
@@ -162,7 +131,9 @@ func (window *Window) Draw() {
|
||||
|
||||
// Update screen
|
||||
window.screen.Show()
|
||||
}
|
||||
|
||||
func (window *Window) ProcessEvents() {
|
||||
// Poll event
|
||||
ev := window.screen.PollEvent()
|
||||
|
||||
@@ -172,73 +143,141 @@ func (window *Window) Draw() {
|
||||
window.screen.Sync()
|
||||
window.SyncBufferOffset()
|
||||
case *tcell.EventMouse:
|
||||
window.mouseInput(ev)
|
||||
window.handleMouseInput(ev)
|
||||
case *tcell.EventKey:
|
||||
window.input(ev)
|
||||
window.handleKeyInput(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) input(ev *tcell.EventKey) {
|
||||
func (window *Window) handleKeyInput(ev *tcell.EventKey) {
|
||||
if ev.Key() == tcell.KeyRight { // Navigation Keys
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
// Get original cursor position
|
||||
pos := window.CurrentBuffer.CursorPos
|
||||
|
||||
if ev.Modifiers()&tcell.ModCtrl != 0 {
|
||||
// Move cursor to start of word
|
||||
// Set variable to one character right of current position
|
||||
window.CurrentBuffer.MoveRight(1)
|
||||
|
||||
// Skip all spaces
|
||||
for unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
|
||||
if !window.CurrentBuffer.MoveRight(1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find end of word
|
||||
for !unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
|
||||
if !window.CurrentBuffer.MoveRight(1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Move cursor one character forwards
|
||||
window.CurrentBuffer.MoveRight(1)
|
||||
}
|
||||
|
||||
// Add to selection
|
||||
if ev.Modifiers() == tcell.ModShift {
|
||||
if ev.Modifiers()&tcell.ModShift != 0 {
|
||||
if window.CurrentBuffer.Selection == nil {
|
||||
// Cancel cursor movement when creating selection without holding ctrl
|
||||
if ev.Modifiers()&tcell.ModCtrl == 0 {
|
||||
window.CurrentBuffer.CursorPos = pos
|
||||
}
|
||||
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: window.CurrentBuffer.CursorPos,
|
||||
selectionStart: pos,
|
||||
selectionEnd: window.CurrentBuffer.CursorPos,
|
||||
}
|
||||
return
|
||||
} else {
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos + 1
|
||||
}
|
||||
// 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
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
|
||||
}
|
||||
} else if window.CurrentBuffer.Selection != nil {
|
||||
// Unset selection
|
||||
window.CurrentBuffer.Selection = nil
|
||||
return
|
||||
}
|
||||
// Move cursor
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
|
||||
}
|
||||
} else if ev.Key() == tcell.KeyLeft {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
// Get original cursor position
|
||||
pos := window.CurrentBuffer.CursorPos
|
||||
|
||||
if ev.Modifiers()&tcell.ModCtrl != 0 {
|
||||
// Move cursor to start of word
|
||||
// Set variable to one character left of current position
|
||||
window.CurrentBuffer.MoveLeft(1)
|
||||
|
||||
// Skip all spaces
|
||||
for unicode.IsSpace(window.CurrentBuffer.GetCharAtPosition(window.CurrentBuffer.CursorPos)) {
|
||||
if !window.CurrentBuffer.MoveLeft(1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 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 if not selecting
|
||||
if window.CurrentBuffer.CursorPos.X != 0 {
|
||||
window.CurrentBuffer.MoveRight(1)
|
||||
}
|
||||
} else {
|
||||
// Move cursor one character backwards
|
||||
window.CurrentBuffer.MoveLeft(1)
|
||||
}
|
||||
|
||||
// Add to selection
|
||||
if ev.Modifiers() == tcell.ModShift {
|
||||
if ev.Modifiers()&tcell.ModShift != 0 {
|
||||
if window.CurrentBuffer.Selection == nil {
|
||||
// Cancel cursor movement when creating selection without holding ctrl
|
||||
if ev.Modifiers()&tcell.ModCtrl == 0 {
|
||||
window.CurrentBuffer.CursorPos = pos
|
||||
}
|
||||
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: window.CurrentBuffer.CursorPos,
|
||||
selectionStart: pos,
|
||||
selectionEnd: window.CurrentBuffer.CursorPos,
|
||||
}
|
||||
return
|
||||
} else {
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos - 1
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
|
||||
}
|
||||
} else if window.CurrentBuffer.Selection != nil {
|
||||
// Unset selection
|
||||
window.CurrentBuffer.Selection = nil
|
||||
return
|
||||
}
|
||||
// Move cursor
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
|
||||
}
|
||||
} else if ev.Key() == tcell.KeyUp {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
// Get original cursor position
|
||||
pos := window.CurrentBuffer.CursorPos
|
||||
// Move cursor
|
||||
x, y := window.GetCursorPos2D()
|
||||
window.SetCursorPos2D(x, y-1)
|
||||
|
||||
if ev.Modifiers()&tcell.ModCtrl != 0 {
|
||||
// Move cursor to top of buffer
|
||||
window.CurrentBuffer.CursorPos.X = 0
|
||||
window.CurrentBuffer.CursorPos.Y = 0
|
||||
} else {
|
||||
// Move cursor one line up
|
||||
window.CurrentBuffer.MoveUp(1)
|
||||
}
|
||||
|
||||
// Add to selection
|
||||
if ev.Modifiers() == tcell.ModShift {
|
||||
if ev.Modifiers()&tcell.ModShift != 0 {
|
||||
// Add to selection
|
||||
if window.CurrentBuffer.Selection == nil {
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: window.CurrentBuffer.CursorPos,
|
||||
selectionEnd: pos,
|
||||
selectionStart: pos,
|
||||
selectionEnd: window.CurrentBuffer.CursorPos,
|
||||
}
|
||||
} else {
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
|
||||
@@ -273,11 +312,18 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
// Get original cursor position
|
||||
pos := window.CurrentBuffer.CursorPos
|
||||
// Move cursor
|
||||
x, y := window.GetCursorPos2D()
|
||||
window.SetCursorPos2D(x, y+1)
|
||||
|
||||
if ev.Modifiers()&tcell.ModCtrl != 0 {
|
||||
// Move cursor to bottom of buffer
|
||||
window.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
|
||||
window.CurrentBuffer.MoveDown(1)
|
||||
}
|
||||
|
||||
// Add to selection
|
||||
if ev.Modifiers() == tcell.ModShift {
|
||||
if ev.Modifiers()&tcell.ModShift != 0 {
|
||||
// Add to selection
|
||||
if window.CurrentBuffer.Selection == nil {
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
@@ -288,9 +334,9 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CurrentBuffer.CursorPos
|
||||
}
|
||||
// Prevent selecting dummy character at the end of the buffer
|
||||
if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
|
||||
window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
|
||||
}
|
||||
//if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
|
||||
// window.CurrentBuffer.Selection.selectionEnd = len(window.CurrentBuffer.Contents) - 1
|
||||
//}
|
||||
} else if window.CurrentBuffer.Selection != nil {
|
||||
// Unset selection
|
||||
window.CurrentBuffer.Selection = nil
|
||||
@@ -332,23 +378,25 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
}
|
||||
|
||||
// Check key bindings
|
||||
for _, keybinding := range Keybinds {
|
||||
if keybinding.IsPressed(ev) && slices.Index(keybinding.cursorModes, window.CursorMode) != -1 {
|
||||
RunCommand(window, keybinding.command)
|
||||
for _, keybinding := range Keybindings.Keybindings {
|
||||
if keybinding.IsPressed(ev) && slices.Index(keybinding.GetCursorModes(), window.CursorMode) != -1 {
|
||||
RunCommand(window, keybinding.Command)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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 index != 0 {
|
||||
str = str[:index-1] + str[index:]
|
||||
window.CurrentBuffer.Contents = str
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
|
||||
if window.CurrentBuffer.Selection != nil {
|
||||
window.CurrentBuffer.CutText(window)
|
||||
} else {
|
||||
window.CurrentBuffer.Delete(1)
|
||||
}
|
||||
} else if window.CursorMode == CursorModeInputBar {
|
||||
str := currentInputRequest.input
|
||||
@@ -362,29 +410,31 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
}
|
||||
} else if ev.Key() == tcell.KeyTab {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
str := window.CurrentBuffer.Contents
|
||||
index := window.CurrentBuffer.CursorPos
|
||||
|
||||
if index == len(str) {
|
||||
str += "\t"
|
||||
} else {
|
||||
str = str[:index] + "\t" + str[index:]
|
||||
if !window.CurrentBuffer.canEdit {
|
||||
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
|
||||
return
|
||||
}
|
||||
window.CurrentBuffer.Contents = str
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
|
||||
|
||||
// Remove selected text
|
||||
if window.CurrentBuffer.Selection != nil {
|
||||
window.CurrentBuffer.CutText(window)
|
||||
}
|
||||
|
||||
window.CurrentBuffer.WriteRune('\t')
|
||||
}
|
||||
} else if ev.Key() == tcell.KeyEnter {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
str := window.CurrentBuffer.Contents
|
||||
index := window.CurrentBuffer.CursorPos
|
||||
|
||||
if index == len(str) {
|
||||
str += "\n"
|
||||
} else {
|
||||
str = str[:index] + "\n" + str[index:]
|
||||
if !window.CurrentBuffer.canEdit {
|
||||
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
|
||||
return
|
||||
}
|
||||
window.CurrentBuffer.Contents = str
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
|
||||
|
||||
// Remove selected text
|
||||
if window.CurrentBuffer.Selection != nil {
|
||||
window.CurrentBuffer.CutText(window)
|
||||
}
|
||||
|
||||
window.CurrentBuffer.WriteRune('\n')
|
||||
} else if window.CursorMode == CursorModeInputBar {
|
||||
if currentInputRequest.input == "" && slices.Index(inputHistory, currentInputRequest.input) == -1 {
|
||||
inputHistory = append(inputHistory, currentInputRequest.input)
|
||||
@@ -398,16 +448,17 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
}
|
||||
} else if ev.Key() == tcell.KeyRune {
|
||||
if window.CursorMode == CursorModeBuffer {
|
||||
str := window.CurrentBuffer.Contents
|
||||
index := window.CurrentBuffer.CursorPos
|
||||
|
||||
if index == len(str) {
|
||||
str += string(ev.Rune())
|
||||
} else {
|
||||
str = str[:index] + string(ev.Rune()) + str[index:]
|
||||
if !window.CurrentBuffer.canEdit {
|
||||
window.PrintMessage(fmt.Sprintf("Buffer '%s' is read-only", window.CurrentBuffer.Name), TYPER_MESSAGE_WARNING)
|
||||
return
|
||||
}
|
||||
window.CurrentBuffer.Contents = str
|
||||
window.SetCursorPos(window.CurrentBuffer.CursorPos + 1)
|
||||
|
||||
// Remove selected text
|
||||
if window.CurrentBuffer.Selection != nil {
|
||||
window.CurrentBuffer.CutText(window)
|
||||
}
|
||||
|
||||
window.CurrentBuffer.WriteRune(ev.Rune())
|
||||
} else if window.CursorMode == CursorModeInputBar {
|
||||
str := currentInputRequest.input
|
||||
index := currentInputRequest.cursorPos
|
||||
@@ -424,31 +475,142 @@ func (window *Window) input(ev *tcell.EventKey) {
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) mouseInput(ev *tcell.EventMouse) {
|
||||
func (window *Window) handleMouseInput(ev *tcell.EventMouse) {
|
||||
mouseX, mouseY := ev.Position()
|
||||
bufferMouseX, bufferMouseY := window.AbsolutePosToBufferArea(mouseX, mouseY)
|
||||
|
||||
// Left click was pressed
|
||||
if ev.Buttons() == tcell.Button1 {
|
||||
// Ensure click was in buffer area
|
||||
// Get last click time
|
||||
lastClickTime := time.UnixMilli(lastClick)
|
||||
|
||||
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}
|
||||
|
||||
// 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+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY),
|
||||
selectionEnd: mouseBufferPos,
|
||||
}
|
||||
|
||||
// Set last click time
|
||||
lastClick = time.Now().UnixMilli()
|
||||
|
||||
return
|
||||
} else {
|
||||
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
|
||||
window.CurrentBuffer.Selection.selectionEnd = 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 currentPos == mouseBufferPos && time.Since(lastClickTime).Milliseconds() < 300 {
|
||||
selectedText := window.CurrentBuffer.GetSelectedText()
|
||||
if window.CurrentBuffer.Selection == nil || strings.HasSuffix(string(selectedText), "\n") {
|
||||
// Select word
|
||||
cursorPos := window.CurrentBuffer.CursorPos
|
||||
startOfWord := window.CurrentBuffer.CursorPos.X
|
||||
endOfWord := window.CurrentBuffer.CursorPos.X
|
||||
|
||||
// Find end of word
|
||||
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 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Find start of word
|
||||
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 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add to selection
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: Position{startOfWord, cursorPos.Y},
|
||||
selectionEnd: Position{endOfWord, cursorPos.Y},
|
||||
}
|
||||
} else {
|
||||
// Select line
|
||||
cursorPos := window.CurrentBuffer.CursorPos
|
||||
|
||||
// Add to selection
|
||||
window.CurrentBuffer.Selection = &Selection{
|
||||
selectionStart: Position{0, cursorPos.Y},
|
||||
selectionEnd: Position{len(window.CurrentBuffer.Contents[cursorPos.Y]), cursorPos.Y},
|
||||
}
|
||||
}
|
||||
|
||||
// Set last click time
|
||||
lastClick = time.Now().UnixMilli()
|
||||
|
||||
return
|
||||
} else {
|
||||
// Clear selection
|
||||
if window.CurrentBuffer.Selection != nil {
|
||||
@@ -456,7 +618,10 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
|
||||
}
|
||||
}
|
||||
// Move cursor
|
||||
window.SetCursorPos2D(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
|
||||
window.CurrentBuffer.CursorPos = mouseBufferPos
|
||||
|
||||
// Set last click time
|
||||
lastClick = time.Now().UnixMilli()
|
||||
}
|
||||
mouseHeld = true
|
||||
} else if ev.Buttons() == tcell.ButtonNone {
|
||||
@@ -467,8 +632,11 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
|
||||
}
|
||||
|
||||
func (window *Window) Close() {
|
||||
window.screen.Fini()
|
||||
window.screen = nil
|
||||
window.closed = true
|
||||
err := window.screen.PostEvent(tcell.NewEventInterrupt(nil))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (window *Window) GetTextAreaDimensions() (int, int, int, int) {
|
||||
@@ -486,160 +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) AbsolutePosToBufferArea(x, y int) (int, int) {
|
||||
x1, y1, _, _ := window.GetTextAreaDimensions()
|
||||
|
||||
x -= x1
|
||||
y -= y1
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user