/* AMU: Custom simple markup language Copyright (C) 2021 Arsen Musayelyan This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( //"fmt" "encoding/json" "fmt" "io/ioutil" "os" "runtime" "strings" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" sourceview "github.com/linuxerwang/sourceview3" "github.com/pkg/browser" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/sourcegraph/go-webkit2/webkit2" "go.arsenm.dev/amu/ast" "go.arsenm.dev/amu/formatter/html" "go.arsenm.dev/amu/parser" ) func init() { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } var head = ` ` const document = ` %s %s ` var openedFile string func main() { // Lock goroutine to current thread for Gtk runtime.LockOSThread() // Initialize Gtk gtk.Init(nil) // Create new Gtk window window, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) if err != nil { log.Fatal().Err(err).Msg("Unable to create window") } // Set window title window.SetTitle("AMULive") // Stop Gtk main loop when window destroyed window.Connect("destroy", gtk.MainQuit) // Create new horizontal box layout with 6px padding layout, err := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 6) if err != nil { log.Fatal().Err(err).Msg("Unable to create box") } // Add layout to window window.Add(layout) // Create new scrolled winfow textScrollWindow, err := gtk.ScrolledWindowNew(nil, nil) if err != nil { log.Fatal().Err(err).Msg("Unable to create scrolled window") } // Create new source view srcView, err := sourceview.SourceViewNew() if err != nil { log.Fatal().Err(err).Msg("Unable to create text virw") } // Set tab width to 4 srcView.SetProperty("tab-width", uint(4)) // Set auto indent to true srcView.SetProperty("auto-indent", true) // Set show line numbers to true srcView.SetShowLineNumbers(true) // Set left margin to 8 (separates text from line numbers) srcView.SetLeftMargin(8) // Set monospace to true srcView.SetMonospace(true) // Set wrap mode to wrap word char (3) srcView.SetWrapMode(gtk.WRAP_WORD_CHAR) // Add text to scrolled window textScrollWindow.Add(srcView) // Add scrolled window to layout with no padding layout.PackStart(textScrollWindow, true, true, 0) // Get source view style context styleCtx, err := srcView.GetStyleContext() if err != nil { log.Fatal().Err(err).Msg("Unable to get style context of text view") } // Get style background color bgCol, err := styleCtx.GetProperty("background-color", gtk.STATE_FLAG_NORMAL) if err != nil { log.Fatal().Err(err).Msg("Unable to get background-color property of style context") } // Get style foreground color fgCol := styleCtx.GetColor(gtk.STATE_FLAG_NORMAL) // Expand head tag template head = fmt.Sprintf(head, fgCol, bgCol) // Create new webview webkit := webkit2.NewWebView() // Enable devtools in webview webkit.Settings().SetProperty("enable-developer-extras", true) // Load empty base HTML document webkit.LoadHTML(fmt.Sprintf(document, head, ""), "amu") // Add webview to layout with no padding layout.PackStart(webkit, true, true, 0) // Get source view buffer srcBuf, err := srcView.GetBuffer() if err != nil { log.Fatal().Err(err).Msg("Error getting buffer from text view") } // On source view change srcBuf.Connect("changed", func() { loadAMU(srcBuf, webkit) }) // On load change webkit.Connect("load-changed", func(_ *glib.Object, le webkit2.LoadEvent) { switch le { case webkit2.LoadStarted, webkit2.LoadCommitted, webkit2.LoadRedirected: // Get current webview URL curURL := webkit.URI() // If url is not "amu" if curURL != "amu" { // Stop loading webkit.RunJavaScript("window.stop();", nil) // Open URL in browser concurrently go browser.OpenURL(curURL) // Load base HTML webkit.LoadHTML(fmt.Sprintf(document, head, ""), "amu") } case webkit2.LoadFinished: // If URL is "amu" if webkit.URI() == "amu" { // Load AMU from source buffer loadAMU(srcBuf, webkit) } } }) // Create new accelerator group accelGroup, err := NewAccel() if err != nil { log.Fatal().Err(err).Msg("Error creating accelerator group") } // Set Ctrl+P to print via JavaScript accelGroup.Add("p", func() { webkit.RunJavaScript("window.print();", nil) }) // Set Ctrl+S to save file accelGroup.Add("s", func() { err := saveFile(window, srcBuf) if err != nil { log.Error().Err(err).Msg("Error saving file") } }) // Set Ctrl+O to open file accelGroup.Add("o", func() { err := openFile(window, srcBuf) if err != nil { log.Error().Err(err).Msg("Error opening file") } }) // Add underlying gtk accelerator group to window window.AddAccelGroup(accelGroup.AccelGroup) // On window delete event window.Connect("delete-event", func() bool { // If source buffer not modified if !srcBuf.GetModified() { // Close window return false } // Create confirmation dialog dlg := gtk.MessageDialogNew( window, gtk.DIALOG_MODAL, gtk.MESSAGE_WARNING, gtk.BUTTONS_YES_NO, "Are you sure you want to close?\nYou have unsaved changes.", ) // Run confirmation dialog and get response respType := dlg.Run() dlg.Close() switch respType { case gtk.RESPONSE_YES: return false case gtk.RESPONSE_NO: return true } return true }) if len(os.Args) > 1 { openedFile = os.Args[1] data, err := ioutil.ReadFile(openedFile) if err != nil { log.Fatal().Err(err).Msg("Error opening start file") } srcBuf.SetText(string(data)) srcBuf.SetModified(false) } window.SetDefaultSize(800, 600) window.ShowAll() gtk.Main() } func loadAMU(srcBuf *sourceview.SourceBuffer, webkit *webkit2.WebView) { // Get all text in buffer src, err := srcBuf.GetText(srcBuf.GetStartIter(), srcBuf.GetEndIter(), true) if err != nil { log.Error().Err(err).Msg("Error getting amu source from text view") return } p := parser.New(strings.NewReader(src)) AST, err := p.Parse() if err != nil { log.Error().Err(err).Msg("Error parsing amu source") return } formatter := html.NewFormatter(AST, ast.FuncMap{}) // Execute source from buffer html := formatter.Format() // Generate full HTML document and encode as JSON for JavaScript data, err := json.Marshal(html) if err != nil { log.Error().Err(err).Msg("Error marshaling string as JSON") return } // Update webview document webkit.RunJavaScript(fmt.Sprintf(`document.body.innerHTML = %s; renderMathInElement(document.body);`, data), nil) }