| Index: experimental/webtry/webtry.go
 | 
| diff --git a/experimental/webtry/webtry.go b/experimental/webtry/webtry.go
 | 
| index 1b678ecb778334dcc6757afd34ee6322eb59b202..16f6f4ff223051c8240d30cc3cd844979d835999 100644
 | 
| --- a/experimental/webtry/webtry.go
 | 
| +++ b/experimental/webtry/webtry.go
 | 
| @@ -13,6 +13,7 @@ import (
 | 
|  	htemplate "html/template"
 | 
|  	"io/ioutil"
 | 
|  	"log"
 | 
| +	"math/rand"
 | 
|  	"net/http"
 | 
|  	"os"
 | 
|  	"os/exec"
 | 
| @@ -45,9 +46,12 @@ var (
 | 
|  	// indexTemplate is the main index.html page we serve.
 | 
|  	indexTemplate *htemplate.Template = nil
 | 
|  
 | 
| -	// recentTemplate is a list of recent  images.
 | 
| +	// recentTemplate is a list of recent images.
 | 
|  	recentTemplate *htemplate.Template = nil
 | 
|  
 | 
| +	// workspaceTemplate is the page for workspaces, a series of webtrys.
 | 
| +	workspaceTemplate *htemplate.Template = nil
 | 
| +
 | 
|  	// db is the database, nil if we don't have an SQL database to store data into.
 | 
|  	db *sql.DB = nil
 | 
|  
 | 
| @@ -56,6 +60,35 @@ var (
 | 
|  
 | 
|  	// imageLink is the regex that matches URLs paths that are direct links to PNGs.
 | 
|  	imageLink = regexp.MustCompile("^/i/([a-f0-9]+.png)$")
 | 
| +
 | 
| +	// workspaceLink is the regex that matches URLs paths for workspaces.
 | 
| +	workspaceLink = regexp.MustCompile("^/w/([a-z0-9-]+)$")
 | 
| +
 | 
| +	// workspaceNameAdj is a list of adjectives for building workspace names.
 | 
| +	workspaceNameAdj = []string{
 | 
| +		"autumn", "hidden", "bitter", "misty", "silent", "empty", "dry", "dark",
 | 
| +		"summer", "icy", "delicate", "quiet", "white", "cool", "spring", "winter",
 | 
| +		"patient", "twilight", "dawn", "crimson", "wispy", "weathered", "blue",
 | 
| +		"billowing", "broken", "cold", "damp", "falling", "frosty", "green",
 | 
| +		"long", "late", "lingering", "bold", "little", "morning", "muddy", "old",
 | 
| +		"red", "rough", "still", "small", "sparkling", "throbbing", "shy",
 | 
| +		"wandering", "withered", "wild", "black", "young", "holy", "solitary",
 | 
| +		"fragrant", "aged", "snowy", "proud", "floral", "restless", "divine",
 | 
| +		"polished", "ancient", "purple", "lively", "nameless",
 | 
| +	}
 | 
| +
 | 
| +	// workspaceNameNoun is a list of nouns for building workspace names.
 | 
| +	workspaceNameNoun = []string{
 | 
| +		"waterfall", "river", "breeze", "moon", "rain", "wind", "sea", "morning",
 | 
| +		"snow", "lake", "sunset", "pine", "shadow", "leaf", "dawn", "glitter",
 | 
| +		"forest", "hill", "cloud", "meadow", "sun", "glade", "bird", "brook",
 | 
| +		"butterfly", "bush", "dew", "dust", "field", "fire", "flower", "firefly",
 | 
| +		"feather", "grass", "haze", "mountain", "night", "pond", "darkness",
 | 
| +		"snowflake", "silence", "sound", "sky", "shape", "surf", "thunder",
 | 
| +		"violet", "water", "wildflower", "wave", "water", "resonance", "sun",
 | 
| +		"wood", "dream", "cherry", "tree", "fog", "frost", "voice", "paper",
 | 
| +		"frog", "smoke", "star",
 | 
| +	}
 | 
|  )
 | 
|  
 | 
|  // flags
 | 
| @@ -90,12 +123,24 @@ func init() {
 | 
|  		panic(err)
 | 
|  	}
 | 
|  	// Convert index.html into a template, which is expanded with the code.
 | 
| -	indexTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/index.html"))
 | 
| +	indexTemplate, err = htemplate.ParseFiles(
 | 
| +		filepath.Join(cwd, "templates/index.html"),
 | 
| +		filepath.Join(cwd, "templates/titlebar.html"),
 | 
| +	)
 | 
|  	if err != nil {
 | 
|  		panic(err)
 | 
|  	}
 | 
| -
 | 
| -	recentTemplate, err = htemplate.ParseFiles(filepath.Join(cwd, "templates/recent.html"))
 | 
| +	recentTemplate, err = htemplate.ParseFiles(
 | 
| +		filepath.Join(cwd, "templates/recent.html"),
 | 
| +		filepath.Join(cwd, "templates/titlebar.html"),
 | 
| +	)
 | 
| +	if err != nil {
 | 
| +		panic(err)
 | 
| +	}
 | 
| +	workspaceTemplate, err = htemplate.ParseFiles(
 | 
| +		filepath.Join(cwd, "templates/workspace.html"),
 | 
| +		filepath.Join(cwd, "templates/titlebar.html"),
 | 
| +	)
 | 
|  	if err != nil {
 | 
|  		panic(err)
 | 
|  	}
 | 
| @@ -123,6 +168,7 @@ func init() {
 | 
|  			panic(err)
 | 
|  		}
 | 
|  	} else {
 | 
| +		log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
 | 
|  		// Fallback to sqlite for local use.
 | 
|  		db, err = sql.Open("sqlite3", "./webtry.db")
 | 
|  		if err != nil {
 | 
| @@ -135,8 +181,25 @@ func init() {
 | 
|               hash      CHAR(64)  DEFAULT ''                 NOT NULL,
 | 
|               PRIMARY KEY(hash)
 | 
|              )`
 | 
| -		db.Exec(sql)
 | 
| -		log.Printf("INFO: Failed to find metadata, unable to connect to MySQL server (Expected when running locally): %q\n", err)
 | 
| +		_, err = db.Exec(sql)
 | 
| +		log.Printf("Info: status creating sqlite table for webtry: %q\n", err)
 | 
| +		sql = `CREATE TABLE workspace (
 | 
| +          name      CHAR(64)  DEFAULT ''                 NOT NULL,
 | 
| +          create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
 | 
| +          PRIMARY KEY(name)
 | 
| +        )`
 | 
| +		_, err = db.Exec(sql)
 | 
| +		log.Printf("Info: status creating sqlite table for workspace: %q\n", err)
 | 
| +		sql = `CREATE TABLE workspacetry (
 | 
| +          name      CHAR(64)  DEFAULT ''                 NOT NULL,
 | 
| +          create_ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP  NOT NULL,
 | 
| +          hash      CHAR(64)  DEFAULT ''                 NOT NULL,
 | 
| +          hidden    INTEGER   DEFAULT 0                  NOT NULL,
 | 
| +
 | 
| +          FOREIGN KEY (name) REFERENCES workspace(name)
 | 
| +        )`
 | 
| +		_, err = db.Exec(sql)
 | 
| +		log.Printf("Info: status creating sqlite table for workspace try: %q\n", err)
 | 
|  	}
 | 
|  }
 | 
|  
 | 
| @@ -231,19 +294,28 @@ func reportError(w http.ResponseWriter, r *http.Request, err error, message stri
 | 
|  	w.Write(resp)
 | 
|  }
 | 
|  
 | 
| -func writeToDatabase(hash string, code string) {
 | 
| +func writeToDatabase(hash string, code string, workspaceName string) {
 | 
|  	if db == nil {
 | 
|  		return
 | 
|  	}
 | 
|  	if _, err := db.Exec("INSERT INTO webtry (code, hash) VALUES(?, ?)", code, hash); err != nil {
 | 
|  		log.Printf("ERROR: Failed to insert code into database: %q\n", err)
 | 
|  	}
 | 
| +	if workspaceName != "" {
 | 
| +		if _, err := db.Exec("INSERT INTO workspacetry (name, hash) VALUES(?, ?)", workspaceName, hash); err != nil {
 | 
| +			log.Printf("ERROR: Failed to insert into workspacetry table: %q\n", err)
 | 
| +		}
 | 
| +	}
 | 
|  }
 | 
|  
 | 
|  func cssHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  	http.ServeFile(w, r, "css/webtry.css")
 | 
|  }
 | 
|  
 | 
| +func jsHandler(w http.ResponseWriter, r *http.Request) {
 | 
| +	http.ServeFile(w, r, "js/run.js")
 | 
| +}
 | 
| +
 | 
|  // imageHandler serves up the PNG of a specific try.
 | 
|  func imageHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  	log.Printf("Image Handler: %q\n", r.URL.Path)
 | 
| @@ -294,6 +366,79 @@ func recentHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  	}
 | 
|  }
 | 
|  
 | 
| +type Workspace struct {
 | 
| +	Name  string
 | 
| +	Code  string
 | 
| +	Tries []Try
 | 
| +}
 | 
| +
 | 
| +// newWorkspace generates a new random workspace name and stores it in the database.
 | 
| +func newWorkspace() (string, error) {
 | 
| +	for i := 0; i < 10; i++ {
 | 
| +		adj := workspaceNameAdj[rand.Intn(len(workspaceNameAdj))]
 | 
| +		noun := workspaceNameNoun[rand.Intn(len(workspaceNameNoun))]
 | 
| +		suffix := rand.Intn(1000)
 | 
| +		name := fmt.Sprintf("%s-%s-%d", adj, noun, suffix)
 | 
| +		if _, err := db.Exec("INSERT INTO workspace (name) VALUES(?)", name); err == nil {
 | 
| +			return name, nil
 | 
| +		} else {
 | 
| +			log.Printf("ERROR: Failed to insert workspace into database: %q\n", err)
 | 
| +		}
 | 
| +	}
 | 
| +	return "", fmt.Errorf("Failed to create a new workspace")
 | 
| +}
 | 
| +
 | 
| +// getCode returns the code for a given hash, or the empty string if not found.
 | 
| +func getCode(hash string) string {
 | 
| +	code := ""
 | 
| +	if err := db.QueryRow("SELECT code FROM webtry WHERE hash=?", hash).Scan(&code); err != nil {
 | 
| +		log.Printf("ERROR: Code for hash is missing: %q\n", err)
 | 
| +	}
 | 
| +	return code
 | 
| +}
 | 
| +
 | 
| +func workspaceHandler(w http.ResponseWriter, r *http.Request) {
 | 
| +	log.Printf("Workspace Handler: %q\n", r.URL.Path)
 | 
| +	if r.Method == "GET" {
 | 
| +		tries := []Try{}
 | 
| +		match := workspaceLink.FindStringSubmatch(r.URL.Path)
 | 
| +		name := ""
 | 
| +		if len(match) == 2 {
 | 
| +			name = match[1]
 | 
| +			rows, err := db.Query("SELECT create_ts, hash FROM workspacetry WHERE name=? ORDER BY create_ts DESC ", name)
 | 
| +			if err != nil {
 | 
| +				reportError(w, r, err, "Failed to select.")
 | 
| +				return
 | 
| +			}
 | 
| +			for rows.Next() {
 | 
| +				var hash string
 | 
| +				var create_ts time.Time
 | 
| +				if err := rows.Scan(&create_ts, &hash); err != nil {
 | 
| +					log.Printf("Error: failed to fetch from database: %q", err)
 | 
| +					continue
 | 
| +				}
 | 
| +				tries = append(tries, Try{Hash: hash, CreateTS: create_ts.Format("2006-02-01")})
 | 
| +			}
 | 
| +		}
 | 
| +		var code string
 | 
| +		if len(tries) == 0 {
 | 
| +			code = DEFAULT_SAMPLE
 | 
| +		} else {
 | 
| +			code = getCode(tries[len(tries)-1].Hash)
 | 
| +		}
 | 
| +		if err := workspaceTemplate.Execute(w, Workspace{Tries: tries, Code: code, Name: name}); err != nil {
 | 
| +			log.Printf("ERROR: Failed to expand template: %q\n", err)
 | 
| +		}
 | 
| +	} else if r.Method == "POST" {
 | 
| +		name, err := newWorkspace()
 | 
| +		if err != nil {
 | 
| +			http.Error(w, "Failed to create a new workspace.", 500)
 | 
| +			return
 | 
| +		}
 | 
| +		http.Redirect(w, r, "/w/"+name, 302)
 | 
| +	}
 | 
| +}
 | 
| +
 | 
|  // hasPreProcessor returns true if any line in the code begins with a # char.
 | 
|  func hasPreProcessor(code string) bool {
 | 
|  	lines := strings.Split(code, "\n")
 | 
| @@ -305,6 +450,11 @@ func hasPreProcessor(code string) bool {
 | 
|  	return false
 | 
|  }
 | 
|  
 | 
| +type TryRequest struct {
 | 
| +	Code string `json:"code"`
 | 
| +	Name string `json:"name"`
 | 
| +}
 | 
| +
 | 
|  // mainHandler handles the GET and POST of the main page.
 | 
|  func mainHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  	log.Printf("Main Handler: %q\n", r.URL.Path)
 | 
| @@ -340,18 +490,22 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  			reportError(w, r, err, "Code too large.")
 | 
|  			return
 | 
|  		}
 | 
| -		code := string(buf.Bytes())
 | 
| -		if hasPreProcessor(code) {
 | 
| +		request := TryRequest{}
 | 
| +		if err := json.Unmarshal(buf.Bytes(), &request); err != nil {
 | 
| +			reportError(w, r, err, "Coulnd't decode JSON.")
 | 
| +			return
 | 
| +		}
 | 
| +		if hasPreProcessor(request.Code) {
 | 
|  			err := fmt.Errorf("Found preprocessor macro in code.")
 | 
|  			reportError(w, r, err, "Preprocessor macros aren't allowed.")
 | 
|  			return
 | 
|  		}
 | 
| -		hash, err := expandCode(LineNumbers(code))
 | 
| +		hash, err := expandCode(LineNumbers(request.Code))
 | 
|  		if err != nil {
 | 
|  			reportError(w, r, err, "Failed to write the code to compile.")
 | 
|  			return
 | 
|  		}
 | 
| -		writeToDatabase(hash, code)
 | 
| +		writeToDatabase(hash, request.Code, request.Name)
 | 
|  		message, err := doCmd(fmt.Sprintf(RESULT_COMPILE, hash, hash), true)
 | 
|  		if err != nil {
 | 
|  			reportError(w, r, err, "Failed to compile the code:\n"+message)
 | 
| @@ -403,8 +557,10 @@ func mainHandler(w http.ResponseWriter, r *http.Request) {
 | 
|  func main() {
 | 
|  	flag.Parse()
 | 
|  	http.HandleFunc("/i/", imageHandler)
 | 
| +	http.HandleFunc("/w/", workspaceHandler)
 | 
|  	http.HandleFunc("/recent/", recentHandler)
 | 
|  	http.HandleFunc("/css/", cssHandler)
 | 
| +	http.HandleFunc("/js/", jsHandler)
 | 
|  	http.HandleFunc("/", mainHandler)
 | 
|  	log.Fatal(http.ListenAndServe(*port, nil))
 | 
|  }
 | 
| 
 |