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(&copyFlag, "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  }