Skip to content
Draft
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
8 changes: 6 additions & 2 deletions internal/tui/views/browser/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ type option struct {
}

var options = []option{
{
name: "edit content",
prompt: prompt.EditContent,
},
{
name: "edit extension",
prompt: prompt.ChangeExtension,
Expand Down Expand Up @@ -81,8 +85,8 @@ func (bwsr Browser) getOptions() []option {

var opts []option
for _, o := range options {
if file.IsBinary() && o.prompt == prompt.ChangeExtension {
// don't allow changing extension for binary files
if file.IsBinary() && (o.prompt == prompt.ChangeExtension || o.prompt == prompt.EditContent) {
// don't allow changing extension or editing content for binary files
continue
}

Expand Down
28 changes: 28 additions & 0 deletions internal/tui/views/prompt/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,31 @@ func newKeyMap(submitted bool) keyMap {
),
}
}

type editContentKeyMap struct {
CtrlS key.Binding
Escape key.Binding
}

func (km editContentKeyMap) ShortHelp() []key.Binding {
return []key.Binding{km.CtrlS, km.Escape}
}

func (km editContentKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{km.CtrlS, km.Escape},
}
}

func newEditContentKeyMap() editContentKeyMap {
return editContentKeyMap{
CtrlS: key.NewBinding(
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "save"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
),
}
}
1 change: 1 addition & 0 deletions internal/tui/views/prompt/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ const (
ChangeVisibility
GenerateSignedURL
DeleteFile
EditContent
)
88 changes: 86 additions & 2 deletions internal/tui/views/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/armon/go-metrics"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
Expand All @@ -34,6 +35,7 @@ type Prompt struct {
file *snips.File
kind Kind
textInput textinput.Model
textarea textarea.Model
extensionSelector list.Model
feedback string
}
Expand All @@ -45,11 +47,19 @@ func New(ctx context.Context, cfg *config.Config, db db.DB, width int) Prompt {
ti.Width = 20
ti.Prompt = styles.BC(styles.Colors.Yellow, "> ")

ta := textarea.New()
ta.SetWidth(width - 4) // Leave some margin
ta.SetHeight(width / 3) // Reasonable default height based on window width
ta.CharLimit = 0 // No character limit for content editing
ta.Prompt = ""
ta.ShowLineNumbers = true

return Prompt{
ctx: ctx,
cfg: cfg,
db: db,
textInput: ti,
textarea: ta,
extensionSelector: NewExtensionSelector(width),
width: width,
}
Expand All @@ -67,8 +77,16 @@ func (p Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyEnter {
return p, p.handleSubmit()
if p.kind == EditContent {
// For content editing, use Ctrl+S to save
if msg.Type == tea.KeyCtrlS {
return p, p.handleSubmit()
}
} else {
// For other prompts, use Enter to submit
if msg.Type == tea.KeyEnter {
return p, p.handleSubmit()
}
}
case FeedbackMsg:
p.feedback = msg.Feedback
Expand All @@ -78,21 +96,38 @@ func (p Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if msg.Kind == ChangeExtension {
commands = append(commands, SelectorInitCmd)
}
if msg.Kind == EditContent && p.file != nil {
commands = append(commands, p.loadFileContentCmd())
}
case msgs.FileLoaded:
p.file = msg.File
if p.kind == EditContent {
commands = append(commands, p.loadFileContentCmd())
}
case msgs.PopView:
p.reset()
return p, nil
case tea.WindowSizeMsg:
p.width = msg.Width
p.extensionSelector.SetWidth(msg.Width)
p.textarea.SetWidth(msg.Width - 4) // Leave some margin
// Update textarea height to use most of the window height
if p.kind == EditContent {
p.textarea.SetHeight(msg.Height - 10) // Leave space for header and other elements
}
case SelectorInitMsg:
// bit of a hack to get the extension selector to filter on init
p.extensionSelector, cmd = p.extensionSelector.Update(tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{'/'},
})
return p, cmd
case loadContentMsg:
p.textarea.SetValue(msg.content)
p.textarea.Focus()
// Set cursor to the beginning of the file
p.textarea.CursorStart()
return p, nil
}

switch p.kind {
Expand All @@ -102,6 +137,9 @@ func (p Prompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case ChangeExtension:
p.extensionSelector, cmd = p.extensionSelector.Update(msg)
commands = append(commands, cmd)
case EditContent:
p.textarea, cmd = p.textarea.Update(msg)
commands = append(commands, cmd)
}
return p, tea.Batch(commands...)
}
Expand All @@ -115,11 +153,15 @@ func (p Prompt) View() string {
}

func (p Prompt) Keys() help.KeyMap {
if p.kind == EditContent {
return newEditContentKeyMap()
}
return newKeyMap(p.finished)
}

func (p *Prompt) reset() {
p.textInput.Reset()
p.textarea.Reset()
p.extensionSelector.ResetFilter()
p.extensionSelector.ResetSelected()
p.feedback = ""
Expand All @@ -143,6 +185,8 @@ func (p Prompt) renderPrompt() string {
question = fmt.Sprintf("How long do you want the signed url for %q to last for?\n%s", p.file.ID, styles.C(styles.Colors.Muted, "(e.g. 30s, 5m, 3h)"))
case DeleteFile:
question = fmt.Sprintf("Are you sure you want to delete %q?\nType the file ID to confirm.", p.file.ID)
case EditContent:
question = fmt.Sprintf("Edit content for %q:\n%s", p.file.ID, styles.C(styles.Colors.Muted, "(Press Ctrl+S to save, Esc to cancel)"))
}

question = lipgloss.NewStyle().
Expand All @@ -159,6 +203,8 @@ func (p Prompt) renderPrompt() string {
prompt = p.textInput.View()
case ChangeExtension:
prompt = p.extensionSelector.View()
case EditContent:
prompt = p.textarea.View()
}

pieces := []string{}
Expand Down Expand Up @@ -266,6 +312,25 @@ func (p Prompt) handleSubmit() tea.Cmd {

msg := styles.C(styles.Colors.Green, fmt.Sprintf("file %q deleted", p.file.ID))
commands = append(commands, cmds.ReloadFiles(p.db, p.file.UserID), SetPromptFeedbackCmd(msg, true))
case EditContent:
content := p.textarea.Value()

// Update the file content using SetContent to handle compression properly
if err := p.file.SetContent([]byte(content), p.cfg.FileCompression); err != nil {
return SetPromptErrorCmd(err)
}
p.file.Size = uint64(len(content))

err := p.db.UpdateFile(p.ctx, p.file)
if err != nil {
return SetPromptErrorCmd(err)
}

metrics.IncrCounter([]string{"file", "edit"}, 1)
log.Info().Str("file_id", p.file.ID).Int("new_size", len(content)).Msg("file content updated")

msg := styles.C(styles.Colors.Green, fmt.Sprintf("file %q content updated", p.file.ID))
commands = append(commands, cmds.ReloadFiles(p.db, p.file.UserID), SetPromptFeedbackCmd(msg, true))
default:
return nil
}
Expand Down Expand Up @@ -304,3 +369,22 @@ func (p Prompt) textInputYN() YNResult {
return Undecided
}
}

func (p Prompt) loadFileContentCmd() tea.Cmd {
return func() tea.Msg {
if p.file == nil {
return nil
}

content, err := p.file.GetContent()
if err != nil {
return nil
}

return loadContentMsg{content: string(content)}
}
}

type loadContentMsg struct {
content string
}