mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
fix: renaming and pro- and de-motion of items
This commit is contained in:
parent
2e9980e73c
commit
2cd2c68cc4
5 changed files with 283 additions and 23 deletions
|
|
@ -26,6 +26,9 @@ type KeybindingsConfig struct {
|
||||||
Right []string `toml:"right"`
|
Right []string `toml:"right"`
|
||||||
ShiftUp []string `toml:"shift_up"`
|
ShiftUp []string `toml:"shift_up"`
|
||||||
ShiftDown []string `toml:"shift_down"`
|
ShiftDown []string `toml:"shift_down"`
|
||||||
|
ShiftLeft []string `toml:"shift_left"`
|
||||||
|
ShiftRight []string `toml:"shift_right"`
|
||||||
|
Rename []string `toml:"rename"`
|
||||||
CycleState []string `toml:"cycle_state"`
|
CycleState []string `toml:"cycle_state"`
|
||||||
ToggleFold []string `toml:"toggle_fold"`
|
ToggleFold []string `toml:"toggle_fold"`
|
||||||
EditNotes []string `toml:"edit_notes"`
|
EditNotes []string `toml:"edit_notes"`
|
||||||
|
|
@ -103,6 +106,9 @@ func DefaultConfig() *Config {
|
||||||
Right: []string{"right", "l"},
|
Right: []string{"right", "l"},
|
||||||
ShiftUp: []string{"shift+up"},
|
ShiftUp: []string{"shift+up"},
|
||||||
ShiftDown: []string{"shift+down"},
|
ShiftDown: []string{"shift+down"},
|
||||||
|
ShiftLeft: []string{"shift+left"},
|
||||||
|
ShiftRight: []string{"shift+right"},
|
||||||
|
Rename: []string{"R"},
|
||||||
CycleState: []string{"t", " "},
|
CycleState: []string{"t", " "},
|
||||||
ToggleFold: []string{"tab"},
|
ToggleFold: []string{"tab"},
|
||||||
EditNotes: []string{"enter"},
|
EditNotes: []string{"enter"},
|
||||||
|
|
@ -255,6 +261,15 @@ func (c *Config) fillDefaults() {
|
||||||
if len(c.Keybindings.ShiftDown) == 0 {
|
if len(c.Keybindings.ShiftDown) == 0 {
|
||||||
c.Keybindings.ShiftDown = defaults.Keybindings.ShiftDown
|
c.Keybindings.ShiftDown = defaults.Keybindings.ShiftDown
|
||||||
}
|
}
|
||||||
|
if len(c.Keybindings.ShiftLeft) == 0 {
|
||||||
|
c.Keybindings.ShiftLeft = defaults.Keybindings.ShiftLeft
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ShiftRight) == 0 {
|
||||||
|
c.Keybindings.ShiftRight = defaults.Keybindings.ShiftRight
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Rename) == 0 {
|
||||||
|
c.Keybindings.Rename = defaults.Keybindings.Rename
|
||||||
|
}
|
||||||
if len(c.Keybindings.CycleState) == 0 {
|
if len(c.Keybindings.CycleState) == 0 {
|
||||||
c.Keybindings.CycleState = defaults.Keybindings.CycleState
|
c.Keybindings.CycleState = defaults.Keybindings.CycleState
|
||||||
}
|
}
|
||||||
|
|
@ -527,6 +542,9 @@ func (c *Config) GetAllKeybindings() map[string][]string {
|
||||||
"right": c.Keybindings.Right,
|
"right": c.Keybindings.Right,
|
||||||
"shift_up": c.Keybindings.ShiftUp,
|
"shift_up": c.Keybindings.ShiftUp,
|
||||||
"shift_down": c.Keybindings.ShiftDown,
|
"shift_down": c.Keybindings.ShiftDown,
|
||||||
|
"shift_left": c.Keybindings.ShiftLeft,
|
||||||
|
"shift_right": c.Keybindings.ShiftRight,
|
||||||
|
"rename": c.Keybindings.Rename,
|
||||||
"cycle_state": c.Keybindings.CycleState,
|
"cycle_state": c.Keybindings.CycleState,
|
||||||
"toggle_fold": c.Keybindings.ToggleFold,
|
"toggle_fold": c.Keybindings.ToggleFold,
|
||||||
"edit_notes": c.Keybindings.EditNotes,
|
"edit_notes": c.Keybindings.EditNotes,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const (
|
||||||
modeHelp
|
modeHelp
|
||||||
modeSettings
|
modeSettings
|
||||||
modeTagEdit
|
modeTagEdit
|
||||||
|
modeRename
|
||||||
)
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,9 @@ type keyMap struct {
|
||||||
Right key.Binding
|
Right key.Binding
|
||||||
ShiftUp key.Binding
|
ShiftUp key.Binding
|
||||||
ShiftDown key.Binding
|
ShiftDown key.Binding
|
||||||
|
ShiftLeft key.Binding
|
||||||
|
ShiftRight key.Binding
|
||||||
|
Rename key.Binding
|
||||||
CycleState key.Binding
|
CycleState key.Binding
|
||||||
ToggleView key.Binding
|
ToggleView key.Binding
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
|
|
@ -63,6 +66,18 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
||||||
key.WithKeys(kb.ShiftDown...),
|
key.WithKeys(kb.ShiftDown...),
|
||||||
key.WithHelp(formatKeyHelp(kb.ShiftDown), "move item down"),
|
key.WithHelp(formatKeyHelp(kb.ShiftDown), "move item down"),
|
||||||
),
|
),
|
||||||
|
ShiftLeft: key.NewBinding(
|
||||||
|
key.WithKeys(kb.ShiftLeft...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.ShiftLeft), "promote item"),
|
||||||
|
),
|
||||||
|
ShiftRight: key.NewBinding(
|
||||||
|
key.WithKeys(kb.ShiftRight...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.ShiftRight), "demote item"),
|
||||||
|
),
|
||||||
|
Rename: key.NewBinding(
|
||||||
|
key.WithKeys(kb.Rename...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.Rename), "rename item"),
|
||||||
|
),
|
||||||
CycleState: key.NewBinding(
|
CycleState: key.NewBinding(
|
||||||
key.WithKeys(kb.CycleState...),
|
key.WithKeys(kb.CycleState...),
|
||||||
key.WithHelp(formatKeyHelp(kb.CycleState), "cycle todo state"),
|
key.WithHelp(formatKeyHelp(kb.CycleState), "cycle todo state"),
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m.updateSettingsAddState(msg)
|
return m.updateSettingsAddState(msg)
|
||||||
case modeTagEdit:
|
case modeTagEdit:
|
||||||
return m.updateTagEdit(msg)
|
return m.updateTagEdit(msg)
|
||||||
|
case modeRename:
|
||||||
|
return m.updateRename(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
@ -122,6 +124,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.ShiftDown):
|
case key.Matches(msg, m.keys.ShiftDown):
|
||||||
m.moveItemDown()
|
m.moveItemDown()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ShiftLeft):
|
||||||
|
m.promoteItem()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ShiftRight):
|
||||||
|
m.demoteItem()
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.CycleState):
|
case key.Matches(msg, m.keys.CycleState):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
|
@ -182,6 +190,17 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, textinput.Blink
|
return m, textinput.Blink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Rename):
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
m.editingItem = items[m.cursor]
|
||||||
|
m.mode = modeRename
|
||||||
|
m.textinput.SetValue(items[m.cursor].Title)
|
||||||
|
m.textinput.Placeholder = "Item title"
|
||||||
|
m.textinput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.Capture):
|
case key.Matches(msg, m.keys.Capture):
|
||||||
m.mode = modeCapture
|
m.mode = modeCapture
|
||||||
m.captureCursor = m.cursor // Store current cursor position
|
m.captureCursor = m.cursor // Store current cursor position
|
||||||
|
|
@ -899,6 +918,151 @@ func (m *uiModel) swapItems(item1, item2 *model.Item) {
|
||||||
swapInList(m.orgFile.Items)
|
swapInList(m.orgFile.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) promoteItem() {
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) == 0 || m.cursor >= len(items) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentItem := items[m.cursor]
|
||||||
|
|
||||||
|
// Can't promote a top-level item
|
||||||
|
if currentItem.Level <= 1 {
|
||||||
|
m.setStatus("Cannot promote - already at top level")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the parent of this item
|
||||||
|
parent := m.findParent(currentItem)
|
||||||
|
if parent == nil {
|
||||||
|
m.setStatus("Cannot promote - no parent found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove item from parent's children
|
||||||
|
for i, child := range parent.Children {
|
||||||
|
if child == currentItem {
|
||||||
|
parent.Children = append(parent.Children[:i], parent.Children[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find grandparent to insert this item after the parent
|
||||||
|
grandparent := m.findParent(parent)
|
||||||
|
if grandparent != nil {
|
||||||
|
// Insert after parent in grandparent's children
|
||||||
|
for i, child := range grandparent.Children {
|
||||||
|
if child == parent {
|
||||||
|
// Decrease level and update all descendants
|
||||||
|
m.adjustItemLevels(currentItem, -1)
|
||||||
|
grandparent.Children = append(grandparent.Children[:i+1], append([]*model.Item{currentItem}, grandparent.Children[i+1:]...)...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Parent is at top level, insert after parent in m.orgFile.Items
|
||||||
|
for i, item := range m.orgFile.Items {
|
||||||
|
if item == parent {
|
||||||
|
// Decrease level and update all descendants
|
||||||
|
m.adjustItemLevels(currentItem, -1)
|
||||||
|
m.orgFile.Items = append(m.orgFile.Items[:i+1], append([]*model.Item{currentItem}, m.orgFile.Items[i+1:]...)...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.setStatus("Item promoted")
|
||||||
|
|
||||||
|
// Update cursor to follow the item
|
||||||
|
items = m.getVisibleItems()
|
||||||
|
for i, item := range items {
|
||||||
|
if item == currentItem {
|
||||||
|
m.cursor = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) demoteItem() {
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) == 0 || m.cursor >= len(items) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentItem := items[m.cursor]
|
||||||
|
|
||||||
|
// Find the previous sibling to make this item its child
|
||||||
|
prevSibling := m.findPreviousSibling(currentItem)
|
||||||
|
if prevSibling == nil {
|
||||||
|
m.setStatus("Cannot demote - no previous sibling")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove item from its current parent's children
|
||||||
|
parent := m.findParent(currentItem)
|
||||||
|
if parent != nil {
|
||||||
|
for i, child := range parent.Children {
|
||||||
|
if child == currentItem {
|
||||||
|
parent.Children = append(parent.Children[:i], parent.Children[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Item is at top level
|
||||||
|
for i, item := range m.orgFile.Items {
|
||||||
|
if item == currentItem {
|
||||||
|
m.orgFile.Items = append(m.orgFile.Items[:i], m.orgFile.Items[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase level and update all descendants
|
||||||
|
m.adjustItemLevels(currentItem, 1)
|
||||||
|
|
||||||
|
// Add as child of previous sibling
|
||||||
|
prevSibling.Children = append(prevSibling.Children, currentItem)
|
||||||
|
prevSibling.Folded = false // Unfold to show the demoted item
|
||||||
|
|
||||||
|
m.setStatus("Item demoted")
|
||||||
|
|
||||||
|
// Update cursor to follow the item
|
||||||
|
items = m.getVisibleItems()
|
||||||
|
for i, item := range items {
|
||||||
|
if item == currentItem {
|
||||||
|
m.cursor = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) findParent(target *model.Item) *model.Item {
|
||||||
|
var findInList func([]*model.Item) *model.Item
|
||||||
|
findInList = func(items []*model.Item) *model.Item {
|
||||||
|
for _, item := range items {
|
||||||
|
// Check if target is a direct child
|
||||||
|
for _, child := range item.Children {
|
||||||
|
if child == target {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recursively check children
|
||||||
|
if result := findInList(item.Children); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return findInList(m.orgFile.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) adjustItemLevels(item *model.Item, delta int) {
|
||||||
|
item.Level += delta
|
||||||
|
for _, child := range item.Children {
|
||||||
|
m.adjustItemLevels(child, delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m uiModel) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
|
|
@ -979,3 +1143,45 @@ func (m *uiModel) updateTagEdit(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateRename handles item rename mode
|
||||||
|
func (m *uiModel) updateRename(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit):
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.Type == tea.KeyEnter:
|
||||||
|
if m.editingItem != nil {
|
||||||
|
newTitle := strings.TrimSpace(m.textinput.Value())
|
||||||
|
if newTitle != "" {
|
||||||
|
m.editingItem.Title = newTitle
|
||||||
|
m.setStatus("Item renamed")
|
||||||
|
} else {
|
||||||
|
m.setStatus("Cannot rename to empty title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.Type == tea.KeyEsc:
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.editingItem = nil
|
||||||
|
m.setStatus("Cancelled")
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,8 @@ func (m uiModel) View() string {
|
||||||
return m.viewSettingsAddState()
|
return m.viewSettingsAddState()
|
||||||
case modeTagEdit:
|
case modeTagEdit:
|
||||||
return m.viewTagEdit()
|
return m.viewTagEdit()
|
||||||
|
case modeRename:
|
||||||
|
return m.viewRename()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build footer (status + help)
|
// Build footer (status + help)
|
||||||
|
|
@ -1005,3 +1007,21 @@ func (m uiModel) viewTagEdit() string {
|
||||||
|
|
||||||
return content.String()
|
return content.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewRename renders the rename item view
|
||||||
|
func (m uiModel) viewRename() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString(m.styles.titleStyle.Render("Rename Item") + "\n\n")
|
||||||
|
|
||||||
|
if m.editingItem != nil {
|
||||||
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("Current: %s", m.editingItem.Title)) + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString(m.textinput.View() + "\n\n")
|
||||||
|
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Enter new title for the item") + "\n\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue