diff --git a/internal/shell/parser.go b/internal/shell/parser.go new file mode 100644 index 0000000000000000000000000000000000000000..38f740ee66d22ac3fad1348b1e87953932a12be7 --- /dev/null +++ b/internal/shell/parser.go @@ -0,0 +1,45 @@ +package shell + +import ( + "fmt" + "strings" + + "mvdan.cc/sh/v3/syntax" +) + +// ParseCommand parses a command string into the command name and arguments. +// It handles quoted arguments and other shell syntax. +func ParseCommand(command string) (string, []string, error) { + parsed, err := syntax.NewParser().Parse(strings.NewReader(command), "") + if err != nil { + return "", nil, fmt.Errorf("failed to parse command: %w", err) + } + + var cmdName string + var cmdArgs []string + + if len(parsed.Stmts) > 0 && parsed.Stmts[0].Cmd != nil { + if callExpr, ok := parsed.Stmts[0].Cmd.(*syntax.CallExpr); ok && len(callExpr.Args) > 0 { + for i, arg := range callExpr.Args { + var argStr string + for _, part := range arg.Parts { + if lit, ok := part.(*syntax.Lit); ok { + argStr += lit.Value + } + } + if i == 0 { + cmdName = argStr + } else { + cmdArgs = append(cmdArgs, argStr) + } + } + } + } + + // Fallback if parsing produced no command name (e.g. empty string or comment only, though unlikely with valid input) + if cmdName == "" { + return command, nil, nil + } + + return cmdName, cmdArgs, nil +} diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 022c43f2ed15b7ab9d46de9ebd988b1159cd0517..f7970635153071477bb0acf8792846b93f6dcb7c 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -21,6 +21,7 @@ import ( "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/session" + "github.com/charmbracelet/crush/internal/shell" "github.com/charmbracelet/crush/internal/tui/components/chat" "github.com/charmbracelet/crush/internal/tui/components/completions" "github.com/charmbracelet/crush/internal/tui/components/core/layout" @@ -30,7 +31,6 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs/quit" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" - "mvdan.cc/sh/v3/syntax" ) type Editor interface { @@ -116,34 +116,9 @@ func (m *editorCmp) openEditor(value string) tea.Cmd { // Parse the EDITOR parts to separate the executable from its arguments. // This properly handles EDITORs with args like `zed --wait`. - parsed, err := syntax.NewParser().Parse(strings.NewReader(editor), "") + cmdName, cmdArgs, err := shell.ParseCommand(editor) if err != nil { - return util.ReportError(fmt.Errorf("failed to parse editor command: %w", err)) - } - - var cmdName string - var cmdArgs []string - if len(parsed.Stmts) > 0 && parsed.Stmts[0].Cmd != nil { - if callExpr, ok := parsed.Stmts[0].Cmd.(*syntax.CallExpr); ok && len(callExpr.Args) > 0 { - for i, arg := range callExpr.Args { - var argStr string - for _, part := range arg.Parts { - if lit, ok := part.(*syntax.Lit); ok { - argStr += lit.Value - } - } - if i == 0 { - cmdName = argStr - } else { - cmdArgs = append(cmdArgs, argStr) - } - } - } - } - - // Fallback if parsing borked - if cmdName == "" { - cmdName = editor + return util.ReportError(err) } cmdArgs = append(cmdArgs, tmpfile.Name()) @@ -169,6 +144,37 @@ func (m *editorCmp) openEditor(value string) tea.Cmd { }) } +func (m *editorCmp) openWorkingDir() tea.Cmd { + editor := os.Getenv("EDITOR") + if editor == "" { + // Use platform-appropriate default editor + if runtime.GOOS == "windows" { + editor = "notepad" + } else { + editor = "nvim" + } + } + + // Parse the EDITOR parts to separate the executable from its arguments. + // This properly handles EDITORs with args like `zed --wait`. + cmdName, cmdArgs, err := shell.ParseCommand(editor) + if err != nil { + return util.ReportError(err) + } + + cmdArgs = append(cmdArgs, ".") + c := exec.CommandContext(context.TODO(), cmdName, cmdArgs...) + c.Stdin = os.Stdin + c.Stdout = os.Stdout + c.Stderr = os.Stderr + return tea.ExecProcess(c, func(err error) tea.Msg { + if err != nil { + return util.ReportError(err) + } + return nil + }) +} + func (m *editorCmp) Init() tea.Cmd { return nil } @@ -343,6 +349,12 @@ func (m *editorCmp) Update(msg tea.Msg) (util.Model, tea.Cmd) { } return m, m.openEditor(m.textarea.Value()) } + if key.Matches(msg, m.keyMap.OpenWorkingDir) { + if m.app.AgentCoordinator.IsSessionBusy(m.session.ID) { + return m, util.ReportWarn("Agent is working, please wait...") + } + return m, m.openWorkingDir() + } if key.Matches(msg, DeleteKeyMaps.Escape) { m.deleteMode = false return m, nil diff --git a/internal/tui/components/chat/editor/keys.go b/internal/tui/components/chat/editor/keys.go index 0ba4571888e547b1c4a85e7ee9dd73ff07ce13d2..d7c7f71fa4e5e72b5521ca135fac1fb3b91d9ad3 100644 --- a/internal/tui/components/chat/editor/keys.go +++ b/internal/tui/components/chat/editor/keys.go @@ -5,10 +5,11 @@ import ( ) type EditorKeyMap struct { - AddFile key.Binding - SendMessage key.Binding - OpenEditor key.Binding - Newline key.Binding + AddFile key.Binding + SendMessage key.Binding + OpenEditor key.Binding + OpenWorkingDir key.Binding + Newline key.Binding } func DefaultEditorKeyMap() EditorKeyMap { @@ -25,6 +26,10 @@ func DefaultEditorKeyMap() EditorKeyMap { key.WithKeys("ctrl+o"), key.WithHelp("ctrl+o", "open editor"), ), + OpenWorkingDir: key.NewBinding( + key.WithKeys("ctrl+."), + key.WithHelp("ctrl+.", "open cwd"), + ), Newline: key.NewBinding( key.WithKeys("shift+enter", "ctrl+j"), // "ctrl+j" is a common keybinding for newline in many editors. If @@ -41,6 +46,7 @@ func (k EditorKeyMap) KeyBindings() []key.Binding { k.AddFile, k.SendMessage, k.OpenEditor, + k.OpenWorkingDir, k.Newline, AttachmentsKeyMaps.AttachmentDeleteMode, AttachmentsKeyMaps.DeleteAllAttachments, diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index f951de8677271dfbf034377afaa492f0d8824889..a080f968bf7c25659483a982ba696d97b562b059 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -1054,6 +1054,10 @@ func (p *chatPage) Help() help.KeyMap { key.WithKeys("ctrl+o"), key.WithHelp("ctrl+o", "open editor"), ), + key.NewBinding( + key.WithKeys("ctrl+."), + key.WithHelp("ctrl+.", "open cwd"), + ), }) if p.editor.HasAttachments() {