]> git.lizzy.rs Git - micro.git/blob - cmd/micro/command.go
Merge pull request #168 from onodera-punpun/syntax_css
[micro.git] / cmd / micro / command.go
1 package main
2
3 import (
4         "bytes"
5         "os"
6         "os/exec"
7         "os/signal"
8         "regexp"
9         "strings"
10 )
11
12 var commands map[string]func([]string)
13
14 var commandActions = map[string]func([]string){
15         "Set":     Set,
16         "Run":     Run,
17         "Bind":    Bind,
18         "Quit":    Quit,
19         "Save":    Save,
20         "Replace": Replace,
21 }
22
23 // InitCommands initializes the default commands
24 func InitCommands() {
25         commands = make(map[string]func([]string))
26
27         defaults := DefaultCommands()
28         parseCommands(defaults)
29 }
30
31 func parseCommands(userCommands map[string]string) {
32         for k, v := range userCommands {
33                 MakeCommand(k, v)
34         }
35 }
36
37 // MakeCommand is a function to easily create new commands
38 // This can be called by plugins in Lua so that plugins can define their own commands
39 func MakeCommand(name, function string) {
40         action := commandActions[function]
41         if _, ok := commandActions[function]; !ok {
42                 // If the user seems to be binding a function that doesn't exist
43                 // We hope that it's a lua function that exists and bind it to that
44                 action = LuaFunctionCommand(function)
45         }
46
47         commands[name] = action
48 }
49
50 // DefaultCommands returns a map containing micro's default commands
51 func DefaultCommands() map[string]string {
52         return map[string]string{
53                 "set":     "Set",
54                 "bind":    "Bind",
55                 "run":     "Run",
56                 "quit":    "Quit",
57                 "save":    "Save",
58                 "replace": "Replace",
59         }
60 }
61
62 // Set sets an option
63 func Set(args []string) {
64         // Set an option and we have to set it for every view
65         for _, view := range views {
66                 SetOption(view, args)
67         }
68 }
69
70 // Bind creates a new keybinding
71 func Bind(args []string) {
72         if len(args) != 2 {
73                 messenger.Error("Incorrect number of arguments")
74                 return
75         }
76         BindKey(args[0], args[1])
77 }
78
79 // Run runs a shell command in the background
80 func Run(args []string) {
81         // Run a shell command in the background (openTerm is false)
82         HandleShellCommand(strings.Join(args, " "), false)
83 }
84
85 // Quit closes the main view
86 func Quit(args []string) {
87         // Close the main view
88         views[mainView].Quit()
89 }
90
91 // Save saves the buffer in the main view
92 func Save(args []string) {
93         // Save the main view
94         views[mainView].Save()
95 }
96
97 // Replace runs search and replace
98 func Replace(args []string) {
99         // This is a regex to parse the replace expression
100         // We allow no quotes if there are no spaces, but if you want to search
101         // for or replace an expression with spaces, you can add double quotes
102         r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
103         replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
104         if len(replaceCmd) < 2 {
105                 // We need to find both a search and replace expression
106                 messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
107                 return
108         }
109
110         var flags string
111         if len(replaceCmd) == 3 {
112                 // The user included some flags
113                 flags = replaceCmd[2]
114         }
115
116         search := string(replaceCmd[0])
117         replace := string(replaceCmd[1])
118
119         // If the search and replace expressions have quotes, we need to remove those
120         if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
121                 search = search[1 : len(search)-1]
122         }
123         if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
124                 replace = replace[1 : len(replace)-1]
125         }
126
127         // We replace all escaped double quotes to real double quotes
128         search = strings.Replace(search, `\"`, `"`, -1)
129         replace = strings.Replace(replace, `\"`, `"`, -1)
130         // Replace some things so users can actually insert newlines and tabs in replacements
131         replace = strings.Replace(replace, "\\n", "\n", -1)
132         replace = strings.Replace(replace, "\\t", "\t", -1)
133
134         regex, err := regexp.Compile(search)
135         if err != nil {
136                 // There was an error with the user's regex
137                 messenger.Error(err.Error())
138                 return
139         }
140
141         view := views[mainView]
142
143         found := false
144         for {
145                 match := regex.FindStringIndex(view.Buf.String())
146                 if match == nil {
147                         break
148                 }
149                 found = true
150                 if strings.Contains(flags, "c") {
151                         // The 'check' flag was used
152                         Search(search, view, true)
153                         view.Relocate()
154                         if settings["syntax"].(bool) {
155                                 view.matches = Match(view)
156                         }
157                         RedrawAll()
158                         choice, canceled := messenger.YesNoPrompt("Perform replacement? (y,n)")
159                         if canceled {
160                                 if view.Cursor.HasSelection() {
161                                         view.Cursor.Loc = view.Cursor.CurSelection[0]
162                                         view.Cursor.ResetSelection()
163                                 }
164                                 messenger.Reset()
165                                 return
166                         }
167                         if choice {
168                                 view.Cursor.DeleteSelection()
169                                 view.Buf.Insert(FromCharPos(match[0], view.Buf), replace)
170                                 view.Cursor.ResetSelection()
171                                 messenger.Reset()
172                         } else {
173                                 if view.Cursor.HasSelection() {
174                                         searchStart = ToCharPos(view.Cursor.CurSelection[1], view.Buf)
175                                 } else {
176                                         searchStart = ToCharPos(view.Cursor.Loc, view.Buf)
177                                 }
178                                 continue
179                         }
180                 } else {
181                         view.Buf.Replace(FromCharPos(match[0], view.Buf), FromCharPos(match[1], view.Buf), replace)
182                 }
183         }
184         if !found {
185                 messenger.Message("Nothing matched " + search)
186         }
187 }
188
189 // RunShellCommand executes a shell command and returns the output/error
190 func RunShellCommand(input string) (string, error) {
191         inputCmd := strings.Split(input, " ")[0]
192         args := strings.Split(input, " ")[1:]
193
194         cmd := exec.Command(inputCmd, args...)
195         outputBytes := &bytes.Buffer{}
196         cmd.Stdout = outputBytes
197         cmd.Stderr = outputBytes
198         cmd.Start()
199         err := cmd.Wait() // wait for command to finish
200         outstring := outputBytes.String()
201         return outstring, err
202 }
203
204 // HandleShellCommand runs the shell command
205 // The openTerm argument specifies whether a terminal should be opened (for viewing output
206 // or interacting with stdin)
207 func HandleShellCommand(input string, openTerm bool) {
208         inputCmd := strings.Split(input, " ")[0]
209         if !openTerm {
210                 // Simply run the command in the background and notify the user when it's done
211                 messenger.Message("Running...")
212                 go func() {
213                         output, err := RunShellCommand(input)
214                         totalLines := strings.Split(output, "\n")
215
216                         if len(totalLines) < 3 {
217                                 if err == nil {
218                                         messenger.Message(inputCmd, " exited without error")
219                                 } else {
220                                         messenger.Message(inputCmd, " exited with error: ", err, ": ", output)
221                                 }
222                         } else {
223                                 messenger.Message(output)
224                         }
225                         // We have to make sure to redraw
226                         RedrawAll()
227                 }()
228         } else {
229                 // Shut down the screen because we're going to interact directly with the shell
230                 screen.Fini()
231                 screen = nil
232
233                 args := strings.Split(input, " ")[1:]
234
235                 // Set up everything for the command
236                 cmd := exec.Command(inputCmd, args...)
237                 cmd.Stdin = os.Stdin
238                 cmd.Stdout = os.Stdout
239                 cmd.Stderr = os.Stderr
240
241                 // This is a trap for Ctrl-C so that it doesn't kill micro
242                 // Instead we trap Ctrl-C to kill the program we're running
243                 c := make(chan os.Signal, 1)
244                 signal.Notify(c, os.Interrupt)
245                 go func() {
246                         for range c {
247                                 cmd.Process.Kill()
248                         }
249                 }()
250
251                 // Start the command
252                 cmd.Start()
253                 cmd.Wait()
254
255                 // This is just so we don't return right away and let the user press enter to return
256                 TermMessage("")
257
258                 // Start the screen back up
259                 InitScreen()
260         }
261 }
262
263 // HandleCommand handles input from the user
264 func HandleCommand(input string) {
265         inputCmd := strings.Split(input, " ")[0]
266         args := strings.Split(input, " ")[1:]
267
268         if _, ok := commands[inputCmd]; !ok {
269                 messenger.Error("Unkown command ", inputCmd)
270         } else {
271                 commands[inputCmd](args)
272         }
273 }