1package main 2 3import ( 4 "flag" 5 "fmt" 6 "log" 7 "os" 8 "path/filepath" 9 "runtime" 10 "runtime/pprof" 11 "sort" 12 "strings" 13 14 "github.com/xyproto/env" 15 "github.com/xyproto/termtitle" 16 "github.com/xyproto/vt100" 17) 18 19const ( 20 versionString = "o 2.46.0" 21) 22 23func main() { 24 var ( 25 versionFlag = flag.Bool("version", false, "version information") 26 helpFlag = flag.Bool("help", false, "quick overview of hotkeys") 27 forceFlag = flag.Bool("f", false, "open even if already open") 28 cpuProfile = flag.String("cpuprofile", "", "write CPU profile to `file`") 29 memProfile = flag.String("memprofile", "", "write memory profile to `file`") 30 ) 31 32 flag.Parse() 33 34 if *versionFlag { 35 fmt.Println(versionString) 36 return 37 } 38 39 if *helpFlag { 40 fmt.Println(versionString + " - simple and limited text editor") 41 fmt.Print(` 42Hotkeys 43 44ctrl-s to save 45ctrl-q to quit 46ctrl-r to open a portal so that this text can be pasted into another file 47ctrl-space to build Go, C++, Zig, V, Rust, Haskell etc, or export Markdown, Adoc or Sdoc 48ctrl-w for Zig, Rust, V and Go, format with the "... fmt" command 49 for C++, format the current file with "clang-format" 50 for HTML, format the file with "tidy", for Python: "autopep8" 51 for Markdown, toggle checkboxes 52 for git interactive rebases, cycle the rebase keywords 53ctrl-a go to start of line, then start of text and then the previous line 54ctrl-e go to end of line and then the next line 55ctrl-n to scroll down 10 lines or go to the next match if a search is active 56ctrl-p to scroll up 10 lines or go to the previous match 57ctrl-k to delete characters to the end of the line, then delete the line 58ctrl-g to toggle filename/line/column/unicode/word count status display 59ctrl-d to delete a single character 60ctrl-o to open the command menu, where the first option is always 61 "Save and quit" 62ctrl-t for C++, toggle between the header and implementation 63ctrl-c to copy the current line, press twice to copy the current block 64ctrl-v to paste one line, press twice to paste the rest 65ctrl-x to cut the current line, press twice to cut the current block 66ctrl-b to toggle a bookmark for the current line, or jump to a bookmark 67ctrl-j to join lines 68ctrl-u to undo (ctrl-z is also possible, but may background the application) 69ctrl-l to jump to a specific line (press return to jump to the top or bottom) 70ctrl-f to find a string, press tab after the text to search and replace 71ctrl-\ to toggle single-line comments for a block of code 72ctrl-~ to jump to matching parenthesis 73esc to redraw the screen and clear the last search 74 75See the man page for more information. 76 77Set NO_COLOR=1 to disable colors. 78 79`) 80 return 81 } 82 83 if *cpuProfile != "" { 84 f, err := os.Create(*cpuProfile) 85 if err != nil { 86 log.Fatal("could not create CPU profile: ", err) 87 } 88 defer f.Close() // error handling omitted for example 89 if err := pprof.StartCPUProfile(f); err != nil { 90 log.Fatal("could not start CPU profile: ", err) 91 } 92 defer pprof.StopCPUProfile() 93 } 94 95 // Check if the executable starts with "g" or "f" 96 var executableName string 97 if len(os.Args) > 0 { 98 executableName = filepath.Base(os.Args[0]) 99 if len(executableName) > 0 { 100 switch executableName[0] { 101 case 'f', 'g': 102 // Start the game 103 if _, err := Game(); err != nil { 104 fmt.Fprintln(os.Stderr, err) 105 os.Exit(1) 106 } else { 107 return 108 } 109 } 110 } 111 } 112 113 filename, lineNumber, colNumber := FilenameAndLineNumberAndColNumber(flag.Arg(0), flag.Arg(1), flag.Arg(2)) 114 if filename == "" { 115 fmt.Fprintln(os.Stderr, "please provide a filename") 116 os.Exit(1) 117 } 118 119 if strings.HasSuffix(filename, ".") && !exists(filename) { 120 // If the filename ends with "." and the file does not exist, assume this was a result of tab-completion going wrong. 121 // If there are multiple files that exist that start with the given filename, open the one first in the alphabet (.cpp before .o) 122 matches, err := filepath.Glob(filename + "*") 123 if err == nil && len(matches) > 0 { // no error and at least 1 match 124 // Use the first match of the sorted results 125 sort.Strings(matches) 126 filename = matches[0] 127 } 128 } else if !strings.Contains(filename, ".") && allLower(filename) && !exists(filename) { 129 // The filename has no ".", is written in lowercase and it does not exist, 130 // but more than one file that starts with the filename exists. Assume tab-completion failed. 131 matches, err := filepath.Glob(filename + "*") 132 if err == nil && len(matches) > 1 { // no error and more than 1 match 133 // Use the first match of the sorted results 134 sort.Strings(matches) 135 filename = matches[0] 136 } 137 } else if !exists(filename) { 138 // Also match "PKGBUILD" if just "Pk" was entered 139 matches, err := filepath.Glob(strings.ToTitle(filename) + "*") 140 if err == nil && len(matches) >= 1 { // no error and at least 1 match 141 // Use the first match of the sorted results 142 sort.Strings(matches) 143 filename = matches[0] 144 } 145 } 146 147 // Set the terminal title, if the current terminal emulator supports it, and NO_COLOR is not set 148 if !envNoColor { 149 termtitle.MustSet(termtitle.GenerateTitle(filename)) 150 } 151 152 // If the editor executable has been named "red", use the red/gray theme by default 153 // Also use the red/gray theme if $SHELL is /bin/csh (typically BSD) 154 theme := NewDefaultTheme() 155 syntaxHighlight := true 156 if envNoColor { 157 theme = NewNoColorTheme() 158 syntaxHighlight = false 159 } else { 160 // Check if the executable starts with "r" or "l" 161 if len(executableName) > 0 { 162 switch executableName[0] { 163 case 'r': // red, ro, rb, rt etc 164 theme = NewRedBlackTheme() 165 case 'l': // light, lo etc 166 theme = NewLightTheme() 167 } 168 } 169 } 170 171 // Initialize the VT100 terminal 172 tty, err := vt100.NewTTY() 173 if err != nil { 174 fmt.Fprintln(os.Stderr, "error: "+err.Error()) 175 os.Exit(1) 176 } 177 defer tty.Close() 178 179 // Run the main editor loop 180 userMessage, err := Loop(tty, filename, lineNumber, colNumber, *forceFlag, theme, syntaxHighlight) 181 182 // Remove the terminal title, if the current terminal emulator supports it 183 // and if NO_COLOR is not set. 184 if !envNoColor { 185 shellName := filepath.Base(env.Str("SHELL", "/bin/sh")) 186 termtitle.MustSet(shellName) 187 } 188 189 // Clear the current color attribute 190 fmt.Print(vt100.Stop()) 191 192 // Respond to the error returned from the main loop, if any 193 if err != nil { 194 if userMessage != "" { 195 quitMessage(tty, userMessage) 196 } else { 197 quitError(tty, err) 198 } 199 } 200 201 // Output memory profile information, if the flag is given 202 if *memProfile != "" { 203 f, err := os.Create(*memProfile) 204 if err != nil { 205 log.Fatal("could not create memory profile: ", err) 206 } 207 defer f.Close() // error handling omitted for example 208 runtime.GC() // get up-to-date statistics 209 if err := pprof.WriteHeapProfile(f); err != nil { 210 log.Fatal("could not write memory profile: ", err) 211 } 212 } 213} 214