Initial commit
This commit is contained in:
commit
6c0c289b52
29
actions.go
Normal file
29
actions.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
// Action is the base interface - anything executable
|
||||||
|
type Action interface {
|
||||||
|
Execute(m *model) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion moves the cursor and returns the range covered
|
||||||
|
type Motion interface {
|
||||||
|
Action
|
||||||
|
}
|
||||||
|
|
||||||
|
// Operator acts on a range (delete, yank, change)
|
||||||
|
type Operator interface {
|
||||||
|
Operate(m *model, start, end Position) tea.Cmd
|
||||||
|
// DoublePress handles dd, yy, cc (line-wise)
|
||||||
|
DoublePress(m *model) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeatable actions track count
|
||||||
|
type Repeatable interface {
|
||||||
|
WithCount(n int) Action
|
||||||
|
}
|
||||||
|
|
||||||
|
type Position struct {
|
||||||
|
Line, Col int
|
||||||
|
}
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1770197578,
|
||||||
|
"narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
50
flake.nix
Normal file
50
flake.nix
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
description = "Go development flake. Adjust as needed.";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Define the development shell.
|
||||||
|
# When you run `nix develop` (or direnv activates), you'll enter this shell.
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
# List all the development tools you need available in this shell's PATH.
|
||||||
|
packages = with pkgs; [
|
||||||
|
go
|
||||||
|
gopls
|
||||||
|
go-tools
|
||||||
|
gcc_multi
|
||||||
|
glibc_multi
|
||||||
|
];
|
||||||
|
|
||||||
|
# Define the shell that will be executed.
|
||||||
|
# Here, we explicitly use zsh.
|
||||||
|
# Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs`
|
||||||
|
# for it to be found in the shell's environment. `inherit pkgs.zsh;` is concise.
|
||||||
|
inherit (pkgs) zsh;
|
||||||
|
|
||||||
|
# Environment variables and commands to run when the shell starts.
|
||||||
|
shellHook = ''
|
||||||
|
# Use the .local directory instead of home
|
||||||
|
export GOPATH="$HOME/.local/go"
|
||||||
|
echo "Settings GOPATH to: $HOME/.local/go "
|
||||||
|
|
||||||
|
export GOOS=linux
|
||||||
|
export GOARCH=amd64
|
||||||
|
export CGO_CFLAGS=-Wno-error=cpp;
|
||||||
|
|
||||||
|
# Exec zsh to replace the current shell process with zsh.
|
||||||
|
# This ensures your prompt and zsh configurations load correctly.
|
||||||
|
exec zsh
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
31
go.mod
Normal file
31
go.mod
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
module git.gophernest.net/azpect/TextEditor
|
||||||
|
|
||||||
|
go 1.25.5
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.5 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
)
|
||||||
48
go.sum
Normal file
48
go.sum
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=
|
||||||
|
github.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
||||||
|
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||||
|
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
||||||
|
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
|
||||||
|
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||||
205
input.go
Normal file
205
input.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
type InputState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateReady InputState = iota
|
||||||
|
StateCount
|
||||||
|
StateOperatorPending
|
||||||
|
StateMotionCount
|
||||||
|
)
|
||||||
|
|
||||||
|
type InputHandler struct {
|
||||||
|
state InputState
|
||||||
|
count1 int
|
||||||
|
count2 int
|
||||||
|
operator Operator
|
||||||
|
operatorKey string // track which key started operator (for dd, yy, cc)
|
||||||
|
buffer string // for display (what user has typed)
|
||||||
|
pending string // partial key sequence (e.g., "g" waiting for second key)
|
||||||
|
keymap *Keymap
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInputHandler() *InputHandler {
|
||||||
|
return &InputHandler{
|
||||||
|
keymap: NewNormalKeymap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) Handle(m *model, key string) tea.Cmd {
|
||||||
|
// ESC always resets everything
|
||||||
|
if key == "esc" {
|
||||||
|
h.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to accumulate count (only if no pending sequence)
|
||||||
|
if h.pending == "" && h.tryAccumulateCount(key) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the sequence (pending + new key)
|
||||||
|
sequence := h.pending + key
|
||||||
|
|
||||||
|
// Check for exact match with full sequence
|
||||||
|
kind, binding := h.keymap.Lookup(sequence)
|
||||||
|
if kind != "" {
|
||||||
|
h.pending = ""
|
||||||
|
h.buffer += key
|
||||||
|
return h.dispatch(m, kind, binding, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// No exact match - could this be a prefix of something?
|
||||||
|
if h.keymap.HasPrefix(sequence) {
|
||||||
|
h.pending = sequence
|
||||||
|
h.buffer += key
|
||||||
|
return nil // wait for more keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a prefix either - if we had pending, try just the new key
|
||||||
|
if h.pending != "" {
|
||||||
|
h.pending = ""
|
||||||
|
kind, binding = h.keymap.Lookup(key)
|
||||||
|
if kind != "" {
|
||||||
|
h.buffer = key
|
||||||
|
return h.dispatch(m, kind, binding, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing matched
|
||||||
|
h.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch routes to the right handler based on current state
|
||||||
|
func (h *InputHandler) dispatch(m *model, kind string, binding any, key string) tea.Cmd {
|
||||||
|
switch h.state {
|
||||||
|
case StateReady, StateCount:
|
||||||
|
return h.handleInitial(m, kind, binding, key)
|
||||||
|
case StateOperatorPending, StateMotionCount:
|
||||||
|
return h.handleAfterOperator(m, kind, binding, key)
|
||||||
|
}
|
||||||
|
h.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) handleInitial(m *model, kind string, binding any, key string) tea.Cmd {
|
||||||
|
count := h.effectiveCount()
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "motion":
|
||||||
|
motion := binding.(Motion)
|
||||||
|
if r, ok := motion.(Repeatable); ok {
|
||||||
|
motion = r.WithCount(count).(Motion)
|
||||||
|
}
|
||||||
|
cmd := motion.Execute(m)
|
||||||
|
h.Reset()
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
case "operator":
|
||||||
|
h.operator = binding.(Operator)
|
||||||
|
h.operatorKey = key
|
||||||
|
h.state = StateOperatorPending
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case "action":
|
||||||
|
action := binding.(Action)
|
||||||
|
if r, ok := action.(Repeatable); ok {
|
||||||
|
action = r.WithCount(count)
|
||||||
|
}
|
||||||
|
cmd := action.Execute(m)
|
||||||
|
h.Reset()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) handleAfterOperator(m *model, kind string, binding any, key string) tea.Cmd {
|
||||||
|
count := h.effectiveCount()
|
||||||
|
|
||||||
|
// dd, yy, cc - same operator key pressed twice
|
||||||
|
if kind == "operator" && key == h.operatorKey {
|
||||||
|
cmd := h.operator.DoublePress(m)
|
||||||
|
h.Reset()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion after operator
|
||||||
|
if kind == "motion" {
|
||||||
|
motion := binding.(Motion)
|
||||||
|
if r, ok := motion.(Repeatable); ok {
|
||||||
|
motion = r.WithCount(count).(Motion)
|
||||||
|
}
|
||||||
|
// Get range here
|
||||||
|
start := m.getCursorPosition()
|
||||||
|
motion.Execute(m)
|
||||||
|
end := m.getCursorPosition()
|
||||||
|
cmd := h.operator.Operate(m, start, end)
|
||||||
|
h.Reset()
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Reset()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) tryAccumulateCount(key string) bool {
|
||||||
|
if len(key) != 1 || key[0] < '0' || key[0] > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
digit := int(key[0] - '0')
|
||||||
|
|
||||||
|
// 0 at start is a motion, not a count
|
||||||
|
if digit == 0 && h.currentCount() == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch h.state {
|
||||||
|
case StateReady, StateCount:
|
||||||
|
h.count1 = h.count1*10 + digit
|
||||||
|
h.state = StateCount
|
||||||
|
case StateOperatorPending, StateMotionCount:
|
||||||
|
h.count2 = h.count2*10 + digit
|
||||||
|
h.state = StateMotionCount
|
||||||
|
}
|
||||||
|
|
||||||
|
h.buffer += key
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) currentCount() int {
|
||||||
|
if h.state == StateOperatorPending || h.state == StateMotionCount {
|
||||||
|
return h.count2
|
||||||
|
}
|
||||||
|
return h.count1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) effectiveCount() int {
|
||||||
|
c1, c2 := h.count1, h.count2
|
||||||
|
if c1 == 0 {
|
||||||
|
c1 = 1
|
||||||
|
}
|
||||||
|
if c2 == 0 {
|
||||||
|
c2 = 1
|
||||||
|
}
|
||||||
|
return c1 * c2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) Reset() {
|
||||||
|
h.state = StateReady
|
||||||
|
h.count1 = 0
|
||||||
|
h.count2 = 0
|
||||||
|
h.operator = nil
|
||||||
|
h.operatorKey = ""
|
||||||
|
h.buffer = ""
|
||||||
|
h.pending = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *InputHandler) Pending() string {
|
||||||
|
return h.buffer
|
||||||
|
}
|
||||||
76
keymap.go
Normal file
76
keymap.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type Keymap struct {
|
||||||
|
motions map[string]Motion
|
||||||
|
operators map[string]Operator
|
||||||
|
actions map[string]Action // standalone actions: i.e., 'i', 'a'
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNormalKeymap() *Keymap {
|
||||||
|
return &Keymap{
|
||||||
|
motions: map[string]Motion{
|
||||||
|
"j": MoveDown{1},
|
||||||
|
"k": MoveUp{1},
|
||||||
|
"h": MoveLeft{1},
|
||||||
|
"l": MoveRight{1},
|
||||||
|
"G": MoveToBottom{},
|
||||||
|
"gg": MoveToTop{}, // multi-key example
|
||||||
|
// "w": MoveWord{1},
|
||||||
|
// "b": MoveWordBack{1},
|
||||||
|
"0": MoveToLineStart{},
|
||||||
|
"$": MoveToLineEnd{},
|
||||||
|
},
|
||||||
|
operators: map[string]Operator{
|
||||||
|
// "d": DeleteOp{},
|
||||||
|
// "c": ChangeOp{},
|
||||||
|
// "y": YankOp{},
|
||||||
|
},
|
||||||
|
actions: map[string]Action{
|
||||||
|
"i": EnterInsert{},
|
||||||
|
"a": EnterInsertAfter{},
|
||||||
|
"I": EnterInsertLineStart{},
|
||||||
|
"A": EnterInsertLineEnd{},
|
||||||
|
"o": OpenLineBelow{},
|
||||||
|
"O": OpenLineAbove{},
|
||||||
|
"x": DeleteChar{1},
|
||||||
|
// "p": Paste{},
|
||||||
|
// "u": Undo{},
|
||||||
|
// "ctrl+r": Redo{},
|
||||||
|
"ctrl+c": Quit{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup returns what type of binding a key is
|
||||||
|
func (km *Keymap) Lookup(key string) (kind string, value any) {
|
||||||
|
if m, ok := km.motions[key]; ok {
|
||||||
|
return "motion", m
|
||||||
|
}
|
||||||
|
if o, ok := km.operators[key]; ok {
|
||||||
|
return "operator", o
|
||||||
|
}
|
||||||
|
if a, ok := km.actions[key]; ok {
|
||||||
|
return "action", a
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPrefix returns true if any binding starts with this prefix
|
||||||
|
func (km *Keymap) HasPrefix(prefix string) bool {
|
||||||
|
for key := range km.motions {
|
||||||
|
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range km.operators {
|
||||||
|
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key := range km.actions {
|
||||||
|
if len(key) > len(prefix) && key[:len(prefix)] == prefix {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
10
main.go
Normal file
10
main.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tea.NewProgram(
|
||||||
|
newModel(),
|
||||||
|
tea.WithAltScreen(),
|
||||||
|
).Run()
|
||||||
|
}
|
||||||
65
model.go
Normal file
65
model.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
type mode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
NormalMode mode = iota
|
||||||
|
InsertMode
|
||||||
|
CommandMode
|
||||||
|
)
|
||||||
|
|
||||||
|
type cursor struct {
|
||||||
|
x int
|
||||||
|
y int
|
||||||
|
}
|
||||||
|
|
||||||
|
type model struct {
|
||||||
|
lines []string
|
||||||
|
cursor cursor
|
||||||
|
s_gutter int
|
||||||
|
mode mode
|
||||||
|
win_h int
|
||||||
|
win_w int
|
||||||
|
command string
|
||||||
|
input *InputHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func newModel() model {
|
||||||
|
return model{
|
||||||
|
lines: []string{
|
||||||
|
"Hello world",
|
||||||
|
"line 2",
|
||||||
|
"line 3",
|
||||||
|
"line 4",
|
||||||
|
"line 5",
|
||||||
|
"line 6",
|
||||||
|
},
|
||||||
|
cursor: cursor{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
s_gutter: 5,
|
||||||
|
mode: NormalMode,
|
||||||
|
command: "",
|
||||||
|
input: NewInputHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) Init() tea.Cmd {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) clampCursorX() {
|
||||||
|
lineLen := len(m.lines[m.cursor.y])
|
||||||
|
if lineLen == 0 {
|
||||||
|
m.cursor.x = 0
|
||||||
|
} else if m.cursor.x >= lineLen {
|
||||||
|
m.cursor.x = lineLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) getCursorPosition() Position {
|
||||||
|
return Position{Line: m.cursor.y, Col: m.cursor.x}
|
||||||
|
}
|
||||||
198
motion.go
Normal file
198
motion.go
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
// MoveDown implements Motion
|
||||||
|
type MoveDown struct {
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveDown) Execute(m *model) tea.Cmd {
|
||||||
|
for i := 0; i < a.count && m.cursor.y < len(m.lines)-1; i++ {
|
||||||
|
m.cursor.y++
|
||||||
|
}
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveDown) WithCount(n int) Action {
|
||||||
|
return MoveDown{count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveUp implements Motion
|
||||||
|
type MoveUp struct {
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveUp) Execute(m *model) tea.Cmd {
|
||||||
|
for i := 0; i < a.count && m.cursor.y > 0; i++ {
|
||||||
|
m.cursor.y--
|
||||||
|
}
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveUp) WithCount(n int) Action {
|
||||||
|
return MoveUp{count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveLeft struct {
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveLeft) Execute(m *model) tea.Cmd {
|
||||||
|
for i := 0; i < a.count && m.cursor.x > 0; i++ {
|
||||||
|
m.cursor.x--
|
||||||
|
}
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveLeft) WithCount(n int) Action {
|
||||||
|
return MoveLeft{count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveRight struct {
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveRight) Execute(m *model) tea.Cmd {
|
||||||
|
lineLen := len(m.lines[m.cursor.y])
|
||||||
|
for i := 0; i < a.count && m.cursor.x <= lineLen; i++ {
|
||||||
|
m.cursor.x++
|
||||||
|
}
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a MoveRight) WithCount(n int) Action {
|
||||||
|
return MoveRight{count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnterInsert struct{}
|
||||||
|
|
||||||
|
func (a EnterInsert) Execute(m *model) tea.Cmd {
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnterInsertAfter struct{}
|
||||||
|
|
||||||
|
func (a EnterInsertAfter) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.x++
|
||||||
|
m.clampCursorX()
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Quit struct{}
|
||||||
|
|
||||||
|
func (a Quit) Execute(m *model) tea.Cmd {
|
||||||
|
return tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveToTop implements Motion (gg)
|
||||||
|
type MoveToTop struct{}
|
||||||
|
|
||||||
|
func (a MoveToTop) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.y = 0
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoveToBottom implements Motion (G)
|
||||||
|
type MoveToBottom struct{}
|
||||||
|
|
||||||
|
func (a MoveToBottom) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.y = len(m.lines) - 1
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnterInsertLineStart struct{}
|
||||||
|
|
||||||
|
func (a EnterInsertLineStart) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.x = 0
|
||||||
|
m.clampCursorX()
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EnterInsertLineEnd struct{}
|
||||||
|
|
||||||
|
func (a EnterInsertLineEnd) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.x = len(m.lines[m.cursor.y])
|
||||||
|
m.clampCursorX()
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveToLineStart struct{}
|
||||||
|
|
||||||
|
func (a MoveToLineStart) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.x = 0
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveToLineEnd struct{}
|
||||||
|
|
||||||
|
func (a MoveToLineEnd) Execute(m *model) tea.Cmd {
|
||||||
|
m.cursor.x = len(m.lines[m.cursor.y])
|
||||||
|
m.clampCursorX()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Count
|
||||||
|
type OpenLineBelow struct{}
|
||||||
|
|
||||||
|
func (a OpenLineBelow) Execute(m *model) tea.Cmd {
|
||||||
|
pos := m.cursor.y
|
||||||
|
|
||||||
|
if pos >= len(m.lines) {
|
||||||
|
m.lines = append(m.lines, "")
|
||||||
|
} else {
|
||||||
|
m.lines = append(m.lines[:pos+1], append([]string{""}, m.lines[pos+1:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.cursor.y++
|
||||||
|
m.clampCursorX()
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Count
|
||||||
|
type OpenLineAbove struct{}
|
||||||
|
|
||||||
|
func (a OpenLineAbove) Execute(m *model) tea.Cmd {
|
||||||
|
pos := m.cursor.y
|
||||||
|
|
||||||
|
if pos <= 0 {
|
||||||
|
m.lines = append([]string{""}, m.lines...)
|
||||||
|
} else {
|
||||||
|
m.lines = append(m.lines[:pos], append([]string{""}, m.lines[pos:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.clampCursorX()
|
||||||
|
m.mode = InsertMode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Visual mode
|
||||||
|
type DeleteChar struct {
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a DeleteChar) Execute(m *model) tea.Cmd {
|
||||||
|
pos := m.cursor.x
|
||||||
|
for i := 0; i < a.count && m.cursor.x < len(m.lines[m.cursor.y]); i++ {
|
||||||
|
line := m.lines[m.cursor.y]
|
||||||
|
m.lines[m.cursor.y] = line[:pos] + line[pos+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a DeleteChar) WithCount(n int) Action {
|
||||||
|
return DeleteChar{count: n}
|
||||||
|
}
|
||||||
34
style.go
Normal file
34
style.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/charmbracelet/lipgloss"
|
||||||
|
|
||||||
|
func (m model) cursorStyle() lipgloss.Style {
|
||||||
|
switch m.mode {
|
||||||
|
case NormalMode:
|
||||||
|
// Block cursor for normal mode
|
||||||
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
|
case InsertMode:
|
||||||
|
// Bar/underline for insert mode
|
||||||
|
return lipgloss.NewStyle().Underline(true)
|
||||||
|
case CommandMode:
|
||||||
|
return lipgloss.NewStyle()
|
||||||
|
// case VisualMode:
|
||||||
|
// // Colored block for visual mode
|
||||||
|
// return lipgloss.NewStyle().
|
||||||
|
// Background(lipgloss.Color("62")).
|
||||||
|
// Foreground(lipgloss.Color("230"))
|
||||||
|
default:
|
||||||
|
return lipgloss.NewStyle().Reverse(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m model) gutterStyle(currentLine bool) lipgloss.Style {
|
||||||
|
fg := lipgloss.Color("243")
|
||||||
|
if currentLine {
|
||||||
|
fg = lipgloss.Color("#d69d00")
|
||||||
|
}
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Width(m.s_gutter).
|
||||||
|
Background(lipgloss.Color("236")).
|
||||||
|
Foreground(fg)
|
||||||
|
}
|
||||||
117
update.go
Normal file
117
update.go
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.win_h = msg.Height
|
||||||
|
m.win_w = msg.Width
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch m.mode {
|
||||||
|
case NormalMode:
|
||||||
|
return m, m.input.Handle(&m, msg.String())
|
||||||
|
|
||||||
|
case InsertMode:
|
||||||
|
switch msg.String() {
|
||||||
|
case "ctrl+c", "ctrl+d":
|
||||||
|
return m, tea.Quit
|
||||||
|
|
||||||
|
case "esc":
|
||||||
|
// Allow i to step back, but a to stay put
|
||||||
|
if m.cursor.x > 0 {
|
||||||
|
m.cursor.x--
|
||||||
|
}
|
||||||
|
m.mode = NormalMode
|
||||||
|
|
||||||
|
case "enter":
|
||||||
|
y := m.cursor.y
|
||||||
|
x := m.cursor.x
|
||||||
|
|
||||||
|
// Simple case, at end, just create a line
|
||||||
|
if x == len(m.lines[y]) {
|
||||||
|
m.lines = append(m.lines[:y+1], append([]string{""}, m.lines[y+1:]...)...)
|
||||||
|
|
||||||
|
// otherwise, splice
|
||||||
|
} else {
|
||||||
|
l := m.lines[y]
|
||||||
|
m.lines[y] = l[:x]
|
||||||
|
m.lines = append(m.lines[:y+1], append([]string{l[x:]}, m.lines[y+1:]...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.cursor.y++
|
||||||
|
m.cursor.x = 0
|
||||||
|
|
||||||
|
case "backspace":
|
||||||
|
x := m.cursor.x
|
||||||
|
y := m.cursor.y
|
||||||
|
l := m.lines[y]
|
||||||
|
if m.cursor.x > 0 {
|
||||||
|
m.lines[y] = l[:x-1] + l[x:]
|
||||||
|
m.cursor.x--
|
||||||
|
} else if m.cursor.y > 0 {
|
||||||
|
new_x := len(m.lines[y-1])
|
||||||
|
m.lines[y-1] = m.lines[y-1] + l
|
||||||
|
|
||||||
|
m.lines = append(m.lines[:y], m.lines[y+1:]...)
|
||||||
|
|
||||||
|
m.cursor.y--
|
||||||
|
m.cursor.x = new_x
|
||||||
|
}
|
||||||
|
|
||||||
|
case "left":
|
||||||
|
if m.cursor.x > 0 {
|
||||||
|
m.cursor.x--
|
||||||
|
}
|
||||||
|
|
||||||
|
case "right":
|
||||||
|
if m.cursor.x < len(m.lines[m.cursor.y]) {
|
||||||
|
m.cursor.x++
|
||||||
|
}
|
||||||
|
|
||||||
|
case "up":
|
||||||
|
if m.cursor.y > 0 {
|
||||||
|
m.cursor.y--
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.cursor.x > len(m.lines[m.cursor.y])-1 {
|
||||||
|
m.cursor.x = len(m.lines[m.cursor.y])
|
||||||
|
}
|
||||||
|
|
||||||
|
case "down":
|
||||||
|
if m.cursor.y < len(m.lines)-1 {
|
||||||
|
m.cursor.y++
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.cursor.x > len(m.lines[m.cursor.y])-1 {
|
||||||
|
m.cursor.x = len(m.lines[m.cursor.y])
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
x := m.cursor.x
|
||||||
|
y := m.cursor.y
|
||||||
|
l := m.lines[y]
|
||||||
|
ch := msg.String()
|
||||||
|
if x < len(l) {
|
||||||
|
m.lines[y] = l[:x] + ch + l[x:]
|
||||||
|
} else {
|
||||||
|
m.lines[y] = l + ch
|
||||||
|
}
|
||||||
|
m.cursor.x += len(ch)
|
||||||
|
}
|
||||||
|
case CommandMode:
|
||||||
|
switch msg.String() {
|
||||||
|
case "esc":
|
||||||
|
m.mode = NormalMode
|
||||||
|
m.command = ""
|
||||||
|
|
||||||
|
default:
|
||||||
|
m.command += msg.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
80
view.go
Normal file
80
view.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m model) View() string {
|
||||||
|
var view strings.Builder
|
||||||
|
|
||||||
|
for y := 0; y < m.win_h-1; y++ {
|
||||||
|
|
||||||
|
if y < len(m.lines) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
gutter string
|
||||||
|
currentLine bool = false
|
||||||
|
lineNumber int
|
||||||
|
)
|
||||||
|
if y > m.cursor.y {
|
||||||
|
lineNumber = y - m.cursor.y
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||||
|
} else if y < m.cursor.y {
|
||||||
|
lineNumber = m.cursor.y - y
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||||
|
} else {
|
||||||
|
lineNumber = y + 1
|
||||||
|
currentLine = true
|
||||||
|
if lineNumber < 100 {
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-2, lineNumber)
|
||||||
|
} else {
|
||||||
|
gutter = fmt.Sprintf("%*d ", m.s_gutter-1, lineNumber)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.WriteString(m.gutterStyle(currentLine).Render(gutter))
|
||||||
|
|
||||||
|
// TODO: Do we need to do offset calculation?
|
||||||
|
|
||||||
|
runes := []rune(m.lines[y])
|
||||||
|
for x := 0; x <= len(runes); x++ {
|
||||||
|
if m.cursor.y == y && m.cursor.x == x {
|
||||||
|
if x < len(runes) {
|
||||||
|
view.WriteString(m.cursorStyle().Render(string(runes[x])))
|
||||||
|
} else {
|
||||||
|
view.WriteString(m.cursorStyle().Render(" "))
|
||||||
|
}
|
||||||
|
} else if x < len(runes) {
|
||||||
|
view.WriteRune(runes[x])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
format := fmt.Sprintf("%%-%ds ", m.s_gutter-1)
|
||||||
|
fmt.Fprintf(&view, format, "~")
|
||||||
|
}
|
||||||
|
|
||||||
|
view.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw status bar
|
||||||
|
var modeString string
|
||||||
|
switch m.mode {
|
||||||
|
case NormalMode:
|
||||||
|
modeString = "NORMAL"
|
||||||
|
case InsertMode:
|
||||||
|
modeString = "INSERT"
|
||||||
|
case CommandMode:
|
||||||
|
modeString = "COMMAND"
|
||||||
|
}
|
||||||
|
|
||||||
|
var bar string
|
||||||
|
if m.mode == CommandMode {
|
||||||
|
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) %s | %s", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.command, m.input.buffer)
|
||||||
|
} else {
|
||||||
|
bar = fmt.Sprintf(" %6s | %d:%d (%d:%d) | %s", modeString, m.cursor.x, m.cursor.y, m.win_w, m.win_h, m.input.buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
view.WriteString(bar)
|
||||||
|
|
||||||
|
return view.String()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user