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
}
type FsActionType int
const (
ActionNewFile FsActionType = iota
ActionNewFolder
ActionNewRoot
)
type RequestInputMsg struct {
Action FsActionType
}
type PerformActionMsg struct {
Action FsActionType
Name string
}
// ==================== FsNode definition ====================
type FsTree struct {
root *FsNode
@@ -72,6 +89,11 @@ func (t *FsTree) Init() tea.Cmd {
func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch m := msg.(type) {
case PerformActionMsg:
err := t.PerformAction(m.Action, m.Name)
if err != nil {
t.errMsg = err.Error()
}
case tea.WindowSizeMsg:
t.width = m.Width
t.height = m.Height
@@ -85,20 +107,11 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "enter", " ":
_ = t.ToggleSelectedExpand()
case "n": // new file
err := t.CreateNode(t.selectedNode, "new_file.txt", FileNode)
if err != nil {
t.errMsg = err.Error()
}
return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewFile} }
case "N": // new folder
err := t.CreateNode(t.selectedNode, "new_folder", FolderNode)
if err != nil {
t.errMsg = err.Error()
}
return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewFolder} }
case "C": // new root node
err := t.CreateNode(t.root, "new_root_folder", FolderNode)
if err != nil {
t.errMsg = err.Error()
}
return t, func() tea.Msg { return RequestInputMsg{Action: ActionNewRoot} }
case "d", "delete": // delete node
err := t.DeleteNode(t.selectedNode)
if err != nil {
@@ -137,6 +150,18 @@ func (t *FsTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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) {
selectedLine := t.selectedNode.line
halfHeight := t.height / 2
+1
View File
@@ -12,6 +12,7 @@ require (
require (
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/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/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
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/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+92 -48
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
@@ -45,14 +46,23 @@ type model struct {
isHoveringDivider bool
contentHeight int
showStatusBar bool
// input handling
textInput textinput.Model
inputMode bool
pendingAction FsActionType
}
func NewModel(rootPath string) *model {
ti := textinput.New()
ti.CharLimit = 156
ti.Width = 30
return &model{
rootPath: rootPath,
loading: true,
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)
h := height
if m.showStatusBar {
if m.showStatusBar || m.inputMode {
h -= statusBarHeight
}
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 {
return m.loadTreeCmd(m.rootPath)
}
@@ -102,21 +129,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// FALLTHROUGHS ARE BAD
case tea.WindowSizeMsg:
m.layout(msg.Width, msg.Height)
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...)
return m, m.resizeChildren()
case treeLoadedMsg:
m.tree = msg.tree
@@ -137,28 +150,65 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
_, cmd := m.noteView.Update(msg)
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:
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() {
case "q", "ctrl+c":
return m, tea.Quit
case "i":
m.showStatusBar = !m.showStatusBar
m.layout(m.terminalWidth, m.terminalHeight)
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...)
return m, m.resizeChildren()
}
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)
// Update children with new sizes
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...)
return m, m.resizeChildren()
}
}
@@ -264,15 +301,22 @@ func (m model) View() string {
notes,
)
if !m.showStatusBar {
if !m.showStatusBar && !m.inputMode {
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().
Width(m.terminalWidth - 2). // Subtract borders
Height(1).
Border(lipgloss.NormalBorder()).
Render("") // Placeholder text to ensure it's visible, can be empty string
Render(statusContent)
return lipgloss.JoinVertical(
lipgloss.Left,