add filename input dialog box
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user