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
2 changes: 1 addition & 1 deletion cmd/readis/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/sethrylan/readis/internal/data"

"github.com/charmbracelet/lipgloss"
"charm.land/lipgloss/v2"
"github.com/dustin/go-humanize"
)

Expand Down
7 changes: 2 additions & 5 deletions cmd/readis/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/sethrylan/readis/internal/data"
"github.com/sethrylan/readis/internal/util"

tea "github.com/charmbracelet/bubbletea"
tea "charm.land/bubbletea/v2"
)

// ldflags added by goreleaser
Expand Down Expand Up @@ -58,10 +58,7 @@ func run() int {
fmt.Printf("invalid redis URI: %s\n", err)
return 1
}
p := tea.NewProgram(
newModel(d),
tea.WithAltScreen(), // use the full size of the terminal in the alternate screen buffer
)
p := tea.NewProgram(newModel(d))

if _, err := p.Run(); err != nil {
fmt.Printf("could not start program: %s\n", err)
Expand Down
73 changes: 42 additions & 31 deletions cmd/readis/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,14 @@ import (
"github.com/sethrylan/readis/internal/ui"
"github.com/sethrylan/readis/internal/util"

"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"charm.land/bubbles/v2/key"
"charm.land/bubbles/v2/list"
"charm.land/bubbles/v2/spinner"
"charm.land/bubbles/v2/textinput"
"charm.land/bubbles/v2/viewport"
tea "charm.land/bubbletea/v2"
"charm.land/glamour/v2"
"charm.land/lipgloss/v2"
)

// appCtx and appCancel manage the application lifecycle context.
Expand Down Expand Up @@ -100,14 +99,16 @@ func (m *model) resizeViews() {

viewportWidth := rightHandWidth + viewportStyle.GetHorizontalBorderSize()
viewportHeight := keylistHeight - headerHeight
m.viewport = viewport.New(viewportWidth, viewportHeight)
m.viewport = viewport.New()
m.viewport.SetWidth(viewportWidth)
m.viewport.SetHeight(viewportHeight)
m.viewport.Style = viewportStyle.Width(viewportWidth)
m.viewport.YPosition = headerHeight
}

func newModel(d *data.Data) *model {
m := &model{
hasDarkBg: termenv.HasDarkBackground(),
hasDarkBg: true, // default to dark; updated via tea.BackgroundColorMsg
}

km := ui.NewListKeyMap()
Expand All @@ -122,12 +123,14 @@ func newModel(d *data.Data) *model {
)

m.textinput = textinput.New()
m.textinput.Cursor.Style = cursorStyle
m.textinput.CharLimit = 80
m.textinput.Placeholder = "Pattern"
m.textinput.Focus()
m.textinput.PromptStyle = focusedStyle
m.textinput.TextStyle = focusedStyle
s := m.textinput.Styles()
s.Cursor.Color = lipgloss.Color("#c9510c")
s.Focused.Prompt = focusedStyle
s.Focused.Text = focusedStyle
m.textinput.SetStyles(s)

delegate := list.NewDefaultDelegate()
delegate.ShowDescription = false
Expand Down Expand Up @@ -159,7 +162,7 @@ func newModel(d *data.Data) *model {
}

func (m *model) Init() tea.Cmd {
return tea.Batch(textinput.Blink, m.spinner.Tick, m.refreshTotalKeys, tickTotalKeys())
return tea.Batch(m.spinner.Tick, m.refreshTotalKeys, tickTotalKeys(), tea.RequestBackgroundColor)
}

// startScan cancels any in-flight scan and starts a new one with a fresh context.
Expand Down Expand Up @@ -193,7 +196,10 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
return m, nil
case tea.KeyMsg:
case tea.BackgroundColorMsg:
m.hasDarkBg = msg.IsDark()
return m, nil
case tea.KeyPressMsg:
util.Debug("key pressed: ", msg.String())
switch msg.String() {
case "ctrl+c", "esc", "q":
Expand Down Expand Up @@ -249,8 +255,8 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

// Only pass user input to the textinput; filter terminal response garbage.
// Terminal color query responses (OSC sequences) can leak into the input
// stream as KeyRunes with Alt+non-letter or as multi-rune batches.
if keyMsg, ok := msg.(tea.KeyMsg); ok {
// stream as KeyPressMsg with Alt+non-letter or as multi-rune batches.
if keyMsg, ok := msg.(tea.KeyPressMsg); ok {
if isTextInput(keyMsg) {
m.textinput, cmd = m.textinput.Update(msg)
cmds = append(cmds, cmd)
Expand Down Expand Up @@ -293,31 +299,32 @@ func (m *model) spinnerView() string {

// isTextInput returns true if the key message is legitimate user input rather
// than terminal response garbage. Terminal OSC color responses leak into the
// input stream as KeyRunes with Alt+non-letter (from the ESC] / ESC\ framing)
// or as multi-rune batches (the "rgb:RRRR/GGGG/BBBB" payload).
func isTextInput(msg tea.KeyMsg) bool {
if msg.Type != tea.KeyRunes {
// input stream as key presses with Alt+non-letter (from the ESC] / ESC\ framing)
// or as multi-character batches (the "rgb:RRRR/GGGG/BBBB" payload).
func isTextInput(msg tea.KeyPressMsg) bool {
if len(msg.Text) == 0 {
return true
}
if msg.Alt && len(msg.Runes) == 1 && !unicode.IsLetter(msg.Runes[0]) {
if msg.Mod.Contains(tea.ModAlt) && len(msg.Text) == 1 && !unicode.IsLetter(rune(msg.Text[0])) {
return false
}
if !msg.Alt && len(msg.Runes) > 1 {
if !msg.Mod.Contains(tea.ModAlt) && len(msg.Text) > 1 {
return false
}
return true
}

func (m *model) headerView() string {
hBorder := headerStyle.GetHorizontalBorderSize()
inputBlock := headerStyle.
Width(leftHandWidth() - 6).
Width(leftHandWidth() - 6 + hBorder).
Align(lipgloss.Left).
Render(lipgloss.JoinVertical(lipgloss.Left,
m.textinput.View(),
m.spinnerView(),
))
statusBlock := headerStyle.
Width(rightHandWidth).
Width(rightHandWidth + hBorder).
Align(lipgloss.Right).
Render(lipgloss.JoinVertical(lipgloss.Right,
m.data.URI(),
Expand Down Expand Up @@ -351,7 +358,7 @@ func (m *model) fetchContent() tea.Cmd {

d := m.data
key := selectedKey.Key
width := m.viewport.Width
width := m.viewport.Width()
hasDarkBg := m.hasDarkBg

return func() tea.Msg {
Expand Down Expand Up @@ -380,15 +387,19 @@ func (m *model) fetchContent() tea.Cmd {
}
}

func (m *model) View() string {
func (m *model) View() tea.View {
if !m.initialized {
return "\n Initializing..."
v := tea.NewView("\n Initializing...")
v.AltScreen = true
return v
}

return docStyle.Render(
v := tea.NewView(docStyle.Render(
lipgloss.JoinVertical(lipgloss.Left,
m.headerView(),
m.resultsView(),
),
)
))
v.AltScreen = true
return v
}
11 changes: 7 additions & 4 deletions cmd/readis/styles.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package main

import "github.com/charmbracelet/lipgloss"
import (
"image/color"

"charm.land/lipgloss/v2"
)

var (
typeLabelWidth = 10 // max is "string"
Expand All @@ -12,7 +16,6 @@ var (

var (
focusedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#c9510c"))
cursorStyle = focusedStyle
docStyle = lipgloss.NewStyle().Margin(1, 2)
headerStyle = lipgloss.NewStyle().
Margin(0, 1, 1).
Expand All @@ -21,7 +24,7 @@ var (
Border(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color("#0a2b3b"))
viewportStyle = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
Border(lipgloss.ThickBorder()).
BorderForeground(lipgloss.Color("#6e5494")).
PaddingRight(2)
spinnerStyle = lipgloss.NewStyle().
Expand All @@ -33,7 +36,7 @@ func leftHandWidth() int {
return typeLabelWidth + keyNameWidth + ttlWidth + sizeWidth + 3
}

func colorForKeyType(keyType string) lipgloss.Color {
func colorForKeyType(keyType string) color.Color {
switch keyType {
case "hash":
return lipgloss.Color("#0000ff")
Expand Down
Binary file modified docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 13 additions & 19 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ module github.com/sethrylan/readis
go 1.26.0

require (
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/muesli/termenv v0.16.0
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
charm.land/glamour/v2 v2.0.0-20260226140904-e36ae5e1858e
charm.land/lipgloss/v2 v2.0.0
github.com/redis/go-redis/v9 v9.18.0
github.com/testcontainers/testcontainers-go/modules/redis v0.40.0
)
Expand All @@ -16,17 +17,17 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.9.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
Expand All @@ -39,7 +40,6 @@ require (
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
Expand All @@ -51,9 +51,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mdelapenya/tlscert v0.2.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
Expand All @@ -64,9 +62,7 @@ require (
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand All @@ -91,16 +87,14 @@ require (
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v0.10.0
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1
github.com/stretchr/testify v1.11.1
Expand Down
Loading