add filename input dialog box

This commit is contained in:
2026-01-04 03:54:56 +00:00
parent dba74150b7
commit 8ff8b48631
4 changed files with 132 additions and 60 deletions
+37 -12
View File
@@ -50,6 +50,23 @@ type nodeSelected struct {
path string path string
} }
type FsActionType int
const (
ActionNewFile FsActionType = iota
ActionNewFolder
ActionNewRoot
)
type RequestInputMsg struct {
Action FsActionType
}
type PerformActionMsg struct {
Action FsActionType
Name string
}
// ==================== FsNode definition ==================== // ==================== FsNode definition ====================
type FsTree struct { type FsTree struct {
root *FsNode root *FsNode
@@ -72,6 +89,11 @@ func (t *FsTree) Init() tea.Cmd {
func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m := msg.(type) { switch m := msg.(type) {
case PerformActionMsg:
err := t.PerformAction(m.Action, m.Name)
if err != nil {
t.errMsg = err.Error()
}
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
t.width = m.Width t.width = m.Width
t.height = m.Height t.height = m.Height
@@ -85,20 +107,11 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "enter", " ": case "enter", " ":
_ = t.ToggleSelectedExpand() _ = t.ToggleSelectedExpand()
case "n": // new file case "n": // new file
err := t.CreateNode(t.selectedNode, "new_file.txt", FileNode) return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewFile} }
if err != nil {
t.errMsg = err.Error()
}
case "N": // new folder case "N": // new folder
err := t.CreateNode(t.selectedNode, "new_folder", FolderNode) return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewFolder} }
if err != nil {
t.errMsg = err.Error()
}
case "C": // new root node case "C": // new root node
err := t.CreateNode(t.root, "new_root_folder", FolderNode) return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewRoot} }
if err != nil {
t.errMsg = err.Error()
}
case "d", "delete": // delete node case "d", "delete": // delete node
err := t.DeleteNode(t.selectedNode) err := t.DeleteNode(t.selectedNode)
if err != nil { if err != nil {
@@ -137,6 +150,18 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return t, nil return t, nil
} }
func (t *FsTree) PerformAction(action FsActionType, name string) error {
switch action {
case ActionNewFile:
return t.CreateNode(t.selectedNode, name, FileNode)
case ActionNewFolder:
return t.CreateNode(t.selectedNode, name, FolderNode)
case ActionNewRoot:
return t.CreateNode(t.root, name, FolderNode)
}
return nil
}
func (t *FsTree) getViewportBounds(totalLines int) (startLine, endLine int) { func (t *FsTree) getViewportBounds(totalLines int) (startLine, endLine int) {
selectedLine := t.selectedNode.line selectedLine := t.selectedNode.line
halfHeight := t.height / 2 halfHeight := t.height / 2
+1
View File
@@ -12,6 +12,7 @@ require (
require ( require (
github.com/alecthomas/chroma/v2 v2.14.0 // 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/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+2
View File
@@ -4,6 +4,8 @@ github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+92 -48
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
) )
@@ -45,14 +46,23 @@ type model struct {
isHoveringDivider bool isHoveringDivider bool
contentHeight int contentHeight int
showStatusBar bool showStatusBar bool
// input handling
textInput textinput.Model
inputMode bool
pendingAction FsActionType
} }
func NewModel(rootPath string) *model { func NewModel(rootPath string) *model {
ti := textinput.New()
ti.CharLimit = 156
ti.Width = 30
return &model{ return &model{
rootPath: rootPath, rootPath: rootPath,
loading: true, loading: true,
noteView: NewNoteView(), noteView: NewNoteView(),
showStatusBar: true, showStatusBar: false,
textInput: ti,
} }
} }
@@ -86,12 +96,29 @@ func (m *model) layout(width, height int) {
m.fsTreeWidth, m.noteViewWidth = calculateLayout(width, m.fsTreeWidth) m.fsTreeWidth, m.noteViewWidth = calculateLayout(width, m.fsTreeWidth)
h := height h := height
if m.showStatusBar { if m.showStatusBar || m.inputMode {
h -= statusBarHeight h -= statusBarHeight
} }
m.contentHeight = max(0, h) m.contentHeight = max(0, h)
} }
func (m *model) resizeChildren() tea.Cmd {
var cmds []tea.Cmd
if m.tree != nil {
_, cmd := m.tree.Update(tea.WindowSizeMsg{
Width: m.fsTreeWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
}
_, cmd := m.noteView.Update(tea.WindowSizeMsg{
Width: m.noteViewWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
return tea.Batch(cmds...)
}
func (m *model) Init() tea.Cmd { func (m *model) Init() tea.Cmd {
return m.loadTreeCmd(m.rootPath) return m.loadTreeCmd(m.rootPath)
} }
@@ -102,21 +129,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// FALLTHROUGHS ARE BAD // FALLTHROUGHS ARE BAD
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.layout(msg.Width, msg.Height) m.layout(msg.Width, msg.Height)
return m, m.resizeChildren()
var cmds []tea.Cmd
if m.tree != nil {
_, cmd := m.tree.Update(tea.WindowSizeMsg{
Width: m.fsTreeWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
}
_, cmd := m.noteView.Update(tea.WindowSizeMsg{
Width: m.noteViewWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
case treeLoadedMsg: case treeLoadedMsg:
m.tree = msg.tree m.tree = msg.tree
@@ -137,28 +150,65 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
_, cmd := m.noteView.Update(msg) _, cmd := m.noteView.Update(msg)
return m, cmd return m, cmd
case PerformActionMsg:
if m.tree != nil {
_, cmd := m.tree.Update(msg)
return m, cmd
}
case RequestInputMsg:
m.inputMode = true
m.pendingAction = msg.Action
m.textInput.Focus()
m.textInput.SetValue("")
switch msg.Action {
case ActionNewFile:
m.textInput.Placeholder = "New File Name"
case ActionNewFolder:
m.textInput.Placeholder = "New Folder Name"
case ActionNewRoot:
m.textInput.Placeholder = "New Root Folder Name"
}
m.layout(m.terminalWidth, m.terminalHeight) // recalc layout for status bar area
return m, m.resizeChildren()
case tea.KeyMsg: case tea.KeyMsg:
if m.inputMode {
switch msg.String() {
case "enter":
val := m.textInput.Value()
m.inputMode = false
m.textInput.Blur()
m.layout(m.terminalWidth, m.terminalHeight)
cmds := []tea.Cmd{m.resizeChildren()}
if val != "" {
cmds = append(cmds, func() tea.Msg {
return PerformActionMsg{
Action: m.pendingAction,
Name: val,
}
})
}
return m, tea.Batch(cmds...)
case "esc":
m.inputMode = false
m.textInput.Blur()
m.layout(m.terminalWidth, m.terminalHeight)
return m, m.resizeChildren()
}
var cmd tea.Cmd
m.textInput, cmd = m.textInput.Update(msg)
return m, cmd
}
switch msg.String() { switch msg.String() {
case "q", "ctrl+c": case "q", "ctrl+c":
return m, tea.Quit return m, tea.Quit
case "i": case "i":
m.showStatusBar = !m.showStatusBar m.showStatusBar = !m.showStatusBar
m.layout(m.terminalWidth, m.terminalHeight) m.layout(m.terminalWidth, m.terminalHeight)
return m, m.resizeChildren()
var cmds []tea.Cmd
if m.tree != nil {
_, cmd := m.tree.Update(tea.WindowSizeMsg{
Width: m.fsTreeWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
}
_, cmd := m.noteView.Update(tea.WindowSizeMsg{
Width: m.noteViewWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
} }
var cmds []tea.Cmd var cmds []tea.Cmd
@@ -187,20 +237,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.fsTreeWidth, m.noteViewWidth = calculateLayout(m.terminalWidth, msg.X) m.fsTreeWidth, m.noteViewWidth = calculateLayout(m.terminalWidth, msg.X)
// Update children with new sizes // Update children with new sizes
var cmds []tea.Cmd return m, m.resizeChildren()
if m.tree != nil {
_, cmd := m.tree.Update(tea.WindowSizeMsg{
Width: m.fsTreeWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
}
_, cmd := m.noteView.Update(tea.WindowSizeMsg{
Width: m.noteViewWidth,
Height: m.contentHeight,
})
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
} }
} }
@@ -264,15 +301,22 @@ func (m model) View() string {
notes, notes,
) )
if !m.showStatusBar { if !m.showStatusBar && !m.inputMode {
return full return full
} }
statusContent := ""
if m.inputMode {
statusContent = m.textInput.View()
} else if m.tree != nil && m.tree.errMsg != "" {
statusContent = lipgloss.NewStyle().Foreground(lipgloss.Color("9")).Render(m.tree.errMsg)
}
statusBar := lipgloss.NewStyle(). statusBar := lipgloss.NewStyle().
Width(m.terminalWidth - 2). // Subtract borders Width(m.terminalWidth - 2). // Subtract borders
Height(1). Height(1).
Border(lipgloss.NormalBorder()). Border(lipgloss.NormalBorder()).
Render("") // Placeholder text to ensure it's visible, can be empty string Render(statusContent)
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,