mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
images and getting closer to feature complete
This commit is contained in:
parent
4e41ad678f
commit
13e52e2880
12 changed files with 531 additions and 81 deletions
BIN
.imgs/capture_prompt.png
Normal file
BIN
.imgs/capture_prompt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
.imgs/delete_prompt.png
Normal file
BIN
.imgs/delete_prompt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
.imgs/editing.png
Normal file
BIN
.imgs/editing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
.imgs/list_view.png
Normal file
BIN
.imgs/list_view.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
16
README.md
Normal file
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Org
|
||||||
|
|
||||||
|
Org is a simple orgmode application inspired from the simplicity of `nano`.
|
||||||
|
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
### List view
|
||||||
|

|
||||||
|
|
||||||
|
## Editing notes
|
||||||
|

|
||||||
|
|
||||||
|
### Prompts
|
||||||
|

|
||||||
|

|
||||||
|
|
@ -2,13 +2,25 @@ package model
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
|
// Priority represents org-mode priority levels
|
||||||
|
type Priority string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PriorityNone Priority = ""
|
||||||
|
PriorityA Priority = "A"
|
||||||
|
PriorityB Priority = "B"
|
||||||
|
PriorityC Priority = "C"
|
||||||
|
)
|
||||||
|
|
||||||
// Item represents a single org-mode item (heading)
|
// Item represents a single org-mode item (heading)
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Level int // Heading level (number of *)
|
Level int // Heading level (number of *)
|
||||||
State TodoState // TODO, PROG, BLOCK, DONE, or empty
|
State TodoState // TODO, PROG, BLOCK, DONE, or empty
|
||||||
|
Priority Priority // Priority: A, B, C, or empty
|
||||||
Title string // The main title text
|
Title string // The main title text
|
||||||
Scheduled *time.Time
|
Scheduled *time.Time
|
||||||
Deadline *time.Time
|
Deadline *time.Time
|
||||||
|
Effort string // Effort estimate (e.g., "8h", "2d")
|
||||||
Notes []string // Notes/content under the heading
|
Notes []string // Notes/content under the heading
|
||||||
Children []*Item // Sub-items
|
Children []*Item // Sub-items
|
||||||
Folded bool // Whether the item is folded (hides notes and children)
|
Folded bool // Whether the item is folded (hides notes and children)
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ import (
|
||||||
|
|
||||||
// Parser patterns
|
// Parser patterns
|
||||||
var (
|
var (
|
||||||
headingPattern = regexp.MustCompile(`^(\*+)\s+(?:(TODO|PROG|BLOCK|DONE)\s+)?(.+)$`)
|
headingPattern = regexp.MustCompile(`^(\*+)\s+(?:(TODO|PROG|BLOCK|DONE)\s+)?(?:\[#([A-C])\]\s+)?(.+)$`)
|
||||||
scheduledPattern = regexp.MustCompile(`SCHEDULED:\s*<([^>]+)>`)
|
scheduledPattern = regexp.MustCompile(`SCHEDULED:\s*<([^>]+)>`)
|
||||||
deadlinePattern = regexp.MustCompile(`DEADLINE:\s*<([^>]+)>`)
|
deadlinePattern = regexp.MustCompile(`DEADLINE:\s*<([^>]+)>`)
|
||||||
clockPattern = regexp.MustCompile(`CLOCK:\s*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
clockPattern = regexp.MustCompile(`CLOCK:\s*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
||||||
drawerStart = regexp.MustCompile(`^\s*:LOGBOOK:\s*$`)
|
effortPattern = regexp.MustCompile(`^\s*:EFFORT:\s*(.+)$`)
|
||||||
|
logbookDrawerStart = regexp.MustCompile(`^\s*:LOGBOOK:\s*$`)
|
||||||
|
propertiesDrawerStart = regexp.MustCompile(`^\s*:PROPERTIES:\s*$`)
|
||||||
drawerEnd = regexp.MustCompile(`^\s*:END:\s*$`)
|
drawerEnd = regexp.MustCompile(`^\s*:END:\s*$`)
|
||||||
codeBlockStart = regexp.MustCompile(`^\s*#\+BEGIN_SRC`)
|
codeBlockStart = regexp.MustCompile(`^\s*#\+BEGIN_SRC`)
|
||||||
codeBlockEnd = regexp.MustCompile(`^\s*#\+END_SRC`)
|
codeBlockEnd = regexp.MustCompile(`^\s*#\+END_SRC`)
|
||||||
|
|
@ -40,25 +42,42 @@ func ParseOrgFile(path string) (*model.OrgFile, error) {
|
||||||
var itemStack []*model.Item // Stack to track parent items
|
var itemStack []*model.Item // Stack to track parent items
|
||||||
var inCodeBlock bool
|
var inCodeBlock bool
|
||||||
var inLogbookDrawer bool
|
var inLogbookDrawer bool
|
||||||
|
var inPropertiesDrawer bool
|
||||||
|
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
|
|
||||||
// Check for drawer boundaries
|
// Check for drawer boundaries
|
||||||
if drawerStart.MatchString(line) {
|
if logbookDrawerStart.MatchString(line) {
|
||||||
inLogbookDrawer = true
|
inLogbookDrawer = true
|
||||||
if currentItem != nil {
|
if currentItem != nil {
|
||||||
currentItem.Notes = append(currentItem.Notes, line)
|
currentItem.Notes = append(currentItem.Notes, line)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if drawerEnd.MatchString(line) && inLogbookDrawer {
|
if propertiesDrawerStart.MatchString(line) {
|
||||||
|
inPropertiesDrawer = true
|
||||||
|
if currentItem != nil {
|
||||||
|
currentItem.Notes = append(currentItem.Notes, line)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if drawerEnd.MatchString(line) {
|
||||||
|
if inLogbookDrawer {
|
||||||
inLogbookDrawer = false
|
inLogbookDrawer = false
|
||||||
if currentItem != nil {
|
if currentItem != nil {
|
||||||
currentItem.Notes = append(currentItem.Notes, line)
|
currentItem.Notes = append(currentItem.Notes, line)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if inPropertiesDrawer {
|
||||||
|
inPropertiesDrawer = false
|
||||||
|
if currentItem != nil {
|
||||||
|
currentItem.Notes = append(currentItem.Notes, line)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for code block boundaries
|
// Check for code block boundaries
|
||||||
if codeBlockStart.MatchString(line) {
|
if codeBlockStart.MatchString(line) {
|
||||||
|
|
@ -88,11 +107,13 @@ func ParseOrgFile(path string) (*model.OrgFile, error) {
|
||||||
if matches := headingPattern.FindStringSubmatch(line); matches != nil {
|
if matches := headingPattern.FindStringSubmatch(line); matches != nil {
|
||||||
level := len(matches[1])
|
level := len(matches[1])
|
||||||
state := model.TodoState(matches[2])
|
state := model.TodoState(matches[2])
|
||||||
title := matches[3]
|
priority := model.Priority(matches[3])
|
||||||
|
title := matches[4]
|
||||||
|
|
||||||
item := &model.Item{
|
item := &model.Item{
|
||||||
Level: level,
|
Level: level,
|
||||||
State: state,
|
State: state,
|
||||||
|
Priority: priority,
|
||||||
Title: title,
|
Title: title,
|
||||||
Notes: []string{},
|
Notes: []string{},
|
||||||
Children: []*model.Item{},
|
Children: []*model.Item{},
|
||||||
|
|
@ -132,6 +153,11 @@ func ParseOrgFile(path string) (*model.OrgFile, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for EFFORT (inside PROPERTIES drawer)
|
||||||
|
if matches := effortPattern.FindStringSubmatch(line); matches != nil {
|
||||||
|
currentItem.Effort = strings.TrimSpace(matches[1])
|
||||||
|
}
|
||||||
|
|
||||||
// Check for CLOCK (can be inside or outside drawer)
|
// Check for CLOCK (can be inside or outside drawer)
|
||||||
if matches := clockPattern.FindStringSubmatch(line); matches != nil {
|
if matches := clockPattern.FindStringSubmatch(line); matches != nil {
|
||||||
if startTime, err := parseClockTimestamp(matches[1]); err == nil {
|
if startTime, err := parseClockTimestamp(matches[1]); err == nil {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
if item.State != model.StateNone {
|
if item.State != model.StateNone {
|
||||||
line += " " + string(item.State)
|
line += " " + string(item.State)
|
||||||
}
|
}
|
||||||
|
if item.Priority != model.PriorityNone {
|
||||||
|
line += " [#" + string(item.Priority) + "]"
|
||||||
|
}
|
||||||
line += " " + item.Title + "\n"
|
line += " " + item.Title + "\n"
|
||||||
|
|
||||||
if _, err := writer.WriteString(line); err != nil {
|
if _, err := writer.WriteString(line); err != nil {
|
||||||
|
|
@ -47,6 +50,7 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
hasScheduled := false
|
hasScheduled := false
|
||||||
hasDeadline := false
|
hasDeadline := false
|
||||||
hasLogbook := false
|
hasLogbook := false
|
||||||
|
hasProperties := false
|
||||||
for _, note := range item.Notes {
|
for _, note := range item.Notes {
|
||||||
if strings.Contains(note, "SCHEDULED:") {
|
if strings.Contains(note, "SCHEDULED:") {
|
||||||
hasScheduled = true
|
hasScheduled = true
|
||||||
|
|
@ -57,6 +61,9 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
if strings.Contains(note, ":LOGBOOK:") {
|
if strings.Contains(note, ":LOGBOOK:") {
|
||||||
hasLogbook = true
|
hasLogbook = true
|
||||||
}
|
}
|
||||||
|
if strings.Contains(note, ":PROPERTIES:") {
|
||||||
|
hasProperties = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Scheduled != nil && !hasScheduled {
|
if item.Scheduled != nil && !hasScheduled {
|
||||||
|
|
@ -73,6 +80,20 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write effort in :PROPERTIES: drawer if not already in notes
|
||||||
|
if item.Effort != "" && !hasProperties {
|
||||||
|
if _, err := writer.WriteString(":PROPERTIES:\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
effortLine := fmt.Sprintf(":EFFORT: %s\n", item.Effort)
|
||||||
|
if _, err := writer.WriteString(effortLine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := writer.WriteString(":END:\n"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Write clock entries in :LOGBOOK: drawer if not already in notes
|
// Write clock entries in :LOGBOOK: drawer if not already in notes
|
||||||
if len(item.ClockEntries) > 0 && !hasLogbook {
|
if len(item.ClockEntries) > 0 && !hasLogbook {
|
||||||
if _, err := writer.WriteString(":LOGBOOK:\n"); err != nil {
|
if _, err := writer.WriteString(":LOGBOOK:\n"); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,14 @@ const (
|
||||||
modeCapture
|
modeCapture
|
||||||
modeAddSubTask
|
modeAddSubTask
|
||||||
modeSetDeadline
|
modeSetDeadline
|
||||||
|
modeSetPriority
|
||||||
|
modeSetEffort
|
||||||
)
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
orgFile *model.OrgFile
|
orgFile *model.OrgFile
|
||||||
cursor int
|
cursor int
|
||||||
|
scrollOffset int // Track the scroll position
|
||||||
mode viewMode
|
mode viewMode
|
||||||
help help.Model
|
help help.Model
|
||||||
keys keyMap
|
keys keyMap
|
||||||
|
|
@ -78,6 +81,44 @@ func (m uiModel) getVisibleItems() []*model.Item {
|
||||||
return m.orgFile.GetAllItems()
|
return m.orgFile.GetAllItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) updateScrollOffset(availableHeight int) {
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build line count for each item
|
||||||
|
itemLineCount := make([]int, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
lineCount := 1 // The item itself
|
||||||
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
|
// Count note lines (simplified - just count notes)
|
||||||
|
lineCount += len(item.Notes)
|
||||||
|
}
|
||||||
|
itemLineCount[i] = lineCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total lines up to cursor
|
||||||
|
totalLinesBeforeCursor := 0
|
||||||
|
for i := 0; i < m.cursor && i < len(itemLineCount); i++ {
|
||||||
|
totalLinesBeforeCursor += itemLineCount[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust scroll offset to keep cursor visible
|
||||||
|
if totalLinesBeforeCursor < m.scrollOffset {
|
||||||
|
// Cursor is above visible area, scroll up
|
||||||
|
m.scrollOffset = totalLinesBeforeCursor
|
||||||
|
} else if totalLinesBeforeCursor >= m.scrollOffset+availableHeight {
|
||||||
|
// Cursor is below visible area, scroll down
|
||||||
|
m.scrollOffset = totalLinesBeforeCursor - availableHeight + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure scroll offset doesn't go negative
|
||||||
|
if m.scrollOffset < 0 {
|
||||||
|
m.scrollOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RunUI starts the terminal UI
|
// RunUI starts the terminal UI
|
||||||
func RunUI(orgFile *model.OrgFile) error {
|
func RunUI(orgFile *model.OrgFile) error {
|
||||||
p := tea.NewProgram(initialModel(orgFile), tea.WithAltScreen())
|
p := tea.NewProgram(initialModel(orgFile), tea.WithAltScreen())
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ type keyMap struct {
|
||||||
ClockIn key.Binding
|
ClockIn key.Binding
|
||||||
ClockOut key.Binding
|
ClockOut key.Binding
|
||||||
SetDeadline key.Binding
|
SetDeadline key.Binding
|
||||||
|
SetPriority key.Binding
|
||||||
|
SetEffort key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = keyMap{
|
var keys = keyMap{
|
||||||
|
|
@ -75,8 +77,8 @@ var keys = keyMap{
|
||||||
key.WithHelp("s", "add sub-task"),
|
key.WithHelp("s", "add sub-task"),
|
||||||
),
|
),
|
||||||
Delete: key.NewBinding(
|
Delete: key.NewBinding(
|
||||||
key.WithKeys("shift+d"),
|
key.WithKeys("D"),
|
||||||
key.WithHelp("shift+d", "delete item"),
|
key.WithHelp("D", "delete item"),
|
||||||
),
|
),
|
||||||
Save: key.NewBinding(
|
Save: key.NewBinding(
|
||||||
key.WithKeys("ctrl+s"),
|
key.WithKeys("ctrl+s"),
|
||||||
|
|
@ -98,6 +100,14 @@ var keys = keyMap{
|
||||||
key.WithKeys("d"),
|
key.WithKeys("d"),
|
||||||
key.WithHelp("d", "set deadline"),
|
key.WithHelp("d", "set deadline"),
|
||||||
),
|
),
|
||||||
|
SetPriority: key.NewBinding(
|
||||||
|
key.WithKeys("p"),
|
||||||
|
key.WithHelp("p", "set priority"),
|
||||||
|
),
|
||||||
|
SetEffort: key.NewBinding(
|
||||||
|
key.WithKeys("e"),
|
||||||
|
key.WithHelp("e", "set effort"),
|
||||||
|
),
|
||||||
Help: key.NewBinding(
|
Help: key.NewBinding(
|
||||||
key.WithKeys("?"),
|
key.WithKeys("?"),
|
||||||
key.WithHelp("?", "toggle help"),
|
key.WithHelp("?", "toggle help"),
|
||||||
|
|
@ -128,7 +138,7 @@ func (k keyMap) getAllBindings() []key.Binding {
|
||||||
k.Up, k.Down, k.Left, k.Right,
|
k.Up, k.Down, k.Left, k.Right,
|
||||||
k.ToggleFold, k.EditNotes, k.ToggleReorder,
|
k.ToggleFold, k.EditNotes, k.ToggleReorder,
|
||||||
k.Capture, k.AddSubTask, k.Delete, k.Save,
|
k.Capture, k.AddSubTask, k.Delete, k.Save,
|
||||||
k.ClockIn, k.ClockOut, k.SetDeadline,
|
k.ClockIn, k.ClockOut, k.SetDeadline, k.SetPriority, k.SetEffort,
|
||||||
k.ToggleView, k.Help, k.Quit,
|
k.ToggleView, k.Help, k.Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m.updateAddSubTask(msg)
|
return m.updateAddSubTask(msg)
|
||||||
case modeSetDeadline:
|
case modeSetDeadline:
|
||||||
return m.updateSetDeadline(msg)
|
return m.updateSetDeadline(msg)
|
||||||
|
case modeSetPriority:
|
||||||
|
return m.updateSetPriority(msg)
|
||||||
|
case modeSetEffort:
|
||||||
|
return m.updateSetEffort(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
@ -35,6 +39,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.help.Width = msg.Width
|
m.help.Width = msg.Width
|
||||||
m.textarea.SetWidth(msg.Width - 4)
|
m.textarea.SetWidth(msg.Width - 4)
|
||||||
m.textarea.SetHeight(msg.Height - 10)
|
m.textarea.SetHeight(msg.Height - 10)
|
||||||
|
m.textinput.Width = 50 // Set a reasonable width for the text input
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
|
@ -52,6 +57,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
} else {
|
} else {
|
||||||
if m.cursor > 0 {
|
if m.cursor > 0 {
|
||||||
m.cursor--
|
m.cursor--
|
||||||
|
// Update scroll to keep cursor visible
|
||||||
|
availableHeight := m.height - 6 // Approximate
|
||||||
|
if availableHeight < 5 {
|
||||||
|
availableHeight = 5
|
||||||
|
}
|
||||||
|
m.updateScrollOffset(availableHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +73,12 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if m.cursor < len(items)-1 {
|
if m.cursor < len(items)-1 {
|
||||||
m.cursor++
|
m.cursor++
|
||||||
|
// Update scroll to keep cursor visible
|
||||||
|
availableHeight := m.height - 6 // Approximate
|
||||||
|
if availableHeight < 5 {
|
||||||
|
availableHeight = 5
|
||||||
|
}
|
||||||
|
m.updateScrollOffset(availableHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,6 +220,25 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.textinput.Focus()
|
m.textinput.Focus()
|
||||||
return m, textinput.Blink
|
return m, textinput.Blink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.SetPriority):
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
m.editingItem = items[m.cursor]
|
||||||
|
m.mode = modeSetPriority
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.SetEffort):
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
m.editingItem = items[m.cursor]
|
||||||
|
m.mode = modeSetEffort
|
||||||
|
m.textinput.SetValue("")
|
||||||
|
m.textinput.Placeholder = "e.g., 8h, 2d, 1w"
|
||||||
|
m.textinput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,6 +309,7 @@ func (m uiModel) updateCapture(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.textinput.Width = 50
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
|
|
@ -314,6 +351,7 @@ func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.textinput.Width = 50
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
|
|
@ -356,6 +394,7 @@ func (m uiModel) updateSetDeadline(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
m.height = msg.Height
|
m.height = msg.Height
|
||||||
|
m.textinput.Width = 50
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
|
|
@ -453,6 +492,98 @@ func parseDeadlineInput(input string) (time.Time, error) {
|
||||||
return time.Time{}, fmt.Errorf("unable to parse date: %s (use YYYY-MM-DD or +N)", input)
|
return time.Time{}, fmt.Errorf("unable to parse date: %s (use YYYY-MM-DD or +N)", input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m uiModel) updateSetPriority(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.String() {
|
||||||
|
case "A", "a":
|
||||||
|
if m.editingItem != nil {
|
||||||
|
m.editingItem.Priority = model.PriorityA
|
||||||
|
m.setStatus("Priority set to A")
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
case "B", "b":
|
||||||
|
if m.editingItem != nil {
|
||||||
|
m.editingItem.Priority = model.PriorityB
|
||||||
|
m.setStatus("Priority set to B")
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
case "C", "c":
|
||||||
|
if m.editingItem != nil {
|
||||||
|
m.editingItem.Priority = model.PriorityC
|
||||||
|
m.setStatus("Priority set to C")
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
case " ", "enter":
|
||||||
|
// Clear priority
|
||||||
|
if m.editingItem != nil {
|
||||||
|
m.editingItem.Priority = model.PriorityNone
|
||||||
|
m.setStatus("Priority cleared")
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
case "esc":
|
||||||
|
m.mode = modeList
|
||||||
|
m.editingItem = nil
|
||||||
|
m.setStatus("Cancelled")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) updateSetEffort(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
m.width = msg.Width
|
||||||
|
m.height = msg.Height
|
||||||
|
m.textinput.Width = 50
|
||||||
|
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch msg.Type {
|
||||||
|
case tea.KeyEnter:
|
||||||
|
input := strings.TrimSpace(m.textinput.Value())
|
||||||
|
if m.editingItem != nil {
|
||||||
|
if input == "" {
|
||||||
|
// Empty input clears the effort
|
||||||
|
m.editingItem.Effort = ""
|
||||||
|
m.setStatus("Effort cleared!")
|
||||||
|
} else {
|
||||||
|
// Set the effort value
|
||||||
|
m.editingItem.Effort = input
|
||||||
|
m.setStatus("Effort set!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.editingItem = nil
|
||||||
|
return m, nil
|
||||||
|
case tea.KeyEsc:
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.editingItem = nil
|
||||||
|
m.setStatus("Cancelled")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
switch item.State {
|
switch item.State {
|
||||||
case model.StateNone:
|
case model.StateNone:
|
||||||
|
|
@ -491,17 +622,25 @@ func (m *uiModel) moveItemUp() {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentItem := items[m.cursor]
|
currentItem := items[m.cursor]
|
||||||
prevItem := items[m.cursor-1]
|
|
||||||
|
|
||||||
// Can only swap items at the same level
|
// Find the previous sibling (not child, not parent's sibling)
|
||||||
if currentItem.Level != prevItem.Level {
|
prevSibling := m.findPreviousSibling(currentItem)
|
||||||
m.setStatus("Cannot move across different levels")
|
if prevSibling == nil {
|
||||||
|
m.setStatus("Cannot move - already at top of list")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.swapItems(currentItem, prevItem)
|
m.swapItems(currentItem, prevSibling)
|
||||||
m.cursor--
|
|
||||||
m.setStatus("Item moved up")
|
m.setStatus("Item moved up")
|
||||||
|
|
||||||
|
// Update cursor to follow the item
|
||||||
|
items = m.getVisibleItems()
|
||||||
|
for i, item := range items {
|
||||||
|
if item == currentItem {
|
||||||
|
m.cursor = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *uiModel) moveItemDown() {
|
func (m *uiModel) moveItemDown() {
|
||||||
|
|
@ -511,17 +650,63 @@ func (m *uiModel) moveItemDown() {
|
||||||
}
|
}
|
||||||
|
|
||||||
currentItem := items[m.cursor]
|
currentItem := items[m.cursor]
|
||||||
nextItem := items[m.cursor+1]
|
|
||||||
|
|
||||||
// Can only swap items at the same level
|
// Find the next sibling (not child, not parent's sibling)
|
||||||
if currentItem.Level != nextItem.Level {
|
nextSibling := m.findNextSibling(currentItem)
|
||||||
m.setStatus("Cannot move across different levels")
|
if nextSibling == nil {
|
||||||
|
m.setStatus("Cannot move - already at bottom of list")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
m.swapItems(currentItem, nextItem)
|
m.swapItems(currentItem, nextSibling)
|
||||||
m.cursor++
|
|
||||||
m.setStatus("Item moved down")
|
m.setStatus("Item moved down")
|
||||||
|
|
||||||
|
// Update cursor to follow the item
|
||||||
|
items = m.getVisibleItems()
|
||||||
|
for i, item := range items {
|
||||||
|
if item == currentItem {
|
||||||
|
m.cursor = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) findPreviousSibling(item *model.Item) *model.Item {
|
||||||
|
// Find the parent list containing this item and return the previous sibling
|
||||||
|
var findInList func([]*model.Item) *model.Item
|
||||||
|
findInList = func(items []*model.Item) *model.Item {
|
||||||
|
for i, it := range items {
|
||||||
|
if it == item && i > 0 {
|
||||||
|
// Found the item and there's a previous sibling
|
||||||
|
return items[i-1]
|
||||||
|
}
|
||||||
|
// Recursively check children
|
||||||
|
if result := findInList(it.Children); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return findInList(m.orgFile.Items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) findNextSibling(item *model.Item) *model.Item {
|
||||||
|
// Find the parent list containing this item and return the next sibling
|
||||||
|
var findInList func([]*model.Item) *model.Item
|
||||||
|
findInList = func(items []*model.Item) *model.Item {
|
||||||
|
for i, it := range items {
|
||||||
|
if it == item && i < len(items)-1 {
|
||||||
|
// Found the item and there's a next sibling
|
||||||
|
return items[i+1]
|
||||||
|
}
|
||||||
|
// Recursively check children
|
||||||
|
if result := findInList(it.Children); result != nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return findInList(m.orgFile.Items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *uiModel) swapItems(item1, item2 *model.Item) {
|
func (m *uiModel) swapItems(item1, item2 *model.Item) {
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,10 @@ func (m uiModel) View() string {
|
||||||
return m.viewAddSubTask()
|
return m.viewAddSubTask()
|
||||||
case modeSetDeadline:
|
case modeSetDeadline:
|
||||||
return m.viewSetDeadline()
|
return m.viewSetDeadline()
|
||||||
|
case modeSetPriority:
|
||||||
|
return m.viewSetPriority()
|
||||||
|
case modeSetEffort:
|
||||||
|
return m.viewSetEffort()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build footer (status + help)
|
// Build footer (status + help)
|
||||||
|
|
@ -130,12 +134,84 @@ func (m uiModel) View() string {
|
||||||
content.WriteString("No items. Press 'c' to capture a new TODO.\n")
|
content.WriteString("No items. Press 'c' to capture a new TODO.\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
itemLines := 0
|
// Build a map of item index to line count (for scrolling)
|
||||||
|
itemLineCount := make([]int, len(items))
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
if itemLines >= availableHeight {
|
lineCount := 1 // The item itself
|
||||||
break // Don't render more items than fit
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
|
highlightedNotes := renderNotesWithHighlighting(filteredNotes)
|
||||||
|
lineCount += len(highlightedNotes)
|
||||||
|
}
|
||||||
|
itemLineCount[i] = lineCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate total lines up to cursor
|
||||||
|
totalLinesBeforeCursor := 0
|
||||||
|
for i := 0; i < m.cursor && i < len(itemLineCount); i++ {
|
||||||
|
totalLinesBeforeCursor += itemLineCount[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the scroll offset (without modifying m - View should be pure)
|
||||||
|
scrollOffset := m.scrollOffset
|
||||||
|
if totalLinesBeforeCursor < scrollOffset {
|
||||||
|
// Cursor is above visible area, scroll up
|
||||||
|
scrollOffset = totalLinesBeforeCursor
|
||||||
|
} else if totalLinesBeforeCursor >= scrollOffset+availableHeight {
|
||||||
|
// Cursor is below visible area, scroll down
|
||||||
|
scrollOffset = totalLinesBeforeCursor - availableHeight + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render items starting from scroll offset
|
||||||
|
itemLines := 0
|
||||||
|
for i, item := range items {
|
||||||
|
// Calculate which line this item starts at
|
||||||
|
itemStartLine := 0
|
||||||
|
for j := 0; j < i; j++ {
|
||||||
|
itemStartLine += itemLineCount[j]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip items before scroll offset
|
||||||
|
if itemStartLine+itemLineCount[i] <= scrollOffset {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop if we've filled the screen
|
||||||
|
if itemLines >= availableHeight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip partial items at the top if needed
|
||||||
|
if itemStartLine < scrollOffset {
|
||||||
|
// This item is partially scrolled off the top
|
||||||
|
linesToSkip := scrollOffset - itemStartLine
|
||||||
|
if linesToSkip < itemLineCount[i] {
|
||||||
|
// Render the visible parts
|
||||||
|
if linesToSkip == 0 {
|
||||||
|
line := m.renderItem(item, i == m.cursor)
|
||||||
|
content.WriteString(line)
|
||||||
|
content.WriteString("\n")
|
||||||
|
itemLines++
|
||||||
|
linesToSkip++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render remaining notes
|
||||||
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
|
indent := strings.Repeat(" ", item.Level)
|
||||||
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
|
highlightedNotes := renderNotesWithHighlighting(filteredNotes)
|
||||||
|
for noteIdx := linesToSkip - 1; noteIdx < len(highlightedNotes) && itemLines < availableHeight; noteIdx++ {
|
||||||
|
content.WriteString(indent)
|
||||||
|
content.WriteString(" " + highlightedNotes[noteIdx])
|
||||||
|
content.WriteString("\n")
|
||||||
|
itemLines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the full item
|
||||||
line := m.renderItem(item, i == m.cursor)
|
line := m.renderItem(item, i == m.cursor)
|
||||||
content.WriteString(line)
|
content.WriteString(line)
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
|
|
@ -144,7 +220,6 @@ func (m uiModel) View() string {
|
||||||
// Show notes if not folded
|
// Show notes if not folded
|
||||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
indent := strings.Repeat(" ", item.Level)
|
indent := strings.Repeat(" ", item.Level)
|
||||||
// Filter out LOGBOOK drawer and apply syntax highlighting to notes
|
|
||||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
highlightedNotes := renderNotesWithHighlighting(filteredNotes)
|
highlightedNotes := renderNotesWithHighlighting(filteredNotes)
|
||||||
for _, note := range highlightedNotes {
|
for _, note := range highlightedNotes {
|
||||||
|
|
@ -177,8 +252,6 @@ func (m uiModel) View() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewConfirmDelete() string {
|
func (m uiModel) viewConfirmDelete() string {
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
dialogStyle := lipgloss.NewStyle().
|
dialogStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("196")).
|
BorderForeground(lipgloss.Color("196")).
|
||||||
|
|
@ -202,21 +275,11 @@ func (m uiModel) viewConfirmDelete() string {
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
// Center the dialog
|
// Center the dialog horizontally and vertically
|
||||||
if m.height > 0 {
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
verticalPadding := (m.height - lipgloss.Height(dialog)) / 2
|
|
||||||
if verticalPadding > 0 {
|
|
||||||
b.WriteString(strings.Repeat("\n", verticalPadding))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(dialog)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewCapture() string {
|
func (m uiModel) viewCapture() string {
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
dialogStyle := lipgloss.NewStyle().
|
dialogStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("99")).
|
BorderForeground(lipgloss.Color("99")).
|
||||||
|
|
@ -232,21 +295,11 @@ func (m uiModel) viewCapture() string {
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
// Center the dialog
|
// Center the dialog horizontally and vertically
|
||||||
if m.height > 0 {
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
verticalPadding := (m.height - lipgloss.Height(dialog)) / 2
|
|
||||||
if verticalPadding > 0 {
|
|
||||||
b.WriteString(strings.Repeat("\n", verticalPadding))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(dialog)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewAddSubTask() string {
|
func (m uiModel) viewAddSubTask() string {
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
dialogStyle := lipgloss.NewStyle().
|
dialogStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("99")).
|
BorderForeground(lipgloss.Color("99")).
|
||||||
|
|
@ -266,21 +319,11 @@ func (m uiModel) viewAddSubTask() string {
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
// Center the dialog
|
// Center the dialog horizontally and vertically
|
||||||
if m.height > 0 {
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
verticalPadding := (m.height - lipgloss.Height(dialog)) / 2
|
|
||||||
if verticalPadding > 0 {
|
|
||||||
b.WriteString(strings.Repeat("\n", verticalPadding))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(dialog)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewSetDeadline() string {
|
func (m uiModel) viewSetDeadline() string {
|
||||||
var b strings.Builder
|
|
||||||
|
|
||||||
dialogStyle := lipgloss.NewStyle().
|
dialogStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("141")).
|
BorderForeground(lipgloss.Color("141")).
|
||||||
|
|
@ -304,16 +347,78 @@ func (m uiModel) viewSetDeadline() string {
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
// Center the dialog
|
// Center the dialog horizontally and vertically
|
||||||
if m.height > 0 {
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
verticalPadding := (m.height - lipgloss.Height(dialog)) / 2
|
}
|
||||||
if verticalPadding > 0 {
|
|
||||||
b.WriteString(strings.Repeat("\n", verticalPadding))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(dialog)
|
|
||||||
|
|
||||||
return b.String()
|
func (m uiModel) viewSetPriority() string {
|
||||||
|
dialogStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("214")).
|
||||||
|
Padding(1, 2).
|
||||||
|
Width(60)
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(titleStyle.Render("Set Priority"))
|
||||||
|
content.WriteString("\n")
|
||||||
|
if m.editingItem != nil {
|
||||||
|
content.WriteString(statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
|
content.WriteString("\n")
|
||||||
|
if m.editingItem.Priority != model.PriorityNone {
|
||||||
|
content.WriteString(statusStyle.Render(fmt.Sprintf("Current: [#%s]", m.editingItem.Priority)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
// Show priority options with styling
|
||||||
|
priorityAStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
|
||||||
|
priorityBStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
|
||||||
|
priorityCStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
|
||||||
|
|
||||||
|
content.WriteString(priorityAStyle.Render("[A] High Priority") + "\n")
|
||||||
|
content.WriteString(priorityBStyle.Render("[B] Medium Priority") + "\n")
|
||||||
|
content.WriteString(priorityCStyle.Render("[C] Low Priority") + "\n")
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(statusStyle.Render("Press Space/Enter to clear priority"))
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(statusStyle.Render("Press ESC to cancel"))
|
||||||
|
|
||||||
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
// Center the dialog horizontally and vertically
|
||||||
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) viewSetEffort() string {
|
||||||
|
dialogStyle := lipgloss.NewStyle().
|
||||||
|
Border(lipgloss.RoundedBorder()).
|
||||||
|
BorderForeground(lipgloss.Color("141")).
|
||||||
|
Padding(1, 2).
|
||||||
|
Width(60)
|
||||||
|
|
||||||
|
var content strings.Builder
|
||||||
|
content.WriteString(titleStyle.Render("Set Effort"))
|
||||||
|
content.WriteString("\n")
|
||||||
|
if m.editingItem != nil {
|
||||||
|
content.WriteString(statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
|
content.WriteString("\n")
|
||||||
|
if m.editingItem.Effort != "" {
|
||||||
|
content.WriteString(statusStyle.Render(fmt.Sprintf("Current: %s", m.editingItem.Effort)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
content.WriteString(m.textinput.View())
|
||||||
|
content.WriteString("\n\n")
|
||||||
|
content.WriteString(statusStyle.Render("Examples: 8h, 2d, 1w, 4h30m"))
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(statusStyle.Render("Leave empty to clear effort"))
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
// Center the dialog horizontally and vertically
|
||||||
|
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, dialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewEditMode() string {
|
func (m uiModel) viewEditMode() string {
|
||||||
|
|
@ -332,10 +437,11 @@ func (m uiModel) viewEditMode() string {
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterLogbookDrawer removes LOGBOOK drawer content and scheduling metadata from notes
|
// filterLogbookDrawer removes LOGBOOK and PROPERTIES drawer content and scheduling metadata from notes
|
||||||
func filterLogbookDrawer(notes []string) []string {
|
func filterLogbookDrawer(notes []string) []string {
|
||||||
var filtered []string
|
var filtered []string
|
||||||
inLogbook := false
|
inLogbook := false
|
||||||
|
inProperties := false
|
||||||
|
|
||||||
for _, note := range notes {
|
for _, note := range notes {
|
||||||
trimmed := strings.TrimSpace(note)
|
trimmed := strings.TrimSpace(note)
|
||||||
|
|
@ -346,14 +452,26 @@ func filterLogbookDrawer(notes []string) []string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for end of LOGBOOK drawer
|
// Check for start of PROPERTIES drawer
|
||||||
if trimmed == ":END:" && inLogbook {
|
if trimmed == ":PROPERTIES:" {
|
||||||
inLogbook = false
|
inProperties = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip lines inside LOGBOOK drawer
|
// Check for end of drawer
|
||||||
|
if trimmed == ":END:" {
|
||||||
if inLogbook {
|
if inLogbook {
|
||||||
|
inLogbook = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inProperties {
|
||||||
|
inProperties = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip lines inside LOGBOOK or PROPERTIES drawer
|
||||||
|
if inLogbook || inProperties {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,9 +635,30 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
b.WriteString(stateStr)
|
b.WriteString(stateStr)
|
||||||
b.WriteString(" ")
|
b.WriteString(" ")
|
||||||
|
|
||||||
|
// Priority
|
||||||
|
if item.Priority != model.PriorityNone {
|
||||||
|
var priorityStyle lipgloss.Style
|
||||||
|
switch item.Priority {
|
||||||
|
case model.PriorityA:
|
||||||
|
priorityStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
|
||||||
|
case model.PriorityB:
|
||||||
|
priorityStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
|
||||||
|
case model.PriorityC:
|
||||||
|
priorityStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
|
||||||
|
}
|
||||||
|
b.WriteString(priorityStyle.Render(fmt.Sprintf("[#%s] ", item.Priority)))
|
||||||
|
}
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
b.WriteString(item.Title)
|
b.WriteString(item.Title)
|
||||||
|
|
||||||
|
// Effort
|
||||||
|
if item.Effort != "" {
|
||||||
|
effortStr := fmt.Sprintf(" (Effort: %s)", item.Effort)
|
||||||
|
effortStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("141")) // Purple
|
||||||
|
b.WriteString(effortStyle.Render(effortStr))
|
||||||
|
}
|
||||||
|
|
||||||
// Clock status
|
// Clock status
|
||||||
if item.IsClockedIn() {
|
if item.IsClockedIn() {
|
||||||
duration := item.GetCurrentClockDuration()
|
duration := item.GetCurrentClockDuration()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue