github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/main.go (about) 1 // main is the main package for the o editor 2 package main 3 4 import ( 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "regexp" 10 "sort" 11 "strings" 12 "sync" 13 "syscall" 14 "time" 15 16 "github.com/spf13/pflag" 17 "github.com/xyproto/env/v2" 18 "github.com/xyproto/files" 19 "github.com/xyproto/vt100" 20 ) 21 22 const versionString = "Orbiton 2.65.11" 23 24 var ( 25 // quitMut disallows Exit(1) while a file is being saved 26 quitMut sync.Mutex 27 28 // avoid writing to ~/.cache ? 29 noWriteToCache bool 30 31 cacheDirForDoc = files.ShortPath(filepath.Join(userCacheDir, "o")) 32 33 // Only for the filename completion, when starting the editor 34 probablyDoesNotWantToEditExtensions = []string{".7z", ".a", ".bak", ".core", ".gz", ".img", ".lock", ".o", ".out", ".pkg", ".pyc", ".pyo", ".swp", ".tar", ".tmp", ".xz", ".zip"} 35 36 editorLaunchTime = time.Now() 37 38 // For when building and running programs with ctrl-space 39 inputFileWhenRunning string 40 ) 41 42 func main() { 43 var ( 44 copyFlag bool 45 forceFlag bool 46 helpFlag bool 47 monitorAndReadOnlyFlag bool 48 noCacheFlag bool 49 pasteFlag bool 50 clearLocksFlag bool 51 lastCommandFlag bool 52 quickHelpFlag bool 53 createDirectoriesFlag bool 54 versionFlag bool 55 ) 56 57 pflag.BoolVarP(©Flag, "copy", "c", false, "copy a file into the clipboard and quit") 58 pflag.BoolVarP(&forceFlag, "force", "f", false, "open even if already open") 59 pflag.BoolVarP(&helpFlag, "help", "h", false, "quick overview of hotkeys and flags") 60 pflag.BoolVarP(&monitorAndReadOnlyFlag, "monitor", "m", false, "open read-only and monitor for changes") 61 pflag.BoolVarP(&noCacheFlag, "no-cache", "n", false, "don't write anything to cache directory") 62 pflag.BoolVarP(&pasteFlag, "paste", "p", false, "paste the clipboard into the file and quit") 63 pflag.BoolVarP(&clearLocksFlag, "clear-locks", "r", false, "clear all file locks") 64 pflag.BoolVarP(&lastCommandFlag, "last-command", "l", false, "output the last build or format command") 65 pflag.BoolVarP(&quickHelpFlag, "quick-help", "q", false, "always display the quick help when starting") 66 pflag.BoolVarP(&createDirectoriesFlag, "create-dir", "d", false, "create diretories when opening a new file") 67 pflag.BoolVarP(&versionFlag, "version", "v", false, "version information") 68 pflag.StringVarP(&inputFileWhenRunning, "input-file", "i", "input.txt", "input file when building and running programs") 69 70 pflag.Parse() 71 72 if versionFlag { 73 fmt.Println(versionString) 74 return 75 } 76 if helpFlag { 77 Usage() 78 return 79 } 80 81 // Output the last used build, export or format command 82 if lastCommandFlag { 83 data, err := os.ReadFile(lastCommandFile) 84 if err != nil { 85 fmt.Println("no available last command") 86 return 87 } 88 // Remove the shebang 89 firstLineAndRest := strings.SplitN(string(data), "\n", 2) 90 if len(firstLineAndRest) != 2 || !strings.HasPrefix(firstLineAndRest[0], "#") { 91 fmt.Fprintf(os.Stderr, "unrecognized contents in %s\n", lastCommandFile) 92 os.Exit(1) 93 } 94 theRest := strings.TrimSpace(firstLineAndRest[1]) 95 replaced := regexp.MustCompile(`/tmp/o\..*$`).ReplaceAllString(theRest, "") 96 fmt.Println(replaced) 97 return 98 } 99 100 noWriteToCache = noCacheFlag || monitorAndReadOnlyFlag 101 102 var ( 103 executableName string 104 firstLetterOfExecutable = rune(0) 105 ) 106 107 if len(os.Args) > 0 { 108 // The executable name is in arg 0 109 executableName = filepath.Base(os.Args[0]) 110 if len(executableName) > 0 { 111 // Get the first rune of the executable name 112 firstLetterOfExecutable = []rune(strings.ToLower(filepath.Base(os.Args[0])))[0] 113 } 114 } 115 116 // If the -p flag is given, or the executable starts with 'p', just paste the clipboard to the given filename and exit 117 if filename := pflag.Arg(0); filename != "" && (pasteFlag || firstLetterOfExecutable == 'p') { 118 const primaryClipboard = false 119 n, headString, tailString, err := WriteClipboardToFile(filename, forceFlag, primaryClipboard) 120 if err != nil { 121 fmt.Fprintf(os.Stderr, "error: %v\n", err) 122 quitMut.Lock() 123 defer quitMut.Unlock() 124 os.Exit(1) 125 } else if n == 0 { 126 fmt.Fprintf(os.Stderr, "Wrote 0 bytes to %s\n", filename) 127 quitMut.Lock() 128 defer quitMut.Unlock() 129 os.Exit(1) 130 } 131 // chmod +x if this looks like a shell script or is in ie. /usr/bin 132 if filepath.Ext(filename) == ".sh" || files.BinDirectory(filename) || strings.HasPrefix(headString, "#!") { 133 os.Chmod(filename, 0o755) 134 } 135 if tailString != "" { 136 fmt.Printf("Wrote %d bytes to %s from the clipboard. Tail bytes: %s\n", n, filename, strings.TrimSpace(strings.ReplaceAll(tailString, "\n", "\\n"))) 137 } else { 138 fmt.Printf("Wrote %d bytes to %s from the clipboard.\n", n, filename) 139 } 140 return 141 } 142 143 // If the -c flag is given, or the executable name starts with 'c', just copy the given filename to the clipboard and exit 144 if filename := pflag.Arg(0); filename != "" && (copyFlag || firstLetterOfExecutable == 'c') { 145 const primaryClipboard = false 146 n, tailString, err := SetClipboardFromFile(filename, primaryClipboard) 147 if err != nil { 148 fmt.Fprintf(os.Stderr, "error: %v\n", err) 149 quitMut.Lock() 150 defer quitMut.Unlock() 151 os.Exit(1) 152 } else if n == 0 { 153 fmt.Fprintf(os.Stderr, "Wrote 0 bytes to %s\n", filename) 154 quitMut.Lock() 155 defer quitMut.Unlock() 156 os.Exit(1) 157 } 158 plural := "s" 159 if n == 1 { 160 plural = "" 161 } 162 if tailString != "" { 163 fmt.Printf("Copied %d byte%s from %s to the clipboard. Tail bytes: %s\n", n, plural, filename, strings.TrimSpace(strings.ReplaceAll(tailString, "\n", "\\n"))) 164 } else { 165 fmt.Printf("Copied %d byte%s from %s to the clipboard.\n", n, plural, filename) 166 } 167 return 168 } 169 170 // If the -r flag is given, clear all file locks and exit. 171 if clearLocksFlag { 172 lockErr := os.Remove(defaultLockFile) 173 174 // Also remove the portal file 175 portalErr := ClearPortal() 176 177 switch { 178 case lockErr == nil && portalErr != nil: 179 fmt.Println("Cleared all locks") 180 case lockErr == nil && portalErr == nil: 181 fmt.Println("Cleared all locks and closed the portal") 182 case lockErr != nil && portalErr == nil: 183 fmt.Fprintf(os.Stderr, "Closed the portal, but could not clear locks: %v\n", lockErr) 184 os.Exit(1) 185 default: // both errors are non-nil 186 fmt.Fprintf(os.Stderr, "Could not clear locks: %v\n", lockErr) 187 os.Exit(1) 188 } 189 190 return 191 } 192 193 traceStart() // if building with -tags trace 194 195 // Check if the executable starts with "g" or "f" ("c" and "p" are already checked for, further up) 196 if len(os.Args) > 0 { 197 switch firstLetterOfExecutable { 198 case 'f', 'g': 199 // Start the game 200 if _, err := Game(); err != nil { 201 fmt.Fprintln(os.Stderr, err) 202 quitMut.Lock() 203 defer quitMut.Unlock() 204 os.Exit(1) 205 } 206 return 207 } 208 if executableName == "osudo" { 209 // Build the environment with the EDITOR variable set to "o" 210 env := append(env.Environ(), "EDITOR=o") 211 212 // Get the path to the visudo executable 213 visudoPath := files.Which("visudo") 214 if visudoPath != "" { // success 215 // Replace the current process with visudo 216 if err := syscall.Exec(visudoPath, []string{"visudo"}, env); err != nil { 217 // Could not exec visudo 218 fmt.Fprintln(os.Stderr, err) 219 quitMut.Lock() 220 defer quitMut.Unlock() 221 os.Exit(1) 222 } 223 // No need to return here, because syscall.Exec replaces the current process 224 } 225 // If visudo was not found, start the editor as normal 226 } 227 } 228 229 var ( 230 err error 231 fnord FilenameOrData 232 lineNumber LineNumber 233 colNumber ColNumber 234 ) 235 236 stdinFilename := len(os.Args) == 1 || (len(os.Args) == 2 && (os.Args[1] == "-" || os.Args[1] == "/dev/stdin")) 237 238 // Check if the parent process is "man" 239 manPageMode := parentIsMan() 240 241 // If no regular filename is given, check if data is ready at stdin 242 fnord.stdin = stdinFilename && (files.DataReadyOnStdin() || manPageMode) 243 244 if fnord.stdin { 245 // TODO: Use a spinner? 246 data, err := io.ReadAll(os.Stdin) 247 if err != nil { 248 fmt.Fprintln(os.Stderr, "could not read from stdin") 249 quitMut.Lock() 250 defer quitMut.Unlock() 251 os.Exit(1) 252 } 253 // Now stop reading further from stdin 254 os.Stdin.Close() 255 256 if lendata := len(data); lendata > 0 { 257 fnord.filename = "-" 258 fnord.data = data 259 fnord.length = lendata 260 } 261 } else { 262 fnord.filename, lineNumber, colNumber = FilenameAndLineNumberAndColNumber(pflag.Arg(0), pflag.Arg(1), pflag.Arg(2)) 263 } 264 // Check if the given filename contains something 265 if fnord.Empty() { 266 if fnord.filename == "" { 267 fmt.Fprintln(os.Stderr, "please provide a filename") 268 quitMut.Lock() 269 defer quitMut.Unlock() 270 os.Exit(1) 271 } 272 273 // If the filename starts with "~", then expand it 274 fnord.ExpandUser() 275 276 // Check if the given filename is not a file or a symlink 277 if !files.IsFileOrSymlink(fnord.filename) { 278 if strings.HasSuffix(fnord.filename, ".") { 279 // If the filename ends with "." and the file does not exist, assume this was a result of tab-completion going wrong. 280 // If there are multiple files that exist that start with the given filename, open the one first in the alphabet (.cpp before .o) 281 matches, err := filepath.Glob(fnord.filename + "*") 282 if err == nil && len(matches) > 0 { // no error and at least 1 match 283 // Filter out any binary files 284 matches = files.FilterOutBinaryFiles(matches) 285 if len(matches) > 0 { 286 sort.Strings(matches) 287 // If the matches contains low priority suffixes, such as ".lock", then move it last 288 for i, fn := range matches { 289 if hasSuffix(fn, probablyDoesNotWantToEditExtensions) { 290 // Move this filename last 291 matches = append(matches[:i], matches[i+1:]...) 292 matches = append(matches, fn) 293 break 294 } 295 } 296 // Use the first filename in the list of matches 297 fnord.filename = matches[0] 298 } 299 } 300 } else if !strings.Contains(fnord.filename, ".") && allLower(fnord.filename) { 301 // The filename has no ".", is written in lowercase and it does not exist, 302 // but more than one file that starts with the filename exists. Assume tab-completion failed. 303 matches, err := filepath.Glob(fnord.filename + "*") 304 if err == nil && len(matches) > 1 { // no error and more than 1 match 305 // Use the first non-binary match of the sorted results 306 matches = files.FilterOutBinaryFiles(matches) 307 if len(matches) > 0 { 308 sort.Strings(matches) 309 fnord.filename = matches[0] 310 } 311 } 312 } else { 313 // Also match ie. "PKGBUILD" if just "Pk" was entered 314 matches, err := filepath.Glob(strings.ToTitle(fnord.filename) + "*") 315 if err == nil && len(matches) >= 1 { // no error and at least 1 match 316 // Use the first non-binary match of the sorted results 317 matches = files.FilterOutBinaryFiles(matches) 318 if len(matches) > 0 { 319 sort.Strings(matches) 320 fnord.filename = matches[0] 321 } 322 } 323 } 324 } 325 } 326 327 // Set the terminal title, if the current terminal emulator supports it, and NO_COLOR is not set 328 fnord.SetTitle() 329 330 // If the editor executable has been named "red", use the red/gray theme by default 331 theme := NewDefaultTheme() 332 syntaxHighlight := true 333 nanoMode := false 334 if envNoColor { 335 theme = NewNoColorDarkBackgroundTheme() 336 syntaxHighlight = false 337 } else if firstLetterOfExecutable != rune(0) { 338 // Check if the executable starts with a specific letter ('f', 'g', 'p' and 'c' are already chekced for) 339 specificLetter = true 340 switch firstLetterOfExecutable { 341 case 'b', 'e': // bo, borland, ed, edit etc. 342 theme = NewDarkBlueEditTheme() 343 // TODO: Later, when specificLetter is examined, use either NewEditLightTheme or NewEditDarkTheme 344 editTheme = true 345 case 'l': // lo, light etc 346 theme = NewLitmusTheme() 347 case 'v': // vs, vscode etc. 348 theme = NewDarkVSTheme() 349 case 'r': // rb, ro, rt, red etc. 350 theme = NewRedBlackTheme() 351 case 's': // s, sw, synthwave etc. 352 theme = NewSynthwaveTheme() 353 case 't': // t, teal 354 theme = NewTealTheme() 355 case 'n': // nan, nano 356 // Check if "Nano mode" should be set 357 nanoMode = executableName == "nan" || executableName == "nano" 358 default: 359 specificLetter = false 360 } 361 } 362 363 // TODO: Move this to themes.go 364 if nanoMode { // make the status bar stand out 365 theme.StatusBackground = theme.DebugInstructionsBackground 366 theme.StatusErrorBackground = theme.DebugInstructionsBackground 367 } 368 369 // Initialize the VT100 terminal 370 tty, err := vt100.NewTTY() 371 if err != nil { 372 fmt.Fprintln(os.Stderr, "error: "+err.Error()) 373 quitMut.Lock() 374 defer quitMut.Unlock() 375 os.Exit(1) 376 } 377 defer tty.Close() 378 379 // Run the main editor loop 380 userMessage, stopParent, err := Loop(tty, fnord, lineNumber, colNumber, forceFlag, theme, syntaxHighlight, monitorAndReadOnlyFlag, nanoMode, manPageMode, createDirectoriesFlag, quickHelpFlag) 381 382 // SIGQUIT the parent PID. Useful if being opened repeatedly by a find command. 383 if stopParent { 384 defer func() { 385 syscall.Kill(os.Getppid(), syscall.SIGQUIT) 386 }() 387 } 388 389 // Remove the terminal title, if the current terminal emulator supports it 390 // and if NO_COLOR is not set. 391 NoTitle() 392 393 // Clear the current color attribute 394 fmt.Print(vt100.Stop()) 395 396 traceComplete() // if building with -tags trace 397 398 // Respond to the error returned from the main loop, if any 399 if err != nil { 400 if userMessage != "" { 401 quitMessage(tty, userMessage) 402 } else { 403 quitError(tty, err) 404 } 405 } 406 }