src

Go monorepo.
Log | Files | Refs

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:
Mcmd/wen/main.go | 20++++++++++++++++++--
Mpkg/editor/buffers.go | 7+++++++
Apkg/editor/command/command.go | 20++++++++++++++++++++
Mpkg/editor/editor.go | 29+++++++++++++++++++----------
Mpkg/editor/input.go | 252++++++++++---------------------------------------------------------------------
Apkg/editor/input/input.go | 220+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/editor/run.go | 28+++++++++++++++-------------
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, )