mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
fix: quality of life, renaming and better shuffling of items (#11)
* fix: renaming and pro- and de-motion of items * adding guides for indentation * updating readme
This commit is contained in:
parent
2e9980e73c
commit
6b404cd722
6 changed files with 318 additions and 30 deletions
|
|
@ -105,6 +105,7 @@ Feel free to fork and create a pull request if there's any features missing for
|
|||
| `c` | Capture new TODO |
|
||||
| `s` | Add sub-task |
|
||||
| `D` | Delete item (with confirmation) |
|
||||
| `R` | Rename item |
|
||||
| `#` | Add/edit tags |
|
||||
| `a` | Toggle agenda view |
|
||||
| `i` | Clock in |
|
||||
|
|
@ -114,8 +115,9 @@ Feel free to fork and create a pull request if there's any features missing for
|
|||
| `e` | Set effort |
|
||||
| `r` | Toggle reorder mode |
|
||||
| `shift+↑/↓` | Move item up/down |
|
||||
| `sift+←/→` | Promote/demote item |
|
||||
| `,` | Open settings |
|
||||
| `ctrl+s` | Save |
|
||||
| `ctrl+s` | Force save |
|
||||
| `?` | Toggle help |
|
||||
| `q` or `ctrl+c` | Quit |
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ type KeybindingsConfig struct {
|
|||
Right []string `toml:"right"`
|
||||
ShiftUp []string `toml:"shift_up"`
|
||||
ShiftDown []string `toml:"shift_down"`
|
||||
ShiftLeft []string `toml:"shift_left"`
|
||||
ShiftRight []string `toml:"shift_right"`
|
||||
Rename []string `toml:"rename"`
|
||||
CycleState []string `toml:"cycle_state"`
|
||||
ToggleFold []string `toml:"toggle_fold"`
|
||||
EditNotes []string `toml:"edit_notes"`
|
||||
|
|
@ -103,6 +106,9 @@ func DefaultConfig() *Config {
|
|||
Right: []string{"right", "l"},
|
||||
ShiftUp: []string{"shift+up"},
|
||||
ShiftDown: []string{"shift+down"},
|
||||
ShiftLeft: []string{"shift+left"},
|
||||
ShiftRight: []string{"shift+right"},
|
||||
Rename: []string{"R"},
|
||||
CycleState: []string{"t", " "},
|
||||
ToggleFold: []string{"tab"},
|
||||
EditNotes: []string{"enter"},
|
||||
|
|
@ -255,6 +261,15 @@ func (c *Config) fillDefaults() {
|
|||
if len(c.Keybindings.ShiftDown) == 0 {
|
||||
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 {
|
||||
c.Keybindings.CycleState = defaults.Keybindings.CycleState
|
||||
}
|
||||
|
|
@ -521,30 +536,33 @@ func (c *Config) UpdateKeybinding(action string, keys []string) error {
|
|||
// GetAllKeybindings returns a map of all keybindings
|
||||
func (c *Config) GetAllKeybindings() map[string][]string {
|
||||
return map[string][]string{
|
||||
"up": c.Keybindings.Up,
|
||||
"down": c.Keybindings.Down,
|
||||
"left": c.Keybindings.Left,
|
||||
"right": c.Keybindings.Right,
|
||||
"shift_up": c.Keybindings.ShiftUp,
|
||||
"shift_down": c.Keybindings.ShiftDown,
|
||||
"cycle_state": c.Keybindings.CycleState,
|
||||
"toggle_fold": c.Keybindings.ToggleFold,
|
||||
"edit_notes": c.Keybindings.EditNotes,
|
||||
"toggle_view": c.Keybindings.ToggleView,
|
||||
"capture": c.Keybindings.Capture,
|
||||
"add_subtask": c.Keybindings.AddSubTask,
|
||||
"delete": c.Keybindings.Delete,
|
||||
"save": c.Keybindings.Save,
|
||||
"up": c.Keybindings.Up,
|
||||
"down": c.Keybindings.Down,
|
||||
"left": c.Keybindings.Left,
|
||||
"right": c.Keybindings.Right,
|
||||
"shift_up": c.Keybindings.ShiftUp,
|
||||
"shift_down": c.Keybindings.ShiftDown,
|
||||
"shift_left": c.Keybindings.ShiftLeft,
|
||||
"shift_right": c.Keybindings.ShiftRight,
|
||||
"rename": c.Keybindings.Rename,
|
||||
"cycle_state": c.Keybindings.CycleState,
|
||||
"toggle_fold": c.Keybindings.ToggleFold,
|
||||
"edit_notes": c.Keybindings.EditNotes,
|
||||
"toggle_view": c.Keybindings.ToggleView,
|
||||
"capture": c.Keybindings.Capture,
|
||||
"add_subtask": c.Keybindings.AddSubTask,
|
||||
"delete": c.Keybindings.Delete,
|
||||
"save": c.Keybindings.Save,
|
||||
"toggle_reorder": c.Keybindings.ToggleReorder,
|
||||
"clock_in": c.Keybindings.ClockIn,
|
||||
"clock_out": c.Keybindings.ClockOut,
|
||||
"set_deadline": c.Keybindings.SetDeadline,
|
||||
"set_priority": c.Keybindings.SetPriority,
|
||||
"set_effort": c.Keybindings.SetEffort,
|
||||
"help": c.Keybindings.Help,
|
||||
"quit": c.Keybindings.Quit,
|
||||
"settings": c.Keybindings.Settings,
|
||||
"tag_item": c.Keybindings.TagItem,
|
||||
"clock_in": c.Keybindings.ClockIn,
|
||||
"clock_out": c.Keybindings.ClockOut,
|
||||
"set_deadline": c.Keybindings.SetDeadline,
|
||||
"set_priority": c.Keybindings.SetPriority,
|
||||
"set_effort": c.Keybindings.SetEffort,
|
||||
"help": c.Keybindings.Help,
|
||||
"quit": c.Keybindings.Quit,
|
||||
"settings": c.Keybindings.Settings,
|
||||
"tag_item": c.Keybindings.TagItem,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const (
|
|||
modeHelp
|
||||
modeSettings
|
||||
modeTagEdit
|
||||
modeRename
|
||||
)
|
||||
|
||||
type uiModel struct {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ type keyMap struct {
|
|||
Right key.Binding
|
||||
ShiftUp key.Binding
|
||||
ShiftDown key.Binding
|
||||
ShiftLeft key.Binding
|
||||
ShiftRight key.Binding
|
||||
Rename key.Binding
|
||||
CycleState key.Binding
|
||||
ToggleView key.Binding
|
||||
Quit key.Binding
|
||||
|
|
@ -63,6 +66,18 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
|||
key.WithKeys(kb.ShiftDown...),
|
||||
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(
|
||||
key.WithKeys(kb.CycleState...),
|
||||
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)
|
||||
case modeTagEdit:
|
||||
return m.updateTagEdit(msg)
|
||||
case modeRename:
|
||||
return m.updateRename(msg)
|
||||
}
|
||||
|
||||
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):
|
||||
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):
|
||||
items := m.getVisibleItems()
|
||||
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
|
||||
}
|
||||
|
||||
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):
|
||||
m.mode = modeCapture
|
||||
m.captureCursor = m.cursor // Store current cursor position
|
||||
|
|
@ -899,6 +918,151 @@ func (m *uiModel) swapItems(item1, item2 *model.Item) {
|
|||
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) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
|
|
@ -979,3 +1143,45 @@ func (m *uiModel) updateTagEdit(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
}
|
||||
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()
|
||||
case modeTagEdit:
|
||||
return m.viewTagEdit()
|
||||
case modeRename:
|
||||
return m.viewRename()
|
||||
}
|
||||
|
||||
// Build footer (status + help)
|
||||
|
|
@ -146,7 +148,13 @@ func (m uiModel) View() string {
|
|||
for i, item := range items {
|
||||
lineCount := 1 // The item itself
|
||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||
indent := strings.Repeat(" ", item.Level)
|
||||
// Build subtle visual guides for notes
|
||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
var notePrefix strings.Builder
|
||||
for j := 1; j <= item.Level; j++ {
|
||||
notePrefix.WriteString(guideStyle.Render("· "))
|
||||
}
|
||||
indent := notePrefix.String()
|
||||
noteIndent := indent + " "
|
||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||
|
|
@ -207,7 +215,13 @@ func (m uiModel) View() string {
|
|||
|
||||
// Render remaining notes
|
||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||
indent := strings.Repeat(" ", item.Level)
|
||||
// Build subtle visual guides for notes
|
||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
||||
var notePrefix strings.Builder
|
||||
for i := 1; i <= item.Level; i++ {
|
||||
notePrefix.WriteString(guideStyle.Render("· "))
|
||||
}
|
||||
indent := notePrefix.String()
|
||||
noteIndent := indent + " "
|
||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||
|
|
@ -231,7 +245,13 @@ func (m uiModel) View() string {
|
|||
|
||||
// Show notes if not folded
|
||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||
indent := strings.Repeat(" ", item.Level)
|
||||
// Build subtle visual guides for notes
|
||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("235"))
|
||||
var notePrefix strings.Builder
|
||||
for i := 1; i <= item.Level; i++ {
|
||||
notePrefix.WriteString(guideStyle.Render("· "))
|
||||
}
|
||||
indent := notePrefix.String()
|
||||
noteIndent := indent + " "
|
||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||
|
|
@ -873,9 +893,17 @@ func wrapText(text string, width int, indent string) []string {
|
|||
func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||
var b strings.Builder
|
||||
|
||||
// Indentation for level
|
||||
indent := strings.Repeat(" ", item.Level-1)
|
||||
b.WriteString(indent)
|
||||
// Indentation with subtle visual nesting guides
|
||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) // Very subtle gray
|
||||
for i := 1; i < item.Level; i++ {
|
||||
if i == item.Level-1 {
|
||||
// Last level before the item - use subtle dot connector
|
||||
b.WriteString(guideStyle.Render("· "))
|
||||
} else {
|
||||
// Parent levels - use subtle dot
|
||||
b.WriteString(guideStyle.Render("· "))
|
||||
}
|
||||
}
|
||||
|
||||
// Fold indicator
|
||||
if len(item.Children) > 0 || len(item.Notes) > 0 {
|
||||
|
|
@ -1005,3 +1033,21 @@ func (m uiModel) viewTagEdit() 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