From 8ff8b4863190a0d4ec20c4911f558b4dc267c5a9 Mon Sep 17 00:00:00 2001 From: ruinivist Date: Sun, 4 Jan 2026 03:54:56 +0000 Subject: [PATCH] add filename input dialog box --- fstree.go | 49 ++++++++++++++----- go.mod | 1 + go.sum | 2 + main.go | 140 +++++++++++++++++++++++++++++++++++------------------- 4 files changed, 132 insertions(+), 60 deletions(-) diff --git a/fstree.go b/fstree.go index 5bfaf1e..8e37b17 100644 --- a/fstree.go +++ b/fstree.go @@ -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 diff --git a/go.mod b/go.mod index 4bcc88f..9072696 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 5972c4b..150244c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index fe3eba9..2621a0a 100644 --- a/main.go +++ b/main.go @@ -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,