mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc00d6891 | ||
|
|
fce607e29d | ||
| c858e70d07 | |||
| 0b88465e21 | |||
| 8ff2b254a4 | |||
| 8ed20e48ff |
12 changed files with 644 additions and 190 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 91 KiB |
20
README.md
20
README.md
|
|
@ -23,6 +23,9 @@ org tasks.org # Open specific org file
|
||||||
org /path/to/work.org # Open specific org file with path
|
org /path/to/work.org # Open specific org file with path
|
||||||
org -m # Multi-file: Load all .org files in current directory
|
org -m # Multi-file: Load all .org files in current directory
|
||||||
org -m /path/to/dir # Multi-file: Load all .org files in specified directory
|
org -m /path/to/dir # Multi-file: Load all .org files in specified directory
|
||||||
|
org -c # Quick capture mode
|
||||||
|
org -c "Task description" # Quick capture with pre-filled text
|
||||||
|
echo "Task" | org # Pipe text to capture
|
||||||
```
|
```
|
||||||
|
|
||||||
### Single-File Mode (Default)
|
### Single-File Mode (Default)
|
||||||
|
|
@ -35,6 +38,20 @@ org tasks.org # Opens tasks.org
|
||||||
org ~/work/project.org # Opens specific file
|
org ~/work/project.org # Opens specific file
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Quick Capture Mode
|
||||||
|
|
||||||
|
Use the `-c` or `--capture` flag to quickly add tasks without navigating through the UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
org -c # Open directly in capture mode
|
||||||
|
org -c "Buy groceries" # Capture with pre-filled text
|
||||||
|
org -c "Write report" tasks.org # Capture to specific file
|
||||||
|
echo "Meeting notes" | org # Pipe text to capture
|
||||||
|
echo "Task" | org ~/work.org # Pipe to specific file
|
||||||
|
```
|
||||||
|
|
||||||
|
This is perfect for quickly capturing tasks from scripts, terminal workflows, or shell aliases. The capture mode skips the need to press 'c' once inside the application, making it faster to add quick TODO items.
|
||||||
|
|
||||||
### Multi-File Mode
|
### Multi-File Mode
|
||||||
|
|
||||||
Use the `-m` or `--multi` flag to load all `.org` files in a directory as top-level items. Each file appears as a top-level item in the interface, with its contents nested underneath. Changes made to items are automatically saved back to their respective files.
|
Use the `-m` or `--multi` flag to load all `.org` files in a directory as top-level items. Each file appears as a top-level item in the interface, with its contents nested underneath. Changes made to items are automatically saved back to their respective files.
|
||||||
|
|
@ -92,6 +109,7 @@ Feel free to fork and create a pull request if there's any features missing for
|
||||||
- **Syntax Highlighting**: Code blocks are automatically highlighted (supports both ```lang and #+BEGIN_SRC formats)
|
- **Syntax Highlighting**: Code blocks are automatically highlighted (supports both ```lang and #+BEGIN_SRC formats)
|
||||||
- **Markdown Support**: Use markdown-style code blocks in your notes
|
- **Markdown Support**: Use markdown-style code blocks in your notes
|
||||||
- **Drawer Management**: LOGBOOK and PROPERTIES drawers are automatically filtered in list view
|
- **Drawer Management**: LOGBOOK and PROPERTIES drawers are automatically filtered in list view
|
||||||
|
- **Fold/Unfold All**: Fold/Unfold all items with shift+tab
|
||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
|
|
@ -101,6 +119,7 @@ Feel free to fork and create a pull request if there's any features missing for
|
||||||
| `←/h`, `→/l` | Cycle state backward/forward |
|
| `←/h`, `→/l` | Cycle state backward/forward |
|
||||||
| `t` or `space` | Cycle TODO state |
|
| `t` or `space` | Cycle TODO state |
|
||||||
| `tab` | Fold/unfold item |
|
| `tab` | Fold/unfold item |
|
||||||
|
| `shift+tab` | Fold/Unfold all items |
|
||||||
| `enter` | Edit notes |
|
| `enter` | Edit notes |
|
||||||
| `c` | Capture new TODO |
|
| `c` | Capture new TODO |
|
||||||
| `s` | Add sub-task |
|
| `s` | Add sub-task |
|
||||||
|
|
@ -111,6 +130,7 @@ Feel free to fork and create a pull request if there's any features missing for
|
||||||
| `i` | Clock in |
|
| `i` | Clock in |
|
||||||
| `o` | Clock out |
|
| `o` | Clock out |
|
||||||
| `d` | Set deadline |
|
| `d` | Set deadline |
|
||||||
|
| `S` | Set scheduled date |
|
||||||
| `p` | Set priority |
|
| `p` | Set priority |
|
||||||
| `e` | Set effort |
|
| `e` | Set effort |
|
||||||
| `r` | Toggle reorder mode |
|
| `r` | Toggle reorder mode |
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
|
@ -15,13 +17,43 @@ import (
|
||||||
func main() {
|
func main() {
|
||||||
var filePath string
|
var filePath string
|
||||||
var multiMode bool
|
var multiMode bool
|
||||||
|
var captureMode bool
|
||||||
flag.BoolVar(&multiMode, "multi", false, "Load all org files in current directory as top-level items")
|
flag.BoolVar(&multiMode, "multi", false, "Load all org files in current directory as top-level items")
|
||||||
flag.BoolVar(&multiMode, "m", false, "Load all org files in current directory (shorthand)")
|
flag.BoolVar(&multiMode, "m", false, "Load all org files in current directory (shorthand)")
|
||||||
|
flag.BoolVar(&captureMode, "capture", false, "Start in capture mode")
|
||||||
|
flag.BoolVar(&captureMode, "c", false, "Start in capture mode (shorthand)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Check for positional argument
|
// Check for positional argument or capture text
|
||||||
if filePath == "" && len(flag.Args()) > 0 {
|
var captureText string
|
||||||
filePath = flag.Args()[0]
|
if len(flag.Args()) > 0 {
|
||||||
|
if captureMode {
|
||||||
|
// First argument is capture text when in capture mode
|
||||||
|
captureText = flag.Args()[0]
|
||||||
|
// Second argument (if present) is the file path
|
||||||
|
if len(flag.Args()) > 1 {
|
||||||
|
filePath = flag.Args()[1]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First argument is file path in normal mode
|
||||||
|
filePath = flag.Args()[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if input is being piped
|
||||||
|
stat, _ := os.Stdin.Stat()
|
||||||
|
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||||
|
// Data is being piped to stdin
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
pipedText, err := io.ReadAll(reader)
|
||||||
|
if err == nil && len(pipedText) > 0 {
|
||||||
|
captureMode = true
|
||||||
|
captureText = string(pipedText)
|
||||||
|
// If no file path was provided via args, check if last arg could be a path
|
||||||
|
if filePath == "" && len(flag.Args()) > 0 {
|
||||||
|
filePath = flag.Args()[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
|
|
@ -81,7 +113,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the UI
|
// Run the UI
|
||||||
if err := ui.RunUI(orgFile, cfg); err != nil {
|
if err := ui.RunUI(orgFile, cfg, captureMode, captureText); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error running UI: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error running UI: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type KeybindingsConfig struct {
|
||||||
Rename []string `toml:"rename"`
|
Rename []string `toml:"rename"`
|
||||||
CycleState []string `toml:"cycle_state"`
|
CycleState []string `toml:"cycle_state"`
|
||||||
ToggleFold []string `toml:"toggle_fold"`
|
ToggleFold []string `toml:"toggle_fold"`
|
||||||
|
ToggleFoldAll []string `toml:"toggle_fold_all"`
|
||||||
EditNotes []string `toml:"edit_notes"`
|
EditNotes []string `toml:"edit_notes"`
|
||||||
ToggleView []string `toml:"toggle_view"`
|
ToggleView []string `toml:"toggle_view"`
|
||||||
Capture []string `toml:"capture"`
|
Capture []string `toml:"capture"`
|
||||||
|
|
@ -41,6 +42,7 @@ type KeybindingsConfig struct {
|
||||||
ClockIn []string `toml:"clock_in"`
|
ClockIn []string `toml:"clock_in"`
|
||||||
ClockOut []string `toml:"clock_out"`
|
ClockOut []string `toml:"clock_out"`
|
||||||
SetDeadline []string `toml:"set_deadline"`
|
SetDeadline []string `toml:"set_deadline"`
|
||||||
|
SetScheduled []string `toml:"set_scheduled"`
|
||||||
SetPriority []string `toml:"set_priority"`
|
SetPriority []string `toml:"set_priority"`
|
||||||
SetEffort []string `toml:"set_effort"`
|
SetEffort []string `toml:"set_effort"`
|
||||||
Help []string `toml:"help"`
|
Help []string `toml:"help"`
|
||||||
|
|
@ -91,9 +93,12 @@ type StatesConfig struct {
|
||||||
|
|
||||||
// UIConfig holds UI-related configurations
|
// UIConfig holds UI-related configurations
|
||||||
type UIConfig struct {
|
type UIConfig struct {
|
||||||
HelpTextWidth int `toml:"help_text_width"`
|
HelpTextWidth int `toml:"help_text_width"`
|
||||||
MinTerminalWidth int `toml:"min_terminal_width"`
|
MinTerminalWidth int `toml:"min_terminal_width"`
|
||||||
AgendaDays int `toml:"agenda_days"`
|
AgendaDays int `toml:"agenda_days"`
|
||||||
|
OrgSyntaxHighlighting bool `toml:"org_syntax_highlighting"`
|
||||||
|
ShowIndentationGuides bool `toml:"show_indentation_guides"`
|
||||||
|
IndentationGuideColor string `toml:"indentation_guide_color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns the default configuration
|
// DefaultConfig returns the default configuration
|
||||||
|
|
@ -111,6 +116,7 @@ func DefaultConfig() *Config {
|
||||||
Rename: []string{"R"},
|
Rename: []string{"R"},
|
||||||
CycleState: []string{"t", " "},
|
CycleState: []string{"t", " "},
|
||||||
ToggleFold: []string{"tab"},
|
ToggleFold: []string{"tab"},
|
||||||
|
ToggleFoldAll: []string{"shift+tab", "backtab"},
|
||||||
EditNotes: []string{"enter"},
|
EditNotes: []string{"enter"},
|
||||||
ToggleView: []string{"a"},
|
ToggleView: []string{"a"},
|
||||||
Capture: []string{"c"},
|
Capture: []string{"c"},
|
||||||
|
|
@ -121,6 +127,7 @@ func DefaultConfig() *Config {
|
||||||
ClockIn: []string{"i"},
|
ClockIn: []string{"i"},
|
||||||
ClockOut: []string{"o"},
|
ClockOut: []string{"o"},
|
||||||
SetDeadline: []string{"d"},
|
SetDeadline: []string{"d"},
|
||||||
|
SetScheduled: []string{"S"},
|
||||||
SetPriority: []string{"p"},
|
SetPriority: []string{"p"},
|
||||||
SetEffort: []string{"e"},
|
SetEffort: []string{"e"},
|
||||||
Help: []string{"?"},
|
Help: []string{"?"},
|
||||||
|
|
@ -161,9 +168,12 @@ func DefaultConfig() *Config {
|
||||||
DefaultNewTaskState: "TODO",
|
DefaultNewTaskState: "TODO",
|
||||||
},
|
},
|
||||||
UI: UIConfig{
|
UI: UIConfig{
|
||||||
HelpTextWidth: 22,
|
HelpTextWidth: 22,
|
||||||
MinTerminalWidth: 40,
|
MinTerminalWidth: 40,
|
||||||
AgendaDays: 7,
|
AgendaDays: 7,
|
||||||
|
OrgSyntaxHighlighting: true,
|
||||||
|
ShowIndentationGuides: true,
|
||||||
|
IndentationGuideColor: "245",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +286,9 @@ func (c *Config) fillDefaults() {
|
||||||
if len(c.Keybindings.ToggleFold) == 0 {
|
if len(c.Keybindings.ToggleFold) == 0 {
|
||||||
c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold
|
c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold
|
||||||
}
|
}
|
||||||
|
if len(c.Keybindings.ToggleFoldAll) == 0 {
|
||||||
|
c.Keybindings.ToggleFoldAll = defaults.Keybindings.ToggleFoldAll
|
||||||
|
}
|
||||||
if len(c.Keybindings.EditNotes) == 0 {
|
if len(c.Keybindings.EditNotes) == 0 {
|
||||||
c.Keybindings.EditNotes = defaults.Keybindings.EditNotes
|
c.Keybindings.EditNotes = defaults.Keybindings.EditNotes
|
||||||
}
|
}
|
||||||
|
|
@ -306,6 +319,9 @@ func (c *Config) fillDefaults() {
|
||||||
if len(c.Keybindings.SetDeadline) == 0 {
|
if len(c.Keybindings.SetDeadline) == 0 {
|
||||||
c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline
|
c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline
|
||||||
}
|
}
|
||||||
|
if len(c.Keybindings.SetScheduled) == 0 {
|
||||||
|
c.Keybindings.SetScheduled = defaults.Keybindings.SetScheduled
|
||||||
|
}
|
||||||
if len(c.Keybindings.SetPriority) == 0 {
|
if len(c.Keybindings.SetPriority) == 0 {
|
||||||
c.Keybindings.SetPriority = defaults.Keybindings.SetPriority
|
c.Keybindings.SetPriority = defaults.Keybindings.SetPriority
|
||||||
}
|
}
|
||||||
|
|
@ -371,10 +387,11 @@ func (c *Config) fillDefaults() {
|
||||||
// Fill states if empty
|
// Fill states if empty
|
||||||
if len(c.States.States) == 0 {
|
if len(c.States.States) == 0 {
|
||||||
c.States.States = defaults.States.States
|
c.States.States = defaults.States.States
|
||||||
}
|
// Also set the default new task state since the entire states section is missing
|
||||||
if c.States.DefaultNewTaskState == "" {
|
|
||||||
c.States.DefaultNewTaskState = defaults.States.DefaultNewTaskState
|
c.States.DefaultNewTaskState = defaults.States.DefaultNewTaskState
|
||||||
}
|
}
|
||||||
|
// Note: We don't fill DefaultNewTaskState if States.States is non-empty because
|
||||||
|
// an empty string is a valid intentional value meaning "no default state".
|
||||||
|
|
||||||
// Fill UI if zero values
|
// Fill UI if zero values
|
||||||
if c.UI.HelpTextWidth == 0 {
|
if c.UI.HelpTextWidth == 0 {
|
||||||
|
|
@ -386,6 +403,9 @@ func (c *Config) fillDefaults() {
|
||||||
if c.UI.AgendaDays == 0 {
|
if c.UI.AgendaDays == 0 {
|
||||||
c.UI.AgendaDays = defaults.UI.AgendaDays
|
c.UI.AgendaDays = defaults.UI.AgendaDays
|
||||||
}
|
}
|
||||||
|
if c.UI.IndentationGuideColor == "" {
|
||||||
|
c.UI.IndentationGuideColor = defaults.UI.IndentationGuideColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildKeyBinding creates a key.Binding from config
|
// BuildKeyBinding creates a key.Binding from config
|
||||||
|
|
@ -507,6 +527,8 @@ func (c *Config) UpdateKeybinding(action string, keys []string) error {
|
||||||
c.Keybindings.CycleState = keys
|
c.Keybindings.CycleState = keys
|
||||||
case "toggle_fold":
|
case "toggle_fold":
|
||||||
c.Keybindings.ToggleFold = keys
|
c.Keybindings.ToggleFold = keys
|
||||||
|
case "toggle_fold_all":
|
||||||
|
c.Keybindings.ToggleFoldAll = keys
|
||||||
case "edit_notes":
|
case "edit_notes":
|
||||||
c.Keybindings.EditNotes = keys
|
c.Keybindings.EditNotes = keys
|
||||||
case "capture":
|
case "capture":
|
||||||
|
|
@ -536,33 +558,35 @@ func (c *Config) UpdateKeybinding(action string, keys []string) error {
|
||||||
// GetAllKeybindings returns a map of all keybindings
|
// GetAllKeybindings returns a map of all keybindings
|
||||||
func (c *Config) GetAllKeybindings() map[string][]string {
|
func (c *Config) GetAllKeybindings() map[string][]string {
|
||||||
return map[string][]string{
|
return map[string][]string{
|
||||||
"up": c.Keybindings.Up,
|
"up": c.Keybindings.Up,
|
||||||
"down": c.Keybindings.Down,
|
"down": c.Keybindings.Down,
|
||||||
"left": c.Keybindings.Left,
|
"left": c.Keybindings.Left,
|
||||||
"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_left": c.Keybindings.ShiftLeft,
|
||||||
"shift_right": c.Keybindings.ShiftRight,
|
"shift_right": c.Keybindings.ShiftRight,
|
||||||
"rename": c.Keybindings.Rename,
|
"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,
|
"toggle_fold_all": c.Keybindings.ToggleFoldAll,
|
||||||
"toggle_view": c.Keybindings.ToggleView,
|
"edit_notes": c.Keybindings.EditNotes,
|
||||||
"capture": c.Keybindings.Capture,
|
"toggle_view": c.Keybindings.ToggleView,
|
||||||
"add_subtask": c.Keybindings.AddSubTask,
|
"capture": c.Keybindings.Capture,
|
||||||
"delete": c.Keybindings.Delete,
|
"add_subtask": c.Keybindings.AddSubTask,
|
||||||
"save": c.Keybindings.Save,
|
"delete": c.Keybindings.Delete,
|
||||||
"toggle_reorder": c.Keybindings.ToggleReorder,
|
"save": c.Keybindings.Save,
|
||||||
"clock_in": c.Keybindings.ClockIn,
|
"toggle_reorder": c.Keybindings.ToggleReorder,
|
||||||
"clock_out": c.Keybindings.ClockOut,
|
"clock_in": c.Keybindings.ClockIn,
|
||||||
"set_deadline": c.Keybindings.SetDeadline,
|
"clock_out": c.Keybindings.ClockOut,
|
||||||
"set_priority": c.Keybindings.SetPriority,
|
"set_deadline": c.Keybindings.SetDeadline,
|
||||||
"set_effort": c.Keybindings.SetEffort,
|
"set_scheduled": c.Keybindings.SetScheduled,
|
||||||
"help": c.Keybindings.Help,
|
"set_priority": c.Keybindings.SetPriority,
|
||||||
"quit": c.Keybindings.Quit,
|
"set_effort": c.Keybindings.SetEffort,
|
||||||
"settings": c.Keybindings.Settings,
|
"help": c.Keybindings.Help,
|
||||||
"tag_item": c.Keybindings.TagItem,
|
"quit": c.Keybindings.Quit,
|
||||||
|
"settings": c.Keybindings.Settings,
|
||||||
|
"tag_item": c.Keybindings.TagItem,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ type Item struct {
|
||||||
Tags []string // Tags for this item (e.g., :work:urgent:)
|
Tags []string // Tags for this item (e.g., :work:urgent:)
|
||||||
Scheduled *time.Time
|
Scheduled *time.Time
|
||||||
Deadline *time.Time
|
Deadline *time.Time
|
||||||
|
Closed *time.Time // Closed timestamp (when task was marked as done)
|
||||||
Effort string // Effort estimate (e.g., "8h", "2d")
|
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
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,16 @@ import (
|
||||||
|
|
||||||
// Parser patterns
|
// Parser patterns
|
||||||
var (
|
var (
|
||||||
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*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
closedPattern = regexp.MustCompile(`CLOSED:\s*\[([^\]]+)\]`)
|
||||||
effortPattern = regexp.MustCompile(`^\s*:EFFORT:\s*(.+)$`)
|
clockPattern = regexp.MustCompile(`CLOCK:\s*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
||||||
logbookDrawerStart = regexp.MustCompile(`^\s*:LOGBOOK:\s*$`)
|
effortPattern = regexp.MustCompile(`^\s*:EFFORT:\s*(.+)$`)
|
||||||
|
logbookDrawerStart = regexp.MustCompile(`^\s*:LOGBOOK:\s*$`)
|
||||||
propertiesDrawerStart = regexp.MustCompile(`^\s*:PROPERTIES:\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`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// buildHeadingPattern creates a regex pattern that matches configured states
|
// buildHeadingPattern creates a regex pattern that matches configured states
|
||||||
|
|
@ -187,6 +188,13 @@ func ParseOrgFile(path string, cfg *config.Config) (*model.OrgFile, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for CLOSED
|
||||||
|
if matches := closedPattern.FindStringSubmatch(line); matches != nil {
|
||||||
|
if t, err := parseClockTimestamp(matches[1]); err == nil {
|
||||||
|
currentItem.Closed = &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for EFFORT (inside PROPERTIES drawer)
|
// Check for EFFORT (inside PROPERTIES drawer)
|
||||||
if matches := effortPattern.FindStringSubmatch(line); matches != nil {
|
if matches := effortPattern.FindStringSubmatch(line); matches != nil {
|
||||||
currentItem.Effort = strings.TrimSpace(matches[1])
|
currentItem.Effort = strings.TrimSpace(matches[1])
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
// Write scheduling info if not already in notes
|
// Write scheduling info if not already in notes
|
||||||
hasScheduled := false
|
hasScheduled := false
|
||||||
hasDeadline := false
|
hasDeadline := false
|
||||||
|
hasClosed := false
|
||||||
hasLogbook := false
|
hasLogbook := false
|
||||||
hasProperties := false
|
hasProperties := false
|
||||||
for _, note := range item.Notes {
|
for _, note := range item.Notes {
|
||||||
|
|
@ -137,6 +138,9 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
if strings.Contains(note, "DEADLINE:") {
|
if strings.Contains(note, "DEADLINE:") {
|
||||||
hasDeadline = true
|
hasDeadline = true
|
||||||
}
|
}
|
||||||
|
if strings.Contains(note, "CLOSED:") {
|
||||||
|
hasClosed = true
|
||||||
|
}
|
||||||
if strings.Contains(note, ":LOGBOOK:") {
|
if strings.Contains(note, ":LOGBOOK:") {
|
||||||
hasLogbook = true
|
hasLogbook = true
|
||||||
}
|
}
|
||||||
|
|
@ -145,6 +149,13 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.Closed != nil && !hasClosed {
|
||||||
|
closedLine := fmt.Sprintf("CLOSED: [%s]\n", formatClockTimestamp(*item.Closed))
|
||||||
|
if _, err := writer.WriteString(closedLine); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if item.Scheduled != nil && !hasScheduled {
|
if item.Scheduled != nil && !hasScheduled {
|
||||||
scheduledLine := fmt.Sprintf("SCHEDULED: <%s>\n", FormatOrgDate(*item.Scheduled))
|
scheduledLine := fmt.Sprintf("SCHEDULED: <%s>\n", FormatOrgDate(*item.Scheduled))
|
||||||
if _, err := writer.WriteString(scheduledLine); err != nil {
|
if _, err := writer.WriteString(scheduledLine); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const (
|
||||||
modeCapture
|
modeCapture
|
||||||
modeAddSubTask
|
modeAddSubTask
|
||||||
modeSetDeadline
|
modeSetDeadline
|
||||||
|
modeSetScheduled
|
||||||
modeSetPriority
|
modeSetPriority
|
||||||
modeSetEffort
|
modeSetEffort
|
||||||
modeHelp
|
modeHelp
|
||||||
|
|
@ -31,31 +32,31 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
orgFile *model.OrgFile
|
orgFile *model.OrgFile
|
||||||
cursor int
|
cursor int
|
||||||
scrollOffset int // Track the scroll position
|
scrollOffset int // Track the scroll position
|
||||||
helpScroll int // Track scroll position in help mode
|
helpScroll int // Track scroll position in help mode
|
||||||
mode viewMode
|
mode viewMode
|
||||||
help help.Model
|
help help.Model
|
||||||
keys keyMap
|
keys keyMap
|
||||||
styles styleMap
|
styles styleMap
|
||||||
config *config.Config
|
config *config.Config
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
statusMsg string
|
statusMsg string
|
||||||
statusExpiry time.Time
|
statusExpiry time.Time
|
||||||
editingItem *model.Item
|
editingItem *model.Item
|
||||||
textarea textarea.Model
|
textarea textarea.Model
|
||||||
textinput textinput.Model
|
textinput textinput.Model
|
||||||
itemToDelete *model.Item
|
itemToDelete *model.Item
|
||||||
reorderMode bool
|
reorderMode bool
|
||||||
settingsCursor int // Cursor position in settings view
|
settingsCursor int // Cursor position in settings view
|
||||||
settingsScroll int // Scroll position in settings view
|
settingsScroll int // Scroll position in settings view
|
||||||
settingsSection settingsSection // Current settings section/tab
|
settingsSection settingsSection // Current settings section/tab
|
||||||
captureCursor int // Store cursor position when entering capture mode
|
captureCursor int // Store cursor position when entering capture mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitialModel(orgFile *model.OrgFile, cfg *config.Config) uiModel {
|
func InitialModel(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) uiModel {
|
||||||
ta := textarea.New()
|
ta := textarea.New()
|
||||||
ta.Placeholder = "Enter notes here (code blocks supported)..."
|
ta.Placeholder = "Enter notes here (code blocks supported)..."
|
||||||
ta.ShowLineNumbers = false
|
ta.ShowLineNumbers = false
|
||||||
|
|
@ -67,10 +68,16 @@ func InitialModel(orgFile *model.OrgFile, cfg *config.Config) uiModel {
|
||||||
h := help.New()
|
h := help.New()
|
||||||
h.ShowAll = false
|
h.ShowAll = false
|
||||||
|
|
||||||
|
mode := modeList
|
||||||
|
if captureMode {
|
||||||
|
mode = modeCapture
|
||||||
|
ti.SetValue(strings.TrimSpace(captureText))
|
||||||
|
}
|
||||||
|
|
||||||
return uiModel{
|
return uiModel{
|
||||||
orgFile: orgFile,
|
orgFile: orgFile,
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
mode: modeList,
|
mode: mode,
|
||||||
help: h,
|
help: h,
|
||||||
keys: newKeyMapFromConfig(cfg),
|
keys: newKeyMapFromConfig(cfg),
|
||||||
styles: newStyleMapFromConfig(cfg),
|
styles: newStyleMapFromConfig(cfg),
|
||||||
|
|
@ -81,6 +88,9 @@ func InitialModel(orgFile *model.OrgFile, cfg *config.Config) uiModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) Init() tea.Cmd {
|
func (m uiModel) Init() tea.Cmd {
|
||||||
|
if m.mode == modeCapture {
|
||||||
|
return textinput.Blink
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +122,7 @@ func (m *uiModel) updateScrollOffset(availableHeight int) {
|
||||||
noteIndent := indent + " "
|
noteIndent := indent + " "
|
||||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||||
highlightedNotes := renderNotesWithHighlighting(wrappedNotes)
|
highlightedNotes := m.renderNotesWithHighlighting(wrappedNotes)
|
||||||
lineCount += len(highlightedNotes)
|
lineCount += len(highlightedNotes)
|
||||||
}
|
}
|
||||||
itemLineCount[i] = lineCount
|
itemLineCount[i] = lineCount
|
||||||
|
|
@ -140,8 +150,12 @@ func (m *uiModel) updateScrollOffset(availableHeight int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUI starts the terminal UI
|
// RunUI starts the terminal UI
|
||||||
func RunUI(orgFile *model.OrgFile, cfg *config.Config) error {
|
func RunUI(orgFile *model.OrgFile, cfg *config.Config, captureMode bool, captureText string) error {
|
||||||
p := tea.NewProgram(InitialModel(orgFile, cfg), tea.WithAltScreen())
|
m := InitialModel(orgFile, cfg, captureMode, captureText)
|
||||||
|
if captureMode {
|
||||||
|
m.textinput.Focus()
|
||||||
|
}
|
||||||
|
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||||
_, err := p.Run()
|
_, err := p.Run()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@ type keyMap struct {
|
||||||
Delete key.Binding
|
Delete key.Binding
|
||||||
Save key.Binding
|
Save key.Binding
|
||||||
ToggleFold key.Binding
|
ToggleFold key.Binding
|
||||||
|
ToggleFoldAll key.Binding
|
||||||
EditNotes key.Binding
|
EditNotes key.Binding
|
||||||
ToggleReorder key.Binding
|
ToggleReorder key.Binding
|
||||||
ClockIn key.Binding
|
ClockIn key.Binding
|
||||||
ClockOut key.Binding
|
ClockOut key.Binding
|
||||||
SetDeadline key.Binding
|
SetDeadline key.Binding
|
||||||
|
SetScheduled key.Binding
|
||||||
SetPriority key.Binding
|
SetPriority key.Binding
|
||||||
SetEffort key.Binding
|
SetEffort key.Binding
|
||||||
Settings key.Binding
|
Settings key.Binding
|
||||||
|
|
@ -86,6 +88,10 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
||||||
key.WithKeys(kb.ToggleFold...),
|
key.WithKeys(kb.ToggleFold...),
|
||||||
key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"),
|
key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"),
|
||||||
),
|
),
|
||||||
|
ToggleFoldAll: key.NewBinding(
|
||||||
|
key.WithKeys(kb.ToggleFoldAll...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.ToggleFoldAll), "fold/unfold all"),
|
||||||
|
),
|
||||||
EditNotes: key.NewBinding(
|
EditNotes: key.NewBinding(
|
||||||
key.WithKeys(kb.EditNotes...),
|
key.WithKeys(kb.EditNotes...),
|
||||||
key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"),
|
key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"),
|
||||||
|
|
@ -126,6 +132,10 @@ func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
||||||
key.WithKeys(kb.SetDeadline...),
|
key.WithKeys(kb.SetDeadline...),
|
||||||
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
|
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
|
||||||
),
|
),
|
||||||
|
SetScheduled: key.NewBinding(
|
||||||
|
key.WithKeys(kb.SetScheduled...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.SetScheduled), "set scheduled"),
|
||||||
|
),
|
||||||
SetPriority: key.NewBinding(
|
SetPriority: key.NewBinding(
|
||||||
key.WithKeys(kb.SetPriority...),
|
key.WithKeys(kb.SetPriority...),
|
||||||
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
|
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
|
||||||
|
|
@ -183,7 +193,7 @@ func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
// This will be overridden by custom rendering in viewFullHelp
|
// This will be overridden by custom rendering in viewFullHelp
|
||||||
return [][]key.Binding{
|
return [][]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.ToggleFoldAll, k.EditNotes, k.ToggleReorder},
|
||||||
{k.Capture, k.AddSubTask, k.Delete, k.Save},
|
{k.Capture, k.AddSubTask, k.Delete, k.Save},
|
||||||
{k.ToggleView, k.Help, k.Quit},
|
{k.ToggleView, k.Help, k.Quit},
|
||||||
}
|
}
|
||||||
|
|
@ -193,9 +203,9 @@ func (k keyMap) FullHelp() [][]key.Binding {
|
||||||
func (k keyMap) getAllBindings() []key.Binding {
|
func (k keyMap) getAllBindings() []key.Binding {
|
||||||
return []key.Binding{
|
return []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.ToggleFoldAll, 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.SetPriority, k.SetEffort,
|
k.ClockIn, k.ClockOut, k.SetDeadline, k.SetScheduled, k.SetPriority, k.SetEffort,
|
||||||
k.TagItem, k.Settings, k.ToggleView, k.Help, k.Quit,
|
k.TagItem, k.Settings, k.ToggleView, k.Help, k.Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ 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 modeSetScheduled:
|
||||||
|
return m.updateSetScheduled(msg)
|
||||||
case modeSetPriority:
|
case modeSetPriority:
|
||||||
return m.updateSetPriority(msg)
|
return m.updateSetPriority(msg)
|
||||||
case modeSetEffort:
|
case modeSetEffort:
|
||||||
|
|
@ -153,6 +155,46 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ToggleFoldAll):
|
||||||
|
if len(m.orgFile.Items) > 0 {
|
||||||
|
// Check if any top-level item is not folded
|
||||||
|
anyUnfolded := false
|
||||||
|
for _, item := range m.orgFile.Items {
|
||||||
|
if !item.Folded {
|
||||||
|
anyUnfolded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if anyUnfolded {
|
||||||
|
// Fold all items recursively (collapse all)
|
||||||
|
var foldAll func([]*model.Item)
|
||||||
|
foldAll = func(items []*model.Item) {
|
||||||
|
for _, item := range items {
|
||||||
|
item.Folded = true
|
||||||
|
if len(item.Children) > 0 {
|
||||||
|
foldAll(item.Children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foldAll(m.orgFile.Items)
|
||||||
|
m.setStatus("All items folded")
|
||||||
|
} else {
|
||||||
|
// Unfold everything recursively
|
||||||
|
var unfoldAll func([]*model.Item)
|
||||||
|
unfoldAll = func(items []*model.Item) {
|
||||||
|
for _, item := range items {
|
||||||
|
item.Folded = false
|
||||||
|
if len(item.Children) > 0 {
|
||||||
|
unfoldAll(item.Children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unfoldAll(m.orgFile.Items)
|
||||||
|
m.setStatus("All items unfolded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.EditNotes):
|
case key.Matches(msg, m.keys.EditNotes):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
|
@ -281,6 +323,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.SetScheduled):
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
m.editingItem = items[m.cursor]
|
||||||
|
m.mode = modeSetScheduled
|
||||||
|
m.textinput.SetValue("")
|
||||||
|
m.textinput.Placeholder = "YYYY-MM-DD or +N (days from today)"
|
||||||
|
m.textinput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.SetPriority):
|
case key.Matches(msg, m.keys.SetPriority):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
|
@ -521,83 +574,11 @@ func (m uiModel) updateAddSubTask(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) updateSetDeadline(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) updateSetDeadline(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmd tea.Cmd
|
return m.updateSetDate(msg, "DEADLINE")
|
||||||
|
|
||||||
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 deadline
|
|
||||||
m.editingItem.Deadline = nil
|
|
||||||
// Remove DEADLINE line from notes (only lines starting with DEADLINE:)
|
|
||||||
var filteredNotes []string
|
|
||||||
for _, note := range m.editingItem.Notes {
|
|
||||||
trimmedNote := strings.TrimSpace(note)
|
|
||||||
if !strings.HasPrefix(trimmedNote, "DEADLINE:") {
|
|
||||||
filteredNotes = append(filteredNotes, note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.editingItem.Notes = filteredNotes
|
|
||||||
m.setStatus("Deadline cleared!")
|
|
||||||
} else {
|
|
||||||
deadline, err := parseDeadlineInput(input)
|
|
||||||
if err != nil {
|
|
||||||
m.setStatus(fmt.Sprintf("Invalid date: %v", err))
|
|
||||||
} else {
|
|
||||||
m.editingItem.Deadline = &deadline
|
|
||||||
// Also update or add DEADLINE line in notes
|
|
||||||
updatedNotes := false
|
|
||||||
for i, note := range m.editingItem.Notes {
|
|
||||||
trimmedNote := strings.TrimSpace(note)
|
|
||||||
if strings.HasPrefix(trimmedNote, "DEADLINE:") {
|
|
||||||
m.editingItem.Notes[i] = fmt.Sprintf("DEADLINE: <%s>", parser.FormatOrgDate(deadline))
|
|
||||||
updatedNotes = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If DEADLINE wasn't in notes, it will be added by writeItem
|
|
||||||
if !updatedNotes {
|
|
||||||
// Remove old deadline lines just to be safe
|
|
||||||
var filteredNotes []string
|
|
||||||
for _, note := range m.editingItem.Notes {
|
|
||||||
trimmedNote := strings.TrimSpace(note)
|
|
||||||
if !strings.HasPrefix(trimmedNote, "DEADLINE:") {
|
|
||||||
filteredNotes = append(filteredNotes, note)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.editingItem.Notes = filteredNotes
|
|
||||||
}
|
|
||||||
m.setStatus("Deadline 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDeadlineInput parses deadline input like "2024-01-15" or "+3" (3 days from now)
|
// parseDateInput parses date input like "2024-01-15" or "+3" (3 days from now)
|
||||||
func parseDeadlineInput(input string) (time.Time, error) {
|
func parseDateInput(input string) (time.Time, error) {
|
||||||
// Check if it's a relative date (+N days)
|
// Check if it's a relative date (+N days)
|
||||||
if strings.HasPrefix(input, "+") {
|
if strings.HasPrefix(input, "+") {
|
||||||
daysStr := strings.TrimPrefix(input, "+")
|
daysStr := strings.TrimPrefix(input, "+")
|
||||||
|
|
@ -625,6 +606,110 @@ 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) updateSetScheduled(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
return m.updateSetDate(msg, "SCHEDULED")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) updateSetDate(msg tea.Msg, dateType string) (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 {
|
||||||
|
var prefixDate string
|
||||||
|
var clearedDateMsg string
|
||||||
|
var setDateMsg string
|
||||||
|
|
||||||
|
if dateType == "DEADLINE" {
|
||||||
|
prefixDate = "DEADLINE:"
|
||||||
|
clearedDateMsg = "Deadline cleared!"
|
||||||
|
setDateMsg = "Deadline set!"
|
||||||
|
} else {
|
||||||
|
prefixDate = "SCHEDULED:"
|
||||||
|
clearedDateMsg = "Scheduled date cleared!"
|
||||||
|
setDateMsg = "Scheduled date set!"
|
||||||
|
}
|
||||||
|
|
||||||
|
if input == "" {
|
||||||
|
// Empty input clears the date
|
||||||
|
if dateType == "DEADLINE" {
|
||||||
|
m.editingItem.Deadline = nil
|
||||||
|
} else {
|
||||||
|
m.editingItem.Scheduled = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove property line from notes
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range m.editingItem.Notes {
|
||||||
|
trimmedNote := strings.TrimSpace(note)
|
||||||
|
if !strings.HasPrefix(trimmedNote, prefixDate) {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.editingItem.Notes = filteredNotes
|
||||||
|
m.setStatus(clearedDateMsg)
|
||||||
|
} else {
|
||||||
|
dateVal, err := parseDateInput(input)
|
||||||
|
if err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Invalid date: %v", err))
|
||||||
|
} else {
|
||||||
|
if dateType == "DEADLINE" {
|
||||||
|
m.editingItem.Deadline = &dateVal
|
||||||
|
} else {
|
||||||
|
m.editingItem.Scheduled = &dateVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also update or add property line in notes
|
||||||
|
updatedNotes := false
|
||||||
|
for i, note := range m.editingItem.Notes {
|
||||||
|
trimmedNote := strings.TrimSpace(note)
|
||||||
|
if strings.HasPrefix(trimmedNote, prefixDate) {
|
||||||
|
m.editingItem.Notes[i] = fmt.Sprintf("%s <%s>", prefixDate, parser.FormatOrgDate(dateVal))
|
||||||
|
updatedNotes = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If property wasn't in notes, it will be added by writeItem
|
||||||
|
if !updatedNotes {
|
||||||
|
// Remove old property lines just to be safe
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range m.editingItem.Notes {
|
||||||
|
trimmedNote := strings.TrimSpace(note)
|
||||||
|
if !strings.HasPrefix(trimmedNote, prefixDate) {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.editingItem.Notes = filteredNotes
|
||||||
|
}
|
||||||
|
m.setStatus(setDateMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) updateSetPriority(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m uiModel) updateSetPriority(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
|
|
@ -726,6 +811,7 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
// Find current state index
|
// Find current state index
|
||||||
currentIndex := -1
|
currentIndex := -1
|
||||||
currentState := string(item.State)
|
currentState := string(item.State)
|
||||||
|
lastStateIndex := len(stateNames) - 1
|
||||||
|
|
||||||
// Handle empty state
|
// Handle empty state
|
||||||
if currentState == "" {
|
if currentState == "" {
|
||||||
|
|
@ -739,15 +825,51 @@ func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the old state to check if we're transitioning to/from DONE
|
||||||
|
oldState := currentState
|
||||||
|
var newState string
|
||||||
|
|
||||||
// Cycle forward
|
// Cycle forward
|
||||||
if currentIndex < 0 || currentIndex >= len(stateNames)-1 {
|
if currentIndex < 0 || currentIndex >= len(stateNames)-1 {
|
||||||
if currentIndex == len(stateNames)-1 {
|
if currentIndex == len(stateNames)-1 {
|
||||||
item.State = model.TodoState("") // Back to empty
|
newState = "" // Back to empty
|
||||||
} else {
|
} else {
|
||||||
item.State = model.TodoState(stateNames[0]) // First state
|
newState = stateNames[0] // First state
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item.State = model.TodoState(stateNames[currentIndex+1])
|
newState = stateNames[currentIndex+1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item state
|
||||||
|
item.State = model.TodoState(newState)
|
||||||
|
|
||||||
|
// Manage CLOSED timestamp
|
||||||
|
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
||||||
|
isInDoneState := (newState == stateNames[lastStateIndex])
|
||||||
|
|
||||||
|
if isInDoneState && !wasInDoneState {
|
||||||
|
// Moving TO done state - add CLOSED timestamp
|
||||||
|
now := time.Now()
|
||||||
|
item.Closed = &now
|
||||||
|
// Remove any existing CLOSED line from notes
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range item.Notes {
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.Notes = filteredNotes
|
||||||
|
} else if wasInDoneState && !isInDoneState {
|
||||||
|
// Moving FROM done state - remove CLOSED timestamp
|
||||||
|
item.Closed = nil
|
||||||
|
// Remove any existing CLOSED line from notes
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range item.Notes {
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.Notes = filteredNotes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -760,6 +882,7 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
// Find current state index
|
// Find current state index
|
||||||
currentIndex := -1
|
currentIndex := -1
|
||||||
currentState := string(item.State)
|
currentState := string(item.State)
|
||||||
|
lastStateIndex := len(stateNames) - 1
|
||||||
|
|
||||||
// Handle empty state
|
// Handle empty state
|
||||||
if currentState == "" {
|
if currentState == "" {
|
||||||
|
|
@ -773,13 +896,49 @@ func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store the old state to check if we're transitioning to/from DONE
|
||||||
|
oldState := currentState
|
||||||
|
var newState string
|
||||||
|
|
||||||
// Cycle backward
|
// Cycle backward
|
||||||
if currentIndex <= 0 {
|
if currentIndex <= 0 {
|
||||||
item.State = model.TodoState("") // Empty state
|
newState = "" // Empty state
|
||||||
} else if currentIndex > len(stateNames) {
|
} else if currentIndex > len(stateNames) {
|
||||||
item.State = model.TodoState(stateNames[len(stateNames)-1])
|
newState = stateNames[len(stateNames)-1]
|
||||||
} else {
|
} else {
|
||||||
item.State = model.TodoState(stateNames[currentIndex-1])
|
newState = stateNames[currentIndex-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item state
|
||||||
|
item.State = model.TodoState(newState)
|
||||||
|
|
||||||
|
// Manage CLOSED timestamp
|
||||||
|
wasInDoneState := (oldState == stateNames[lastStateIndex])
|
||||||
|
isInDoneState := (newState == stateNames[lastStateIndex])
|
||||||
|
|
||||||
|
if isInDoneState && !wasInDoneState {
|
||||||
|
// Moving TO done state - add CLOSED timestamp
|
||||||
|
now := time.Now()
|
||||||
|
item.Closed = &now
|
||||||
|
// Remove any existing CLOSED line from notes
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range item.Notes {
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.Notes = filteredNotes
|
||||||
|
} else if wasInDoneState && !isInDoneState {
|
||||||
|
// Moving FROM done state - remove CLOSED timestamp
|
||||||
|
item.Closed = nil
|
||||||
|
// Remove any existing CLOSED line from notes
|
||||||
|
var filteredNotes []string
|
||||||
|
for _, note := range item.Notes {
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(note), "CLOSED:") {
|
||||||
|
filteredNotes = append(filteredNotes, note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item.Notes = filteredNotes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ import (
|
||||||
type settingsSection int
|
type settingsSection int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
settingsSectionTags settingsSection = iota
|
settingsSectionGeneral settingsSection = iota
|
||||||
|
settingsSectionTags
|
||||||
settingsSectionStates
|
settingsSectionStates
|
||||||
settingsSectionKeybindings
|
settingsSectionKeybindings
|
||||||
)
|
)
|
||||||
|
|
@ -76,7 +77,7 @@ func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.Left):
|
case key.Matches(msg, m.keys.Left):
|
||||||
// Previous section
|
// Previous section
|
||||||
if m.settingsSection > settingsSectionTags {
|
if m.settingsSection > settingsSectionGeneral {
|
||||||
m.settingsSection--
|
m.settingsSection--
|
||||||
m.settingsCursor = 0
|
m.settingsCursor = 0
|
||||||
m.settingsScroll = 0
|
m.settingsScroll = 0
|
||||||
|
|
@ -101,6 +102,8 @@ func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.Capture):
|
case key.Matches(msg, m.keys.Capture):
|
||||||
// Add new tag or state
|
// Add new tag or state
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
// No capture action in General
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
m.addNewTag()
|
m.addNewTag()
|
||||||
case settingsSectionStates:
|
case settingsSectionStates:
|
||||||
|
|
@ -128,6 +131,8 @@ func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
// getSettingsItemCount returns the number of items in the current settings view
|
// getSettingsItemCount returns the number of items in the current settings view
|
||||||
func (m *uiModel) getSettingsItemCount() int {
|
func (m *uiModel) getSettingsItemCount() int {
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
return 3 // Org syntax highlighting toggle, show indentation guides toggle, indentation guide color
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
return len(m.config.Tags.Tags) + 1 // +1 for "Add new tag" option
|
return len(m.config.Tags.Tags) + 1 // +1 for "Add new tag" option
|
||||||
case settingsSectionStates:
|
case settingsSectionStates:
|
||||||
|
|
@ -172,6 +177,40 @@ func (m *uiModel) updateSettingsScrollOffset() {
|
||||||
// startSettingsEdit starts editing a settings item
|
// startSettingsEdit starts editing a settings item
|
||||||
func (m *uiModel) startSettingsEdit() {
|
func (m *uiModel) startSettingsEdit() {
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
// Setting 0: Toggle org syntax highlighting
|
||||||
|
if m.settingsCursor == 0 {
|
||||||
|
m.config.UI.OrgSyntaxHighlighting = !m.config.UI.OrgSyntaxHighlighting
|
||||||
|
if m.config.UI.OrgSyntaxHighlighting {
|
||||||
|
m.setStatus("Org syntax highlighting enabled (saved)")
|
||||||
|
} else {
|
||||||
|
m.setStatus("Org syntax highlighting disabled (saved)")
|
||||||
|
}
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Setting 1: Toggle show indentation guides
|
||||||
|
if m.settingsCursor == 1 {
|
||||||
|
m.config.UI.ShowIndentationGuides = !m.config.UI.ShowIndentationGuides
|
||||||
|
if m.config.UI.ShowIndentationGuides {
|
||||||
|
m.setStatus("Indentation guides enabled (saved)")
|
||||||
|
} else {
|
||||||
|
m.setStatus("Indentation guides disabled (saved)")
|
||||||
|
}
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Setting 2: Edit indentation guide color
|
||||||
|
if m.settingsCursor == 2 {
|
||||||
|
m.textinput.SetValue(m.config.UI.IndentationGuideColor)
|
||||||
|
m.textinput.Placeholder = "Enter color (e.g., 245, 99)"
|
||||||
|
m.textinput.Focus()
|
||||||
|
}
|
||||||
|
return
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
return
|
return
|
||||||
|
|
@ -237,6 +276,23 @@ func (m *uiModel) startSettingsEdit() {
|
||||||
// saveSettingsEdit saves the edited value and auto-saves to disk
|
// saveSettingsEdit saves the edited value and auto-saves to disk
|
||||||
func (m *uiModel) saveSettingsEdit() {
|
func (m *uiModel) saveSettingsEdit() {
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
// Setting 2: Indentation guide color
|
||||||
|
if m.settingsCursor == 2 {
|
||||||
|
newColor := strings.TrimSpace(m.textinput.Value())
|
||||||
|
if newColor != "" {
|
||||||
|
m.config.UI.IndentationGuideColor = newColor
|
||||||
|
m.setStatus(fmt.Sprintf("Indentation guide color set to '%s' (saved)", newColor))
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
||||||
|
} else {
|
||||||
|
// Reload styles
|
||||||
|
m.styles = newStyleMapFromConfig(m.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
return
|
return
|
||||||
|
|
@ -350,6 +406,9 @@ func (m *uiModel) saveSettingsEdit() {
|
||||||
// deleteSettingsItem deletes the current settings item and auto-saves
|
// deleteSettingsItem deletes the current settings item and auto-saves
|
||||||
func (m *uiModel) deleteSettingsItem() {
|
func (m *uiModel) deleteSettingsItem() {
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
// Cannot delete general settings
|
||||||
|
return
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
return
|
return
|
||||||
|
|
@ -437,6 +496,12 @@ func (m *uiModel) viewSettings() string {
|
||||||
activeTabStyle := lipgloss.NewStyle().Padding(0, 2).Bold(true).Foreground(lipgloss.Color(m.config.Colors.Title))
|
activeTabStyle := lipgloss.NewStyle().Padding(0, 2).Bold(true).Foreground(lipgloss.Color(m.config.Colors.Title))
|
||||||
|
|
||||||
tabs := ""
|
tabs := ""
|
||||||
|
if m.settingsSection == settingsSectionGeneral {
|
||||||
|
tabs += activeTabStyle.Render("[General]")
|
||||||
|
} else {
|
||||||
|
tabs += tabStyle.Render("General")
|
||||||
|
}
|
||||||
|
tabs += " "
|
||||||
if m.settingsSection == settingsSectionTags {
|
if m.settingsSection == settingsSectionTags {
|
||||||
tabs += activeTabStyle.Render("[Tags]")
|
tabs += activeTabStyle.Render("[Tags]")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -459,6 +524,8 @@ func (m *uiModel) viewSettings() string {
|
||||||
// Instructions
|
// Instructions
|
||||||
var instructions string
|
var instructions string
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
instructions = "←/→: Switch tabs • ↑/↓: Navigate • Enter: Toggle setting\nctrl+s: Save • q/,: Exit"
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
instructions = "←/→: Switch tabs • ↑/↓: Navigate • Enter: Edit • D: Delete\nc: Add new tag • ctrl+s: Save • q/,: Exit"
|
instructions = "←/→: Switch tabs • ↑/↓: Navigate • Enter: Edit • D: Delete\nc: Add new tag • ctrl+s: Save • q/,: Exit"
|
||||||
case settingsSectionStates:
|
case settingsSectionStates:
|
||||||
|
|
@ -470,6 +537,8 @@ func (m *uiModel) viewSettings() string {
|
||||||
|
|
||||||
// Render the appropriate section
|
// Render the appropriate section
|
||||||
switch m.settingsSection {
|
switch m.settingsSection {
|
||||||
|
case settingsSectionGeneral:
|
||||||
|
content.WriteString(m.viewSettingsGeneral())
|
||||||
case settingsSectionTags:
|
case settingsSectionTags:
|
||||||
content.WriteString(m.viewSettingsTags())
|
content.WriteString(m.viewSettingsTags())
|
||||||
case settingsSectionStates:
|
case settingsSectionStates:
|
||||||
|
|
@ -488,6 +557,68 @@ func (m *uiModel) viewSettings() string {
|
||||||
return content.String()
|
return content.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewSettingsGeneral renders the general settings section
|
||||||
|
func (m *uiModel) viewSettingsGeneral() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
// Calculate visible window
|
||||||
|
reservedLines := 10
|
||||||
|
if m.textinput.Focused() {
|
||||||
|
reservedLines += 3
|
||||||
|
}
|
||||||
|
availableHeight := m.height - reservedLines
|
||||||
|
if availableHeight < 3 {
|
||||||
|
availableHeight = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("34")).Bold(true)
|
||||||
|
disabledStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("196"))
|
||||||
|
colorStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(m.config.UI.IndentationGuideColor))
|
||||||
|
|
||||||
|
// Setting 0: Org syntax highlighting toggle
|
||||||
|
line := ""
|
||||||
|
if m.settingsCursor == 0 && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
line += "Org syntax highlighting: "
|
||||||
|
if m.config.UI.OrgSyntaxHighlighting {
|
||||||
|
line += enabledStyle.Render("Enabled")
|
||||||
|
} else {
|
||||||
|
line += disabledStyle.Render("Disabled")
|
||||||
|
}
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
|
||||||
|
// Setting 1: Show indentation guides toggle
|
||||||
|
line = ""
|
||||||
|
if m.settingsCursor == 1 && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
line += "Show indentation guides: "
|
||||||
|
if m.config.UI.ShowIndentationGuides {
|
||||||
|
line += enabledStyle.Render("Enabled")
|
||||||
|
} else {
|
||||||
|
line += disabledStyle.Render("Disabled")
|
||||||
|
}
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
|
||||||
|
// Setting 2: Indentation guide color
|
||||||
|
line = ""
|
||||||
|
if m.settingsCursor == 2 && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
line += "Indentation guide color: "
|
||||||
|
line += colorStyle.Render(m.config.UI.IndentationGuideColor)
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
// viewSettingsTags renders the tags section
|
// viewSettingsTags renders the tags section
|
||||||
func (m *uiModel) viewSettingsTags() string {
|
func (m *uiModel) viewSettingsTags() string {
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ func (m uiModel) View() string {
|
||||||
return m.viewAddSubTask()
|
return m.viewAddSubTask()
|
||||||
case modeSetDeadline:
|
case modeSetDeadline:
|
||||||
return m.viewSetDeadline()
|
return m.viewSetDeadline()
|
||||||
|
case modeSetScheduled:
|
||||||
|
return m.viewSetScheduled()
|
||||||
case modeSetPriority:
|
case modeSetPriority:
|
||||||
return m.viewSetPriority()
|
return m.viewSetPriority()
|
||||||
case modeSetEffort:
|
case modeSetEffort:
|
||||||
|
|
@ -148,17 +150,24 @@ func (m uiModel) View() string {
|
||||||
for i, item := range items {
|
for i, item := range items {
|
||||||
lineCount := 1 // The item itself
|
lineCount := 1 // The item itself
|
||||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
// Build subtle visual guides for notes
|
// Build indentation for notes
|
||||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
|
||||||
var notePrefix strings.Builder
|
var notePrefix strings.Builder
|
||||||
for j := 1; j <= item.Level; j++ {
|
if m.config.UI.ShowIndentationGuides {
|
||||||
notePrefix.WriteString(guideStyle.Render("· "))
|
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(m.config.UI.IndentationGuideColor))
|
||||||
|
for j := 1; j <= item.Level; j++ {
|
||||||
|
notePrefix.WriteString(guideStyle.Render("· "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No visual guides, just use spaces
|
||||||
|
for j := 1; j <= item.Level; j++ {
|
||||||
|
notePrefix.WriteString(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
indent := notePrefix.String()
|
indent := notePrefix.String()
|
||||||
noteIndent := indent + " "
|
noteIndent := indent + " "
|
||||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||||
highlightedNotes := renderNotesWithHighlighting(wrappedNotes)
|
highlightedNotes := m.renderNotesWithHighlighting(wrappedNotes)
|
||||||
lineCount += len(highlightedNotes)
|
lineCount += len(highlightedNotes)
|
||||||
}
|
}
|
||||||
itemLineCount[i] = lineCount
|
itemLineCount[i] = lineCount
|
||||||
|
|
@ -215,17 +224,24 @@ func (m uiModel) View() string {
|
||||||
|
|
||||||
// Render remaining notes
|
// Render remaining notes
|
||||||
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
if !item.Folded && len(item.Notes) > 0 && m.mode == modeList {
|
||||||
// Build subtle visual guides for notes
|
// Build indentation for notes
|
||||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
|
|
||||||
var notePrefix strings.Builder
|
var notePrefix strings.Builder
|
||||||
for i := 1; i <= item.Level; i++ {
|
if m.config.UI.ShowIndentationGuides {
|
||||||
notePrefix.WriteString(guideStyle.Render("· "))
|
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(m.config.UI.IndentationGuideColor))
|
||||||
|
for i := 1; i <= item.Level; i++ {
|
||||||
|
notePrefix.WriteString(guideStyle.Render("· "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No visual guides, just use spaces
|
||||||
|
for i := 1; i <= item.Level; i++ {
|
||||||
|
notePrefix.WriteString(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
indent := notePrefix.String()
|
indent := notePrefix.String()
|
||||||
noteIndent := indent + " "
|
noteIndent := indent + " "
|
||||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||||
highlightedNotes := renderNotesWithHighlighting(wrappedNotes)
|
highlightedNotes := m.renderNotesWithHighlighting(wrappedNotes)
|
||||||
for noteIdx := linesToSkip - 1; noteIdx < len(highlightedNotes) && itemLines < availableHeight; noteIdx++ {
|
for noteIdx := linesToSkip - 1; noteIdx < len(highlightedNotes) && itemLines < availableHeight; noteIdx++ {
|
||||||
content.WriteString(indent)
|
content.WriteString(indent)
|
||||||
content.WriteString(" " + highlightedNotes[noteIdx])
|
content.WriteString(" " + highlightedNotes[noteIdx])
|
||||||
|
|
@ -245,17 +261,24 @@ 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 {
|
||||||
// Build subtle visual guides for notes
|
// Build indentation for notes
|
||||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("235"))
|
|
||||||
var notePrefix strings.Builder
|
var notePrefix strings.Builder
|
||||||
for i := 1; i <= item.Level; i++ {
|
if m.config.UI.ShowIndentationGuides {
|
||||||
notePrefix.WriteString(guideStyle.Render("· "))
|
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(m.config.UI.IndentationGuideColor))
|
||||||
|
for i := 1; i <= item.Level; i++ {
|
||||||
|
notePrefix.WriteString(guideStyle.Render("· "))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No visual guides, just use spaces
|
||||||
|
for i := 1; i <= item.Level; i++ {
|
||||||
|
notePrefix.WriteString(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
indent := notePrefix.String()
|
indent := notePrefix.String()
|
||||||
noteIndent := indent + " "
|
noteIndent := indent + " "
|
||||||
filteredNotes := filterLogbookDrawer(item.Notes)
|
filteredNotes := filterLogbookDrawer(item.Notes)
|
||||||
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
wrappedNotes := wrapNoteLines(filteredNotes, m.width, noteIndent)
|
||||||
highlightedNotes := renderNotesWithHighlighting(wrappedNotes)
|
highlightedNotes := m.renderNotesWithHighlighting(wrappedNotes)
|
||||||
for _, note := range highlightedNotes {
|
for _, note := range highlightedNotes {
|
||||||
if itemLines >= availableHeight {
|
if itemLines >= availableHeight {
|
||||||
break
|
break
|
||||||
|
|
@ -358,6 +381,14 @@ func (m uiModel) viewAddSubTask() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m uiModel) viewSetDeadline() string {
|
func (m uiModel) viewSetDeadline() string {
|
||||||
|
return m.viewSetDate("Set Deadline", "Leave empty to clear deadline")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) viewSetScheduled() string {
|
||||||
|
return m.viewSetDate("Set Scheduled Date", "Leave empty to clear scheduled date")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m uiModel) viewSetDate(title, helpMsg string) string {
|
||||||
dialogStyle := lipgloss.NewStyle().
|
dialogStyle := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(lipgloss.Color("141")).
|
BorderForeground(lipgloss.Color("141")).
|
||||||
|
|
@ -365,7 +396,7 @@ func (m uiModel) viewSetDeadline() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(m.styles.titleStyle.Render("Set Deadline"))
|
content.WriteString(m.styles.titleStyle.Render(title))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
|
|
@ -375,7 +406,7 @@ func (m uiModel) viewSetDeadline() string {
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(m.styles.statusStyle.Render("Examples: 2025-12-31, +7 (7 days from now)"))
|
content.WriteString(m.styles.statusStyle.Render("Examples: 2025-12-31, +7 (7 days from now)"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear deadline"))
|
content.WriteString(m.styles.statusStyle.Render(helpMsg))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
|
|
@ -467,7 +498,7 @@ func (m uiModel) viewHelp() string {
|
||||||
navigationBindings := []key.Binding{m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right}
|
navigationBindings := []key.Binding{m.keys.Up, m.keys.Down, m.keys.Left, m.keys.Right}
|
||||||
itemBindings := []key.Binding{m.keys.ToggleFold, m.keys.EditNotes, m.keys.CycleState}
|
itemBindings := []key.Binding{m.keys.ToggleFold, m.keys.EditNotes, m.keys.CycleState}
|
||||||
taskBindings := []key.Binding{m.keys.Capture, m.keys.AddSubTask, m.keys.Delete}
|
taskBindings := []key.Binding{m.keys.Capture, m.keys.AddSubTask, m.keys.Delete}
|
||||||
timeBindings := []key.Binding{m.keys.ClockIn, m.keys.ClockOut, m.keys.SetDeadline, m.keys.SetEffort}
|
timeBindings := []key.Binding{m.keys.ClockIn, m.keys.ClockOut, m.keys.SetDeadline, m.keys.SetScheduled, m.keys.SetEffort}
|
||||||
organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.TagItem, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder}
|
organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.TagItem, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder}
|
||||||
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit}
|
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit}
|
||||||
|
|
||||||
|
|
@ -626,8 +657,8 @@ func filterLogbookDrawer(notes []string) []string {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip SCHEDULED and DEADLINE lines
|
// Skip SCHEDULED, DEADLINE, and CLOSED lines
|
||||||
if strings.HasPrefix(trimmed, "SCHEDULED:") || strings.HasPrefix(trimmed, "DEADLINE:") {
|
if strings.HasPrefix(trimmed, "SCHEDULED:") || strings.HasPrefix(trimmed, "DEADLINE:") || strings.HasPrefix(trimmed, "CLOSED:") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -661,7 +692,7 @@ func wrapNoteLines(notes []string, width int, indent string) []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderNotesWithHighlighting renders notes with syntax highlighting for code blocks
|
// renderNotesWithHighlighting renders notes with syntax highlighting for code blocks
|
||||||
func renderNotesWithHighlighting(notes []string) []string {
|
func (m uiModel) renderNotesWithHighlighting(notes []string) []string {
|
||||||
if len(notes) == 0 {
|
if len(notes) == 0 {
|
||||||
return notes
|
return notes
|
||||||
}
|
}
|
||||||
|
|
@ -762,7 +793,13 @@ func renderNotesWithHighlighting(notes []string) []string {
|
||||||
if inCodeBlock {
|
if inCodeBlock {
|
||||||
codeLines = append(codeLines, note)
|
codeLines = append(codeLines, note)
|
||||||
} else {
|
} else {
|
||||||
result = append(result, note)
|
// Apply org-mode syntax highlighting to non-code text if enabled
|
||||||
|
if m.config.UI.OrgSyntaxHighlighting {
|
||||||
|
highlighted := highlightCode(note, "org")
|
||||||
|
result = append(result, highlighted)
|
||||||
|
} else {
|
||||||
|
result = append(result, note)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -894,14 +931,21 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
// Indentation with subtle visual nesting guides
|
// Indentation with subtle visual nesting guides
|
||||||
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("245")) // Very subtle gray
|
if m.config.UI.ShowIndentationGuides {
|
||||||
for i := 1; i < item.Level; i++ {
|
guideStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(m.config.UI.IndentationGuideColor))
|
||||||
if i == item.Level-1 {
|
for i := 1; i < item.Level; i++ {
|
||||||
// Last level before the item - use subtle dot connector
|
if i == item.Level-1 {
|
||||||
b.WriteString(guideStyle.Render("· "))
|
// Last level before the item - use subtle dot connector
|
||||||
} else {
|
b.WriteString(guideStyle.Render("· "))
|
||||||
// Parent levels - use subtle dot
|
} else {
|
||||||
b.WriteString(guideStyle.Render("· "))
|
// Parent levels - use subtle dot
|
||||||
|
b.WriteString(guideStyle.Render("· "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No visual guides, just use spaces for indentation
|
||||||
|
for i := 1; i < item.Level; i++ {
|
||||||
|
b.WriteString(" ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue