Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

## [Unreleased]

## [0.2.0] - 2026-05-22

### Added
- **Modernized UI**: Completely redesigned REPL interface with styles and better visual hierarchy.
- **Orca Banner**: Colorful ASCII orca banner with gradient styling on startup.
- **Report Issue link**: Clickable link in the status bar to report issues on GitHub and banner.
- **Loading Spinner**: Visual spinner indicator that displays during query execution.

### Fixed
- **Version update**: Updated version string.
- **fix interactive page**: fix the interactive page to show only form when width is lesser.

### Refactored
- **UI Component Architecture**: Extracted child components (Input, Status, Spinner) into separate modules for better maintainability.
- **Style Management**: Separated style definitions into dedicated source files for cleaner organization.

## [0.1.2] - 2026-05-20

### Added
Expand Down Expand Up @@ -39,4 +55,4 @@ The first release of pgxcli, a command-line interface for PostgreSQL inspired by
- configurable syntax highlighting
- Support for multiple table formats
- Linux packages: deb, rpm, apk, archlinux
- Windows MSI installer
- Windows MSI installer
31 changes: 21 additions & 10 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import (
"context"
"fmt"
"log/slog"
"os"
"strconv"
"strings"
"time"

tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"github.com/balaji01-4d/pgxspecial"
"github.com/balajz/pgxcli/internal/app/commands"
"github.com/balajz/pgxcli/internal/app/renderer"
"github.com/balajz/pgxcli/internal/app/ui"
"github.com/balajz/pgxcli/internal/cliio"
Expand All @@ -34,8 +35,8 @@ type Application interface {
Close() error
}

var builtinsCommand = map[string]func(){
"\\clear": commands.ClearScreen,
var builtinsCommand = map[string]func() tea.Cmd{
"\\clear": func() tea.Cmd { return tea.ClearScreen },
}

// pgxCLI is the main implementation of the Application interface.
Expand All @@ -47,15 +48,18 @@ type pgxCLI struct {
logger *slog.Logger
completer *completer.Completer
client *database.Client

version string
}

func New(cfg *config.Config, printer cliio.Printer, logger *slog.Logger, completer *completer.Completer, client *database.Client) (Application, error) {
func New(cfg *config.Config, printer cliio.Printer, logger *slog.Logger, completer *completer.Completer, client *database.Client, version string) (Application, error) {
return &pgxCLI{
config: cfg,
logger: logger,
Printer: printer,
completer: completer,
client: client,
version: version,
}, nil
}

Expand All @@ -69,8 +73,7 @@ func (p *pgxCLI) execute(ctx context.Context, query string) tea.Cmd {

if cmd, ok := builtinsCommand[query]; ok {
p.logger.Debug("executing builtin command", "command", query)
cmd()
return promptReady
return tea.Sequence(cmd(), promptReady)
}

return func() tea.Msg {
Expand All @@ -85,7 +88,9 @@ func (p *pgxCLI) execute(ctx context.Context, query string) tea.Cmd {
result, quit, err := p.handleSpecialCommand(ctx, metaResult, p.client)
if quit {
p.logger.Info("REPL exiting via quit command")
return ui.ExecCmdMsg{Cmd: tea.Quit}
return ui.ExecCmdMsg{Cmd: func() tea.Msg {
return ui.QuitRequestMsg{}
}}
}

if err != nil {
Expand Down Expand Up @@ -139,6 +144,7 @@ func (p *pgxCLI) execute(ctx context.Context, query string) tea.Cmd {
}

func (p *pgxCLI) Start(ctx context.Context) error {
p.printBanner(p.version)
executeFunc := func(query string) tea.Cmd {
return p.execute(ctx, query)
}
Expand All @@ -149,6 +155,7 @@ func (p *pgxCLI) Start(ctx context.Context) error {
p.completer.GetKeyWords(),
p.config.Main.HistoryFile,
string(p.config.Main.Style),
p.version,
executeFunc,
p.Cancel,
)
Expand Down Expand Up @@ -239,6 +246,10 @@ func (p *pgxCLI) Cancel(ctx context.Context) error {
return p.client.Cancel(ctx)
}

func (p *pgxCLI) printBanner(version string) {
lipgloss.Fprint(os.Stdout, ui.Banner(version)+"\n")
}

func (p *pgxCLI) handleQueryResult(r result.Result) (tea.Cmd, error) {
res, ok := r.(*result.QueryResult)
if !ok {
Expand Down Expand Up @@ -268,16 +279,16 @@ func (p *pgxCLI) printViaPager(str string) tea.Cmd {
if p.Printer.ShouldUsePager(str) {
cmd, ok := cliio.PagerCmd(str)
if !ok {
return ui.PrintCmd(str)
return ui.PrintCmd(str, ui.DefaultStyles().AppOutput)
}
return ui.ShowPagerCmd(cmd)
}

return ui.PrintCmd(str)
return ui.PrintCmd(str, ui.DefaultStyles().AppOutput)
}

func (p *pgxCLI) printError(err error) tea.Cmd {
return ui.PrintErrCmd(err)
return ui.PrintErrCmd(err, ui.DefaultStyles().ErrorOutput)
}

func (p *pgxCLI) Close() error {
Expand Down
13 changes: 0 additions & 13 deletions internal/app/commands/clear_screen.go

This file was deleted.

3 changes: 1 addition & 2 deletions internal/ui/ascii.txt → internal/app/ui/ascii.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

::: :::: .
:::: :::::::::::::
:::: :::: :::
Expand All @@ -12,4 +11,4 @@
:::-▆▆▆▆▆▆▆▆▆;::;;;;;;;:::▆▆▆▆
:::: ▆▆▆▆::::;;;;;::;
.::: ;:::::
;:::
;:::
128 changes: 128 additions & 0 deletions internal/app/ui/banner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package ui

import (
_ "embed"
"fmt"
"os"
"strings"

"charm.land/lipgloss/v2"
"github.com/charmbracelet/x/term"
)

var issueLink = "https://github.com/balajz/pgxcli/issues"

var (
primaryStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#8B5CF6")).
Bold(true)

secondaryStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#A78BFA"))

mutedStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#A1A1AA"))

accentStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#C4B5FD")).
Italic(true)

linkStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#7C3AED")).
Underline(true)
)

//go:embed ascii.txt
var asciiArt string

func gradientColor(t float64) (r, g, b int) {
type rgb = [3]float64
pine := rgb{62, 143, 176}
foam := rgb{156, 207, 216}
iris := rgb{196, 167, 231}

lerp := func(a, b, t float64) float64 { return a + t*(b-a) }

var c1, c2 rgb
var t2 float64
if t <= 0.5 {
c1, c2, t2 = pine, foam, t*2
} else {
c1, c2, t2 = foam, iris, (t-0.5)*2
}

return int(lerp(c1[0], c2[0], t2)),
int(lerp(c1[1], c2[1], t2)),
int(lerp(c1[2], c2[2], t2))
}

func orcaStr() string {
lines := strings.Split(asciiArt, "\n")
for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" {
lines = lines[1:]
}
for len(lines) > 0 && strings.TrimSpace(lines[len(lines)-1]) == "" {
lines = lines[:len(lines)-1]
}
for i, line := range lines {
lines[i] = strings.TrimRight(line, " ")
}

total := len(lines)
var sb strings.Builder

for i, line := range lines {
if i > 0 {
sb.WriteByte('\n')
}

t := 0.0
if total > 1 {
t = float64(i) / float64(total-1)
}
r, g, b := gradientColor(t)
hexColor := lipgloss.Color(fmt.Sprintf("#%02x%02x%02x", r, g, b))
style := lipgloss.NewStyle().Foreground(hexColor)

sb.WriteString(style.Render(line))
}

return sb.String()
}

func Banner(version string) string {
leftPane := orcaStr()

rightPane := lipgloss.JoinVertical(
lipgloss.Left,
secondaryStyle.Render("welcome to ")+
primaryStyle.Render("pgxcli ")+
mutedStyle.Render("v"+version)+"\n\n",

accentStyle.Render("Happy Postgresing!\n\n"),

linkStyle.
Hyperlink(issueLink).
Render("Report Issues ↗"),
)

content := lipgloss.JoinHorizontal(
lipgloss.Top,
leftPane,
" ", // gap between art and text
rightPane,
)

banner := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(lipgloss.Color("#8B5CF6")).
Padding(1, 4).
Render(content)

w, _, err := term.GetSize(os.Stdout.Fd())
if err == nil && w > 0 {
banner = lipgloss.Place(w, lipgloss.Height(banner), lipgloss.Center, lipgloss.Top, banner)
}

return banner
}
Loading
Loading