Skip to content
Merged

Args #10

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ go.work.sum
/build/
/dist/
**/.vscode

# Test orchestration state
.wibey-test/
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ extra_args = [] # extra args passed straight to nvim
You don't need to create this file to get started — the defaults shown
above are exactly what's used if it's absent.

## Command-line arguments

Use `--maximized` to start with a maximized window. Arguments after `--` are
forwarded unchanged to Nvim, including commands and Nvim flags:

```sh
simplenvim --maximized -- -c term -c edit /Users/k0g0kfq/data1/.nnn/n.todo
```

## Documentation

- [`doc/developer.md`](doc/developer.md) — everything about building,
Expand Down
13 changes: 11 additions & 2 deletions doc/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,18 @@ For shipping a `.dmg`, see [Releasing](#releasing-official-build);
| Flag | Description |
|---|---|
| `-nvim /path/to/nvim` | Overrides the `nvim` executable to launch (default: whatever the config file says, or plain `nvim` resolved via `PATH`). |
| `--maximized` | Starts the editor window maximized. |

Any remaining positional arguments are treated as files to open, exactly
like `nvim file1 file2`.
Positional arguments open files as usual. To forward Nvim flags or commands,
put them after `--`; they are passed to Nvim unchanged and in order:

```sh
/Applications/SimpleNvimEditor.app/Contents/MacOS/simplenvim \
--maximized -- -c term -c edit /Users/k0g0kfq/data1/.nnn/n.todo
```

All simplenvim flags must come before `--`. This boundary prevents Nvim flags
such as `-c` from being mistaken for application flags.

For the `config.toml` file format (font/Nvim-launch settings, default
values, and file locations per OS), see the
Expand Down
21 changes: 9 additions & 12 deletions src/cmd/simplenvim/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,27 @@
package main

import (
"flag"
"fmt"
"os"

gioapp "gioui.org/app"

editorapp "github.com/kgfly/SimpleNvimEditor/internal/app"
"github.com/kgfly/SimpleNvimEditor/internal/cli"
"github.com/kgfly/SimpleNvimEditor/internal/config"
)

// Set at build time via -ldflags "-X main.version=1.0.0".
var version = "dev"

func main() {
showVersion := flag.Bool("version", false, "print version and exit")
nvimPath := flag.String("nvim", "", "path to the nvim executable (overrides config file)")
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [flags] [file...]\n\n", os.Args[0])
flag.PrintDefaults()
opts, err := cli.Parse(os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "simplenvim: %v\n", err)
os.Exit(2)
}
flag.Parse()

if *showVersion {
if opts.ShowVersion {
fmt.Println(version)
return
}
Expand All @@ -36,12 +34,11 @@ func main() {
fmt.Fprintf(os.Stderr, "simplenvim: loading config: %v\n", err)
os.Exit(1)
}
if *nvimPath != "" {
cfg.Nvim.Command = *nvimPath
if opts.NvimPath != "" {
cfg.Nvim.Command = opts.NvimPath
}

files := flag.Args()
a := editorapp.New(cfg, files)
a := editorapp.New(cfg, opts.NvimArgs, editorapp.Options{Maximized: opts.Maximized})

go func() {
win := new(gioapp.Window)
Expand Down
33 changes: 21 additions & 12 deletions src/internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ const anyModifier = key.ModCtrl | key.ModCommand | key.ModShift | key.ModAlt | k

// App owns everything needed to run one editor window.
type App struct {
cfg config.Config
files []string
cfg config.Config
nvimArgs []string
options Options

win *gioapp.Window
fonts render.Fonts
Expand All @@ -59,23 +60,31 @@ type App struct {
ime imeShadow
}

// New creates an App that will open the given files (may be empty).
func New(cfg config.Config, files []string) *App {
// Options controls how the editor window starts.
type Options struct {
Maximized bool
}

// New creates an App that starts Nvim with the given arguments (may be empty).
func New(cfg config.Config, nvimArgs []string, options Options) *App {
return &App{
cfg: cfg,
files: files,
state: uistate.New(),
ime: newIMEShadow(),
cfg: cfg,
nvimArgs: append([]string(nil), nvimArgs...),
options: options,
state: uistate.New(),
ime: newIMEShadow(),
}
}

// Run drives win's event loop until the window is closed. It blocks the
// calling goroutine, matching Gio's own convention (see gioui.org/app doc).
func (a *App) Run(win *gioapp.Window) error {
a.win = win
win.Option(
gioapp.Size(unit.Dp(1000), unit.Dp(650)),
)
windowOptions := []gioapp.Option{gioapp.Size(unit.Dp(1000), unit.Dp(650))}
if a.options.Maximized {
windowOptions = append(windowOptions, gioapp.Maximized.Option())
}
win.Option(windowOptions...)

a.fonts = render.Fonts{
Shaper: render.NewShaper(a.cfg.Editor),
Expand Down Expand Up @@ -322,7 +331,7 @@ func (a *App) syncSize(size image.Point) {
// big the initial grid should be, and starts the goroutine that pumps its
// redraw events into our state model.
func (a *App) startNvim() {
proc, err := nvimproc.Spawn(a.cfg.Nvim.Command, a.cfg.Nvim.ExtraArgs, a.files, a.cols, a.rows)
proc, err := nvimproc.Spawn(a.cfg.Nvim.Command, a.cfg.Nvim.ExtraArgs, a.nvimArgs, a.cols, a.rows)
if err != nil {
// Nothing meaningful to render without Nvim; surfacing to stderr
// is enough for the MVP (see IMPLEMENTATION_PLAN.md for a real
Expand Down
21 changes: 12 additions & 9 deletions src/internal/app/icon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,49 +23,52 @@ func TestAppIcon(t *testing.T) {

func TestNewApp(t *testing.T) {
cfg := config.Default()
a := New(cfg, []string{"file1.go", "file2.go"})
a := New(cfg, []string{"file1.go", "file2.go"}, Options{Maximized: true})
if a == nil {
t.Fatal("New returned nil")
}
if a.state == nil {
t.Fatal("New should initialize state")
}
if len(a.files) != 2 {
t.Fatalf("files = %v, want 2 entries", a.files)
if !a.options.Maximized {
t.Fatal("New should retain window options")
}
if len(a.nvimArgs) != 2 {
t.Fatalf("nvimArgs = %v, want 2 entries", a.nvimArgs)
}
}

func TestQuitWithNilProc(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
// Should not panic.
a.quit()
}

func TestOnKeyWithNilProc(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
a.onKey(key.Event{Name: "A", State: key.Press})
}

func TestOnEditWithNilProc(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
a.onEdit(key.EditEvent{Text: "hello"})
}

func TestOnPointerWithNilProc(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
a.onPointer(pointer.Event{Kind: pointer.Press, Buttons: pointer.ButtonPrimary})
}

func TestCaretWithZeroMetrics(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
c := a.caret()
if c.Ascent != 0 || c.Descent != 0 {
t.Fatalf("caret with zero metrics should be zero, got %+v", c)
}
}

func TestSyncSizeWithZeroCellDims(t *testing.T) {
a := New(config.Default(), nil)
a := New(config.Default(), nil, Options{})
// CellWidth and CellHeight are 0, should return early.
a.syncSize(image.Pt(800, 600))
if a.proc != nil {
Expand Down
40 changes: 40 additions & 0 deletions src/internal/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Package cli parses arguments owned by simplenvim and preserves arguments
// intended for the Neovim child process.
package cli

import (
"bytes"
"flag"
"fmt"
)

// Options holds simplenvim flags and the opaque arguments forwarded to Nvim.
type Options struct {
ShowVersion bool
NvimPath string
Maximized bool
NvimArgs []string
}

// Parse parses simplenvim arguments. Application flags must precede --;
// arguments after that boundary are passed to Nvim unchanged.
func Parse(args []string) (Options, error) {
var opts Options
var usage bytes.Buffer

flags := flag.NewFlagSet("simplenvim", flag.ContinueOnError)
flags.SetOutput(&usage)
flags.BoolVar(&opts.ShowVersion, "version", false, "print version and exit")
flags.StringVar(&opts.NvimPath, "nvim", "", "path to the nvim executable (overrides config file)")
flags.BoolVar(&opts.Maximized, "maximized", false, "start the editor window maximized")
flags.Usage = func() {
fmt.Fprint(&usage, "Usage: simplenvim [flags] [file...]\n\n")
flags.PrintDefaults()
}

if err := flags.Parse(args); err != nil {
return Options{}, fmt.Errorf("parse arguments: %w\n%s", err, usage.String())
}
opts.NvimArgs = append([]string(nil), flags.Args()...)
return opts, nil
}
8 changes: 4 additions & 4 deletions src/internal/nvimproc/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ type Process struct {
cmds chan func()
}

// Spawn starts `command --embed [extraArgs...] [files...]` as a child
// Spawn starts `command --embed [extraArgs...] [nvimArgs...]` as a child
// process and attaches a UI to it with the given initial grid size.
//
// command is resolved the same way on every OS: exec.Command performs a
// PATH lookup, so "nvim" works on Linux, macOS, and Windows alike as long
// as the binary is installed and on PATH (or an absolute path is given).
func Spawn(command string, extraArgs, files []string, cols, rows int) (*Process, error) {
args := make([]string, 0, len(extraArgs)+len(files)+2)
func Spawn(command string, extraArgs, nvimArgs []string, cols, rows int) (*Process, error) {
args := make([]string, 0, len(extraArgs)+len(nvimArgs)+2)
args = append(args, "--embed")
args = append(args, extraArgs...)
args = append(args, files...)
args = append(args, nvimArgs...)

v, err := nvim.NewChildProcess(
nvim.ChildProcessCommand(command),
Expand Down
46 changes: 46 additions & 0 deletions src/test/integration/cliargs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package integration_test

import (
"path/filepath"
"testing"

"github.com/kgfly/SimpleNvimEditor/internal/cli"
"github.com/kgfly/SimpleNvimEditor/internal/nvimproc"
)

func TestCLIForwardsCommandsToNvimInOrder(t *testing.T) {
file := filepath.Join(t.TempDir(), "n.todo")
args := []string{"--maximized", "--", "-u", "NONE", "-n", "-c", "let g:cli_term = 1", "-c", "edit " + file}

opts, err := cli.Parse(args)
if err != nil {
t.Fatalf("cli.Parse(%q): %v", args, err)
}
if !opts.Maximized {
t.Fatal("Maximized = false, want true")
}

proc, err := nvimproc.Spawn(requireNvim(t), nil, opts.NvimArgs, 40, 10)
if err != nil {
t.Fatalf("nvimproc.Spawn: %v", err)
}
t.Cleanup(func() {
proc.RequestQuit()
<-proc.Exited
})

value, err := proc.Nvim.CommandOutput("echo get(g:, 'cli_term', 0)")
if err != nil {
t.Fatalf("query command result: %v", err)
}
if value != "1" {
t.Fatalf("first -c command value = %q, want 1", value)
}
name, err := proc.Nvim.CommandOutput("echo expand('%:p')")
if err != nil {
t.Fatalf("query edited file: %v", err)
}
if name != file {
t.Fatalf("edited file = %q, want %q", name, file)
}
}
46 changes: 46 additions & 0 deletions src/test/unit/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package unit_test

import (
"reflect"
"testing"

"github.com/kgfly/SimpleNvimEditor/internal/cli"
)

func TestParseForwardsNvimCommandsAfterBoundary(t *testing.T) {
args := []string{
"--maximized", "--", "-c", "term", "-c", "edit",
"/Users/k0g0kfq/data1/.nnn/n.todo",
}

opts, err := cli.Parse(args)
if err != nil {
t.Fatalf("Parse(%q): %v", args, err)
}
if !opts.Maximized {
t.Fatal("Maximized = false, want true")
}
want := []string{"-c", "term", "-c", "edit", "/Users/k0g0kfq/data1/.nnn/n.todo"}
if !reflect.DeepEqual(opts.NvimArgs, want) {
t.Fatalf("NvimArgs = %q, want %q", opts.NvimArgs, want)
}
}

func TestParseAcceptsApplicationFlagsAndFiles(t *testing.T) {
opts, err := cli.Parse([]string{"--nvim", "/custom/nvim", "notes.md"})
if err != nil {
t.Fatalf("Parse: %v", err)
}
if opts.NvimPath != "/custom/nvim" {
t.Fatalf("NvimPath = %q, want /custom/nvim", opts.NvimPath)
}
if !reflect.DeepEqual(opts.NvimArgs, []string{"notes.md"}) {
t.Fatalf("NvimArgs = %q, want [notes.md]", opts.NvimArgs)
}
}

func TestParseRejectsNvimFlagsBeforeBoundary(t *testing.T) {
if _, err := cli.Parse([]string{"-c", "term"}); err == nil {
t.Fatal("Parse accepted Nvim flag before --")
}
}
Loading