diff --git a/client/backend.go b/client/backend.go index 7bb3a42..c72d6cc 100644 --- a/client/backend.go +++ b/client/backend.go @@ -15,6 +15,7 @@ const ( statisticsPath = "/telemetry/statistics" crashPath = "/telemetry/crash" registrationPath = "/license/registration" + verificationPath = "/license/verification" jsonContentType = "application/json" ) @@ -44,8 +45,18 @@ func (c *BackendClient) ReportInstallation(statistics *metadata.Statistics) { } } -func (c *BackendClient) ReportUsageStatistics(error string, statistics *metadata.Statistics) { - // TODO +func (c *BackendClient) ReportStatistics(statistics *metadata.Statistics) { + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(statistics) + if err != nil { + c.ReportCrash(err.Error(), statistics) + } + + _, err = http.Post(backendUrl+statisticsPath, jsonContentType, buf) + if err != nil { + c.ReportCrash(err.Error(), statistics) + } } func (c *BackendClient) ReportCrash(error string, statistics *metadata.Statistics) { @@ -100,3 +111,35 @@ func (c *BackendClient) RegisterLicenseKey(licenseKey string, statistics *metada return &license, nil } + +func (c *BackendClient) VerifyLicenseKey(licenseKey string) (*metadata.License, error) { + + req := struct { + LicenseKey string + }{ + licenseKey, + } + + buf := new(bytes.Buffer) + err := json.NewEncoder(buf).Encode(req) + if err != nil { + c.ReportCrash(err.Error(), nil) + } + + response, err := http.Post( + backendUrl+verificationPath, jsonContentType, buf) + + if err != nil { + return nil, err + } + + if response.StatusCode != 200 { + body, _ := ioutil.ReadAll(response.Body) + return nil, errors.New(string(body)) + } + + var license metadata.License + json.NewDecoder(response.Body).Decode(&license) + + return &license, nil +} diff --git a/component/intro.go b/component/intro.go index 4e4d404..5210ffd 100644 --- a/component/intro.go +++ b/component/intro.go @@ -63,30 +63,21 @@ func NewIntro(palette console.Palette) *Intro { func (intro *Intro) Draw(buffer *ui.Buffer) { - logo := []string{ - " __ ", - " _________ ____ ___ ____ / /__ _____", - " / ___/ __ `/ __ `__ \\/ __ \\/ / _ \\/ ___/", - " (__ ) /_/ / / / / / / /_/ / / __/ / ", - "/____/\\__,_/_/ /_/ /_/ .___/_/\\___/_/ ", - " /_/ ", - } - - introText := append(logo, []string{ + introText := append(util.AsciiLogo, []string{ "", "", "", "Welcome.", "Sampler is free of charge for personal use, but license must be purchased to use it for business purposes.", - "Clicking below indicates you agree to the terms of the license agreement and privacy policy: www.sampler.dev/license", + "By proceeding, you agree to the terms of the license agreement and privacy policy: www.sampler.dev/license", "", "", "", "How do you plan to use Sampler?", }...) - commericalText := append(logo, []string{ + commericalText := append(util.AsciiLogo, []string{ "", "", "", "", "Please visit www.sampler.dev to purchase a license and then start Sampler with --license flag", }...) - personalText := append(logo, []string{ + personalText := append(util.AsciiLogo, []string{ "", "", "", "", "Sampler is always free for non-commercial use, but you can support the project and buy a personal license:", "www.sampler.dev", diff --git a/component/layout/layout.go b/component/layout/layout.go index 2535d97..7fff6f3 100644 --- a/component/layout/layout.go +++ b/component/layout/layout.go @@ -10,6 +10,7 @@ import ( "github.com/sqshq/sampler/data" "image" "math" + "time" ) type Layout struct { @@ -18,10 +19,12 @@ type Layout struct { statusbar *component.StatusBar menu *component.Menu intro *component.Intro + nag *component.NagWindow ChangeModeEvents chan Mode mode Mode selection int positionsChanged bool + startupTime time.Time } type Mode rune @@ -29,25 +32,28 @@ type Mode rune const ( ModeDefault Mode = 0 ModeIntro Mode = 1 - ModePause Mode = 2 - ModeComponentSelect Mode = 3 - ModeMenuOptionSelect Mode = 4 - ModeComponentMove Mode = 5 - ModeComponentResize Mode = 6 - ModeChartPinpoint Mode = 7 + ModeNag Mode = 2 + ModePause Mode = 3 + ModeComponentSelect Mode = 4 + ModeMenuOptionSelect Mode = 5 + ModeComponentMove Mode = 6 + ModeComponentResize Mode = 7 + ModeChartPinpoint Mode = 8 ) const ( - minDimension = 3 - statusbarHeight = 1 + minDimension = 3 + statusbarHeight = 1 + nagWindowDurationSec = 5 ) -func NewLayout(statusline *component.StatusBar, menu *component.Menu, intro *component.Intro) *Layout { +func NewLayout(statusline *component.StatusBar, menu *component.Menu, intro *component.Intro, nag *component.NagWindow) *Layout { width, height := ui.TerminalDimensions() block := *ui.NewBlock() block.SetRect(0, 0, width, height) intro.SetRect(0, 0, width, height) + nag.SetRect(0, 0, width, height) statusline.SetRect(0, height-statusbarHeight, width, height) return &Layout{ @@ -56,9 +62,11 @@ func NewLayout(statusline *component.StatusBar, menu *component.Menu, intro *com statusbar: statusline, menu: menu, intro: intro, + nag: nag, mode: ModeDefault, selection: 0, ChangeModeEvents: make(chan Mode, 10), + startupTime: time.Now(), } } @@ -66,10 +74,14 @@ func (l *Layout) AddComponent(cpt *component.Component) { l.Components = append(l.Components, cpt) } -func (l *Layout) RunIntro() { +func (l *Layout) StartWithIntro() { l.mode = ModeIntro } +func (l *Layout) StartWithNagWindow() { + l.mode = ModeNag +} + func (l *Layout) changeMode(m Mode) { if m == ModeComponentResize || m == ModeComponentMove { l.positionsChanged = true @@ -79,7 +91,7 @@ func (l *Layout) changeMode(m Mode) { } func (l *Layout) HandleMouseClick(x int, y int) { - if l.mode == ModeIntro { + if l.mode == ModeIntro || l.mode == ModeNag { return } l.menu.Idle() @@ -145,6 +157,8 @@ func (l *Layout) HandleKeyboardEvent(e string) { } else { l.changeMode(ModeDefault) } + case ModeNag: + l.nag.Accept() } case console.KeyEsc: l.resetAlerts() @@ -310,15 +324,28 @@ func (l *Layout) Draw(buffer *ui.Buffer) { columnWidth := float64(l.GetRect().Dx()) / float64(console.ColumnsCount) rowHeight := float64(l.GetRect().Dy()-statusbarHeight) / float64(console.RowsCount) + for _, c := range l.Components { + rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight) + c.SetRect(rectangle.Min.X, rectangle.Min.Y, rectangle.Max.X, rectangle.Max.Y) + } + if l.mode == ModeIntro { l.intro.SetRect(l.Min.X, l.Min.Y, l.Max.X, l.Max.Y) l.intro.Draw(buffer) return } + if l.mode == ModeNag { + if l.nag.IsAccepted() && time.Since(l.startupTime).Seconds() > nagWindowDurationSec { + l.mode = ModeDefault + } else { + l.nag.SetRect(l.Min.X, l.Min.Y, l.Max.X, l.Max.Y) + l.nag.Draw(buffer) + return + } + } + for _, c := range l.Components { - rectangle := calculateComponentCoordinates(c, columnWidth, rowHeight) - c.SetRect(rectangle.Min.X, rectangle.Min.Y, rectangle.Max.X, rectangle.Max.Y) c.Draw(buffer) } diff --git a/component/nag.go b/component/nag.go new file mode 100644 index 0000000..201c903 --- /dev/null +++ b/component/nag.go @@ -0,0 +1,53 @@ +package component + +import ( + ui "github.com/gizak/termui/v3" + "github.com/sqshq/sampler/component/util" + "github.com/sqshq/sampler/console" +) + +type NagWindow struct { + *ui.Block + palette console.Palette + accepted bool +} + +func NewNagWindow(palette console.Palette) *NagWindow { + return &NagWindow{ + Block: NewBlock("", false, palette), + palette: palette, + accepted: false, + } +} + +func (n *NagWindow) Accept() { + n.accepted = true +} + +func (n *NagWindow) IsAccepted() bool { + return n.accepted +} + +func (n *NagWindow) Draw(buffer *ui.Buffer) { + + text := append(util.AsciiLogo, []string{ + "", "", "", + "Thank you for using Sampler.", + "It is always free for non-commercial use, but you can support the project and buy a personal license.", + "", + "Please visit www.sampler.dev", + }...) + + for i, a := range text { + util.PrintString( + a, + ui.NewStyle(n.palette.BaseColor), + util.GetMiddlePoint(n.Block.Rectangle, a, i-15), + buffer) + } + + buffer.SetString(string(buttonOk), ui.NewStyle(n.palette.ReverseColor, n.palette.BaseColor), + util.GetMiddlePoint(n.Block.Rectangle, string(buttonOk), 4)) + + n.Block.Draw(buffer) +} diff --git a/component/util/format.go b/component/util/format.go index 09e58bd..6889bfd 100644 --- a/component/util/format.go +++ b/component/util/format.go @@ -9,6 +9,15 @@ import ( "strings" ) +var AsciiLogo = []string{ + " __ ", + " _________ ____ ___ ____ / /__ _____", + " / ___/ __ `/ __ `__ \\/ __ \\/ / _ \\/ ___/", + " (__ ) /_/ / / / / / / /_/ / / __/ / ", + "/____/\\__,_/_/ /_/ /_/ .___/_/\\___/_/ ", + " /_/ ", +} + func FormatValue(value float64, scale int) string { if math.Abs(value) == math.MaxFloat64 { return "Inf" diff --git a/example_int_shell.yml b/example_int_shell.yml deleted file mode 100644 index 2056fed..0000000 --- a/example_int_shell.yml +++ /dev/null @@ -1,33 +0,0 @@ -variables: - PGPASSWORD: fred - mongoconnection: mongo --quiet --host=localhost blog - mysqlconnection: mysql -u root -s --database mysql --skip-column-names - neo4jconnection: cypher-shell -u neo4j -p 121314 --format plain - postgresconnection: psql -h localhost -U postgres --no-align --tuples-only - sshconnection: ssh -i ~/sqshq.pem ec2-user@3.215.108.82 -textboxes: -- title: Neo4j - position: [[0, 0], [10, 40]] - pty: true - init: $neo4jconnection - sample: RETURN rand(); - transform: echo "$sample" | tail -n 1 -- title: Postgres - position: [[10, 0], [9, 40]] - init: $postgresconnection - sample: select random(); -- title: MySQL - position: [[19, 0], [10, 40]] - pty: true - init: $mysqlconnection - sample: select rand(); -- title: MongoDB - position: [[29, 0], [10, 40]] - rate-ms: 500 - init: $mongoconnection - sample: sleep(3000);Date.now(); -- title: SSH - position: [[39, 0], [41, 40]] - pty: true - init: $sshconnection - sample: top diff --git a/main.go b/main.go index f2a6d1c..2245814 100644 --- a/main.go +++ b/main.go @@ -89,18 +89,23 @@ func main() { defer player.Close() palette := console.GetPalette(*cfg.Theme) - lout := layout.NewLayout( - component.NewStatusLine(*opt.ConfigFile, palette, license), component.NewMenu(palette), component.NewIntro(palette)) + lout := layout.NewLayout(component.NewStatusLine(*opt.ConfigFile, palette, license), + component.NewMenu(palette), component.NewIntro(palette), component.NewNagWindow(palette)) if statistics.LaunchCount == 0 { if !opt.DisableTelemetry { go bc.ReportInstallation(statistics) } - lout.RunIntro() - } else /* with random */ { - // TODO if license == nil lout.showNagWindow() with timeout and OK button - // TODO if license != nil, verify license - // TODO report statistics + lout.StartWithIntro() + } else if statistics.LaunchCount%20 == 0 { // once in a while + if license == nil || !license.Valid { + lout.StartWithNagWindow() + } else { + go verifyLicense(license, bc) + } + if !opt.DisableTelemetry { + go bc.ReportStatistics(statistics) + } } metadata.PersistStatistics(cfg) @@ -130,3 +135,10 @@ func registerLicense(statistics *metadata.Statistics, opt config.Options, bc *cl console.Exit("License successfully verified, Sampler can be restarted without --license flag now. Thank you.") } } + +func verifyLicense(license *metadata.License, bc *client.BackendClient) { + verifiedLicense, _ := bc.VerifyLicenseKey(*license.Key) + if verifiedLicense != nil { + metadata.SaveLicense(*verifiedLicense) + } +}