Skip to content
Open
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
42 changes: 12 additions & 30 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,9 @@ type App struct {
currentTab int // 0=Data, 1=Columns, 2=Constraints, 3=Indexes

// Code editor for viewing/editing database object definitions
codeEditor *components.CodeEditor
showCodeEditor bool
isLoadingObjectDetails bool // Loading indicator for function/sequence/etc details
codeEditor *components.CodeEditor
showCodeEditor bool
isLoadingObjectDetails bool // Loading indicator for function/sequence/etc details

// Favorites
showFavorites bool
Expand Down Expand Up @@ -875,8 +875,8 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
"j": true, "k": true, "up": true, "down": true, // Scrolling
"g": true, "G": true, // Scroll to top/bottom
"ctrl+d": true, "ctrl+u": true, // Page scroll
"y": true, // Copy
"e": true, // Enter edit mode
"y": true, // Copy
"e": true, // Enter edit mode
"esc": true, // Close (q is reserved for quitting app)
}
key := msg.String()
Expand Down Expand Up @@ -2118,7 +2118,7 @@ func (a *App) renderNormalView() string {
// Width must account for border: lipgloss Width() sets content area,
// border chars are added outside, so subtract border width (2) to avoid overflow
topBar := lipgloss.NewStyle().
Width(a.state.Width - 2).
Width(a.state.Width-2).
Background(lipgloss.Color("#313244")).
Foreground(lipgloss.Color("#cdd6f4")).
Border(lipgloss.RoundedBorder()).
Expand Down Expand Up @@ -2214,7 +2214,7 @@ func (a *App) renderNormalView() string {
// Create modern bottom bar
// Width must account for border: subtract border width (2) to avoid overflow
bottomBar := lipgloss.NewStyle().
Width(a.state.Width - 2).
Width(a.state.Width-2).
Background(lipgloss.Color("#313244")).
Foreground(lipgloss.Color("#cdd6f4")).
Border(lipgloss.RoundedBorder()).
Expand Down Expand Up @@ -3160,17 +3160,7 @@ func (a *App) connectToHistoryEntry(entry models.ConnectionHistoryEntry) (tea.Mo

// connectToDiscoveredInstance connects using a discovered instance
func (a *App) connectToDiscoveredInstance(instance models.DiscoveredInstance) (tea.Model, tea.Cmd) {
// Create connection config from discovered instance
config := models.ConnectionConfig{
Host: instance.Host,
Port: instance.Port,
Database: "postgres", // Default database
User: os.Getenv("USER"), // Current user
Password: "", // No password for now
SSLMode: "prefer",
}

return a.performConnection(config)
return a.performConnection(discovery.BuildConnectionConfig(instance))
}

// performConnection starts an async connection attempt
Expand Down Expand Up @@ -3362,15 +3352,7 @@ func (a *App) handleConnectionDialog(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
return a, nil
}

// Create connection config from discovered instance
config = models.ConnectionConfig{
Host: instance.Host,
Port: instance.Port,
Database: "postgres",
User: os.Getenv("USER"),
Password: "",
SSLMode: "prefer",
}
config = discovery.BuildConnectionConfig(*instance)
}

return a.performConnection(config)
Expand Down Expand Up @@ -4501,9 +4483,9 @@ func (a *App) overlayLine(background, foreground string, startX int) string {

// SearchTableResultMsg is sent when table search completes
type SearchTableResultMsg struct {
Query string
Data *metadata.TableData
Err error
Query string
Data *metadata.TableData
Err error
}

// searchTable executes a table-wide search
Expand Down
4 changes: 2 additions & 2 deletions internal/app/delegates/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ func (d *ConnectionDelegate) handleConnectionResult(msg messages.ConnectionResul
if msg.Err != nil {
// Connection failed - clear pending password (don't save wrong password)
app.ClearPendingPasswordSave()
app.ShowError("Connection Failed", fmt.Sprintf("Could not connect to %s:%d\n\nError: %v",
msg.Config.Host, msg.Config.Port, msg.Err))
app.ShowError("Connection Failed", fmt.Sprintf("Could not connect to %s\n\nError: %v",
msg.Config.DisplayTarget(), msg.Err))
return true, nil
}

Expand Down
88 changes: 88 additions & 0 deletions internal/db/discovery/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package discovery

import (
"os"
"strings"

"github.com/rebelice/lazypg/internal/models"
)

// BuildConnectionConfig turns a discovered instance into a connection config.
func BuildConnectionConfig(instance models.DiscoveredInstance) models.ConnectionConfig {
switch instance.Source {
case models.SourceEnvironment:
if envConfig := GetEnvironmentConfig(); envConfig != nil && envConfig.Host == instance.Host && envConfig.Port == instance.Port {
return *envConfig
}
case models.SourcePgPass:
if pgpassConfig := buildPgPassConfig(instance.Host, instance.Port); pgpassConfig != nil {
return *pgpassConfig
}
}

return buildDefaultConfig(instance)
}

func buildPgPassConfig(host string, port int) *models.ConnectionConfig {
entries, err := ParsePgPass()
if err != nil {
return nil
}

for _, entry := range entries {
if entry.Host != host || entry.Port != port {
continue
}

user := entry.User
if user == "" || user == "*" {
user = defaultUser()
}

database := entry.Database
if database == "" || database == "*" {
database = defaultDatabase(user)
}

return &models.ConnectionConfig{
Host: host,
Port: port,
Database: database,
User: user,
Password: entry.Password,
SSLMode: "prefer",
}
}

return nil
}

func buildDefaultConfig(instance models.DiscoveredInstance) models.ConnectionConfig {
user := defaultUser()

return models.ConnectionConfig{
Host: instance.Host,
Port: instance.Port,
Database: defaultDatabase(user),
User: user,
SSLMode: "prefer",
}
}

func defaultUser() string {
for _, key := range []string{"PGUSER", "USER", "USERNAME"} {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
}

return "postgres"
}

func defaultDatabase(user string) string {
if user != "" {
return user
}

return "postgres"
}
18 changes: 15 additions & 3 deletions internal/db/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@ func (d *Discoverer) DiscoverAll(ctx context.Context) []models.DiscoveredInstanc
instances = append(instances, *envInstance)
}

// 2. Scan localhost ports
// 2. Scan common Unix socket directories
unixSocketInstances := d.scanner.ScanUnixSockets(ctx)
instances = append(instances, unixSocketInstances...)

// 3. Scan localhost ports
localInstances := d.scanner.ScanLocalhost(ctx)
instances = append(instances, localInstances...)

// 3. Parse .pgpass
// 4. Parse .pgpass
pgpassInstances := GetDiscoveredInstances()
instances = append(instances, pgpassInstances...)

Expand All @@ -42,7 +46,15 @@ func (d *Discoverer) DiscoverAll(ctx context.Context) []models.DiscoveredInstanc

// Sort by source priority
sort.Slice(instances, func(i, j int) bool {
return instances[i].Source < instances[j].Source
if instances[i].Source != instances[j].Source {
return instances[i].Source < instances[j].Source
}

if instances[i].Host != instances[j].Host {
return instances[i].Host < instances[j].Host
}

return instances[i].Port < instances[j].Port
})

return instances
Expand Down
161 changes: 161 additions & 0 deletions internal/db/discovery/unix_socket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package discovery

import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"

"github.com/rebelice/lazypg/internal/models"
)

var defaultUnixSocketDirs = []string{
"/var/run/postgresql",
"/run/postgresql",
"/tmp",
"/private/tmp",
"/var/pgsql_socket",
"/private/var/run/postgresql",
"/opt/homebrew/var/run/postgresql",
"/usr/local/var/run/postgresql",
}

// ScanUnixSockets scans common PostgreSQL socket directories.
func (s *Scanner) ScanUnixSockets(ctx context.Context) []models.DiscoveredInstance {
if runtime.GOOS == "windows" {
return nil
}

return s.ScanUnixSocketDirs(ctx, candidateUnixSocketDirs())
}

// ScanUnixSocketDirs scans the provided directories for PostgreSQL socket files.
func (s *Scanner) ScanUnixSocketDirs(ctx context.Context, dirs []string) []models.DiscoveredInstance {
if runtime.GOOS == "windows" {
return nil
}

instances := make([]models.DiscoveredInstance, 0)
seen := make(map[string]struct{})

for _, dir := range uniqueSocketDirs(dirs) {
if ctx.Err() != nil {
break
}

for _, instance := range s.scanUnixSocketDir(ctx, dir) {
key := instance.DisplayTarget()
if _, exists := seen[key]; exists {
continue
}

seen[key] = struct{}{}
instances = append(instances, instance)
}
}

return instances
}

func (s *Scanner) scanUnixSocketDir(ctx context.Context, dir string) []models.DiscoveredInstance {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}

instances := make([]models.DiscoveredInstance, 0)
for _, entry := range entries {
if ctx.Err() != nil {
break
}

port, ok := postgresSocketPort(entry.Name())
if !ok {
continue
}

instance := s.scanUnixSocket(ctx, dir, port)
if instance.Available {
instances = append(instances, instance)
}
}

return instances
}

func (s *Scanner) scanUnixSocket(ctx context.Context, dir string, port int) models.DiscoveredInstance {
instance := models.DiscoveredInstance{
Host: dir,
Port: port,
Source: models.SourceUnixSocket,
}

start := time.Now()
socketPath := filepath.Join(dir, fmt.Sprintf(".s.PGSQL.%d", port))

dialer := &net.Dialer{Timeout: s.timeout}
conn, err := dialer.DialContext(ctx, "unix", socketPath)
instance.ResponseTime = time.Since(start)
if err != nil {
return instance
}

_ = conn.Close()
instance.Available = true
return instance
}

func candidateUnixSocketDirs() []string {
dirs := make([]string, 0, len(defaultUnixSocketDirs)+1)

if host := strings.TrimSpace(os.Getenv("PGHOST")); strings.HasPrefix(host, "/") {
dirs = append(dirs, host)
}

dirs = append(dirs, defaultUnixSocketDirs...)
return dirs
}

func uniqueSocketDirs(dirs []string) []string {
unique := make([]string, 0, len(dirs))
seen := make(map[string]struct{})

for _, dir := range dirs {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}

if _, exists := seen[dir]; exists {
continue
}

seen[dir] = struct{}{}
unique = append(unique, dir)
}

return unique
}

func postgresSocketPort(name string) (int, bool) {
if !strings.HasPrefix(name, ".s.PGSQL.") {
return 0, false
}

portStr := strings.TrimPrefix(name, ".s.PGSQL.")
if portStr == "" || strings.Contains(portStr, ".") {
return 0, false
}

port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
return 0, false
}

return port, true
}
Loading