Main: more robust error handling.

Previously, the editor would panic at any error. After I recently
implemented dialogs, I changed all panics to create error dialogs,
instead. This will make the editor more stable.
This commit is contained in:
Luke I. Wilson 2021-04-02 14:30:38 -05:00
parent f64af6fa29
commit d1dd389dba
2 changed files with 132 additions and 94 deletions

210
main.go
View File

@ -25,6 +25,8 @@ var theme = ui.Theme{
} }
var ( var (
screen tcell.Screen
menuBar *ui.MenuBar menuBar *ui.MenuBar
tabContainer *ui.TabContainer tabContainer *ui.TabContainer
dialog ui.Component // nil if not present (has exclusive focus) dialog ui.Component // nil if not present (has exclusive focus)
@ -40,6 +42,58 @@ func changeFocus(to ui.Component) {
to.SetFocused(true) to.SetFocused(true)
} }
func showErrorDialog(title string, message string, callback func()) {
dialog = ui.NewMessageDialog(title, message, ui.MessageKindError, nil, &theme, func(string) {
if callback != nil {
callback()
} else {
dialog = nil
changeFocus(tabContainer) // Default behavior: focus tabContainer
}
})
changeFocus(dialog)
}
// Shows the Save As... dialog for saving unnamed files
func saveAs() {
callback := func(filePaths []string) {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit)
// If we got the callback, it is safe to assume there are one or more files
f, err := os.OpenFile(filePaths[0], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm)
if err != nil {
showErrorDialog("Could not open file for writing", fmt.Sprintf("File at %#v could not be opened with write permissions. Maybe another program has it open? %v", filePaths[0], err), nil)
return
}
defer f.Close()
_, err = te.Buffer.WriteTo(f)
if err != nil {
showErrorDialog("Failed to write to file", fmt.Sprintf("File at %#v was opened for writing, but an error occurred while writing the buffer. %v", filePaths[0], err), nil)
return
}
te.Dirty = false
dialog = nil // Hide the file selector
changeFocus(tabContainer)
tab.Name = filePaths[0]
}
dialog = ui.NewFileSelectorDialog(
&screen,
"Select a file to overwrite",
false,
&theme,
callback,
func() { // Dialog canceled
dialog = nil
changeFocus(tabContainer)
},
)
changeFocus(dialog)
}
func main() { func main() {
flag.Parse() flag.Parse()
if *cpuprofile != "" { if *cpuprofile != "" {
@ -54,38 +108,31 @@ func main() {
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
s, e := tcell.NewScreen() screen, err := tcell.NewScreen()
if e != nil { if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", e) fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1) os.Exit(1)
} }
if e := s.Init(); e != nil { if err := screen.Init(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", e) fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1) os.Exit(1)
} }
defer s.Fini() // Useful for handling panics defer screen.Fini() // Useful for handling panics
var closing bool var closing bool
sizex, sizey := s.Size() sizex, sizey := screen.Size()
tabContainer = ui.NewTabContainer(&theme) tabContainer = ui.NewTabContainer(&theme)
tabContainer.SetPos(0, 1) tabContainer.SetPos(0, 1)
tabContainer.SetSize(sizex, sizey-2) tabContainer.SetSize(sizex, sizey-2)
_, err := ClipInitialize(ClipExternal) changeFocus(tabContainer) // tabContainer focused by default
if err != nil {
panic(err)
}
// Open files from command-line arguments // Open files from command-line arguments
if len(os.Args) > 1 { if flag.NArg() > 0 {
for i := 1; i < len(os.Args); i++ { for i := 0; i < flag.NArg(); i++ {
if os.Args[i] == "-cpuprofile" || os.Args[i] == "-memprofile" { arg := flag.Arg(i)
i++ _, err := os.Stat(arg)
continue
}
_, err := os.Stat(os.Args[i])
var dirty bool var dirty bool
var bytes []byte var bytes []byte
@ -93,59 +140,30 @@ func main() {
if errors.Is(err, os.ErrNotExist) { // If the file does not exist... if errors.Is(err, os.ErrNotExist) { // If the file does not exist...
dirty = true dirty = true
} else { // If the file exists... } else { // If the file exists...
file, err := os.Open(os.Args[i]) file, err := os.Open(arg)
if err != nil { if err != nil {
panic("File could not be opened at path " + os.Args[i]) showErrorDialog("File could not be opened", fmt.Sprintf("File at %#v could not be opened. %v", arg, err), nil)
continue
} }
defer file.Close() defer file.Close()
bytes, err = ioutil.ReadAll(file) bytes, err = ioutil.ReadAll(file)
if err != nil { if err != nil {
panic("Could not read all of " + os.Args[i]) showErrorDialog("Could not read file", fmt.Sprintf("File at %#v was opened, but could not be read. %v", arg, err), nil)
continue
} }
} }
textEdit := ui.NewTextEdit(&s, os.Args[i], bytes, &theme) textEdit := ui.NewTextEdit(&screen, arg, bytes, &theme)
textEdit.Dirty = dirty textEdit.Dirty = dirty
tabContainer.AddTab(os.Args[i], textEdit) tabContainer.AddTab(arg, textEdit)
} }
tabContainer.SetFocused(true) // Lets any opened TextEdit component know to be focused
} }
saveAs := func() { _, err = ClipInitialize(ClipExternal)
callback := func(filePaths []string) {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit)
// If we got the callback, it is safe to assume there are one or more files
f, err := os.OpenFile(filePaths[0], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm)
if err != nil { if err != nil {
panic(err) showErrorDialog("Error Initializing Clipboard", fmt.Sprintf("%v\n\nAn internal clipboard will be used, instead.", err), nil)
}
defer f.Close()
_, err = te.Buffer.WriteTo(f)
if err != nil {
panic(fmt.Sprint("Error occurred while writing buffer to file: ", err))
}
te.Dirty = false
dialog = nil // Hide the file selector
changeFocus(tabContainer)
tab.Name = filePaths[0]
}
dialog = ui.NewFileSelectorDialog(
&s,
"Select a file to overwrite",
false,
&theme,
callback,
func() { // Dialog canceled
dialog = nil
changeFocus(tabContainer)
},
)
changeFocus(dialog)
} }
menuBar = ui.NewMenuBar(&theme) menuBar = ui.NewMenuBar(&theme)
@ -153,35 +171,44 @@ func main() {
fileMenu := ui.NewMenu("File", 0, &theme) fileMenu := ui.NewMenu("File", 0, &theme)
fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() { fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() {
textEdit := ui.NewTextEdit(&s, "", []byte{}, &theme) // No file path, no contents textEdit := ui.NewTextEdit(&screen, "", []byte{}, &theme) // No file path, no contents
tabContainer.AddTab("noname", textEdit) tabContainer.AddTab("noname", textEdit)
changeFocus(tabContainer) changeFocus(tabContainer)
tabContainer.FocusTab(tabContainer.GetTabCount()-1) tabContainer.FocusTab(tabContainer.GetTabCount()-1)
}}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() { }}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() {
callback := func(filePaths []string) { callback := func(filePaths []string) {
var errOccurred bool
for _, path := range filePaths { for _, path := range filePaths {
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
panic("Could not open file at path " + path) showErrorDialog("File could not be opened", fmt.Sprintf("File at %#v could not be opened. %v", path, err), nil)
errOccurred = true
continue
} }
defer file.Close() defer file.Close()
bytes, err := ioutil.ReadAll(file) bytes, err := ioutil.ReadAll(file)
if err != nil { if err != nil {
panic("Could not read all of file") showErrorDialog("Could not read file", fmt.Sprintf("File at %#v was opened, but could not be read. %v", path, err), nil)
errOccurred = true
continue
} }
textEdit := ui.NewTextEdit(&s, path, bytes, &theme) textEdit := ui.NewTextEdit(&screen, path, bytes, &theme)
tabContainer.AddTab(path, textEdit) tabContainer.AddTab(path, textEdit)
} }
dialog = nil // Hide the file selector
if !errOccurred { // Prevent hiding the error dialog
dialog = nil // Hide the file selector
changeFocus(tabContainer) changeFocus(tabContainer)
if tabContainer.GetTabCount() > 0 {
tabContainer.FocusTab(tabContainer.GetTabCount()-1) tabContainer.FocusTab(tabContainer.GetTabCount()-1)
} }
}
}
dialog = ui.NewFileSelectorDialog( dialog = ui.NewFileSelectorDialog(
&s, &screen,
"Comma-separated files or a directory", "Comma-separated files or a directory",
true, true,
&theme, &theme,
@ -199,13 +226,15 @@ func main() {
if te.FilePath != "" { if te.FilePath != "" {
f, err := os.OpenFile(te.FilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm) f, err := os.OpenFile(te.FilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm)
if err != nil { if err != nil {
panic(err) showErrorDialog("Could not open file for writing", fmt.Sprintf("File at %#v could not be opened with write permissions. Maybe another program has it open? %v", te.FilePath, err), nil)
return
} }
defer f.Close() defer f.Close()
_, err = te.Buffer.WriteTo(f) // TODO: check count _, err = te.Buffer.WriteTo(f) // TODO: check count
if err != nil { if err != nil {
panic(fmt.Sprint("Error occurred while writing buffer to file: ", err)) showErrorDialog("Failed to write to file", fmt.Sprintf("File at %#v was opened for writing, but an error occurred while writing the buffer. %v", te.FilePath, err), nil)
return
} }
te.Dirty = false te.Dirty = false
@ -256,23 +285,34 @@ func main() {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
bytes := te.GetSelectedBytes() bytes := te.GetSelectedBytes()
var err error
if len(bytes) > 0 { // If something is selected... if len(bytes) > 0 { // If something is selected...
te.Delete(false) // Delete the selection te.Delete(false) // Delete the selection
// TODO: better error handling within editor err = ClipWrite(string(bytes)) // Add the selectedStr to clipboard
_ = ClipWrite(string(bytes)) // Add the selectedStr to clipboard if err != nil {
showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil)
} }
}
if err == nil { // Prevent hiding error dialog
changeFocus(tabContainer) changeFocus(tabContainer)
} }
}
}}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() { }}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() {
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := tab.Child.(*ui.TextEdit) te := tab.Child.(*ui.TextEdit)
bytes := te.GetSelectedBytes() bytes := te.GetSelectedBytes()
var err error
if len(bytes) > 0 { // If there is something selected... if len(bytes) > 0 { // If there is something selected...
_ = ClipWrite(string(bytes)) // Add selectedStr to clipboard err = ClipWrite(string(bytes)) // Add selectedStr to clipboard
if err != nil {
showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil)
} }
}
if err == nil {
changeFocus(tabContainer) changeFocus(tabContainer)
} }
}
}}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() { }}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() {
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
@ -280,12 +320,12 @@ func main() {
contents, err := ClipRead() contents, err := ClipRead()
if err != nil { if err != nil {
panic(err) showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil)
} } else {
te.Insert(contents) te.Insert(contents)
changeFocus(tabContainer) changeFocus(tabContainer)
} }
}
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() {
}}, &ui.ItemEntry{Name: "Select Line", QuickChar: 7, Callback: func() { }}, &ui.ItemEntry{Name: "Select Line", QuickChar: 7, Callback: func() {
@ -295,7 +335,7 @@ func main() {
searchMenu := ui.NewMenu("Search", 0, &theme) searchMenu := ui.NewMenu("Search", 0, &theme)
searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Find and Replace...", Shortcut: "Ctrl+F", Callback: func() { searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Find and Replace...", Shortcut: "Ctrl+F", Callback: func() {
s.Beep() screen.Beep()
}}, &ui.ItemEntry{Name: "Find in Directory...", QuickChar: 8, Callback: func() { }}, &ui.ItemEntry{Name: "Find in Directory...", QuickChar: 8, Callback: func() {
}}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Go to line...", Shortcut: "Ctrl+G", Callback: func() { }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Go to line...", Shortcut: "Ctrl+G", Callback: func() {
@ -307,18 +347,16 @@ func main() {
menuBar.AddMenu(editMenu) menuBar.AddMenu(editMenu)
menuBar.AddMenu(searchMenu) menuBar.AddMenu(searchMenu)
changeFocus(tabContainer) // TabContainer is focused by default
for !closing { for !closing {
s.Clear() screen.Clear()
// Draw background (grey and black checkerboard) // Draw background (grey and black checkerboard)
ui.DrawRect(s, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack)) ui.DrawRect(screen, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack))
if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open if tabContainer.GetTabCount() > 0 { // Draw the tab container only if a tab is open
tabContainer.Draw(s) tabContainer.Draw(screen)
} }
menuBar.Draw(s) // Always draw the menu bar menuBar.Draw(screen) // Always draw the menu bar
if dialog != nil { if dialog != nil {
// Update fileSelector dialog pos and size // Update fileSelector dialog pos and size
@ -326,11 +364,11 @@ func main() {
dialog.SetSize(diagMinX, diagMinY) dialog.SetSize(diagMinX, diagMinY)
dialog.SetPos(sizex/2-diagMinX/2, sizey/2-diagMinY/2) // Center dialog.SetPos(sizex/2-diagMinX/2, sizey/2-diagMinY/2) // Center
dialog.Draw(s) dialog.Draw(screen)
} }
// Draw statusbar // Draw statusbar
ui.DrawRect(s, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"]) ui.DrawRect(screen, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"])
if tabContainer.GetTabCount() > 0 { if tabContainer.GetTabCount() > 0 {
focusedTab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) focusedTab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx())
te := focusedTab.Child.(*ui.TextEdit) te := focusedTab.Child.(*ui.TextEdit)
@ -352,19 +390,19 @@ func main() {
} }
str := fmt.Sprintf(" Filetype: %s %d, %d %s %s", "None", line+1, col+1, delim, tabs) str := fmt.Sprintf(" Filetype: %s %d, %d %s %s", "None", line+1, col+1, delim, tabs)
ui.DrawStr(s, 0, sizey-1, str, theme["StatusBar"]) ui.DrawStr(screen, 0, sizey-1, str, theme["StatusBar"])
} }
s.Show() screen.Show()
switch ev := s.PollEvent().(type) { switch ev := screen.PollEvent().(type) {
case *tcell.EventResize: case *tcell.EventResize:
sizex, sizey = s.Size() sizex, sizey = screen.Size()
menuBar.SetSize(sizex, 1) menuBar.SetSize(sizex, 1)
tabContainer.SetSize(sizex, sizey-2) tabContainer.SetSize(sizex, sizey-2)
s.Sync() // Redraw everything screen.Sync() // Redraw everything
case *tcell.EventKey: case *tcell.EventKey:
// On Escape, we change focus between editor and the MenuBar. // On Escape, we change focus between editor and the MenuBar.
if dialog == nil { if dialog == nil {

View File

@ -10,7 +10,7 @@ import (
// //
// Many components implement their own `New...()` function. In those constructor // Many components implement their own `New...()` function. In those constructor
// functions, it is good practice for that component to set its size to be its // functions, it is good practice for that component to set its size to be its
// minimum size. TODO: implement that behavior // minimum size.
type Component interface { type Component interface {
// A component knows its position and size, which is used to draw itself in // A component knows its position and size, which is used to draw itself in
// its bounding rectangle. // its bounding rectangle.