Repository: danvergara/dblab (go)

Issue:
[FEATURE] It might be a good idea to add a CONTRIBUTION file and a CODE_OF_CONDUCT
**Is your feature request related to a problem? Please describe.**
I'd like to contribute with the code of `dblab`, but I don't know very well `golang`

**Describe the solution you'd like**
Maybe could be a good idea to add a `CONTRIBUTING` file for new contributors to get some context of the project and for the environment setup, also could be a good idea to add a `CODE_OF_CONDUCT` to the repo, for the interaction between developers

**Describe alternatives you've considered**
I tried to run the code after I cloned, but I got an error `Error: empty values required to open a session in database`, 
I'm using `Goland`, then I noticed that I'm running the app without parameters so when I added `--help`, the error disappear



Pull request:
feat(form): add a package to use a little tui to ask for the connection params to create a connection with database
# run dblab with no flags or parameters

## Description

Right now, dblab can not be launched with no parameters. So, I include a little tui to ask for the connection data.

![dblab-default-form](https://user-images.githubusercontent.com/12239167/120042358-8aa2e980-bfcf-11eb-886d-ee97a1a87362.gif)

Fixes #38 (this problem is mentioned there)

## Type of change

Please delete options that are not relevant.

- [X] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [X] This change requires a documentation update

## How Has This Been Tested?
Build the binary locally and run it with no arguments. Added more tests to make sure these changes work as expected.

## Checklist:

- [X] My code follows the style guidelines of this project
- [X] I have performed a self-review of my own code
- [X] I have commented my code, particularly in hard-to-understand areas
- [X] I have made corresponding changes to the documentation
- [X] I have added tests that prove my fix is effective or that my feature works
- [X] New and existing unit tests pass locally with my changes
- [X] I have checked my code and corrected any misspellings


Patch:
diff --git a/Makefile b/Makefile
index 517541d..980c43e 100644
--- a/Makefile
+++ b/Makefile
@@ -56,6 +56,11 @@ up:
 down:
 	docker-compose down
 
+.PHONY: form
+## form: Runs the application with no arguments
+form: build
+	./dblab
+
 .PHONY: help
 ## help: Prints this help message
 help:
diff --git a/README.md b/README.md
index e422cfb..db4c4b7 100644
--- a/README.md
+++ b/README.md
@@ -55,6 +55,12 @@ Use "dblab [command] --help" for more information about a command.
 
 ## Usage
 
+You can start the app passing no flags or parameters, you'll be asked for connection data instead.
+
+<video controls="controls" autoplay loop style="width:70%">
+  <source type="video/webm" src="screenshots/dblab-default-form.webm"></source>
+</video>
+
 ```sh
 $ dblab --host localhost --user myuser --db users --pass password --ssl disable --port 5432 --driver postgres
 ```
diff --git a/cmd/root.go b/cmd/root.go
index 33d17b6..699ad6a 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -24,6 +24,7 @@ import (
 	"github.com/danvergara/dblab/pkg/client"
 	"github.com/danvergara/dblab/pkg/command"
 	"github.com/danvergara/dblab/pkg/connection"
+	"github.com/danvergara/dblab/pkg/form"
 	"github.com/danvergara/dblab/pkg/gui"
 	"github.com/jroimartin/gocui"
 	"github.com/spf13/cobra"
@@ -52,7 +53,10 @@ func NewRootCmd() *cobra.Command {
 		Short: "Interactive databse client",
 		Long:  `dblab is a terminal UI based interactive database client for Postgres, MySQL and SQLite.`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			opts := command.Options{
+			var opts command.Options
+			var err error
+
+			opts = command.Options{
 				Driver: driver,
 				URL:    url,
 				Host:   host,
@@ -63,6 +67,13 @@ func NewRootCmd() *cobra.Command {
 				SSL:    ssl,
 			}
 
+			if form.IsEmpty(opts) {
+				opts, err = form.Run()
+				if err != nil {
+					return err
+				}
+			}
+
 			if err := connection.ValidateOpts(opts); err != nil {
 				return err
 			}
diff --git a/go.mod b/go.mod
index d2e22e4..e2a375a 100644
--- a/go.mod
+++ b/go.mod
@@ -4,16 +4,21 @@ go 1.16
 
 require (
 	github.com/bxcodec/faker/v3 v3.6.0
+	github.com/charmbracelet/bubbles v0.7.9
+	github.com/charmbracelet/bubbletea v0.13.4
+	github.com/charmbracelet/lipgloss v0.2.1
 	github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2
 	github.com/fatih/color v1.10.0
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/golang-migrate/migrate/v4 v4.14.1
+	github.com/google/go-cmp v0.5.5
 	github.com/hashicorp/errwrap v1.1.0 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/jmoiron/sqlx v1.3.3
 	github.com/jroimartin/gocui v0.4.0
 	github.com/lib/pq v1.10.0
 	github.com/mitchellh/go-homedir v1.1.0
+	github.com/muesli/termenv v0.8.1
 	github.com/nsf/termbox-go v1.1.0 // indirect
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/spf13/cobra v1.1.3
diff --git a/go.sum b/go.sum
index c3964c2..f40bb1f 100644
--- a/go.sum
+++ b/go.sum
@@ -49,6 +49,8 @@ github.com/apache/arrow/go/arrow v0.0.0-20200601151325-b2287a20f230/go.mod h1:QN
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
+github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
 github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
@@ -62,6 +64,14 @@ github.com/bxcodec/faker/v3 v3.6.0/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfes
 github.com/cenkalti/backoff/v4 v4.0.2/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/charmbracelet/bubbles v0.7.9 h1:VaXFgxebUf7i+vhDCT72msF+5C+rj4uUkjik6/zgWLU=
+github.com/charmbracelet/bubbles v0.7.9/go.mod h1:5WX1sSSjNCgCrzvRMN/z23HxvWaa+AI16Ch0KPZPeDs=
+github.com/charmbracelet/bubbletea v0.13.1/go.mod h1:tp9tr9Dadh0PLhgiwchE5zZJXm5543JYjHG9oY+5qSg=
+github.com/charmbracelet/bubbletea v0.13.4 h1:IsUD1A9JQsmOkrWIsYhEG57voUc2rPwmomQyUwH2mkc=
+github.com/charmbracelet/bubbletea v0.13.4/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE=
+github.com/charmbracelet/lipgloss v0.1.2/go.mod h1:5D8zradw52m7QmxRF6QgwbwJi9je84g8MkWiGN07uKg=
+github.com/charmbracelet/lipgloss v0.2.1 h1:knjqLRtlcDsxss6i0CUhNLuXrNW3TQhFDXlXII2UaZA=
+github.com/charmbracelet/lipgloss v0.2.1/go.mod h1:uiZUfrHLQN14I0lJ5591WtcHyY1X76pOIPSaRKPY6dE=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -72,6 +82,8 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe
 github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
 github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2 h1:tjT4Jp4gxECvsJcYpAMtW2I3YqzBTPuB67OejxXs86s=
 github.com/common-nighthawk/go-figure v0.0.0-20200609044655-c4b36f998cf2/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
+github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc=
+github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
 github.com/containerd/containerd v1.4.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
 github.com/containerd/containerd v1.4.1 h1:pASeJT3R3YyVn+94qEPk0SnU1OQ20Jd/T+SPKy9xehY=
 github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -172,8 +184,11 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
 github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
 github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -290,6 +305,9 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E=
 github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/markbates/pkger v0.15.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
@@ -302,8 +320,10 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
+github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
@@ -325,6 +345,11 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
 github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
+github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
+github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
+github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk=
+github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
 github.com/mutecomm/go-sqlcipher/v4 v4.4.0/go.mod h1:PyN04SaWalavxRGH9E8ZftG6Ju7rsPrGmQRjrEaVpiY=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
@@ -365,6 +390,9 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
@@ -451,6 +479,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -575,9 +605,15 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201029080932-201ba4db2418 h1:HlFl4V6pEMziuLXyRkm5BIYq1y1GAbb02pRlWvI54OM=
+golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed h1:Ei4bQjjpYUsS4efOUz+5Nz++IVkHk87n2zBA0NxBWc0=
+golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -642,6 +678,7 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
diff --git a/pkg/form/form.go b/pkg/form/form.go
new file mode 100644
index 0000000..d52bd58
--- /dev/null
+++ b/pkg/form/form.go
@@ -0,0 +1,392 @@
+package form
+
+import (
+	"fmt"
+
+	"github.com/charmbracelet/bubbles/textinput"
+	tea "github.com/charmbracelet/bubbletea"
+	"github.com/charmbracelet/lipgloss"
+	"github.com/danvergara/dblab/pkg/command"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"github.com/muesli/termenv"
+)
+
+var (
+	focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+	noStyle      = lipgloss.NewStyle()
+	term         = termenv.ColorProfile()
+)
+
+// Model is a meta-model.
+type Model struct {
+	// menu management.
+	cursor int
+	steps  int
+
+	// driver.
+	drivers []string
+	driver  string
+
+	// std data.
+	hostInput     textinput.Model
+	portInput     textinput.Model
+	userInput     textinput.Model
+	passwordInput textinput.Model
+	databaseInput textinput.Model
+
+	// ssl.
+	modes []string
+	ssl   string
+}
+
+// Init initialize the meta-model.
+func (m *Model) Init() tea.Cmd {
+	return textinput.Blink
+}
+
+// Update update the view of the meta-model.
+func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	// Make sure these keys always quit
+	if msg, ok := msg.(tea.KeyMsg); ok {
+		k := msg.String()
+		if k == "esc" || k == "ctrl+c" {
+			return m, tea.Quit
+		}
+	}
+
+	switch m.steps {
+	case 0:
+		return updateDriver(msg, m)
+	case 1:
+		return updateStd(msg, m)
+	case 2:
+		return updateSSL(msg, m)
+	}
+
+	return m, tea.Quit
+}
+
+// View displays the content on the terminal.
+func (m *Model) View() string {
+	var s string
+
+	switch m.steps {
+	case 0:
+		s = driverView(m)
+	case 1:
+		s = standardView(m)
+	case 2:
+		s = sslView(m)
+	}
+
+	return fmt.Sprint(s)
+}
+
+// Host returns the host value.
+func (m *Model) Host() string {
+	return m.hostInput.Value()
+}
+
+// Port returns the Port value.
+func (m *Model) Port() string {
+	return m.portInput.Value()
+}
+
+// User returns the user value.
+func (m *Model) User() string {
+	return m.userInput.Value()
+}
+
+// Password returns the password value.
+func (m *Model) Password() string {
+	return m.passwordInput.Value()
+}
+
+// Database returns te database name value.
+func (m *Model) Database() string {
+	return m.databaseInput.Value()
+}
+
+// SSL returns te ssl name value.
+func (m *Model) SSL() string {
+	return m.ssl
+}
+
+func updateDriver(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	// Is it a key press?
+	case tea.KeyMsg:
+
+		switch msg.String() {
+		// the "up" and "k" keys mve the cursor up.
+		case "up", "k":
+			if m.cursor > 0 {
+				m.cursor--
+			}
+		// the "down" and "j" keys move the cursor down.
+		case "down", "j":
+			if m.cursor < len(m.drivers)-1 {
+				m.cursor++
+			}
+		case "enter":
+			driver := m.drivers[m.cursor]
+			m.driver = driver
+			m.cursor = 0
+			m.steps = 1
+			return m, nil
+		}
+	}
+
+	return m, nil
+}
+
+func updateStd(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
+	var cmd tea.Cmd
+
+	switch msg := msg.(type) {
+	case tea.KeyMsg:
+		switch msg.String() {
+		case "tab", "shift+tab", "enter", "up", "down":
+			inputs := []textinput.Model{
+				m.hostInput,
+				m.portInput,
+				m.userInput,
+				m.passwordInput,
+				m.databaseInput,
+			}
+
+			s := msg.String()
+
+			// If so, exit.
+			if s == "enter" && m.cursor == len(inputs)-1 {
+				m.steps = 2
+				m.cursor = 0
+				return m, nil
+			}
+
+			if s == "up" || s == "shift+tab" {
+				m.cursor--
+			} else {
+				m.cursor++
+			}
+
+			if m.cursor > len(inputs) {
+				m.cursor = 0
+			} else if m.cursor < 0 {
+				m.cursor = len(inputs)
+			}
+
+			for i := 0; i <= len(inputs)-1; i++ {
+				if i == m.cursor {
+					// Set focused state.
+					inputs[i].Focus()
+					inputs[i].PromptStyle = focusedStyle
+					inputs[i].TextStyle = focusedStyle
+					continue
+				}
+				// Remove focused state.
+				inputs[i].Blur()
+				inputs[i].PromptStyle = noStyle
+				inputs[i].TextStyle = noStyle
+			}
+
+			m.hostInput = inputs[0]
+			m.portInput = inputs[1]
+			m.userInput = inputs[2]
+			m.passwordInput = inputs[3]
+			m.databaseInput = inputs[4]
+
+			return m, nil
+		}
+	}
+
+	m, cmd = updateInputs(msg, m)
+	return m, cmd
+}
+
+func updateSSL(msg tea.Msg, m *Model) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	// Is it a key press?
+	case tea.KeyMsg:
+
+		switch msg.String() {
+		// These keys should exit the program.
+		// the "up" and "k" keys mve the cursor up.
+		case "up", "k":
+			if m.cursor > 0 {
+				m.cursor--
+			}
+		// the "down" and "j" keys move the cursor down.
+		case "down", "j":
+			if m.cursor < len(m.modes)-1 {
+				m.cursor++
+			}
+		case "enter":
+			m.ssl = m.modes[m.cursor]
+			m.steps = 3
+			m.cursor = 0
+			return m, tea.Quit
+		}
+	}
+
+	return m, nil
+}
+
+func updateInputs(msg tea.Msg, m *Model) (*Model, tea.Cmd) {
+	var (
+		cmd  tea.Cmd
+		cmds []tea.Cmd
+	)
+
+	m.hostInput, cmd = m.hostInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	m.portInput, cmd = m.portInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	m.userInput, cmd = m.userInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	m.passwordInput, cmd = m.passwordInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	m.databaseInput, cmd = m.databaseInput.Update(msg)
+	cmds = append(cmds, cmd)
+
+	return m, tea.Batch(cmds...)
+}
+
+func driverView(m *Model) string {
+	// The header.
+	s := "Select the database driver:"
+	var choices string
+	// Iterate over the driver.
+	for i, driver := range m.drivers {
+		choices += fmt.Sprintf("%s\n", checkbox(driver, m.cursor == i))
+	}
+
+	return fmt.Sprintf("%s\n\n%s", s, choices)
+}
+
+func standardView(m *Model) string {
+	s := "Introduce the connection params:\n\n"
+
+	inputs := []string{
+		m.hostInput.View(),
+		m.portInput.View(),
+		m.userInput.View(),
+		m.passwordInput.View(),
+		m.databaseInput.View(),
+	}
+
+	for i := 0; i < len(inputs); i++ {
+		s += inputs[i]
+		if i < len(inputs)-1 {
+			s += "\n"
+		}
+	}
+
+	s += "\n"
+	return s
+}
+
+func sslView(m *Model) string {
+	switch m.driver {
+	case "postgres":
+		m.modes = []string{"disable", "require", "verify-full"}
+	case "mysql":
+		m.modes = []string{"true", "false", "skip-verify", "preferred"}
+	default:
+		m.modes = []string{"disable", "require", "verify-full"}
+	}
+
+	// The header.
+	s := "\nSelect the ssl mode:"
+	var choices string
+	// Iterate over the driver.
+	for i, mode := range m.modes {
+		choices += fmt.Sprintf("%s\n", checkbox(mode, m.cursor == i))
+	}
+
+	return fmt.Sprintf("%s\n\n%s", s, choices)
+}
+
+func checkbox(label string, checked bool) string {
+	if checked {
+		return colorFg("[x] "+label, "212")
+	}
+	return fmt.Sprintf("[ ] %s", label)
+}
+
+// Color a string's foreground with the given value.
+func colorFg(val, color string) string {
+	return termenv.String(val).Foreground(term.Color(color)).String()
+}
+
+func initModel() Model {
+	host := textinput.NewModel()
+	host.Placeholder = "Host"
+	host.Focus()
+	host.PromptStyle = focusedStyle
+	host.TextStyle = focusedStyle
+	host.CharLimit = 200
+
+	port := textinput.NewModel()
+	port.Placeholder = "Port"
+	port.CharLimit = 200
+
+	user := textinput.NewModel()
+	user.Placeholder = "Username"
+	user.CharLimit = 200
+
+	password := textinput.NewModel()
+	password.Placeholder = "Password"
+	password.EchoMode = textinput.EchoPassword
+	password.EchoCharacter = '*'
+	password.CharLimit = 200
+
+	database := textinput.NewModel()
+	database.Placeholder = "Database"
+	database.CharLimit = 200
+
+	m := Model{
+		// the supported drivers by the client.
+		drivers: []string{"postgres", "mysql"},
+		// our default value.
+		driver: "postgres",
+
+		hostInput:     host,
+		portInput:     port,
+		userInput:     user,
+		passwordInput: password,
+		databaseInput: database,
+	}
+
+	return m
+}
+
+// Run runs the menus programs to introduced the required data to connect with a database.
+func Run() (command.Options, error) {
+	m := initModel()
+	if err := tea.NewProgram(&m).Start(); err != nil {
+		return command.Options{}, err
+	}
+
+	opts := command.Options{
+		Driver: m.driver,
+		Host:   m.Host(),
+		Port:   m.Port(),
+		User:   m.User(),
+		Pass:   m.Password(),
+		DBName: m.Database(),
+		SSL:    m.SSL(),
+	}
+
+	return opts, nil
+}
+
+// IsEmpty checks if the given options objects is empty.
+func IsEmpty(opts command.Options) bool {
+	return cmp.Equal(opts, command.Options{}, cmpopts.IgnoreFields(command.Options{}, "SSL"))
+}


Test patch:
diff --git a/pkg/form/form_test.go b/pkg/form/form_test.go
new file mode 100644
index 0000000..1c50df1
--- /dev/null
+++ b/pkg/form/form_test.go
@@ -0,0 +1,58 @@
+package form_test
+
+import (
+	"testing"
+
+	"github.com/danvergara/dblab/pkg/command"
+	"github.com/danvergara/dblab/pkg/form"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsEmpty(t *testing.T) {
+	var cases = []struct {
+		name   string
+		given  command.Options
+		wanted bool
+	}{
+		{
+			name:   "total empty",
+			given:  command.Options{},
+			wanted: true,
+		},
+		{
+			name: "ignoring ssl",
+			given: command.Options{
+				SSL: "disable",
+			},
+			wanted: true,
+		},
+		{
+			name: "not empty",
+			given: command.Options{
+				Driver: "postgres",
+				Host:   "localhost",
+				Port:   "5432",
+				User:   "user",
+				Pass:   "password",
+				DBName: "users",
+			},
+			wanted: false,
+		},
+		{
+			name: "not empty with url",
+			given: command.Options{
+				URL: "postgres://user:password@host:port/db?sslmode=mode",
+			},
+			wanted: false,
+		},
+	}
+
+	for _, tc := range cases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			actual := form.IsEmpty(tc.given)
+
+			assert.Equal(t, tc.wanted, actual)
+		})
+	}
+}