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