console.go raw

   1  package gui
   2  
   3  import (
   4  	"encoding/json"
   5  	"fmt"
   6  	"regexp"
   7  	"sort"
   8  	"strconv"
   9  	"strings"
  10  
  11  	"github.com/atotto/clipboard"
  12  	"golang.org/x/exp/shiny/materialdesign/icons"
  13  
  14  	l "github.com/p9c/p9/pkg/gel/gio/layout"
  15  	ctl2 "github.com/p9c/p9/cmd/ctl"
  16  
  17  	icons2 "golang.org/x/exp/shiny/materialdesign/icons"
  18  
  19  	"github.com/p9c/p9/pkg/gel"
  20  )
  21  
  22  type Console struct {
  23  	*gel.Window
  24  	output         []l.Widget
  25  	outputList     *gel.List
  26  	editor         *gel.Editor
  27  	clearClickable *gel.Clickable
  28  	clearButton    *gel.IconButton
  29  	copyClickable  *gel.Clickable
  30  	copyButton     *gel.IconButton
  31  	pasteClickable *gel.Clickable
  32  	pasteButton    *gel.IconButton
  33  	submitFunc     func(txt string)
  34  	clickables     []*gel.Clickable
  35  }
  36  
  37  var findSpaceRegexp = regexp.MustCompile(`\s+`)
  38  
  39  func (wg *WalletGUI) ConsolePage() *Console {
  40  	D.Ln("running ConsolePage")
  41  	c := &Console{
  42  		Window:         wg.Window,
  43  		editor:         wg.Editor().SingleLine().Submit(true),
  44  		clearClickable: wg.Clickable(),
  45  		copyClickable:  wg.Clickable(),
  46  		pasteClickable: wg.Clickable(),
  47  		outputList:     wg.List().ScrollToEnd(),
  48  	}
  49  	c.submitFunc = func(txt string) {
  50  		go func() {
  51  			D.Ln("submit", txt)
  52  			c.output = append(
  53  				c.output,
  54  				func(gtx l.Context) l.Dimensions {
  55  					return wg.VFlex().
  56  						Rigid(wg.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
  57  						Rigid(
  58  							wg.Flex().
  59  								Flexed(
  60  									1,
  61  									wg.Body2(txt).Color("DocText").Font("bariol bold").Fn,
  62  								).
  63  								Fn,
  64  						).Fn(gtx)
  65  				},
  66  			)
  67  			c.editor.SetText("")
  68  			split := strings.Split(txt, " ")
  69  			method, args := split[0], split[1:]
  70  			var params []interface{}
  71  			var e error
  72  			var result []byte
  73  			var o string
  74  			var errString, prev string
  75  			for i := range args {
  76  				params = append(params, args[i])
  77  			}
  78  			if method == "clear" || method == "cls" {
  79  				// clear the list of display widgets
  80  				c.output = c.output[:0]
  81  				// free up the pool widgets used in the current output
  82  				for i := range c.clickables {
  83  					wg.WidgetPool.FreeClickable(c.clickables[i])
  84  				}
  85  				c.clickables = c.clickables[:0]
  86  				return
  87  			}
  88  			if method == "help" {
  89  				if len(args) == 0 {
  90  					D.Ln("rpc called help")
  91  					var result1, result2 []byte
  92  					if result1, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) {
  93  					}
  94  					r1 := string(result1)
  95  					if r1, e = strconv.Unquote(r1); E.Chk(e) {
  96  					}
  97  					o = r1 + "\n"
  98  					if result2, e = ctl2.Call(wg.cx.Config, true, method, params...); E.Chk(e) {
  99  					}
 100  					r2 := string(result2)
 101  					if r2, e = strconv.Unquote(r2); E.Chk(e) {
 102  					}
 103  					o += r2 + "\n"
 104  					splitted := strings.Split(o, "\n")
 105  					sort.Strings(splitted)
 106  					var dedup []string
 107  					for i := range splitted {
 108  						if i > 0 {
 109  							if splitted[i] != prev {
 110  								dedup = append(dedup, splitted[i])
 111  							}
 112  						}
 113  						prev = splitted[i]
 114  					}
 115  					o = strings.Join(dedup, "\n")
 116  					if errString != "" {
 117  						o += "BTCJSONError:\n"
 118  						o += errString
 119  					}
 120  					splitResult := strings.Split(o, "\n")
 121  					const maxPerWidget = 6
 122  					for i := 0; i < len(splitResult)-maxPerWidget; i += maxPerWidget {
 123  						sri := strings.Join(splitResult[i:i+maxPerWidget], "\n")
 124  						c.output = append(
 125  							c.output,
 126  							func(gtx l.Context) l.Dimensions {
 127  								return wg.Flex().
 128  									Flexed(
 129  										1,
 130  										wg.Caption(sri).
 131  											Color("DocText").
 132  											Font("bariol regular").
 133  											MaxLines(maxPerWidget).Fn,
 134  									).
 135  									Fn(gtx)
 136  							},
 137  						)
 138  					}
 139  					return
 140  				} else {
 141  					var out string
 142  					var isErr bool
 143  					if result, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) {
 144  						isErr = true
 145  						out = e.Error()
 146  						I.Ln(out)
 147  						// if out, e = strconv.Unquote(); E.Chk(e) {
 148  						// }
 149  					} else {
 150  						if out, e = strconv.Unquote(string(result)); E.Chk(e) {
 151  						}
 152  					}
 153  					strings.ReplaceAll(out, "\t", "  ")
 154  					D.Ln(out)
 155  					splitResult := strings.Split(out, "\n")
 156  					outputColor := "DocText"
 157  					if isErr {
 158  						outputColor = "Danger"
 159  					}
 160  					for i := range splitResult {
 161  						sri := splitResult[i]
 162  						c.output = append(
 163  							c.output,
 164  							func(gtx l.Context) l.Dimensions {
 165  								return c.Theme.Flex().AlignStart().
 166  									Rigid(
 167  										wg.Body2(sri).
 168  											Color(outputColor).
 169  											Font("go regular").MaxLines(4).
 170  											Fn,
 171  									).
 172  									Fn(gtx)
 173  							},
 174  						)
 175  					}
 176  					return
 177  				}
 178  			} else {
 179  				D.Ln("method", method, "args", args)
 180  				if result, e = ctl2.Call(wg.cx.Config, false, method, params...); E.Chk(e) {
 181  					var errR string
 182  					if result, e = ctl2.Call(wg.cx.Config, true, method, params...); E.Chk(e) {
 183  						if e != nil {
 184  							errR = e.Error()
 185  						}
 186  						c.output = append(
 187  							c.output, c.Theme.Flex().AlignStart().
 188  								Rigid(wg.Body2(errR).Color("Danger").Fn).Fn,
 189  						)
 190  						return
 191  					}
 192  					if e != nil {
 193  						errR = e.Error()
 194  					}
 195  					c.output = append(
 196  						c.output, c.Theme.Flex().AlignStart().
 197  							Rigid(
 198  								wg.Body2(errR).Color("Danger").Fn,
 199  							).Fn,
 200  					)
 201  				}
 202  				c.output = append(c.output, wg.console.JSONWidget("DocText", result)...)
 203  			}
 204  			c.outputList.JumpToEnd()
 205  		}()
 206  	}
 207  	clearClickableFn := func() {
 208  		c.editor.SetText("")
 209  		c.editor.Focus()
 210  	}
 211  	copyClickableFn := func() {
 212  		go func() {
 213  			if e := clipboard.WriteAll(c.editor.Text()); E.Chk(e) {
 214  			}
 215  		}()
 216  		c.editor.Focus()
 217  	}
 218  	pasteClickableFn := func() {
 219  		// // col := c.editor.Caret.Col
 220  		// go func() {
 221  		// 	txt := c.editor.Text()
 222  		// 	var e error
 223  		// 	var cb string
 224  		// 	if cb, e = clipboard.ReadAll(); E.Chk(e) {
 225  		// 	}
 226  		// 	cb = findSpaceRegexp.ReplaceAllString(cb, " ")
 227  		// 	txt = txt[:col] + cb + txt[col:]
 228  		// 	c.editor.SetText(txt)
 229  		// 	c.editor.Move(col + len(cb))
 230  		// }()
 231  	}
 232  	c.clearButton = wg.IconButton(c.clearClickable.SetClick(clearClickableFn)).
 233  		Icon(
 234  			wg.Icon().
 235  				Color("DocText").
 236  				Src(&icons2.ContentBackspace),
 237  		).
 238  		Background("").
 239  		ButtonInset(0.25)
 240  	c.copyButton = wg.IconButton(c.copyClickable.SetClick(copyClickableFn)).
 241  		Icon(
 242  			wg.Icon().
 243  				Color("DocText").
 244  				Src(&icons2.ContentContentCopy),
 245  		).
 246  		Background("").
 247  		ButtonInset(0.25)
 248  	c.pasteButton = wg.IconButton(c.pasteClickable.SetClick(pasteClickableFn)).
 249  		Icon(
 250  			wg.Icon().
 251  				Color("DocText").
 252  				Src(&icons2.ContentContentPaste),
 253  		).
 254  		Background("").
 255  		ButtonInset(0.25)
 256  	c.output = append(
 257  		c.output, func(gtx l.Context) l.Dimensions {
 258  			return c.Theme.Flex().AlignStart().Rigid(c.H6("Welcome to the Parallelcoin RPC console").Color("DocText").Fn).Fn(gtx)
 259  		}, func(gtx l.Context) l.Dimensions {
 260  			return c.Theme.Flex().AlignStart().Rigid(c.Caption("Type 'help' to get available commands and 'clear' or 'cls' to clear the screen").Color("DocText").Fn).Fn(gtx)
 261  		},
 262  	)
 263  	return c
 264  }
 265  
 266  func (c *Console) Fn(gtx l.Context) l.Dimensions {
 267  	le := func(gtx l.Context, index int) l.Dimensions {
 268  		if index >= len(c.output) || index < 0 {
 269  			return l.Dimensions{}
 270  		} else {
 271  			return c.output[index](gtx)
 272  		}
 273  	}
 274  	fn := c.Theme.VFlex().
 275  		Flexed(
 276  			0.1,
 277  			c.Fill(
 278  				"PanelBg", l.Center, c.TextSize.V, 0, func(gtx l.Context) l.Dimensions {
 279  					return c.Inset(
 280  						0.25,
 281  						c.outputList.
 282  							ScrollToEnd().
 283  							End().
 284  							Background("PanelBg").
 285  							Color("DocBg").
 286  							Active("Primary").
 287  							Vertical().
 288  							Length(len(c.output)).
 289  							ListElement(le).
 290  							Fn,
 291  					).
 292  						Fn(gtx)
 293  				},
 294  			).Fn,
 295  		).
 296  		Rigid(
 297  			c.Fill(
 298  				"DocBg", l.Center, c.TextSize.V, 0, c.Inset(
 299  					0.25,
 300  					c.Theme.Flex().
 301  						Flexed(
 302  							1,
 303  							c.TextInput(c.editor.SetSubmit(c.submitFunc), "enter an rpc command").
 304  								Color("DocText").
 305  								Fn,
 306  						).
 307  						Rigid(c.copyButton.Fn).
 308  						Rigid(c.pasteButton.Fn).
 309  						Rigid(c.clearButton.Fn).
 310  						Fn,
 311  				).Fn,
 312  			).Fn,
 313  		).
 314  		Fn
 315  	return fn(gtx)
 316  }
 317  
 318  type JSONElement struct {
 319  	key   string
 320  	value interface{}
 321  }
 322  
 323  type JSONElements []JSONElement
 324  
 325  func (je JSONElements) Len() int {
 326  	return len(je)
 327  }
 328  
 329  func (je JSONElements) Less(i, j int) bool {
 330  	return je[i].key < je[j].key
 331  }
 332  
 333  func (je JSONElements) Swap(i, j int) {
 334  	je[i], je[j] = je[j], je[i]
 335  }
 336  
 337  func GetJSONElements(in map[string]interface{}) (je JSONElements) {
 338  	for i := range in {
 339  		je = append(
 340  			je, JSONElement{
 341  				key:   i,
 342  				value: in[i],
 343  			},
 344  		)
 345  	}
 346  	sort.Sort(je)
 347  	return
 348  }
 349  
 350  func (c *Console) getIndent(n int, size float32, widget l.Widget) (out l.Widget) {
 351  	o := c.Theme.Flex()
 352  	for i := 0; i < n; i++ {
 353  		o.Rigid(c.Inset(size/2, gel.EmptySpace(0, 0)).Fn)
 354  	}
 355  	o.Rigid(widget)
 356  	out = o.Fn
 357  	return
 358  }
 359  
 360  func (c *Console) JSONWidget(color string, j []byte) (out []l.Widget) {
 361  	var ifc interface{}
 362  	var e error
 363  	if e = json.Unmarshal(j, &ifc); E.Chk(e) {
 364  	}
 365  	return c.jsonWidget(color, 0, "", ifc)
 366  }
 367  
 368  func (c *Console) jsonWidget(color string, depth int, key string, in interface{}) (out []l.Widget) {
 369  	switch in.(type) {
 370  	case []interface{}:
 371  		if key != "" {
 372  			out = append(
 373  				out, c.getIndent(
 374  					depth, 1,
 375  					func(gtx l.Context) l.Dimensions {
 376  						return c.Body2(key).Font("bariol bold").Color(color).Fn(gtx)
 377  					},
 378  				),
 379  			)
 380  		}
 381  		D.Ln("got type []interface{}")
 382  		res := in.([]interface{})
 383  		if len(res) == 0 {
 384  			out = append(
 385  				out, c.getIndent(
 386  					depth+1, 1,
 387  					func(gtx l.Context) l.Dimensions {
 388  						return c.Body2("[]").Color(color).Fn(gtx)
 389  					},
 390  				),
 391  			)
 392  		} else {
 393  			for i := range res {
 394  				// D.S(res[i])
 395  				out = append(out, c.jsonWidget(color, depth+1, fmt.Sprint(i), res[i])...)
 396  			}
 397  		}
 398  	case map[string]interface{}:
 399  		if key != "" {
 400  			out = append(
 401  				out, c.getIndent(
 402  					depth, 1,
 403  					func(gtx l.Context) l.Dimensions {
 404  						return c.Body2(key).Font("bariol bold").Color(color).Fn(gtx)
 405  					},
 406  				),
 407  			)
 408  		}
 409  		D.Ln("got type map[string]interface{}")
 410  		res := in.(map[string]interface{})
 411  		je := GetJSONElements(res)
 412  		// D.S(je)
 413  		if len(res) == 0 {
 414  			out = append(
 415  				out, c.getIndent(
 416  					depth+1, 1,
 417  					func(gtx l.Context) l.Dimensions {
 418  						return c.Body2("{}").Color(color).Fn(gtx)
 419  					},
 420  				),
 421  			)
 422  		} else {
 423  			for i := range je {
 424  				D.S(je[i])
 425  				out = append(out, c.jsonWidget(color, depth+1, je[i].key, je[i].value)...)
 426  			}
 427  		}
 428  	case JSONElement:
 429  		res := in.(JSONElement)
 430  		key = res.key
 431  		switch res.value.(type) {
 432  		case string:
 433  			D.Ln("got type string")
 434  			res := res.value.(string)
 435  			clk := c.Theme.WidgetPool.GetClickable()
 436  			out = append(
 437  				out,
 438  				c.jsonElement(
 439  					key, color, depth, func(gtx l.Context) l.Dimensions {
 440  						return c.Theme.Flex().
 441  							Rigid(c.Body2("\"" + res + "\"").Color(color).Fn).
 442  							Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
 443  							Rigid(
 444  								c.IconButton(clk).
 445  									Background("").
 446  									ButtonInset(0).
 447  									Color(color).
 448  									Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)).
 449  									SetClick(
 450  										func() {
 451  											go func() {
 452  												if e := clipboard.WriteAll(res); E.Chk(e) {
 453  												}
 454  											}()
 455  										},
 456  									).Fn,
 457  							).Fn(gtx)
 458  					},
 459  				),
 460  			)
 461  		case float64:
 462  			D.Ln("got type float64")
 463  			res := res.value.(float64)
 464  			clk := c.Theme.WidgetPool.GetClickable()
 465  			out = append(
 466  				out,
 467  				c.jsonElement(
 468  					key, color, depth, func(gtx l.Context) l.Dimensions {
 469  						return c.Theme.Flex().
 470  							Rigid(c.Body2(fmt.Sprint(res)).Color(color).Fn).
 471  							Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
 472  							Rigid(
 473  								c.IconButton(clk).
 474  									Background("").
 475  									ButtonInset(0).
 476  									Color(color).
 477  									Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)).
 478  									SetClick(
 479  										func() {
 480  											go func() {
 481  												if e := clipboard.WriteAll(fmt.Sprint(res)); E.Chk(e) {
 482  												}
 483  											}()
 484  										},
 485  									).Fn,
 486  							).Fn(gtx)
 487  						// return c.th.ButtonLayout(clk).Embed(c.th.Body2().Color(color).Fn).Fn(gtx)
 488  					},
 489  				),
 490  			)
 491  		case bool:
 492  			D.Ln("got type bool")
 493  			res := res.value.(bool)
 494  			out = append(
 495  				out,
 496  				c.jsonElement(
 497  					key, color, depth, func(gtx l.Context) l.Dimensions {
 498  						return c.Body2(fmt.Sprint(res)).Color(color).Fn(gtx)
 499  					},
 500  				),
 501  			)
 502  		}
 503  	case string:
 504  		D.Ln("got type string")
 505  		res := in.(string)
 506  		clk := c.Theme.WidgetPool.GetClickable()
 507  		out = append(
 508  			out,
 509  			c.jsonElement(
 510  				key, color, depth, func(gtx l.Context) l.Dimensions {
 511  					return c.Theme.Flex().
 512  						Rigid(c.Body2("\"" + res + "\"").Color(color).Fn).
 513  						Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
 514  						Rigid(
 515  							c.IconButton(clk).
 516  								Background("").
 517  								ButtonInset(0).
 518  								Color(color).
 519  								Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)).
 520  								SetClick(
 521  									func() {
 522  										go func() {
 523  											if e := clipboard.WriteAll(res); E.Chk(e) {
 524  											}
 525  										}()
 526  									},
 527  								).Fn,
 528  						).Fn(gtx)
 529  				},
 530  			),
 531  		)
 532  	case float64:
 533  		D.Ln("got type float64")
 534  		res := in.(float64)
 535  		clk := c.Theme.WidgetPool.GetClickable()
 536  		out = append(
 537  			out,
 538  			c.jsonElement(
 539  				key, color, depth, func(gtx l.Context) l.Dimensions {
 540  					return c.Theme.Flex().
 541  						Rigid(c.Body2(fmt.Sprint(res)).Color(color).Fn).
 542  						Rigid(c.Inset(0.25, gel.EmptySpace(0, 0)).Fn).
 543  						Rigid(
 544  							c.IconButton(clk).
 545  								Background("").
 546  								ButtonInset(0).
 547  								Color(color).
 548  								Icon(c.Icon().Color("DocBg").Scale(1).Src(&icons.ContentContentCopy)).
 549  								SetClick(
 550  									func() {
 551  										go func() {
 552  											if e := clipboard.WriteAll(fmt.Sprint(res)); E.Chk(e) {
 553  											}
 554  										}()
 555  									},
 556  								).Fn,
 557  						).Fn(gtx)
 558  					// return c.th.ButtonLayout(clk).Embed(c.th.Body2(fmt.Sprint(res)).Color(color).Fn).Fn(gtx)
 559  				},
 560  			),
 561  		)
 562  	case bool:
 563  		D.Ln("got type bool")
 564  		res := in.(bool)
 565  		out = append(
 566  			out,
 567  			c.jsonElement(
 568  				key, color, depth, func(gtx l.Context) l.Dimensions {
 569  					return c.Body2(fmt.Sprint(res)).Color(color).Fn(gtx)
 570  				},
 571  			),
 572  		)
 573  	default:
 574  		D.S(in)
 575  	}
 576  	return
 577  }
 578  
 579  func (c *Console) jsonElement(key, color string, depth int, w l.Widget) l.Widget {
 580  	return func(gtx l.Context) l.Dimensions {
 581  		return c.Theme.Flex().
 582  			Rigid(
 583  				c.getIndent(
 584  					depth, 1,
 585  					c.Body2(key).Font("bariol bold").Color(color).Fn,
 586  				),
 587  			).
 588  			Rigid(c.Inset(0.125, gel.EmptySpace(0, 0)).Fn).
 589  			Rigid(w).
 590  			Fn(gtx)
 591  	}
 592  }
 593