Compare commits

...

5 Commits

19 changed files with 611 additions and 146 deletions

View File

@ -13,8 +13,10 @@ build:
install: build/typer
# Create directories
install -dm755 $(DESTDIR)$(BINDIR)
install -dm755 $(DESTDIR)$(SYSCONFDIR)
# Install files
install -Dm755 build/typer $(DESTDIR)$(BINDIR)/typer
cp -r config -T $(DESTDIR)$(SYSCONFDIR)/typer
uninstall:
rm $(DESTDIR)$(BINDIR)/typer

8
config/config.yml Normal file
View File

@ -0,0 +1,8 @@
# 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
tab_indentation: 4 # Length of tab characters

40
config/keybindings.yml Normal file
View File

@ -0,0 +1,40 @@
keybindings:
- keybinding: "Ctrl-Q"
cursor_modes: ["buffer"]
command: "quit"
- keybinding: "Ctrl-C"
cursor_modes: ["buffer"]
command: "copy"
- keybinding: "Ctrl-V"
cursor_modes: ["buffer"]
command: "paste"
- keybinding: "Ctrl-S"
cursor_modes: ["buffer"]
command: "save"
- keybinding: "Ctrl-O"
cursor_modes: ["buffer"]
command: "open"
- keybinding: "Ctrl-R"
cursor_modes: ["buffer"]
command: "reload"
- keybinding: "PgUp"
cursor_modes: ["buffer"]
command: "prev-buffer"
- keybinding: "PgDn"
cursor_modes: ["buffer"]
command: "prev-buffer"
- keybinding: "Ctrl-N"
cursor_modes: ["buffer"]
command: "new-buffer"
- keybinding: "Delete"
cursor_modes: ["buffer"]
command: "close-buffer"
- keybinding: "F1"
cursor_modes: ["buffer","dropdown"]
command: "menu-file"
- keybinding: "F2"
cursor_modes: ["buffer","dropdown"]
command: "menu-edit"
- keybinding: "F3"
cursor_modes: ["buffer","dropdown"]
command: "menu-buffers"

21
config/styles/classic.yml Normal file
View File

@ -0,0 +1,21 @@
# Metadata
name: "classic"
description: "Style imitating the look of classic text editors and IDEs from the and 90s"
style_type: "256-color"
# Colors
colors:
buffer_area_bg: "darkblue" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "blue" # Buffer area selected text and cursor background color
top_menu_bg: "245" # Top menu background color
top_menu_fg: "black" # Top menu text color
dropdown_bg: "lightgray" # Dropdown background color
dropdown_fg: "black" # Dropdown text color
dropdown_sel: "blue" # Dropdown selected option background color
line_index_bg: "247" # Line index background color
line_index_fg: "black" # Line index text color
message_bar_bg: "245" # Message bar background color
message_bar_fg: "black" # Message bar text color
input_bar_bg: "245" # Input bar background color
input_bar_fg: "black" # Input bar text color

View File

@ -0,0 +1,21 @@
# Metadata
name: "default-fallback"
description: "The default look of Typer - Fallback style"
style_type: "8-color"
# Colors
colors:
buffer_area_bg: "black" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "navy" # Buffer area selected text and cursor background color
top_menu_bg: "white" # Top menu background color
top_menu_fg: "black" # Top -menu text color
dropdown_bg: "white" # Dropdown background color
dropdown_fg: "black" # Dropdown text color
dropdown_sel: "navy" # Dropdown selected option background color
line_index_bg: "white" # Line index background color
line_index_fg: "black" # Line index text color
message_bar_bg: "white" # Message bar background color
message_bar_fg: "black" # Message bar text color
input_bar_bg: "white" # Input bar background color
input_bar_fg: "black" # Input bar text color

21
config/styles/default.yml Normal file
View File

@ -0,0 +1,21 @@
# Metadata
name: "default"
description: "The default look of Typer"
style_type: "256-color"
# Colors
colors:
buffer_area_bg: "234" # Buffer area background color
buffer_area_fg: "white" # Buffer area text color
buffer_area_sel: "243" # Buffer area selected text and cursor background color
top_menu_bg: "236" # Top menu background color
top_menu_fg: "white" # Top menu text color
dropdown_bg: "236" # Dropdown background color
dropdown_fg: "white" # Dropdown text color
dropdown_sel: "240" # Dropdown selected option background color
line_index_bg: "235" # Line index background color
line_index_fg: "dimgray" # Line index text color
message_bar_bg: "236" # Message bar background color
message_bar_fg: "white" # Message bar text color
input_bar_bg: "236" # Input bar background color
input_bar_fg: "white" # Input bar text color

View File

@ -63,6 +63,18 @@ func (buffer *Buffer) Save() error {
return nil
}
func (buffer *Buffer) GetSelectionEdges() (int, int) {
if buffer.Selection == nil {
return -1, -1
}
if buffer.Selection.selectionStart < buffer.Selection.selectionEnd {
return buffer.Selection.selectionStart, buffer.Selection.selectionEnd
} else {
return buffer.Selection.selectionEnd, buffer.Selection.selectionStart
}
}
func (buffer *Buffer) GetSelectedText() string {
if buffer.Selection == nil {
return ""

59
src/config.go Normal file
View File

@ -0,0 +1,59 @@
package main
import (
"gopkg.in/yaml.v3"
"log"
"os"
"path"
)
type TyperConfig struct {
SelectedStyle string `yaml:"selected_style,omitempty"`
FallbackStyle string `yaml:"fallback_style,omitempty"`
ShowTopMenu bool `yaml:"show_top_menu,omitempty"`
ShowLineIndex bool `yaml:"show_line_index,omitempty"`
TabIndentation int `yaml:"tab_indentation,omitempty"`
}
var Config TyperConfig
func readConfig() {
Config = TyperConfig{
SelectedStyle: "default",
FallbackStyle: "default-fallback",
ShowTopMenu: true,
ShowLineIndex: true,
TabIndentation: 4,
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if _, err := os.Stat(path.Join(homeDir, ".config/typer/config.yml")); err == nil {
data, err := os.ReadFile(path.Join(homeDir, ".config/typer/config.yml"))
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
err = yaml.Unmarshal(data, &Config)
if err != nil {
log.Fatalf("Could not unmarshal config.yml: %s", err)
}
} else if _, err := os.Stat("/etc/typer/config.yml"); err == nil {
reader, err := os.Open("/etc/typer/config.yml")
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
err = yaml.NewDecoder(reader).Decode(&Config)
if err != nil {
log.Fatalf("Could not read config.yml: %s", err)
}
reader.Close()
}
// Validate config options
if Config.TabIndentation < 1 {
Config.TabIndentation = 1
}
}

View File

@ -50,13 +50,13 @@ func ClearDropdowns() {
}
func drawDropdowns(window *Window) {
dropdownStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
dropdownStyle := tcell.StyleDefault.Background(CurrentStyle.DropdownBg).Foreground(CurrentStyle.DropdownFg)
for _, d := range dropdowns {
drawBox(window.screen, d.PosX, d.PosY, d.PosX+d.Width+1, d.PosY+len(d.Options)+1, dropdownStyle)
line := d.PosY
line := 1
for i, option := range d.Options {
if d.Selected == i {
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(tcell.Color240), option)
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle.Background(CurrentStyle.DropdownSel), option)
} else {
drawText(window.screen, d.PosX+1, d.PosY+line, d.PosX+d.Width+1, d.PosY+line, dropdownStyle, option)
}

View File

@ -12,4 +12,5 @@ require (
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -79,3 +79,6 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -31,10 +31,6 @@ func RequestInput(window *Window, text string, defaultInput string) chan string
return request.inputChannel
}
func IsRequestingInput() bool {
return currentInputRequest != nil
}
func drawInputBar(window *Window) {
if currentInputRequest == nil {
return
@ -42,7 +38,7 @@ func drawInputBar(window *Window) {
screen := window.screen
inputBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
inputBarStyle := tcell.StyleDefault.Background(CurrentStyle.InputBarBg).Foreground(CurrentStyle.InputBarFg)
sizeX, sizeY := screen.Size()

View File

@ -2,106 +2,86 @@ package main
import (
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"strings"
)
type TyperKeybindings struct {
Keybindings []Keybinding `yaml:"keybindings"`
}
type Keybinding struct {
keybind string
cursorModes []CursorMode
command string
Keybinding string `yaml:"keybinding"`
CursorModes []string `yaml:"cursor_modes"`
Command string `yaml:"command"`
}
var Keybinds = make([]Keybinding, 0)
var Keybindings TyperKeybindings
func initKeybindings() {
// Add key bindings
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-Q",
cursorModes: []CursorMode{CursorModeBuffer},
command: "quit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-C",
cursorModes: []CursorMode{CursorModeBuffer},
command: "copy",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-V",
cursorModes: []CursorMode{CursorModeBuffer},
command: "paste",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-S",
cursorModes: []CursorMode{CursorModeBuffer},
command: "save",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-O",
cursorModes: []CursorMode{CursorModeBuffer},
command: "open",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-R",
cursorModes: []CursorMode{CursorModeBuffer},
command: "reload",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "PgUp",
cursorModes: []CursorMode{CursorModeBuffer},
command: "prev-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "PgDn",
cursorModes: []CursorMode{CursorModeBuffer},
command: "next-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-N",
cursorModes: []CursorMode{CursorModeBuffer},
command: "new-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Delete",
cursorModes: []CursorMode{CursorModeBuffer},
command: "close-buffer",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "Ctrl-Q",
cursorModes: []CursorMode{CursorModeBuffer},
command: "quit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F1",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-file",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F2",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-edit",
})
Keybinds = append(Keybinds, Keybinding{
keybind: "F3",
cursorModes: []CursorMode{CursorModeBuffer, CursorModeDropdown},
command: "menu-buffers",
})
func readKeybindings() {
Keybindings = TyperKeybindings{
Keybindings: make([]Keybinding, 0),
}
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if _, err := os.Stat(path.Join(homeDir, ".config/typer/keybindings.yml")); err == nil {
data, err := os.ReadFile(path.Join(homeDir, ".config/typer/keybindings.yml"))
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
err = yaml.Unmarshal(data, &Keybindings)
if err != nil {
log.Fatalf("Could not unmarshal keybindings.yml: %s", err)
}
} else if _, err := os.Stat("/etc/typer/keybindings.yml"); err == nil {
reader, err := os.Open("/etc/typer/keybindings.yml")
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
err = yaml.NewDecoder(reader).Decode(&Keybindings)
if err != nil {
log.Fatalf("Could not read keybindings.yml: %s", err)
}
reader.Close()
}
}
func (keybind *Keybinding) IsPressed(ev *tcell.EventKey) bool {
keys := strings.SplitN(keybind.keybind, "+", 2)
func (keybinding *Keybinding) GetCursorModes() []CursorMode {
ret := make([]CursorMode, 0)
for _, cursorModeStr := range keybinding.CursorModes {
for key, value := range CursorModeNames {
if cursorModeStr == value {
ret = append(ret, key)
}
}
}
return ret
}
func (keybinding *Keybinding) IsPressed(ev *tcell.EventKey) bool {
keys := strings.SplitN(keybinding.Keybinding, "+", 2)
if len(keys) == 0 {
return false
} else if len(keys) == 1 {
for k, v := range tcell.KeyNames {
if k != tcell.KeyRune {
if keybind.keybind == v {
if keybinding.Keybinding == v {
if ev.Key() == k {
return true
}
}
} else {
if keybind.keybind == string(ev.Rune()) {
if keybinding.Keybinding == string(ev.Rune()) {
return true
}
}

View File

@ -10,7 +10,7 @@ func drawLineIndex(window *Window) {
screen := window.screen
buffer := window.CurrentBuffer
lineIndexStyle := tcell.StyleDefault.Foreground(tcell.ColorDimGray).Background(tcell.Color235)
lineIndexStyle := tcell.StyleDefault.Background(CurrentStyle.LineIndexBg).Foreground(CurrentStyle.LineIndexFg)
_, sizeY := screen.Size()

View File

@ -6,12 +6,18 @@ import (
)
func main() {
// Read config
readConfig()
// Read key bindings
readKeybindings()
// Read styles
readStyles()
// Initialize commands
initCommands()
// Initialize key bindings
initKeybindings()
window, err := CreateWindow()
if err != nil {
log.Fatalf("Failed to create window: %v", err)

View File

@ -33,7 +33,7 @@ func PrintMessage(window *Window, message string) {
func drawMessageBar(window *Window) {
screen := window.screen
messageBarStyle := tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color236)
messageBarStyle := tcell.StyleDefault.Background(CurrentStyle.MessageBarBg).Foreground(CurrentStyle.MessageBarFg)
sizeX, sizeY := screen.Size()

187
src/style.go Normal file
View File

@ -0,0 +1,187 @@
package main
import (
"fmt"
"github.com/gdamore/tcell/v2"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
"reflect"
"slices"
"strconv"
"strings"
)
type TyperStyle struct {
// Metadata
Name string
Description string
StyleType string
// Colors
BufferAreaBg tcell.Color `name:"buffer_area_bg"`
BufferAreaFg tcell.Color `name:"buffer_area_fg"`
BufferAreaSel tcell.Color `name:"buffer_area_sel"`
TopMenuBg tcell.Color `name:"top_menu_bg"`
TopMenuFg tcell.Color `name:"top_menu_fg"`
DropdownBg tcell.Color `name:"dropdown_bg"`
DropdownFg tcell.Color `name:"dropdown_fg"`
DropdownSel tcell.Color `name:"dropdown_sel"`
LineIndexBg tcell.Color `name:"line_index_bg"`
LineIndexFg tcell.Color `name:"line_index_fg"`
MessageBarBg tcell.Color `name:"message_bar_bg"`
MessageBarFg tcell.Color `name:"message_bar_fg"`
InputBarBg tcell.Color `name:"input_bar_bg"`
InputBarFg tcell.Color `name:"input_bar_fg"`
}
type typerStyleYaml struct {
// Metadata
Name string `yaml:"name"`
Description string `yaml:"description"`
StyleType string `yaml:"style_type"`
// Colors
Colors map[string]string `yaml:"colors"`
}
var AvailableStyles = make(map[string]TyperStyle)
var CurrentStyle TyperStyle
func readStyles() {
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Could not get home directory: %s", err)
}
if stat, err := os.Stat(path.Join(homeDir, ".config/typer/styles/")); err == nil && stat.IsDir() {
entries, err := os.ReadDir(path.Join(homeDir, ".config/typer/styles/"))
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
for _, entry := range entries {
entryPath := path.Join(homeDir, ".config/typer/styles/", entry.Name())
style, err := readStyleYamlFile(entryPath)
if err != nil {
log.Fatalf("Could not read style file (%s): %s", entryPath, err)
}
if _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
if stat, err := os.Stat("/etc/typer/styles/"); err == nil && stat.IsDir() {
entries, err := os.ReadDir("/etc/typer/styles/")
if err != nil {
log.Fatalf("Could not read user style directory: %s", err)
}
for _, entry := range entries {
entryPath := path.Join("/etc/typer/styles/", entry.Name())
style, err := readStyleYamlFile(entryPath)
if err != nil {
log.Fatalf("Could not read style file (%s): %s", entryPath, err)
}
if _, ok := AvailableStyles[style.Name]; !ok {
AvailableStyles[style.Name] = style
}
}
}
}
func readStyleYamlFile(filepath string) (TyperStyle, error) {
styleYaml := typerStyleYaml{}
data, err := os.ReadFile(filepath)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not read file: %s", err)
}
err = yaml.Unmarshal(data, &styleYaml)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not unmarshal style: %s", err)
}
style := TyperStyle{
Name: styleYaml.Name,
Description: styleYaml.Description,
StyleType: styleYaml.StyleType,
}
for name, colorStr := range styleYaml.Colors {
var color tcell.Color
if n, err := strconv.Atoi(colorStr); err == nil && n >= 0 && n < 256 {
color = tcell.ColorValid + tcell.Color(n)
} else if strings.HasPrefix(colorStr, "#") && len(colorStr) == 7 {
n, err := strconv.ParseInt(colorStr[1:], 16, 32)
if err != nil {
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
}
color = tcell.NewHexColor(int32(n))
} else if c, ok := tcell.ColorNames[colorStr]; ok {
color = c
} else {
return TyperStyle{}, fmt.Errorf("could not parse color (%s): %s", colorStr, err)
}
pt := reflect.TypeOf(&style)
t := pt.Elem()
pv := reflect.ValueOf(&style)
v := pv.Elem()
for i := 0; i < t.NumField(); i++ {
field := v.Field(i)
if tag, ok := t.Field(i).Tag.Lookup("name"); ok && tag == name {
field.Set(reflect.ValueOf(color))
}
}
}
return style, nil
}
func SetCurrentStyle(screen tcell.Screen) {
availableTypes := make([]string, 1)
availableTypes[0] = "8-color"
if screen.Colors() >= 16 {
availableTypes = append(availableTypes, "16-color")
}
if screen.Colors() >= 256 {
availableTypes = append(availableTypes, "256-color")
}
if screen.Colors() >= 16777216 {
availableTypes = append(availableTypes, "true-color")
}
if style, ok := AvailableStyles[Config.SelectedStyle]; ok && slices.Index(availableTypes, style.StyleType) != -1 {
CurrentStyle = style
} else if style, ok := AvailableStyles[Config.FallbackStyle]; ok {
CurrentStyle = style
} else {
CurrentStyle = TyperStyle{
Name: "fallback",
Description: "Fallback style",
StyleType: "8-color",
BufferAreaBg: tcell.ColorBlack,
BufferAreaFg: tcell.ColorWhite,
BufferAreaSel: tcell.ColorNavy,
TopMenuBg: tcell.ColorWhite,
TopMenuFg: tcell.ColorBlack,
DropdownBg: tcell.ColorWhite,
DropdownFg: tcell.ColorBlack,
DropdownSel: tcell.ColorNavy,
LineIndexBg: tcell.ColorWhite,
LineIndexFg: tcell.ColorBlack,
MessageBarBg: tcell.ColorWhite,
MessageBarFg: tcell.ColorBlack,
InputBarBg: tcell.ColorWhite,
InputBarFg: tcell.ColorBlack,
}
}
}

View File

@ -22,7 +22,13 @@ func initTopMenu() {
Name: "File",
Action: func(window *Window) {
ClearDropdowns()
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, 1, 0, func(i int) {
y := 0
if window.ShowTopMenu {
y++
}
d := CreateDropdownMenu([]string{"New", "Save", "Open", "Close", "Quit"}, 0, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "new-buffer")
@ -45,7 +51,13 @@ func initTopMenu() {
Name: "Edit",
Action: func(window *Window) {
ClearDropdowns()
d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, 1, 0, func(i int) {
y := 0
if window.ShowTopMenu {
y++
}
d := CreateDropdownMenu([]string{"Copy", "Paste"}, 0, y, 0, func(i int) {
switch i {
case 0:
RunCommand(window, "copy")
@ -63,6 +75,12 @@ func initTopMenu() {
Name: "Buffers",
Action: func(window *Window) {
ClearDropdowns()
y := 0
if window.ShowTopMenu {
y++
}
buffersSlice := make([]string, 0)
for _, buffer := range Buffers {
if window.CurrentBuffer == buffer {
@ -74,7 +92,7 @@ func initTopMenu() {
slices.Sort(buffersSlice)
d := CreateDropdownMenu(buffersSlice, 0, 1, 0, func(i int) {
d := CreateDropdownMenu(buffersSlice, 0, y, 0, func(i int) {
start := strings.Index(buffersSlice[i], "[")
end := strings.Index(buffersSlice[i], "]")
@ -100,7 +118,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()

View File

@ -4,6 +4,7 @@ import (
"github.com/gdamore/tcell/v2"
"log"
"slices"
"strings"
)
type CursorMode uint8
@ -15,6 +16,13 @@ const (
CursorModeInputBar
)
var CursorModeNames = map[CursorMode]string{
CursorModeDisabled: "disabled",
CursorModeBuffer: "buffer",
CursorModeDropdown: "dropdown",
CursorModeInputBar: "input_bar",
}
type Window struct {
ShowTopMenu bool
ShowLineIndex bool
@ -31,8 +39,8 @@ var mouseHeld = false
func CreateWindow() (*Window, error) {
window := Window{
ShowTopMenu: true,
ShowLineIndex: true,
ShowTopMenu: Config.ShowTopMenu,
ShowLineIndex: Config.ShowLineIndex,
CursorMode: CursorModeBuffer,
CurrentBuffer: nil,
@ -56,7 +64,8 @@ func CreateWindow() (*Window, error) {
}
// Set screen style
screen.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.Color234))
SetCurrentStyle(screen)
screen.SetStyle(tcell.StyleDefault.Foreground(CurrentStyle.BufferAreaFg).Background(CurrentStyle.BufferAreaBg))
// Enable mouse
screen.EnableMouse()
@ -77,49 +86,43 @@ func (window *Window) drawCurrentBuffer() {
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 x-buffer.OffsetX >= bufferX && y-buffer.OffsetY >= bufferY {
// Default style
style := tcell.StyleDefault.Background(CurrentStyle.BufferAreaBg).Foreground(CurrentStyle.BufferAreaFg)
for i, r := range buffer.Contents {
if r == '\n' {
x = 0
if window.ShowLineIndex {
x += bufferX
// Change background if under cursor
if i == buffer.CursorPos {
style = style.Background(CurrentStyle.BufferAreaSel)
}
y++
// Change background if selected
if buffer.Selection != nil {
if edge1, edge2 := buffer.GetSelectionEdges(); i >= edge1 && i <= edge2 {
style = style.Background(CurrentStyle.BufferAreaSel)
// Show selection on entire tab space
if r == '\t' {
for j := 0; j < int(Config.TabIndentation); j++ {
window.screen.SetContent(x+j-buffer.OffsetX, y-buffer.OffsetY, r, nil, style)
}
}
}
}
window.screen.SetContent(x-buffer.OffsetX, y-buffer.OffsetY, r, nil, style)
}
if r != '\n' {
// Change position for next character
if r == '\n' {
x = bufferX
y++
} else if r == '\t' {
x += int(Config.TabIndentation)
} else {
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() {
@ -332,9 +335,9 @@ func (window *Window) input(ev *tcell.EventKey) {
}
// Check key bindings
for _, keybinding := range Keybinds {
if keybinding.IsPressed(ev) && slices.Index(keybinding.cursorModes, window.CursorMode) != -1 {
RunCommand(window, keybinding.command)
for _, keybinding := range Keybindings.Keybindings {
if keybinding.IsPressed(ev) && slices.Index(keybinding.GetCursorModes(), window.CursorMode) != -1 {
RunCommand(window, keybinding.Command)
return
}
}
@ -345,7 +348,17 @@ func (window *Window) input(ev *tcell.EventKey) {
str := window.CurrentBuffer.Contents
index := window.CurrentBuffer.CursorPos
if index != 0 {
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
} else if index != 0 {
str = str[:index-1] + str[index:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(window.CurrentBuffer.CursorPos - 1)
@ -363,6 +376,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyTab {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@ -376,6 +403,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyEnter {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@ -399,6 +440,20 @@ func (window *Window) input(ev *tcell.EventKey) {
} else if ev.Key() == tcell.KeyRune {
if window.CursorMode == CursorModeBuffer {
str := window.CurrentBuffer.Contents
// Remove selected text
if window.CurrentBuffer.Selection != nil {
edge1, edge2 := window.CurrentBuffer.GetSelectionEdges()
if edge2 == len(window.CurrentBuffer.Contents) {
edge2 = len(window.CurrentBuffer.Contents) - 1
}
str = str[:edge1] + str[edge2+1:]
window.CurrentBuffer.Contents = str
window.SetCursorPos(edge1)
window.CurrentBuffer.Selection = nil
}
index := window.CurrentBuffer.CursorPos
if index == len(str) {
@ -426,24 +481,23 @@ func (window *Window) input(ev *tcell.EventKey) {
func (window *Window) mouseInput(ev *tcell.EventMouse) {
mouseX, mouseY := ev.Position()
bufferMouseX, bufferMouseY := window.AbsolutePosToBufferArea(mouseX, mouseY)
// Left click was pressed
if ev.Buttons() == tcell.Button1 {
// Ensure click was in buffer area
x1, y1, x2, y2 := window.GetTextAreaDimensions()
if mouseX >= x1 && mouseY >= y1 && mouseX <= x2 && mouseY <= y2 {
bufferMouseX, bufferMouseY := window.AbsolutePosToCursorPos2D(mouseX, mouseY)
if mouseHeld {
// Add to selection
if window.CurrentBuffer.Selection == nil {
window.CurrentBuffer.Selection = &Selection{
selectionStart: window.CurrentBuffer.CursorPos,
selectionEnd: window.CursorPos2DToCursorPos(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY),
selectionEnd: window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY),
}
return
} else {
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
window.CurrentBuffer.Selection.selectionEnd = window.CursorPos2DToCursorPos(bufferMouseX, bufferMouseY)
}
// Prevent selecting dummy character at the end of the buffer
if window.CurrentBuffer.Selection.selectionEnd >= len(window.CurrentBuffer.Contents) {
@ -456,7 +510,7 @@ func (window *Window) mouseInput(ev *tcell.EventMouse) {
}
}
// Move cursor
window.SetCursorPos2D(bufferMouseX+window.CurrentBuffer.OffsetX, bufferMouseY+window.CurrentBuffer.OffsetY)
window.SetCursorPos2D(bufferMouseX, bufferMouseY)
}
mouseHeld = true
} else if ev.Buttons() == tcell.ButtonNone {
@ -531,12 +585,48 @@ func (window *Window) CursorPos2DToCursorPos(x, y int) int {
return lines[y].charIndex + x
}
func (window *Window) AbsolutePosToBufferArea(x, y int) (int, int) {
func (window *Window) AbsolutePosToCursorPos2D(x, y int) (int, int) {
x1, y1, _, _ := window.GetTextAreaDimensions()
x -= x1
y -= y1
x += window.CurrentBuffer.OffsetX
y += window.CurrentBuffer.OffsetY
if x < 0 {
x = 0
}
if y < 0 {
y = 0
}
split := strings.SplitAfter(window.CurrentBuffer.Contents+" ", "\n")
if y >= len(split) {
y = len(split) - 1
}
line := split[y]
posInLine := make([]int, 0)
for i, char := range []rune(line) {
if char == '\t' {
for j := 0; j < Config.TabIndentation; j++ {
posInLine = append(posInLine, i)
}
} else {
posInLine = append(posInLine, i)
}
}
if len(posInLine) == 0 {
x = 0
} else if x >= len(posInLine) {
x = posInLine[len(posInLine)-1]
} else {
x = posInLine[x]
}
return x, y
}