commit 8ae64a9ef0bb29d7e78ec39614753908a1b752d3
parent ca0e8252dc4dab34b587edfc50720cc1dba26e7e
Author: dwrz <dwrz@dwrz.net>
Date: Fri, 16 Dec 2022 20:15:29 +0000
Refactor wen
Squashed commit of the following:
commit ac4e63e7522a6a74197b126aabefab34228fc108
Author: dwrz <dwrz@dwrz.net>
Date: Thu Dec 15 21:57:17 2022 +0000
Refactor wen main
commit 398266218e43315bfb8931770d41194b269884b6
Author: dwrz <dwrz@dwrz.net>
Date: Thu Dec 15 21:57:10 2022 +0000
Refactor editor pkg
commit 9e288c7e7a21f58212b4ba53b4e6e6601d2756e6
Author: dwrz <dwrz@dwrz.net>
Date: Thu Dec 15 21:54:10 2022 +0000
Add editor command pkg
commit 2af4de0bf3c2008e230c364fa3c18a68f4a148c3
Author: dwrz <dwrz@dwrz.net>
Date: Thu Dec 15 21:54:04 2022 +0000
Add editor input pkg
Diffstat:
7 files changed, 330 insertions(+), 246 deletions(-)
diff --git a/cmd/wen/main.go b/cmd/wen/main.go
@@ -1,8 +1,11 @@
package main
import (
+ "context"
"os"
+ "os/signal"
"path/filepath"
+ "syscall"
"code.dwrz.net/src/pkg/editor"
"code.dwrz.net/src/pkg/log"
@@ -12,6 +15,9 @@ import (
func main() {
var l = log.New(os.Stderr)
+ // Setup the main context.
+ ctx, cancel := context.WithCancel(context.Background())
+
// Setup workspace and log file.
cdir, err := os.UserCacheDir()
if err != nil {
@@ -49,10 +55,20 @@ func main() {
l.Error.Fatalf("failed to create editor: %v", err)
}
- // TODO: handle signals, sigwinch.
+ // TODO: refactor; handle sigwinch.
+ go func() {
+ var signals = make(chan os.Signal, 1)
+ signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
+
+ // Block until we receive a signal.
+ s := <-signals
+ l.Debug.Printf("received signal: %s", s)
+
+ cancel()
+ }()
// Run the editor.
- if err := editor.Run(os.Args[1:]); err != nil {
+ if err := editor.Run(ctx, os.Args[1:]); err != nil {
l.Error.Fatal(err)
}
}
diff --git a/pkg/editor/buffers.go b/pkg/editor/buffers.go
@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "time"
"code.dwrz.net/src/pkg/editor/buffer"
"code.dwrz.net/src/pkg/editor/message"
@@ -61,6 +62,12 @@ func (e *Editor) load(files []string) {
}
e.messages <- message.New("loaded buffers")
+
+ // Set the initial message.
+ go func() {
+ time.Sleep(1 * time.Second)
+ e.messages <- message.New("Ctrl-Q: Quit")
+ }()
}
// setBuffer stores a buffer in the editor's buffer map.
diff --git a/pkg/editor/command/command.go b/pkg/editor/command/command.go
@@ -0,0 +1,20 @@
+package command
+
+type Command int
+
+const (
+ Backspace Command = iota
+ CursorDown
+ CursorLeft
+ CursorRight
+ CursorUp
+ Delete
+ End
+ Home
+ Insert
+ Open
+ PageDown
+ PageUp
+ Quit
+ Save
+)
diff --git a/pkg/editor/editor.go b/pkg/editor/editor.go
@@ -7,6 +7,7 @@ import (
"code.dwrz.net/src/pkg/editor/buffer"
"code.dwrz.net/src/pkg/editor/canvas"
+ "code.dwrz.net/src/pkg/editor/input"
"code.dwrz.net/src/pkg/editor/message"
"code.dwrz.net/src/pkg/log"
"code.dwrz.net/src/pkg/terminal"
@@ -15,10 +16,10 @@ import (
type Editor struct {
canvas *canvas.Canvas
errs chan error
- in *os.File
- input chan input
+ input chan *input.Event
log *log.Logger
messages chan *message.Message
+ reader *input.Reader
terminal *terminal.Terminal
tmpdir string
@@ -37,21 +38,29 @@ type Parameters struct {
func New(p Parameters) (*Editor, error) {
var editor = &Editor{
- buffers: map[string]*buffer.Buffer{},
- canvas: canvas.New(canvas.Parameters{
- Log: p.Log,
- Out: p.Out,
- Terminal: p.Terminal,
- }),
+ buffers: map[string]*buffer.Buffer{},
errs: make(chan error),
- in: p.In,
- input: make(chan input),
+ input: make(chan *input.Event),
log: p.Log,
messages: make(chan *message.Message),
terminal: p.Terminal,
tmpdir: p.TempDir,
}
+ // Setup user input.
+ editor.reader = input.New(input.Parameters{
+ Chan: editor.input,
+ In: p.In,
+ Log: p.Log,
+ })
+
+ // Setup the canvas.
+ editor.canvas = canvas.New(canvas.Parameters{
+ Log: p.Log,
+ Out: p.Out,
+ Terminal: p.Terminal,
+ })
+
// Create the initial scratch buffer.
scratch, err := buffer.Create(buffer.NewBufferParams{
Name: editor.tmpdir + "/scratch",
diff --git a/pkg/editor/input.go b/pkg/editor/input.go
@@ -1,258 +1,52 @@
package editor
import (
- "bufio"
"fmt"
- "unicode/utf8"
+ "code.dwrz.net/src/pkg/editor/command"
+ "code.dwrz.net/src/pkg/editor/input"
"code.dwrz.net/src/pkg/editor/message"
- "code.dwrz.net/src/pkg/terminal"
)
-type command int
-
-const (
- Backspace command = iota
- CursorDown
- CursorLeft
- CursorRight
- CursorUp
- Delete
- End
- Home
- Insert
- Open
- PageDown
- PageUp
- Quit
- Save
-)
-
-type input struct {
- Command command
- Rune rune
-}
-
-// TODO: reading one rune at a time is slow, especially when pasting large
-// quantities of text into the active buffer. It would be nice to take more
-// input at once, while still being able to handle escape sequences without
-// too many edge cases.
-func (e *Editor) readInput() {
- var buf = bufio.NewReader(e.in)
- for {
- r, size, err := buf.ReadRune()
- if err != nil {
- e.errs <- fmt.Errorf("failed to read stdin: %w", err)
- }
- e.log.Debug.Printf(
- "read rune %s %v (%d)",
- string(r), []byte(string(r)), size,
- )
- switch r {
- case utf8.RuneError:
- e.log.Error.Printf(
- "rune error: %s (%d)", string(r), size,
- )
-
- // Handle escape sequences.
- case terminal.Escape:
- e.parseEscapeSequence(buf)
-
- case terminal.Delete:
- e.input <- input{Command: Backspace}
-
- case 'q' & terminal.Control:
- e.input <- input{Command: Quit}
-
- case 's' & terminal.Control:
- e.input <- input{Command: Save}
-
- default:
- e.input <- input{Command: Insert, Rune: r}
- }
- }
-}
-
-func (e *Editor) parseEscapeSequence(buf *bufio.Reader) {
- r1, _, err := buf.ReadRune()
- if err != nil {
- e.errs <- fmt.Errorf("failed to read stdin: %w", err)
- return
- }
-
- // Ignore invalid escape sequences.
- if r1 != '[' && r1 != 'O' {
- e.input <- input{Command: Insert, Rune: r1}
- return
- }
-
- // We've received an input of Esc + [ or Esc + O.
- // Determine the escape sequence.
- r2, _, err := buf.ReadRune()
- if err != nil {
- e.errs <- fmt.Errorf("failed to read stdin: %w", err)
- return
- }
-
- // Check letter escape sequences.
- switch r2 {
- case 'A':
- e.input <- input{Command: CursorUp}
- return
- case 'B':
- e.input <- input{Command: CursorDown}
- return
- case 'C':
- e.input <- input{Command: CursorRight}
- return
- case 'D':
- e.input <- input{Command: CursorLeft}
- return
-
- case 'O':
- r3, _, err := buf.ReadRune()
- if err != nil {
- e.errs <- fmt.Errorf("failed to read stdin: %w", err)
- }
- switch r3 {
- case 'P': // F1
- return
- case 'Q': // F2
- return
- case 'R': // F3
- return
- case 'S': // F4
- return
- default:
- // No match.
- e.input <- input{Command: Insert, Rune: r1}
- e.input <- input{Command: Insert, Rune: r2}
- e.input <- input{Command: Insert, Rune: r3}
- return
- }
- }
-
- // Check for single digit numerical escape sequences.
- r3, _, err := buf.ReadRune()
- if err != nil {
- e.errs <- fmt.Errorf("failed to read stdin: %w", err)
- }
- switch {
- case r2 == '1' && r3 == '~':
- e.input <- input{Command: Home}
- return
- case r2 == '2' && r3 == '~':
- e.input <- input{Command: Insert}
- return
- case r2 == '3' && r3 == '~':
- e.input <- input{Command: Delete}
- return
- case r2 == '4' && r3 == '~':
- e.input <- input{Command: End}
- return
- case r2 == '5' && r3 == '~':
- e.input <- input{Command: PageUp}
- return
- case r2 == '6' && r3 == '~':
- e.input <- input{Command: PageDown}
- return
- case r2 == '7' && r3 == '~':
- e.input <- input{Command: Home}
- return
- case r2 == '8' && r3 == '~':
- e.input <- input{Command: End}
- return
- case r2 == '9' && r3 == '~':
- e.input <- input{Command: End}
- return
- }
-
- // Check for double digit numerical escape sequences.
- r4, _, err := buf.ReadRune()
- if err != nil {
- e.errs <- err
- }
- switch {
- case r2 == '1' && r3 == '0' && r4 == '~':
- return
- case r2 == '1' && r3 == '1' && r4 == '~':
- return
- case r2 == '1' && r3 == '2' && r4 == '~':
- return
- case r2 == '1' && r3 == '3' && r4 == '~':
- return
- case r2 == '1' && r3 == '4' && r4 == '~':
- return
- case r2 == '1' && r3 == '4' && r4 == '~':
- return
- case r2 == '1' && r3 == '6' && r4 == '~':
- return
- case r2 == '1' && r3 == '7' && r4 == '~':
- return
- case r2 == '1' && r3 == '8' && r4 == '~':
- return
- case r2 == '1' && r3 == '9' && r4 == '~':
- return
- case r2 == '2' && r3 == '0' && r4 == '~':
- return
- case r2 == '2' && r3 == '1' && r4 == '~':
- return
- case r2 == '2' && r3 == '2' && r4 == '~':
- return
- case r2 == '2' && r3 == '3' && r4 == '~':
- return
- case r2 == '2' && r3 == '4' && r4 == '~':
- return
- case r4 == '~':
- return
- }
-
- // No match.
- e.input <- input{Command: Insert, Rune: r1}
- e.input <- input{Command: Insert, Rune: r2}
- e.input <- input{Command: Insert, Rune: r3}
- e.input <- input{Command: Insert, Rune: r4}
-
-}
-
-func (e *Editor) processInput(input input) error {
+func (e *Editor) bufferInput(input *input.Event) error {
size, err := e.terminal.Size()
if err != nil {
return fmt.Errorf("failed to get terminal size: %w", err)
}
switch input.Command {
- case Backspace:
+ case command.Backspace:
e.active.Backspace()
- case CursorDown:
+ case command.CursorDown:
e.active.CursorDown()
- case CursorLeft:
+ case command.CursorLeft:
e.active.CursorLeft()
- case CursorRight:
+ case command.CursorRight:
e.active.CursorRight()
- case CursorUp:
+ case command.CursorUp:
e.active.CursorUp()
- case Insert:
+ case command.Insert:
e.active.Insert(input.Rune)
- case End:
+ case command.End:
e.active.CursorEnd()
- case Home:
+ case command.Home:
e.active.CursorHome()
- case PageDown:
+ case command.PageDown:
e.active.PageDown(int(size.Rows))
- case PageUp:
+ case command.PageUp:
e.active.PageUp(int(size.Rows))
- case Save:
+ case command.Save:
+ // Get the filename.
if err := e.active.Save(); err != nil {
go func() {
e.messages <- message.New(fmt.Sprintf(
@@ -270,3 +64,19 @@ func (e *Editor) processInput(input input) error {
return nil
}
+
+func (e *Editor) promptInput(input *input.Event) error {
+ switch input.Command {
+ case command.Backspace:
+
+ default:
+ e.log.Debug.Printf("unrecognized input: %#v", input)
+ }
+
+ // If a newline was received, take the input.
+ // Pass it back to the caller.
+ // Make it the caller's job to set the prompt again if not happy
+ // with return value.
+
+ return nil
+}
diff --git a/pkg/editor/input/input.go b/pkg/editor/input/input.go
@@ -0,0 +1,220 @@
+package input
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "unicode/utf8"
+
+ "code.dwrz.net/src/pkg/editor/command"
+ "code.dwrz.net/src/pkg/log"
+ "code.dwrz.net/src/pkg/terminal"
+)
+
+type Event struct {
+ Command command.Command
+ Rune rune
+}
+
+type Reader struct {
+ buf *bufio.Reader
+ events chan *Event
+ log *log.Logger
+}
+
+type Parameters struct {
+ Chan chan *Event
+ In *os.File
+ Log *log.Logger
+}
+
+func New(p Parameters) *Reader {
+ return &Reader{
+ buf: bufio.NewReader(p.In),
+ events: p.Chan,
+ log: p.Log,
+ }
+}
+
+// TODO: reading one rune at a time is slow, especially when pasting large
+// quantities of text into the active buffer. It would be nice to take more
+// input at once, while still being able to handle escape sequences without
+// too many edge cases.
+func (i *Reader) Run() error {
+ for {
+ r, size, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+ i.log.Debug.Printf(
+ "read rune %s %v (%d)",
+ string(r), []byte(string(r)), size,
+ )
+ switch r {
+ case utf8.RuneError:
+ i.log.Error.Printf(
+ "rune error: %s (%d)", string(r), size,
+ )
+
+ // Handle escape sequences.
+ case terminal.Escape:
+ if err := i.parseEscapeSequence(); err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+
+ case terminal.Delete:
+ i.events <- &Event{Command: command.Backspace}
+
+ case 'q' & terminal.Control:
+ i.events <- &Event{Command: command.Quit}
+
+ case 's' & terminal.Control:
+ i.events <- &Event{Command: command.Save}
+
+ default:
+ i.events <- &Event{Command: command.Insert, Rune: r}
+ }
+ }
+}
+
+func (i *Reader) parseEscapeSequence() error {
+ r1, _, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+
+ // Ignore invalid escape sequences.
+ if r1 != '[' && r1 != 'O' {
+ i.events <- &Event{Command: command.Insert, Rune: r1}
+ return nil
+ }
+
+ // We've received an input of Esc + [ or Esc + O.
+ // Determine the escape sequence.
+ r2, _, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+
+ }
+
+ // Check letter escape sequences.
+ switch r2 {
+ case 'A':
+ i.events <- &Event{Command: command.CursorUp}
+ return nil
+ case 'B':
+ i.events <- &Event{Command: command.CursorDown}
+ return nil
+ case 'C':
+ i.events <- &Event{Command: command.CursorRight}
+ return nil
+ case 'D':
+ i.events <- &Event{Command: command.CursorLeft}
+ return nil
+
+ case 'O':
+ r3, _, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+ switch r3 {
+ case 'P': // F1
+ return nil
+ case 'Q': // F2
+ return nil
+ case 'R': // F3
+ return nil
+ case 'S': // F4
+ return nil
+ default:
+ // No match.
+ i.events <- &Event{Command: command.Insert, Rune: r1}
+ i.events <- &Event{Command: command.Insert, Rune: r2}
+ i.events <- &Event{Command: command.Insert, Rune: r3}
+ return nil
+ }
+ }
+
+ // Check for single digit numerical escape sequences.
+ r3, _, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+ switch {
+ case r2 == '1' && r3 == '~':
+ i.events <- &Event{Command: command.Home}
+ return nil
+ case r2 == '2' && r3 == '~':
+ i.events <- &Event{Command: command.Insert}
+ return nil
+ case r2 == '3' && r3 == '~':
+ i.events <- &Event{Command: command.Delete}
+ return nil
+ case r2 == '4' && r3 == '~':
+ i.events <- &Event{Command: command.End}
+ return nil
+ case r2 == '5' && r3 == '~':
+ i.events <- &Event{Command: command.PageUp}
+ return nil
+ case r2 == '6' && r3 == '~':
+ i.events <- &Event{Command: command.PageDown}
+ return nil
+ case r2 == '7' && r3 == '~':
+ i.events <- &Event{Command: command.Home}
+ return nil
+ case r2 == '8' && r3 == '~':
+ i.events <- &Event{Command: command.End}
+ return nil
+ case r2 == '9' && r3 == '~':
+ i.events <- &Event{Command: command.End}
+ return nil
+ }
+
+ // Check for double digit numerical escape sequences.
+ r4, _, err := i.buf.ReadRune()
+ if err != nil {
+ return fmt.Errorf("failed to read: %w", err)
+ }
+ switch {
+ case r2 == '1' && r3 == '0' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '1' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '2' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '3' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '4' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '4' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '6' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '7' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '8' && r4 == '~':
+ return nil
+ case r2 == '1' && r3 == '9' && r4 == '~':
+ return nil
+ case r2 == '2' && r3 == '0' && r4 == '~':
+ return nil
+ case r2 == '2' && r3 == '1' && r4 == '~':
+ return nil
+ case r2 == '2' && r3 == '2' && r4 == '~':
+ return nil
+ case r2 == '2' && r3 == '3' && r4 == '~':
+ return nil
+ case r2 == '2' && r3 == '4' && r4 == '~':
+ return nil
+ case r4 == '~':
+ return nil
+ }
+
+ // No match.
+ i.events <- &Event{Command: command.Insert, Rune: r1}
+ i.events <- &Event{Command: command.Insert, Rune: r2}
+ i.events <- &Event{Command: command.Insert, Rune: r3}
+ i.events <- &Event{Command: command.Insert, Rune: r4}
+
+ return nil
+}
diff --git a/pkg/editor/run.go b/pkg/editor/run.go
@@ -1,14 +1,14 @@
package editor
import (
+ "context"
"fmt"
- "time"
"code.dwrz.net/src/pkg/build"
- "code.dwrz.net/src/pkg/editor/message"
+ "code.dwrz.net/src/pkg/editor/command"
)
-func (e *Editor) Run(files []string) error {
+func (e *Editor) Run(ctx context.Context, files []string) error {
e.terminal.SetRaw()
e.canvas.Reset()
@@ -22,20 +22,22 @@ func (e *Editor) Run(files []string) error {
defer e.quit()
// Start reading user input.
- go e.readInput()
-
- // Open all the files.
- go e.load(files)
-
- // Set the initial message.
go func() {
- time.Sleep(1 * time.Second)
- e.messages <- message.New("Ctrl-Q: Quit")
+ if err := e.reader.Run(); err != nil {
+ e.errs <- err
+ }
}()
+ // Open the files.
+ go e.load(files)
+
// Main loop.
for {
select {
+ case <-ctx.Done():
+ e.log.Debug.Printf("context done: stopping")
+ return nil
+
case err := <-e.errs:
return err
@@ -46,10 +48,10 @@ func (e *Editor) Run(files []string) error {
}
case input := <-e.input:
- if input.Command == Quit {
+ if input.Command == command.Quit {
return nil
}
- if err := e.processInput(input); err != nil {
+ if err := e.bufferInput(input); err != nil {
return fmt.Errorf(
"failed to process input: %w", err,
)