mirror of
https://github.com/RWejlgaard/org.git
synced 2026-05-06 04:34:45 +00:00
Adding settings view and remappable keybindings and custom states support and tags
This commit is contained in:
parent
8f6ec4a79f
commit
43573a6e79
14 changed files with 1832 additions and 186 deletions
129
README.md
129
README.md
|
|
@ -26,9 +26,10 @@ org # Opens ./todo.org by default
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Task Management
|
### Task Management
|
||||||
- **TODO States**: Cycle through TODO, PROG (in progress), BLOCK (blocked), and DONE states
|
- **Customizable TODO States**: Define your own workflow states with custom colors (default: TODO, PROG, BLOCK, DONE)
|
||||||
- **Hierarchical Tasks**: Create sub-tasks and organize items with multiple levels
|
- **Hierarchical Tasks**: Create sub-tasks and organize items with multiple levels
|
||||||
- **Priority Levels**: Set priorities (A, B, C) with color-coded indicators
|
- **Priority Levels**: Set priorities (A, B, C) with color-coded indicators
|
||||||
|
- **Tags**: Organize tasks with tags like `:work:urgent:` with customizable colors
|
||||||
- **Folding**: Collapse and expand tasks and notes with Tab key
|
- **Folding**: Collapse and expand tasks and notes with Tab key
|
||||||
- **Quick Capture**: Press 'c' to quickly capture new TODO items
|
- **Quick Capture**: Press 'c' to quickly capture new TODO items
|
||||||
- **Reorder Mode**: Reorganize tasks with shift+up/down arrows
|
- **Reorder Mode**: Reorganize tasks with shift+up/down arrows
|
||||||
|
|
@ -63,6 +64,7 @@ org # Opens ./todo.org by default
|
||||||
| `c` | Capture new TODO |
|
| `c` | Capture new TODO |
|
||||||
| `s` | Add sub-task |
|
| `s` | Add sub-task |
|
||||||
| `D` | Delete item (with confirmation) |
|
| `D` | Delete item (with confirmation) |
|
||||||
|
| `#` | Add/edit tags |
|
||||||
| `a` | Toggle agenda view |
|
| `a` | Toggle agenda view |
|
||||||
| `i` | Clock in |
|
| `i` | Clock in |
|
||||||
| `o` | Clock out |
|
| `o` | Clock out |
|
||||||
|
|
@ -71,10 +73,13 @@ org # Opens ./todo.org by default
|
||||||
| `e` | Set effort |
|
| `e` | Set effort |
|
||||||
| `r` | Toggle reorder mode |
|
| `r` | Toggle reorder mode |
|
||||||
| `shift+↑/↓` | Move item up/down |
|
| `shift+↑/↓` | Move item up/down |
|
||||||
|
| `,` | Open settings |
|
||||||
| `ctrl+s` | Save |
|
| `ctrl+s` | Save |
|
||||||
| `?` | Toggle help |
|
| `?` | Toggle help |
|
||||||
| `q` or `ctrl+c` | Quit |
|
| `q` or `ctrl+c` | Quit |
|
||||||
|
|
||||||
|
**Note**: All keybindings can be customized in the configuration file.
|
||||||
|
|
||||||
### Auto-save
|
### Auto-save
|
||||||
Changes are automatically saved when you quit the application.
|
Changes are automatically saved when you quit the application.
|
||||||
|
|
||||||
|
|
@ -91,9 +96,129 @@ Changes are automatically saved when you quit the application.
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application can be configured using a TOML configuration file located at:
|
||||||
|
- Linux/macOS: `~/.config/org/config.toml`
|
||||||
|
- Windows: `%APPDATA%\org\config.toml`
|
||||||
|
|
||||||
|
The configuration file is automatically created with default values on first run.
|
||||||
|
|
||||||
|
### Configuration Structure
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
Define custom tags with colors:
|
||||||
|
```toml
|
||||||
|
[tags]
|
||||||
|
enabled = true
|
||||||
|
default_tag = "work"
|
||||||
|
|
||||||
|
[[tags.tags]]
|
||||||
|
name = "work"
|
||||||
|
color = "99" # Blue
|
||||||
|
|
||||||
|
[[tags.tags]]
|
||||||
|
name = "personal"
|
||||||
|
color = "141" # Purple
|
||||||
|
|
||||||
|
[[tags.tags]]
|
||||||
|
name = "urgent"
|
||||||
|
color = "196" # Red
|
||||||
|
```
|
||||||
|
|
||||||
|
#### States
|
||||||
|
Customize TODO states with colors:
|
||||||
|
```toml
|
||||||
|
[states]
|
||||||
|
[[states.states]]
|
||||||
|
name = "TODO"
|
||||||
|
color = "202" # Orange
|
||||||
|
|
||||||
|
[[states.states]]
|
||||||
|
name = "PROG"
|
||||||
|
color = "220" # Yellow
|
||||||
|
|
||||||
|
[[states.states]]
|
||||||
|
name = "BLOCK"
|
||||||
|
color = "196" # Red
|
||||||
|
|
||||||
|
[[states.states]]
|
||||||
|
name = "DONE"
|
||||||
|
color = "34" # Green
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Colors
|
||||||
|
Customize UI colors (using ANSI color codes):
|
||||||
|
```toml
|
||||||
|
[colors]
|
||||||
|
todo = "202" # Orange
|
||||||
|
progress = "220" # Yellow
|
||||||
|
blocked = "196" # Red
|
||||||
|
done = "34" # Green
|
||||||
|
cursor = "240" # Gray
|
||||||
|
title = "99" # Blue
|
||||||
|
scheduled = "141" # Purple
|
||||||
|
overdue = "196" # Red
|
||||||
|
status = "241" # Dark gray
|
||||||
|
note = "246" # Light gray
|
||||||
|
folded = "243" # Medium gray
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Keybindings
|
||||||
|
Customize all keybindings (can specify multiple keys per action):
|
||||||
|
```toml
|
||||||
|
[keybindings]
|
||||||
|
up = ["up", "k"]
|
||||||
|
down = ["down", "j"]
|
||||||
|
left = ["left", "h"]
|
||||||
|
right = ["right", "l"]
|
||||||
|
cycle_state = ["t", " "]
|
||||||
|
toggle_fold = ["tab"]
|
||||||
|
edit_notes = ["enter"]
|
||||||
|
capture = ["c"]
|
||||||
|
add_subtask = ["s"]
|
||||||
|
delete = ["D"]
|
||||||
|
tag_item = ["#"]
|
||||||
|
settings = [","]
|
||||||
|
toggle_view = ["a"]
|
||||||
|
save = ["ctrl+s"]
|
||||||
|
help = ["?"]
|
||||||
|
quit = ["q", "ctrl+c"]
|
||||||
|
# ... and more
|
||||||
|
```
|
||||||
|
|
||||||
|
### Settings UI
|
||||||
|
|
||||||
|
Press `,` (comma) to open the settings interface where you can:
|
||||||
|
|
||||||
|
#### Tags Tab
|
||||||
|
- Add new tags with custom colors
|
||||||
|
- Edit tag names and colors (format: `name,color`)
|
||||||
|
- Delete tags with `D`
|
||||||
|
- Reorder tags with `shift+up/down`
|
||||||
|
|
||||||
|
#### States Tab
|
||||||
|
- Add new TODO states with custom colors
|
||||||
|
- Edit state names and colors (format: `name,color`)
|
||||||
|
- Delete states with `D`
|
||||||
|
- Reorder states with `shift+up/down` (affects cycling order)
|
||||||
|
|
||||||
|
#### Keybindings Tab
|
||||||
|
- View all keybindings
|
||||||
|
- Edit keybindings (format: comma-separated keys, e.g., `up,k`)
|
||||||
|
- Multiple keys can be bound to the same action
|
||||||
|
|
||||||
|
**Navigation**: Use left/right arrows to switch between tabs
|
||||||
|
**Auto-save**: All changes are automatically saved to the config file
|
||||||
|
|
||||||
## File Format
|
## File Format
|
||||||
|
|
||||||
The application uses standard Org-mode file format (.org), making it compatible with Emacs Org-mode and other Org-mode tools.
|
The application uses standard Org-mode file format (.org), making it compatible with Emacs Org-mode and other Org-mode tools. Tags are stored in the standard org-mode format:
|
||||||
|
|
||||||
|
```org
|
||||||
|
* TODO Task title :work:urgent:
|
||||||
|
* DONE Completed task :personal:
|
||||||
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
"github.com/rwejlgaard/org/internal/parser"
|
"github.com/rwejlgaard/org/internal/parser"
|
||||||
"github.com/rwejlgaard/org/internal/ui"
|
"github.com/rwejlgaard/org/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
@ -31,6 +32,13 @@ func main() {
|
||||||
filePath = filepath.Join(cwd, "todo.org")
|
filePath = filepath.Join(cwd, "todo.org")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
cfg, err := config.LoadConfig()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Error loading config, using defaults: %v\n", err)
|
||||||
|
cfg = config.DefaultConfig()
|
||||||
|
}
|
||||||
|
|
||||||
// Parse the org file
|
// Parse the org file
|
||||||
orgFile, err := parser.ParseOrgFile(filePath)
|
orgFile, err := parser.ParseOrgFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -39,7 +47,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the UI
|
// Run the UI
|
||||||
if err := ui.RunUI(orgFile); err != nil {
|
if err := ui.RunUI(orgFile, cfg); 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -10,6 +10,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -1,3 +1,5 @@
|
||||||
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
|
|
|
||||||
544
internal/config/config.go
Normal file
544
internal/config/config.go
Normal file
|
|
@ -0,0 +1,544 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
Keybindings KeybindingsConfig `toml:"keybindings"`
|
||||||
|
Colors ColorsConfig `toml:"colors"`
|
||||||
|
Tags TagsConfig `toml:"tags"`
|
||||||
|
States StatesConfig `toml:"states"`
|
||||||
|
UI UIConfig `toml:"ui"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeybindingsConfig holds all keybinding configurations
|
||||||
|
type KeybindingsConfig struct {
|
||||||
|
Up []string `toml:"up"`
|
||||||
|
Down []string `toml:"down"`
|
||||||
|
Left []string `toml:"left"`
|
||||||
|
Right []string `toml:"right"`
|
||||||
|
ShiftUp []string `toml:"shift_up"`
|
||||||
|
ShiftDown []string `toml:"shift_down"`
|
||||||
|
CycleState []string `toml:"cycle_state"`
|
||||||
|
ToggleFold []string `toml:"toggle_fold"`
|
||||||
|
EditNotes []string `toml:"edit_notes"`
|
||||||
|
ToggleView []string `toml:"toggle_view"`
|
||||||
|
Capture []string `toml:"capture"`
|
||||||
|
AddSubTask []string `toml:"add_subtask"`
|
||||||
|
Delete []string `toml:"delete"`
|
||||||
|
Save []string `toml:"save"`
|
||||||
|
ToggleReorder []string `toml:"toggle_reorder"`
|
||||||
|
ClockIn []string `toml:"clock_in"`
|
||||||
|
ClockOut []string `toml:"clock_out"`
|
||||||
|
SetDeadline []string `toml:"set_deadline"`
|
||||||
|
SetPriority []string `toml:"set_priority"`
|
||||||
|
SetEffort []string `toml:"set_effort"`
|
||||||
|
Help []string `toml:"help"`
|
||||||
|
Quit []string `toml:"quit"`
|
||||||
|
Settings []string `toml:"settings"`
|
||||||
|
TagItem []string `toml:"tag_item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorsConfig holds color configurations
|
||||||
|
type ColorsConfig struct {
|
||||||
|
Todo string `toml:"todo"`
|
||||||
|
Progress string `toml:"progress"`
|
||||||
|
Blocked string `toml:"blocked"`
|
||||||
|
Done string `toml:"done"`
|
||||||
|
Cursor string `toml:"cursor"`
|
||||||
|
Title string `toml:"title"`
|
||||||
|
Scheduled string `toml:"scheduled"`
|
||||||
|
Overdue string `toml:"overdue"`
|
||||||
|
Status string `toml:"status"`
|
||||||
|
Note string `toml:"note"`
|
||||||
|
Folded string `toml:"folded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagConfig represents a single tag configuration
|
||||||
|
type TagConfig struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
Color string `toml:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagsConfig holds tag configurations
|
||||||
|
type TagsConfig struct {
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
Tags []TagConfig `toml:"tags"`
|
||||||
|
DefaultTag string `toml:"default_tag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateConfig represents a single TODO state configuration
|
||||||
|
type StateConfig struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
Color string `toml:"color"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatesConfig holds TODO state configurations
|
||||||
|
type StatesConfig struct {
|
||||||
|
States []StateConfig `toml:"states"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIConfig holds UI-related configurations
|
||||||
|
type UIConfig struct {
|
||||||
|
HelpTextWidth int `toml:"help_text_width"`
|
||||||
|
MinTerminalWidth int `toml:"min_terminal_width"`
|
||||||
|
AgendaDays int `toml:"agenda_days"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultConfig returns the default configuration
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Keybindings: KeybindingsConfig{
|
||||||
|
Up: []string{"up", "k"},
|
||||||
|
Down: []string{"down", "j"},
|
||||||
|
Left: []string{"left", "h"},
|
||||||
|
Right: []string{"right", "l"},
|
||||||
|
ShiftUp: []string{"shift+up"},
|
||||||
|
ShiftDown: []string{"shift+down"},
|
||||||
|
CycleState: []string{"t", " "},
|
||||||
|
ToggleFold: []string{"tab"},
|
||||||
|
EditNotes: []string{"enter"},
|
||||||
|
ToggleView: []string{"a"},
|
||||||
|
Capture: []string{"c"},
|
||||||
|
AddSubTask: []string{"s"},
|
||||||
|
Delete: []string{"D"},
|
||||||
|
Save: []string{"ctrl+s"},
|
||||||
|
ToggleReorder: []string{"r"},
|
||||||
|
ClockIn: []string{"i"},
|
||||||
|
ClockOut: []string{"o"},
|
||||||
|
SetDeadline: []string{"d"},
|
||||||
|
SetPriority: []string{"p"},
|
||||||
|
SetEffort: []string{"e"},
|
||||||
|
Help: []string{"?"},
|
||||||
|
Quit: []string{"q", "ctrl+c"},
|
||||||
|
Settings: []string{","},
|
||||||
|
TagItem: []string{"#"},
|
||||||
|
},
|
||||||
|
Colors: ColorsConfig{
|
||||||
|
Todo: "202",
|
||||||
|
Progress: "220",
|
||||||
|
Blocked: "196",
|
||||||
|
Done: "34",
|
||||||
|
Cursor: "240",
|
||||||
|
Title: "99",
|
||||||
|
Scheduled: "141",
|
||||||
|
Overdue: "196",
|
||||||
|
Status: "241",
|
||||||
|
Note: "246",
|
||||||
|
Folded: "243",
|
||||||
|
},
|
||||||
|
Tags: TagsConfig{
|
||||||
|
Enabled: true,
|
||||||
|
DefaultTag: "work",
|
||||||
|
Tags: []TagConfig{
|
||||||
|
{Name: "work", Color: "99"},
|
||||||
|
{Name: "personal", Color: "141"},
|
||||||
|
{Name: "urgent", Color: "196"},
|
||||||
|
{Name: "important", Color: "220"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
States: StatesConfig{
|
||||||
|
States: []StateConfig{
|
||||||
|
{Name: "TODO", Color: "202"},
|
||||||
|
{Name: "PROG", Color: "220"},
|
||||||
|
{Name: "BLOCK", Color: "196"},
|
||||||
|
{Name: "DONE", Color: "34"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UI: UIConfig{
|
||||||
|
HelpTextWidth: 22,
|
||||||
|
MinTerminalWidth: 40,
|
||||||
|
AgendaDays: 7,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfigPath returns the path to the config file
|
||||||
|
func GetConfigPath() (string, error) {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
orgConfigDir := filepath.Join(configDir, "org")
|
||||||
|
configPath := filepath.Join(orgConfigDir, "config.toml")
|
||||||
|
|
||||||
|
return configPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadConfig loads the configuration from the config file
|
||||||
|
func LoadConfig() (*Config, error) {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If config file doesn't exist, create it with defaults
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
defaultCfg := DefaultConfig()
|
||||||
|
if err := defaultCfg.Save(); err != nil {
|
||||||
|
// If we can't save, just return defaults
|
||||||
|
return defaultCfg, nil
|
||||||
|
}
|
||||||
|
return defaultCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
if _, err := toml.DecodeFile(configPath, &config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge with defaults for any missing values
|
||||||
|
config.fillDefaults()
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the configuration to the config file
|
||||||
|
func (c *Config) Save() error {
|
||||||
|
configPath, err := GetConfigPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure config directory exists
|
||||||
|
configDir := filepath.Dir(configPath)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create config directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or truncate the file
|
||||||
|
f, err := os.Create(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create config file: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Encode config to TOML
|
||||||
|
encoder := toml.NewEncoder(f)
|
||||||
|
if err := encoder.Encode(c); err != nil {
|
||||||
|
return fmt.Errorf("failed to write config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillDefaults fills in any missing config values with defaults
|
||||||
|
func (c *Config) fillDefaults() {
|
||||||
|
defaults := DefaultConfig()
|
||||||
|
|
||||||
|
// Fill keybindings if empty
|
||||||
|
if len(c.Keybindings.Up) == 0 {
|
||||||
|
c.Keybindings.Up = defaults.Keybindings.Up
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Down) == 0 {
|
||||||
|
c.Keybindings.Down = defaults.Keybindings.Down
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Left) == 0 {
|
||||||
|
c.Keybindings.Left = defaults.Keybindings.Left
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Right) == 0 {
|
||||||
|
c.Keybindings.Right = defaults.Keybindings.Right
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ShiftUp) == 0 {
|
||||||
|
c.Keybindings.ShiftUp = defaults.Keybindings.ShiftUp
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ShiftDown) == 0 {
|
||||||
|
c.Keybindings.ShiftDown = defaults.Keybindings.ShiftDown
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.CycleState) == 0 {
|
||||||
|
c.Keybindings.CycleState = defaults.Keybindings.CycleState
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ToggleFold) == 0 {
|
||||||
|
c.Keybindings.ToggleFold = defaults.Keybindings.ToggleFold
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.EditNotes) == 0 {
|
||||||
|
c.Keybindings.EditNotes = defaults.Keybindings.EditNotes
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ToggleView) == 0 {
|
||||||
|
c.Keybindings.ToggleView = defaults.Keybindings.ToggleView
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Capture) == 0 {
|
||||||
|
c.Keybindings.Capture = defaults.Keybindings.Capture
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.AddSubTask) == 0 {
|
||||||
|
c.Keybindings.AddSubTask = defaults.Keybindings.AddSubTask
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Delete) == 0 {
|
||||||
|
c.Keybindings.Delete = defaults.Keybindings.Delete
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Save) == 0 {
|
||||||
|
c.Keybindings.Save = defaults.Keybindings.Save
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ToggleReorder) == 0 {
|
||||||
|
c.Keybindings.ToggleReorder = defaults.Keybindings.ToggleReorder
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ClockIn) == 0 {
|
||||||
|
c.Keybindings.ClockIn = defaults.Keybindings.ClockIn
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.ClockOut) == 0 {
|
||||||
|
c.Keybindings.ClockOut = defaults.Keybindings.ClockOut
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.SetDeadline) == 0 {
|
||||||
|
c.Keybindings.SetDeadline = defaults.Keybindings.SetDeadline
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.SetPriority) == 0 {
|
||||||
|
c.Keybindings.SetPriority = defaults.Keybindings.SetPriority
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.SetEffort) == 0 {
|
||||||
|
c.Keybindings.SetEffort = defaults.Keybindings.SetEffort
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Help) == 0 {
|
||||||
|
c.Keybindings.Help = defaults.Keybindings.Help
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Quit) == 0 {
|
||||||
|
c.Keybindings.Quit = defaults.Keybindings.Quit
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.Settings) == 0 {
|
||||||
|
c.Keybindings.Settings = defaults.Keybindings.Settings
|
||||||
|
}
|
||||||
|
if len(c.Keybindings.TagItem) == 0 {
|
||||||
|
c.Keybindings.TagItem = defaults.Keybindings.TagItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill colors if empty
|
||||||
|
if c.Colors.Todo == "" {
|
||||||
|
c.Colors.Todo = defaults.Colors.Todo
|
||||||
|
}
|
||||||
|
if c.Colors.Progress == "" {
|
||||||
|
c.Colors.Progress = defaults.Colors.Progress
|
||||||
|
}
|
||||||
|
if c.Colors.Blocked == "" {
|
||||||
|
c.Colors.Blocked = defaults.Colors.Blocked
|
||||||
|
}
|
||||||
|
if c.Colors.Done == "" {
|
||||||
|
c.Colors.Done = defaults.Colors.Done
|
||||||
|
}
|
||||||
|
if c.Colors.Cursor == "" {
|
||||||
|
c.Colors.Cursor = defaults.Colors.Cursor
|
||||||
|
}
|
||||||
|
if c.Colors.Title == "" {
|
||||||
|
c.Colors.Title = defaults.Colors.Title
|
||||||
|
}
|
||||||
|
if c.Colors.Scheduled == "" {
|
||||||
|
c.Colors.Scheduled = defaults.Colors.Scheduled
|
||||||
|
}
|
||||||
|
if c.Colors.Overdue == "" {
|
||||||
|
c.Colors.Overdue = defaults.Colors.Overdue
|
||||||
|
}
|
||||||
|
if c.Colors.Status == "" {
|
||||||
|
c.Colors.Status = defaults.Colors.Status
|
||||||
|
}
|
||||||
|
if c.Colors.Note == "" {
|
||||||
|
c.Colors.Note = defaults.Colors.Note
|
||||||
|
}
|
||||||
|
if c.Colors.Folded == "" {
|
||||||
|
c.Colors.Folded = defaults.Colors.Folded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill tags if empty
|
||||||
|
if len(c.Tags.Tags) == 0 {
|
||||||
|
c.Tags.Tags = defaults.Tags.Tags
|
||||||
|
}
|
||||||
|
if c.Tags.DefaultTag == "" {
|
||||||
|
c.Tags.DefaultTag = defaults.Tags.DefaultTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill states if empty
|
||||||
|
if len(c.States.States) == 0 {
|
||||||
|
c.States.States = defaults.States.States
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill UI if zero values
|
||||||
|
if c.UI.HelpTextWidth == 0 {
|
||||||
|
c.UI.HelpTextWidth = defaults.UI.HelpTextWidth
|
||||||
|
}
|
||||||
|
if c.UI.MinTerminalWidth == 0 {
|
||||||
|
c.UI.MinTerminalWidth = defaults.UI.MinTerminalWidth
|
||||||
|
}
|
||||||
|
if c.UI.AgendaDays == 0 {
|
||||||
|
c.UI.AgendaDays = defaults.UI.AgendaDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildKeyBinding creates a key.Binding from config
|
||||||
|
func BuildKeyBinding(keys []string, help string, description string) key.Binding {
|
||||||
|
return key.NewBinding(
|
||||||
|
key.WithKeys(keys...),
|
||||||
|
key.WithHelp(help, description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagColor returns the color for a given tag name
|
||||||
|
func (c *Config) GetTagColor(tagName string) string {
|
||||||
|
for _, tag := range c.Tags.Tags {
|
||||||
|
if tag.Name == tagName {
|
||||||
|
return tag.Color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return a default color if tag not found
|
||||||
|
return "99"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTag adds a new tag to the configuration
|
||||||
|
func (c *Config) AddTag(name, color string) {
|
||||||
|
// Check if tag already exists
|
||||||
|
for i, tag := range c.Tags.Tags {
|
||||||
|
if tag.Name == name {
|
||||||
|
c.Tags.Tags[i].Color = color
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.Tags.Tags = append(c.Tags.Tags, TagConfig{Name: name, Color: color})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTag removes a tag from the configuration
|
||||||
|
func (c *Config) RemoveTag(name string) {
|
||||||
|
for i, tag := range c.Tags.Tags {
|
||||||
|
if tag.Name == name {
|
||||||
|
c.Tags.Tags = append(c.Tags.Tags[:i], c.Tags.Tags[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTagColor updates the color of an existing tag
|
||||||
|
func (c *Config) UpdateTagColor(name, color string) {
|
||||||
|
for i, tag := range c.Tags.Tags {
|
||||||
|
if tag.Name == name {
|
||||||
|
c.Tags.Tags[i].Color = color
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStateColor returns the color for a given state name
|
||||||
|
func (c *Config) GetStateColor(stateName string) string {
|
||||||
|
for _, state := range c.States.States {
|
||||||
|
if state.Name == stateName {
|
||||||
|
return state.Color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return a default color if state not found
|
||||||
|
return "99"
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddState adds a new state to the configuration
|
||||||
|
func (c *Config) AddState(name, color string) {
|
||||||
|
// Check if state already exists
|
||||||
|
for i, state := range c.States.States {
|
||||||
|
if state.Name == name {
|
||||||
|
c.States.States[i].Color = color
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.States.States = append(c.States.States, StateConfig{Name: name, Color: color})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveState removes a state from the configuration
|
||||||
|
func (c *Config) RemoveState(name string) {
|
||||||
|
for i, state := range c.States.States {
|
||||||
|
if state.Name == name {
|
||||||
|
c.States.States = append(c.States.States[:i], c.States.States[i+1:]...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStateColor updates the color of an existing state
|
||||||
|
func (c *Config) UpdateStateColor(name, color string) {
|
||||||
|
for i, state := range c.States.States {
|
||||||
|
if state.Name == name {
|
||||||
|
c.States.States[i].Color = color
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStateNames returns all configured state names
|
||||||
|
func (c *Config) GetStateNames() []string {
|
||||||
|
names := make([]string, len(c.States.States))
|
||||||
|
for i, state := range c.States.States {
|
||||||
|
names[i] = state.Name
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateKeybinding updates a keybinding in the configuration
|
||||||
|
func (c *Config) UpdateKeybinding(action string, keys []string) error {
|
||||||
|
// Use reflection would be complex, so we handle specific cases
|
||||||
|
switch action {
|
||||||
|
case "up":
|
||||||
|
c.Keybindings.Up = keys
|
||||||
|
case "down":
|
||||||
|
c.Keybindings.Down = keys
|
||||||
|
case "left":
|
||||||
|
c.Keybindings.Left = keys
|
||||||
|
case "right":
|
||||||
|
c.Keybindings.Right = keys
|
||||||
|
case "cycle_state":
|
||||||
|
c.Keybindings.CycleState = keys
|
||||||
|
case "toggle_fold":
|
||||||
|
c.Keybindings.ToggleFold = keys
|
||||||
|
case "edit_notes":
|
||||||
|
c.Keybindings.EditNotes = keys
|
||||||
|
case "capture":
|
||||||
|
c.Keybindings.Capture = keys
|
||||||
|
case "add_subtask":
|
||||||
|
c.Keybindings.AddSubTask = keys
|
||||||
|
case "delete":
|
||||||
|
c.Keybindings.Delete = keys
|
||||||
|
case "tag_item":
|
||||||
|
c.Keybindings.TagItem = keys
|
||||||
|
case "settings":
|
||||||
|
c.Keybindings.Settings = keys
|
||||||
|
case "toggle_view":
|
||||||
|
c.Keybindings.ToggleView = keys
|
||||||
|
case "save":
|
||||||
|
c.Keybindings.Save = keys
|
||||||
|
case "help":
|
||||||
|
c.Keybindings.Help = keys
|
||||||
|
case "quit":
|
||||||
|
c.Keybindings.Quit = keys
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown action: %s", action)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllKeybindings returns a map of all keybindings
|
||||||
|
func (c *Config) GetAllKeybindings() map[string][]string {
|
||||||
|
return map[string][]string{
|
||||||
|
"up": c.Keybindings.Up,
|
||||||
|
"down": c.Keybindings.Down,
|
||||||
|
"left": c.Keybindings.Left,
|
||||||
|
"right": c.Keybindings.Right,
|
||||||
|
"shift_up": c.Keybindings.ShiftUp,
|
||||||
|
"shift_down": c.Keybindings.ShiftDown,
|
||||||
|
"cycle_state": c.Keybindings.CycleState,
|
||||||
|
"toggle_fold": c.Keybindings.ToggleFold,
|
||||||
|
"edit_notes": c.Keybindings.EditNotes,
|
||||||
|
"toggle_view": c.Keybindings.ToggleView,
|
||||||
|
"capture": c.Keybindings.Capture,
|
||||||
|
"add_subtask": c.Keybindings.AddSubTask,
|
||||||
|
"delete": c.Keybindings.Delete,
|
||||||
|
"save": c.Keybindings.Save,
|
||||||
|
"toggle_reorder": c.Keybindings.ToggleReorder,
|
||||||
|
"clock_in": c.Keybindings.ClockIn,
|
||||||
|
"clock_out": c.Keybindings.ClockOut,
|
||||||
|
"set_deadline": c.Keybindings.SetDeadline,
|
||||||
|
"set_priority": c.Keybindings.SetPriority,
|
||||||
|
"set_effort": c.Keybindings.SetEffort,
|
||||||
|
"help": c.Keybindings.Help,
|
||||||
|
"quit": c.Keybindings.Quit,
|
||||||
|
"settings": c.Keybindings.Settings,
|
||||||
|
"tag_item": c.Keybindings.TagItem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ type Item struct {
|
||||||
State TodoState // TODO, PROG, BLOCK, DONE, or empty
|
State TodoState // TODO, PROG, BLOCK, DONE, or empty
|
||||||
Priority Priority // Priority: A, B, C, or empty
|
Priority Priority // Priority: A, B, C, or empty
|
||||||
Title string // The main title text
|
Title string // The main title text
|
||||||
|
Tags []string // Tags for this item (e.g., :work:urgent:)
|
||||||
Scheduled *time.Time
|
Scheduled *time.Time
|
||||||
Deadline *time.Time
|
Deadline *time.Time
|
||||||
Effort string // Effort estimate (e.g., "8h", "2d")
|
Effort string // Effort estimate (e.g., "8h", "2d")
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
// Parser patterns
|
// Parser patterns
|
||||||
var (
|
var (
|
||||||
headingPattern = regexp.MustCompile(`^(\*+)\s+(?:(TODO|PROG|BLOCK|DONE)\s+)?(?:\[#([A-C])\]\s+)?(.+)$`)
|
headingPattern = regexp.MustCompile(`^(\*+)\s+(?:(TODO|PROG|BLOCK|DONE)\s+)?(?:\[#([A-C])\]\s+)?(.+?)(?:\s+(:[[:alnum:]_@#%:]+:)\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*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
||||||
|
|
@ -108,13 +108,24 @@ func ParseOrgFile(path string) (*model.OrgFile, error) {
|
||||||
level := len(matches[1])
|
level := len(matches[1])
|
||||||
state := model.TodoState(matches[2])
|
state := model.TodoState(matches[2])
|
||||||
priority := model.Priority(matches[3])
|
priority := model.Priority(matches[3])
|
||||||
title := matches[4]
|
title := strings.TrimSpace(matches[4])
|
||||||
|
tagsStr := matches[5]
|
||||||
|
|
||||||
|
// Parse tags from :tag1:tag2: format
|
||||||
|
var tags []string
|
||||||
|
if tagsStr != "" {
|
||||||
|
tagsStr = strings.Trim(tagsStr, ":")
|
||||||
|
if tagsStr != "" {
|
||||||
|
tags = strings.Split(tagsStr, ":")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
item := &model.Item{
|
item := &model.Item{
|
||||||
Level: level,
|
Level: level,
|
||||||
State: state,
|
State: state,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Tags: tags,
|
||||||
Notes: []string{},
|
Notes: []string{},
|
||||||
Children: []*model.Item{},
|
Children: []*model.Item{},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,14 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
||||||
if item.Priority != model.PriorityNone {
|
if item.Priority != model.PriorityNone {
|
||||||
line += " [#" + string(item.Priority) + "]"
|
line += " [#" + string(item.Priority) + "]"
|
||||||
}
|
}
|
||||||
line += " " + item.Title + "\n"
|
line += " " + item.Title
|
||||||
|
|
||||||
|
// Add tags if present
|
||||||
|
if len(item.Tags) > 0 {
|
||||||
|
line += " :" + strings.Join(item.Tags, ":") + ":"
|
||||||
|
}
|
||||||
|
|
||||||
|
line += "\n"
|
||||||
|
|
||||||
if _, err := writer.WriteString(line); err != nil {
|
if _, err := writer.WriteString(line); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
"github.com/rwejlgaard/org/internal/model"
|
"github.com/rwejlgaard/org/internal/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,6 +24,8 @@ const (
|
||||||
modeSetPriority
|
modeSetPriority
|
||||||
modeSetEffort
|
modeSetEffort
|
||||||
modeHelp
|
modeHelp
|
||||||
|
modeSettings
|
||||||
|
modeTagEdit
|
||||||
)
|
)
|
||||||
|
|
||||||
type uiModel struct {
|
type uiModel struct {
|
||||||
|
|
@ -33,6 +36,8 @@ type uiModel struct {
|
||||||
mode viewMode
|
mode viewMode
|
||||||
help help.Model
|
help help.Model
|
||||||
keys keyMap
|
keys keyMap
|
||||||
|
styles styleMap
|
||||||
|
config *config.Config
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
statusMsg string
|
statusMsg string
|
||||||
|
|
@ -40,11 +45,14 @@ type uiModel struct {
|
||||||
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
|
||||||
|
settingsScroll int // Scroll position in settings view
|
||||||
|
settingsSection settingsSection // Current settings section/tab
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(orgFile *model.OrgFile) uiModel {
|
func InitialModel(orgFile *model.OrgFile, cfg *config.Config) 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
|
||||||
|
|
@ -61,7 +69,9 @@ func initialModel(orgFile *model.OrgFile) uiModel {
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
mode: modeList,
|
mode: modeList,
|
||||||
help: h,
|
help: h,
|
||||||
keys: keys,
|
keys: newKeyMapFromConfig(cfg),
|
||||||
|
styles: newStyleMapFromConfig(cfg),
|
||||||
|
config: cfg,
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
textinput: ti,
|
textinput: ti,
|
||||||
}
|
}
|
||||||
|
|
@ -122,8 +132,8 @@ func (m *uiModel) updateScrollOffset(availableHeight int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunUI starts the terminal UI
|
// RunUI starts the terminal UI
|
||||||
func RunUI(orgFile *model.OrgFile) error {
|
func RunUI(orgFile *model.OrgFile, cfg *config.Config) error {
|
||||||
p := tea.NewProgram(initialModel(orgFile), tea.WithAltScreen())
|
p := tea.NewProgram(InitialModel(orgFile, cfg), tea.WithAltScreen())
|
||||||
_, err := p.Run()
|
_, err := p.Run()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/charmbracelet/bubbles/key"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
Up key.Binding
|
Up key.Binding
|
||||||
|
|
@ -25,97 +30,134 @@ type keyMap struct {
|
||||||
SetDeadline key.Binding
|
SetDeadline key.Binding
|
||||||
SetPriority key.Binding
|
SetPriority key.Binding
|
||||||
SetEffort key.Binding
|
SetEffort key.Binding
|
||||||
|
Settings key.Binding
|
||||||
|
TagItem key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
var keys = keyMap{
|
// newKeyMapFromConfig creates a keyMap from configuration
|
||||||
Up: key.NewBinding(
|
func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
||||||
key.WithKeys("up", "k"),
|
kb := cfg.Keybindings
|
||||||
key.WithHelp("↑/k", "move up"),
|
|
||||||
),
|
return keyMap{
|
||||||
Down: key.NewBinding(
|
Up: key.NewBinding(
|
||||||
key.WithKeys("down", "j"),
|
key.WithKeys(kb.Up...),
|
||||||
key.WithHelp("↓/j", "move down"),
|
key.WithHelp(formatKeyHelp(kb.Up), "move up"),
|
||||||
),
|
),
|
||||||
Left: key.NewBinding(
|
Down: key.NewBinding(
|
||||||
key.WithKeys("left", "h"),
|
key.WithKeys(kb.Down...),
|
||||||
key.WithHelp("←/h", "cycle state backward"),
|
key.WithHelp(formatKeyHelp(kb.Down), "move down"),
|
||||||
),
|
),
|
||||||
Right: key.NewBinding(
|
Left: key.NewBinding(
|
||||||
key.WithKeys("right", "l"),
|
key.WithKeys(kb.Left...),
|
||||||
key.WithHelp("→/l", "cycle state forward"),
|
key.WithHelp(formatKeyHelp(kb.Left), "cycle state backward"),
|
||||||
),
|
),
|
||||||
ShiftUp: key.NewBinding(
|
Right: key.NewBinding(
|
||||||
key.WithKeys("shift+up"),
|
key.WithKeys(kb.Right...),
|
||||||
key.WithHelp("shift+↑", "move item up"),
|
key.WithHelp(formatKeyHelp(kb.Right), "cycle state forward"),
|
||||||
),
|
),
|
||||||
ShiftDown: key.NewBinding(
|
ShiftUp: key.NewBinding(
|
||||||
key.WithKeys("shift+down"),
|
key.WithKeys(kb.ShiftUp...),
|
||||||
key.WithHelp("shift+↓", "move item down"),
|
key.WithHelp(formatKeyHelp(kb.ShiftUp), "move item up"),
|
||||||
),
|
),
|
||||||
CycleState: key.NewBinding(
|
ShiftDown: key.NewBinding(
|
||||||
key.WithKeys("t", " "),
|
key.WithKeys(kb.ShiftDown...),
|
||||||
key.WithHelp("t/space", "cycle todo state"),
|
key.WithHelp(formatKeyHelp(kb.ShiftDown), "move item down"),
|
||||||
),
|
),
|
||||||
ToggleFold: key.NewBinding(
|
CycleState: key.NewBinding(
|
||||||
key.WithKeys("tab"),
|
key.WithKeys(kb.CycleState...),
|
||||||
key.WithHelp("tab", "fold/unfold"),
|
key.WithHelp(formatKeyHelp(kb.CycleState), "cycle todo state"),
|
||||||
),
|
),
|
||||||
EditNotes: key.NewBinding(
|
ToggleFold: key.NewBinding(
|
||||||
key.WithKeys("enter"),
|
key.WithKeys(kb.ToggleFold...),
|
||||||
key.WithHelp("enter", "edit notes"),
|
key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"),
|
||||||
),
|
),
|
||||||
ToggleView: key.NewBinding(
|
EditNotes: key.NewBinding(
|
||||||
key.WithKeys("a"),
|
key.WithKeys(kb.EditNotes...),
|
||||||
key.WithHelp("a", "toggle agenda view"),
|
key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"),
|
||||||
),
|
),
|
||||||
Capture: key.NewBinding(
|
ToggleView: key.NewBinding(
|
||||||
key.WithKeys("c"),
|
key.WithKeys(kb.ToggleView...),
|
||||||
key.WithHelp("c", "capture TODO"),
|
key.WithHelp(formatKeyHelp(kb.ToggleView), "toggle agenda view"),
|
||||||
),
|
),
|
||||||
AddSubTask: key.NewBinding(
|
Capture: key.NewBinding(
|
||||||
key.WithKeys("s"),
|
key.WithKeys(kb.Capture...),
|
||||||
key.WithHelp("s", "add sub-task"),
|
key.WithHelp(formatKeyHelp(kb.Capture), "capture TODO"),
|
||||||
),
|
),
|
||||||
Delete: key.NewBinding(
|
AddSubTask: key.NewBinding(
|
||||||
key.WithKeys("D"),
|
key.WithKeys(kb.AddSubTask...),
|
||||||
key.WithHelp("D", "delete item"),
|
key.WithHelp(formatKeyHelp(kb.AddSubTask), "add sub-task"),
|
||||||
),
|
),
|
||||||
Save: key.NewBinding(
|
Delete: key.NewBinding(
|
||||||
key.WithKeys("ctrl+s"),
|
key.WithKeys(kb.Delete...),
|
||||||
key.WithHelp("ctrl+s", "save"),
|
key.WithHelp(formatKeyHelp(kb.Delete), "delete item"),
|
||||||
),
|
),
|
||||||
ToggleReorder: key.NewBinding(
|
Save: key.NewBinding(
|
||||||
key.WithKeys("r"),
|
key.WithKeys(kb.Save...),
|
||||||
key.WithHelp("r", "reorder mode"),
|
key.WithHelp(formatKeyHelp(kb.Save), "save"),
|
||||||
),
|
),
|
||||||
ClockIn: key.NewBinding(
|
ToggleReorder: key.NewBinding(
|
||||||
key.WithKeys("i"),
|
key.WithKeys(kb.ToggleReorder...),
|
||||||
key.WithHelp("i", "clock in"),
|
key.WithHelp(formatKeyHelp(kb.ToggleReorder), "reorder mode"),
|
||||||
),
|
),
|
||||||
ClockOut: key.NewBinding(
|
ClockIn: key.NewBinding(
|
||||||
key.WithKeys("o"),
|
key.WithKeys(kb.ClockIn...),
|
||||||
key.WithHelp("o", "clock out"),
|
key.WithHelp(formatKeyHelp(kb.ClockIn), "clock in"),
|
||||||
),
|
),
|
||||||
SetDeadline: key.NewBinding(
|
ClockOut: key.NewBinding(
|
||||||
key.WithKeys("d"),
|
key.WithKeys(kb.ClockOut...),
|
||||||
key.WithHelp("d", "set deadline"),
|
key.WithHelp(formatKeyHelp(kb.ClockOut), "clock out"),
|
||||||
),
|
),
|
||||||
SetPriority: key.NewBinding(
|
SetDeadline: key.NewBinding(
|
||||||
key.WithKeys("p"),
|
key.WithKeys(kb.SetDeadline...),
|
||||||
key.WithHelp("p", "set priority"),
|
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
|
||||||
),
|
),
|
||||||
SetEffort: key.NewBinding(
|
SetPriority: key.NewBinding(
|
||||||
key.WithKeys("e"),
|
key.WithKeys(kb.SetPriority...),
|
||||||
key.WithHelp("e", "set effort"),
|
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
|
||||||
),
|
),
|
||||||
Help: key.NewBinding(
|
SetEffort: key.NewBinding(
|
||||||
key.WithKeys("?"),
|
key.WithKeys(kb.SetEffort...),
|
||||||
key.WithHelp("?", "toggle help"),
|
key.WithHelp(formatKeyHelp(kb.SetEffort), "set effort"),
|
||||||
),
|
),
|
||||||
Quit: key.NewBinding(
|
Help: key.NewBinding(
|
||||||
key.WithKeys("q", "ctrl+c"),
|
key.WithKeys(kb.Help...),
|
||||||
key.WithHelp("q", "quit"),
|
key.WithHelp(formatKeyHelp(kb.Help), "toggle help"),
|
||||||
),
|
),
|
||||||
|
Quit: key.NewBinding(
|
||||||
|
key.WithKeys(kb.Quit...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.Quit), "quit"),
|
||||||
|
),
|
||||||
|
Settings: key.NewBinding(
|
||||||
|
key.WithKeys(kb.Settings...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.Settings), "settings"),
|
||||||
|
),
|
||||||
|
TagItem: key.NewBinding(
|
||||||
|
key.WithKeys(kb.TagItem...),
|
||||||
|
key.WithHelp(formatKeyHelp(kb.TagItem), "add/edit tags"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatKeyHelp formats a slice of keys for display in help
|
||||||
|
func formatKeyHelp(keys []string) string {
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Take first two keys for display
|
||||||
|
if len(keys) == 1 {
|
||||||
|
return formatKey(keys[0])
|
||||||
|
}
|
||||||
|
return formatKey(keys[0]) + "/" + formatKey(keys[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatKey formats a single key for display
|
||||||
|
func formatKey(k string) string {
|
||||||
|
// Convert key names to symbols where appropriate
|
||||||
|
k = strings.ReplaceAll(k, "up", "↑")
|
||||||
|
k = strings.ReplaceAll(k, "down", "↓")
|
||||||
|
k = strings.ReplaceAll(k, "left", "←")
|
||||||
|
k = strings.ReplaceAll(k, "right", "→")
|
||||||
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k keyMap) ShortHelp() []key.Binding {
|
func (k keyMap) ShortHelp() []key.Binding {
|
||||||
|
|
@ -139,6 +181,6 @@ func (k keyMap) getAllBindings() []key.Binding {
|
||||||
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.SetPriority, k.SetEffort,
|
k.ClockIn, k.ClockOut, k.SetDeadline, k.SetPriority, k.SetEffort,
|
||||||
k.ToggleView, k.Help, k.Quit,
|
k.TagItem, k.Settings, k.ToggleView, k.Help, k.Quit,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m.updateSetEffort(msg)
|
return m.updateSetEffort(msg)
|
||||||
case modeHelp:
|
case modeHelp:
|
||||||
return m.updateHelp(msg)
|
return m.updateHelp(msg)
|
||||||
|
case modeSettings:
|
||||||
|
return m.updateSettings(msg)
|
||||||
|
case modeSettingsAddTag:
|
||||||
|
return m.updateSettingsAddTag(msg)
|
||||||
|
case modeSettingsAddState:
|
||||||
|
return m.updateSettingsAddState(msg)
|
||||||
|
case modeTagEdit:
|
||||||
|
return m.updateTagEdit(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
|
@ -99,9 +107,10 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.Right):
|
case key.Matches(msg, m.keys.Right):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
items[m.cursor].CycleState()
|
m.cycleStateForward(items[m.cursor])
|
||||||
// Auto clock out when changing to DONE
|
// Auto clock out when changing to last state (typically DONE)
|
||||||
if items[m.cursor].State == model.StateDONE && items[m.cursor].IsClockedIn() {
|
stateNames := m.config.GetStateNames()
|
||||||
|
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
||||||
items[m.cursor].ClockOut()
|
items[m.cursor].ClockOut()
|
||||||
}
|
}
|
||||||
m.setStatus("State changed")
|
m.setStatus("State changed")
|
||||||
|
|
@ -116,9 +125,10 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
case key.Matches(msg, m.keys.CycleState):
|
case key.Matches(msg, m.keys.CycleState):
|
||||||
items := m.getVisibleItems()
|
items := m.getVisibleItems()
|
||||||
if len(items) > 0 && m.cursor < len(items) {
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
items[m.cursor].CycleState()
|
m.cycleStateForward(items[m.cursor])
|
||||||
// Auto clock out when changing to DONE
|
// Auto clock out when changing to last state (typically DONE)
|
||||||
if items[m.cursor].State == model.StateDONE && items[m.cursor].IsClockedIn() {
|
stateNames := m.config.GetStateNames()
|
||||||
|
if len(stateNames) > 0 && string(items[m.cursor].State) == stateNames[len(stateNames)-1] && items[m.cursor].IsClockedIn() {
|
||||||
items[m.cursor].ClockOut()
|
items[m.cursor].ClockOut()
|
||||||
}
|
}
|
||||||
m.setStatus("State changed")
|
m.setStatus("State changed")
|
||||||
|
|
@ -145,6 +155,22 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, textarea.Blink
|
return m, textarea.Blink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Settings):
|
||||||
|
m.mode = modeSettings
|
||||||
|
m.initSettings()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.TagItem):
|
||||||
|
items := m.getVisibleItems()
|
||||||
|
if len(items) > 0 && m.cursor < len(items) {
|
||||||
|
m.editingItem = items[m.cursor]
|
||||||
|
m.mode = modeTagEdit
|
||||||
|
m.textinput.SetValue(strings.Join(items[m.cursor].Tags, ":"))
|
||||||
|
m.textinput.Placeholder = "tag1:tag2:tag3"
|
||||||
|
m.textinput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
|
}
|
||||||
|
|
||||||
case key.Matches(msg, m.keys.Capture):
|
case key.Matches(msg, m.keys.Capture):
|
||||||
m.mode = modeCapture
|
m.mode = modeCapture
|
||||||
m.textinput.SetValue("")
|
m.textinput.SetValue("")
|
||||||
|
|
@ -587,18 +613,69 @@ func (m uiModel) updateSetEffort(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *uiModel) cycleStateForward(item *model.Item) {
|
||||||
|
stateNames := m.config.GetStateNames()
|
||||||
|
if len(stateNames) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find current state index
|
||||||
|
currentIndex := -1
|
||||||
|
currentState := string(item.State)
|
||||||
|
|
||||||
|
// Handle empty state
|
||||||
|
if currentState == "" {
|
||||||
|
currentIndex = -1
|
||||||
|
} else {
|
||||||
|
for i, name := range stateNames {
|
||||||
|
if name == currentState {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle forward
|
||||||
|
if currentIndex < 0 || currentIndex >= len(stateNames)-1 {
|
||||||
|
if currentIndex == len(stateNames)-1 {
|
||||||
|
item.State = model.TodoState("") // Back to empty
|
||||||
|
} else {
|
||||||
|
item.State = model.TodoState(stateNames[0]) // First state
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.State = model.TodoState(stateNames[currentIndex+1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
func (m *uiModel) cycleStateBackward(item *model.Item) {
|
||||||
switch item.State {
|
stateNames := m.config.GetStateNames()
|
||||||
case model.StateNone:
|
if len(stateNames) == 0 {
|
||||||
item.State = model.StateDONE
|
return
|
||||||
case model.StateTODO:
|
}
|
||||||
item.State = model.StateNone
|
|
||||||
case model.StatePROG:
|
// Find current state index
|
||||||
item.State = model.StateTODO
|
currentIndex := -1
|
||||||
case model.StateBLOCK:
|
currentState := string(item.State)
|
||||||
item.State = model.StatePROG
|
|
||||||
case model.StateDONE:
|
// Handle empty state
|
||||||
item.State = model.StateBLOCK
|
if currentState == "" {
|
||||||
|
currentIndex = len(stateNames) // One past the last state
|
||||||
|
} else {
|
||||||
|
for i, name := range stateNames {
|
||||||
|
if name == currentState {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle backward
|
||||||
|
if currentIndex <= 0 {
|
||||||
|
item.State = model.TodoState("") // Empty state
|
||||||
|
} else if currentIndex > len(stateNames) {
|
||||||
|
item.State = model.TodoState(stateNames[len(stateNames)-1])
|
||||||
|
} else {
|
||||||
|
item.State = model.TodoState(stateNames[currentIndex-1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -774,3 +851,46 @@ func (m uiModel) updateHelp(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTagEdit handles tag editing mode
|
||||||
|
func (m *uiModel) updateTagEdit(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit):
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.Type == tea.KeyEnter:
|
||||||
|
if m.editingItem != nil {
|
||||||
|
// Parse tags from input (colon-separated)
|
||||||
|
tagsStr := m.textinput.Value()
|
||||||
|
var tags []string
|
||||||
|
if tagsStr != "" {
|
||||||
|
tags = strings.Split(tagsStr, ":")
|
||||||
|
// Remove empty strings
|
||||||
|
var filteredTags []string
|
||||||
|
for _, tag := range tags {
|
||||||
|
tag = strings.TrimSpace(tag)
|
||||||
|
if tag != "" {
|
||||||
|
filteredTags = append(filteredTags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags = filteredTags
|
||||||
|
}
|
||||||
|
m.editingItem.Tags = tags
|
||||||
|
m.setStatus("Tags updated")
|
||||||
|
}
|
||||||
|
m.mode = modeList
|
||||||
|
m.textinput.Blur()
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
723
internal/ui/settings.go
Normal file
723
internal/ui/settings.go
Normal file
|
|
@ -0,0 +1,723 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// settingsSection represents different sections in the settings view
|
||||||
|
type settingsSection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
settingsSectionTags settingsSection = iota
|
||||||
|
settingsSectionStates
|
||||||
|
settingsSectionKeybindings
|
||||||
|
)
|
||||||
|
|
||||||
|
// settingsState tracks the state of the settings editor
|
||||||
|
type settingsState struct {
|
||||||
|
section settingsSection
|
||||||
|
cursor int
|
||||||
|
scroll int
|
||||||
|
editing bool
|
||||||
|
editingField string
|
||||||
|
editingValue string
|
||||||
|
modified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// initSettings initializes the settings state
|
||||||
|
func (m *uiModel) initSettings() {
|
||||||
|
m.settingsCursor = 0
|
||||||
|
m.settingsScroll = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSettings handles updates in settings mode
|
||||||
|
func (m *uiModel) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
// If editing, handle text input
|
||||||
|
if m.textinput.Focused() {
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit):
|
||||||
|
m.textinput.Blur()
|
||||||
|
return m, nil
|
||||||
|
case msg.Type == tea.KeyEnter:
|
||||||
|
// Save the edited value
|
||||||
|
m.saveSettingsEdit()
|
||||||
|
m.textinput.Blur()
|
||||||
|
return m, nil
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation and actions
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit), key.Matches(msg, m.keys.Settings):
|
||||||
|
// Exit settings
|
||||||
|
m.mode = modeList
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Up):
|
||||||
|
if m.settingsCursor > 0 {
|
||||||
|
m.settingsCursor--
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Down):
|
||||||
|
maxCursor := m.getSettingsItemCount() - 1
|
||||||
|
if m.settingsCursor < maxCursor {
|
||||||
|
m.settingsCursor++
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ShiftUp):
|
||||||
|
// Move item up
|
||||||
|
m.moveSettingsItemUp()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.ShiftDown):
|
||||||
|
// Move item down
|
||||||
|
m.moveSettingsItemDown()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Left):
|
||||||
|
// Previous section
|
||||||
|
if m.settingsSection > settingsSectionTags {
|
||||||
|
m.settingsSection--
|
||||||
|
m.settingsCursor = 0
|
||||||
|
m.settingsScroll = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Right):
|
||||||
|
// Next section
|
||||||
|
if m.settingsSection < settingsSectionKeybindings {
|
||||||
|
m.settingsSection++
|
||||||
|
m.settingsCursor = 0
|
||||||
|
m.settingsScroll = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.EditNotes):
|
||||||
|
// Enter edit mode
|
||||||
|
m.startSettingsEdit()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Delete):
|
||||||
|
// Delete tag
|
||||||
|
m.deleteSettingsItem()
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Capture):
|
||||||
|
// Add new tag or state
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
m.addNewTag()
|
||||||
|
case settingsSectionStates:
|
||||||
|
m.addNewState()
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Cannot add keybindings yet
|
||||||
|
}
|
||||||
|
|
||||||
|
case key.Matches(msg, m.keys.Save):
|
||||||
|
// Save config to disk
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving config: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Configuration saved!")
|
||||||
|
// Reload keybindings and styles
|
||||||
|
m.keys = newKeyMapFromConfig(m.config)
|
||||||
|
m.styles = newStyleMapFromConfig(m.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSettingsItemCount returns the number of items in the current settings view
|
||||||
|
func (m *uiModel) getSettingsItemCount() int {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
return len(m.config.Tags.Tags) + 1 // +1 for "Add new tag" option
|
||||||
|
case settingsSectionStates:
|
||||||
|
return len(m.config.States.States) + 1 // +1 for "Add new state" option
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
return len(m.config.GetAllKeybindings())
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startSettingsEdit starts editing a settings item
|
||||||
|
func (m *uiModel) startSettingsEdit() {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tag := m.config.Tags.Tags[m.settingsCursor]
|
||||||
|
m.textinput.SetValue(tag.Name + "," + tag.Color)
|
||||||
|
m.textinput.Placeholder = "name,color (e.g., work,99)"
|
||||||
|
m.textinput.Focus()
|
||||||
|
|
||||||
|
case settingsSectionStates:
|
||||||
|
if m.settingsCursor >= len(m.config.States.States) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := m.config.States.States[m.settingsCursor]
|
||||||
|
m.textinput.SetValue(state.Name + "," + state.Color)
|
||||||
|
m.textinput.Placeholder = "name,color (e.g., TODO,202)"
|
||||||
|
m.textinput.Focus()
|
||||||
|
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Edit keybinding
|
||||||
|
keybindings := m.config.GetAllKeybindings()
|
||||||
|
|
||||||
|
// Convert to sorted slice
|
||||||
|
type kbPair struct {
|
||||||
|
action string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
var kbList []kbPair
|
||||||
|
for action, keys := range keybindings {
|
||||||
|
kbList = append(kbList, kbPair{action, keys})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
for i := 0; i < len(kbList)-1; i++ {
|
||||||
|
for j := i + 1; j < len(kbList); j++ {
|
||||||
|
if kbList[i].action > kbList[j].action {
|
||||||
|
kbList[i], kbList[j] = kbList[j], kbList[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.settingsCursor >= len(kbList) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := kbList[m.settingsCursor]
|
||||||
|
m.textinput.SetValue(strings.Join(kb.keys, ","))
|
||||||
|
m.textinput.Placeholder = "Enter keys separated by commas (e.g., up,k)"
|
||||||
|
m.textinput.Focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveSettingsEdit saves the edited value and auto-saves to disk
|
||||||
|
func (m *uiModel) saveSettingsEdit() {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Parse "name,color" format
|
||||||
|
parts := strings.Split(m.textinput.Value(), ",")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
tag := &m.config.Tags.Tags[m.settingsCursor]
|
||||||
|
tag.Name = strings.TrimSpace(parts[0])
|
||||||
|
tag.Color = strings.TrimSpace(parts[1])
|
||||||
|
m.setStatus(fmt.Sprintf("Updated tag '%s' (saved)", tag.Name))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Invalid format. Use: name,color")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionStates:
|
||||||
|
if m.settingsCursor >= len(m.config.States.States) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Parse "name,color" format
|
||||||
|
parts := strings.Split(m.textinput.Value(), ",")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
state := &m.config.States.States[m.settingsCursor]
|
||||||
|
state.Name = strings.TrimSpace(parts[0])
|
||||||
|
state.Color = strings.TrimSpace(parts[1])
|
||||||
|
m.setStatus(fmt.Sprintf("Updated state '%s' (saved)", state.Name))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Invalid format. Use: name,color")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Save keybinding
|
||||||
|
keybindings := m.config.GetAllKeybindings()
|
||||||
|
|
||||||
|
// Convert to sorted slice
|
||||||
|
type kbPair struct {
|
||||||
|
action string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
var kbList []kbPair
|
||||||
|
for action, keys := range keybindings {
|
||||||
|
kbList = append(kbList, kbPair{action, keys})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort alphabetically
|
||||||
|
for i := 0; i < len(kbList)-1; i++ {
|
||||||
|
for j := i + 1; j < len(kbList); j++ {
|
||||||
|
if kbList[i].action > kbList[j].action {
|
||||||
|
kbList[i], kbList[j] = kbList[j], kbList[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.settingsCursor >= len(kbList) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
kb := kbList[m.settingsCursor]
|
||||||
|
// Parse comma-separated keys
|
||||||
|
keysStr := m.textinput.Value()
|
||||||
|
var newKeys []string
|
||||||
|
for _, k := range strings.Split(keysStr, ",") {
|
||||||
|
k = strings.TrimSpace(k)
|
||||||
|
if k != "" {
|
||||||
|
newKeys = append(newKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newKeys) > 0 {
|
||||||
|
if err := m.config.UpdateKeybinding(kb.action, newKeys); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error updating keybinding: %v", err))
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
m.setStatus(fmt.Sprintf("Updated keybinding for '%s' (saved)", kb.action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save configuration to disk
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
||||||
|
} else {
|
||||||
|
// Reload keybindings and styles from updated config
|
||||||
|
m.keys = newKeyMapFromConfig(m.config)
|
||||||
|
m.styles = newStyleMapFromConfig(m.config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteSettingsItem deletes the current settings item and auto-saves
|
||||||
|
func (m *uiModel) deleteSettingsItem() {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tag := m.config.Tags.Tags[m.settingsCursor]
|
||||||
|
m.config.RemoveTag(tag.Name)
|
||||||
|
m.setStatus(fmt.Sprintf("Deleted tag '%s' (saved)", tag.Name))
|
||||||
|
|
||||||
|
// Adjust cursor if needed
|
||||||
|
if m.settingsCursor >= len(m.config.Tags.Tags) {
|
||||||
|
m.settingsCursor = len(m.config.Tags.Tags) - 1
|
||||||
|
if m.settingsCursor < 0 {
|
||||||
|
m.settingsCursor = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionStates:
|
||||||
|
if m.settingsCursor >= len(m.config.States.States) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state := m.config.States.States[m.settingsCursor]
|
||||||
|
m.config.RemoveState(state.Name)
|
||||||
|
m.setStatus(fmt.Sprintf("Deleted state '%s' (saved)", state.Name))
|
||||||
|
|
||||||
|
// Adjust cursor if needed
|
||||||
|
if m.settingsCursor >= len(m.config.States.States) {
|
||||||
|
m.settingsCursor = len(m.config.States.States) - 1
|
||||||
|
if m.settingsCursor < 0 {
|
||||||
|
m.settingsCursor = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Keybindings cannot be deleted
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save configuration to disk
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error auto-saving config: %v", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewTag adds a new tag
|
||||||
|
func (m *uiModel) addNewTag() {
|
||||||
|
m.textinput.SetValue("")
|
||||||
|
m.textinput.Placeholder = "Enter tag name"
|
||||||
|
m.textinput.Focus()
|
||||||
|
m.textinput.Blur() // Will be refocused when user types
|
||||||
|
|
||||||
|
// Prompt for tag name first, then color
|
||||||
|
m.mode = modeSettingsAddTag
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewState adds a new state
|
||||||
|
func (m *uiModel) addNewState() {
|
||||||
|
m.textinput.SetValue("")
|
||||||
|
m.textinput.Placeholder = "Enter state name"
|
||||||
|
m.textinput.Focus()
|
||||||
|
m.textinput.Blur() // Will be refocused when user types
|
||||||
|
|
||||||
|
// Prompt for state name first, then color
|
||||||
|
m.mode = modeSettingsAddState
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettings renders the settings view
|
||||||
|
func (m *uiModel) viewSettings() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
// Title
|
||||||
|
title := m.styles.titleStyle.Render("⚙ Settings")
|
||||||
|
content.WriteString(title + "\n\n")
|
||||||
|
|
||||||
|
// Tab selector
|
||||||
|
tabStyle := lipgloss.NewStyle().Padding(0, 2)
|
||||||
|
activeTabStyle := lipgloss.NewStyle().Padding(0, 2).Bold(true).Foreground(lipgloss.Color(m.config.Colors.Title))
|
||||||
|
|
||||||
|
tabs := ""
|
||||||
|
if m.settingsSection == settingsSectionTags {
|
||||||
|
tabs += activeTabStyle.Render("[Tags]")
|
||||||
|
} else {
|
||||||
|
tabs += tabStyle.Render("Tags")
|
||||||
|
}
|
||||||
|
tabs += " "
|
||||||
|
if m.settingsSection == settingsSectionStates {
|
||||||
|
tabs += activeTabStyle.Render("[States]")
|
||||||
|
} else {
|
||||||
|
tabs += tabStyle.Render("States")
|
||||||
|
}
|
||||||
|
tabs += " "
|
||||||
|
if m.settingsSection == settingsSectionKeybindings {
|
||||||
|
tabs += activeTabStyle.Render("[Keybindings]")
|
||||||
|
} else {
|
||||||
|
tabs += tabStyle.Render("Keybindings")
|
||||||
|
}
|
||||||
|
content.WriteString(tabs + "\n\n")
|
||||||
|
|
||||||
|
// Instructions
|
||||||
|
var instructions string
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
instructions = "←/→: Switch tabs • ↑/↓: Navigate • Enter: Edit • D: Delete • c: Add new tag • ctrl+s: Save • q/,: Exit"
|
||||||
|
case settingsSectionStates:
|
||||||
|
instructions = "←/→: Switch tabs • ↑/↓: Navigate • shift+↑/↓: Reorder • Enter: Edit • D: Delete • c: Add new state • ctrl+s: Save • q/,: Exit"
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
instructions = "←/→: Switch tabs • ↑/↓: Navigate • Enter: Edit keybinding • ctrl+s: Save • q/,: Exit"
|
||||||
|
}
|
||||||
|
content.WriteString(m.styles.statusStyle.Render(instructions) + "\n\n")
|
||||||
|
|
||||||
|
// Render the appropriate section
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
content.WriteString(m.viewSettingsTags())
|
||||||
|
case settingsSectionStates:
|
||||||
|
content.WriteString(m.viewSettingsStates())
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
content.WriteString(m.viewSettingsKeybindings())
|
||||||
|
}
|
||||||
|
|
||||||
|
// If editing, show input
|
||||||
|
if m.textinput.Focused() {
|
||||||
|
content.WriteString("\n")
|
||||||
|
content.WriteString(m.textinput.View() + "\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Enter: Save • ESC/q: Cancel") + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettingsTags renders the tags section
|
||||||
|
func (m *uiModel) viewSettingsTags() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
for i, tag := range m.config.Tags.Tags {
|
||||||
|
line := ""
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
if i == m.settingsCursor && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag name with its color
|
||||||
|
tagStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(tag.Color))
|
||||||
|
line += fmt.Sprintf(":%s: ", tag.Name)
|
||||||
|
line += tagStyle.Render(fmt.Sprintf("(color: %s)", tag.Color))
|
||||||
|
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new tag option
|
||||||
|
if m.settingsCursor == len(m.config.Tags.Tags) && !m.textinput.Focused() {
|
||||||
|
content.WriteString("▶ ")
|
||||||
|
} else {
|
||||||
|
content.WriteString(" ")
|
||||||
|
}
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("+ Add new tag (press 'c')") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettingsStates renders the states section
|
||||||
|
func (m *uiModel) viewSettingsStates() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
for i, state := range m.config.States.States {
|
||||||
|
line := ""
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
if i == m.settingsCursor && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// State name with its color
|
||||||
|
stateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(state.Color))
|
||||||
|
line += stateStyle.Render(fmt.Sprintf("%s", state.Name))
|
||||||
|
line += fmt.Sprintf(" (color: %s)", state.Color)
|
||||||
|
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new state option
|
||||||
|
if m.settingsCursor == len(m.config.States.States) && !m.textinput.Focused() {
|
||||||
|
content.WriteString("▶ ")
|
||||||
|
} else {
|
||||||
|
content.WriteString(" ")
|
||||||
|
}
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("+ Add new state (press 'c')") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettingsKeybindings renders the keybindings section
|
||||||
|
func (m *uiModel) viewSettingsKeybindings() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
// Get all keybindings
|
||||||
|
keybindings := m.config.GetAllKeybindings()
|
||||||
|
|
||||||
|
// Convert to sorted slice for consistent display
|
||||||
|
type kbPair struct {
|
||||||
|
action string
|
||||||
|
keys []string
|
||||||
|
}
|
||||||
|
var kbList []kbPair
|
||||||
|
for action, keys := range keybindings {
|
||||||
|
kbList = append(kbList, kbPair{action, keys})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple alphabetical sort by action name
|
||||||
|
for i := 0; i < len(kbList)-1; i++ {
|
||||||
|
for j := i + 1; j < len(kbList); j++ {
|
||||||
|
if kbList[i].action > kbList[j].action {
|
||||||
|
kbList[i], kbList[j] = kbList[j], kbList[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, kb := range kbList {
|
||||||
|
line := ""
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
if i == m.settingsCursor && !m.textinput.Focused() {
|
||||||
|
line += "▶ "
|
||||||
|
} else {
|
||||||
|
line += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format keybinding
|
||||||
|
keysStr := strings.Join(kb.keys, ", ")
|
||||||
|
line += fmt.Sprintf("%-20s : %s", kb.action, keysStr)
|
||||||
|
|
||||||
|
content.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// modeSettingsAddTag is a special mode for adding tags
|
||||||
|
const modeSettingsAddTag viewMode = 100
|
||||||
|
|
||||||
|
// modeSettingsAddState is a special mode for adding states
|
||||||
|
const modeSettingsAddState viewMode = 101
|
||||||
|
|
||||||
|
// updateSettingsAddTag handles the add tag flow
|
||||||
|
func (m *uiModel) updateSettingsAddTag(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if !m.textinput.Focused() {
|
||||||
|
m.textinput.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit):
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.mode = modeSettings
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.Type == tea.KeyEnter:
|
||||||
|
tagName := m.textinput.Value()
|
||||||
|
if tagName != "" {
|
||||||
|
// Default color
|
||||||
|
m.config.AddTag(tagName, "99")
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus(fmt.Sprintf("Added tag '%s' (saved)", tagName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.mode = modeSettings
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettingsAddTag renders the add tag view
|
||||||
|
func (m *uiModel) viewSettingsAddTag() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString(m.styles.titleStyle.Render("Add New Tag") + "\n\n")
|
||||||
|
content.WriteString(m.textinput.View() + "\n\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Enter tag name • Press Enter to add • ESC to cancel") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateSettingsAddState handles the add state flow
|
||||||
|
func (m *uiModel) updateSettingsAddState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.KeyMsg:
|
||||||
|
if !m.textinput.Focused() {
|
||||||
|
m.textinput.Focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case key.Matches(msg, m.keys.Quit):
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.mode = modeSettings
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
case msg.Type == tea.KeyEnter:
|
||||||
|
stateName := m.textinput.Value()
|
||||||
|
if stateName != "" {
|
||||||
|
// Default color
|
||||||
|
m.config.AddState(stateName, "99")
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus(fmt.Sprintf("Added state '%s' (saved)", stateName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.textinput.Blur()
|
||||||
|
m.mode = modeSettings
|
||||||
|
return m, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textinput, cmd = m.textinput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewSettingsAddState renders the add state view
|
||||||
|
func (m *uiModel) viewSettingsAddState() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString(m.styles.titleStyle.Render("Add New State") + "\n\n")
|
||||||
|
content.WriteString(m.textinput.View() + "\n\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Enter state name • Press Enter to add • ESC to cancel") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveSettingsItemUp moves the current settings item up and auto-saves
|
||||||
|
func (m *uiModel) moveSettingsItemUp() {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
if m.settingsCursor > 0 && m.settingsCursor < len(m.config.Tags.Tags) {
|
||||||
|
// Swap with previous item
|
||||||
|
m.config.Tags.Tags[m.settingsCursor], m.config.Tags.Tags[m.settingsCursor-1] =
|
||||||
|
m.config.Tags.Tags[m.settingsCursor-1], m.config.Tags.Tags[m.settingsCursor]
|
||||||
|
m.settingsCursor--
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Reordered (saved)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionStates:
|
||||||
|
if m.settingsCursor > 0 && m.settingsCursor < len(m.config.States.States) {
|
||||||
|
// Swap with previous item
|
||||||
|
m.config.States.States[m.settingsCursor], m.config.States.States[m.settingsCursor-1] =
|
||||||
|
m.config.States.States[m.settingsCursor-1], m.config.States.States[m.settingsCursor]
|
||||||
|
m.settingsCursor--
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Reordered (saved)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Keybindings cannot be reordered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveSettingsItemDown moves the current settings item down and auto-saves
|
||||||
|
func (m *uiModel) moveSettingsItemDown() {
|
||||||
|
switch m.settingsSection {
|
||||||
|
case settingsSectionTags:
|
||||||
|
if m.settingsCursor >= 0 && m.settingsCursor < len(m.config.Tags.Tags)-1 {
|
||||||
|
// Swap with next item
|
||||||
|
m.config.Tags.Tags[m.settingsCursor], m.config.Tags.Tags[m.settingsCursor+1] =
|
||||||
|
m.config.Tags.Tags[m.settingsCursor+1], m.config.Tags.Tags[m.settingsCursor]
|
||||||
|
m.settingsCursor++
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Reordered (saved)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionStates:
|
||||||
|
if m.settingsCursor >= 0 && m.settingsCursor < len(m.config.States.States)-1 {
|
||||||
|
// Swap with next item
|
||||||
|
m.config.States.States[m.settingsCursor], m.config.States.States[m.settingsCursor+1] =
|
||||||
|
m.config.States.States[m.settingsCursor+1], m.config.States.States[m.settingsCursor]
|
||||||
|
m.settingsCursor++
|
||||||
|
// Auto-save
|
||||||
|
if err := m.config.Save(); err != nil {
|
||||||
|
m.setStatus(fmt.Sprintf("Error saving: %v", err))
|
||||||
|
} else {
|
||||||
|
m.setStatus("Reordered (saved)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case settingsSectionKeybindings:
|
||||||
|
// Keybindings cannot be reordered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,18 +1,40 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "github.com/charmbracelet/lipgloss"
|
import (
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
// Styles for UI rendering
|
"github.com/rwejlgaard/org/internal/config"
|
||||||
var (
|
|
||||||
todoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("202")) // Orange
|
|
||||||
progStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("220")) // Yellow
|
|
||||||
blockStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) // Red
|
|
||||||
doneStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("34")) // Green
|
|
||||||
cursorStyle = lipgloss.NewStyle().Background(lipgloss.Color("240"))
|
|
||||||
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("99"))
|
|
||||||
scheduledStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("141")) // Purple
|
|
||||||
overdueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) // Red
|
|
||||||
statusStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Italic(true)
|
|
||||||
noteStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("246")).Italic(true)
|
|
||||||
foldedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// styleMap holds all the styles used in the UI
|
||||||
|
type styleMap struct {
|
||||||
|
todoStyle lipgloss.Style
|
||||||
|
progStyle lipgloss.Style
|
||||||
|
blockStyle lipgloss.Style
|
||||||
|
doneStyle lipgloss.Style
|
||||||
|
cursorStyle lipgloss.Style
|
||||||
|
titleStyle lipgloss.Style
|
||||||
|
scheduledStyle lipgloss.Style
|
||||||
|
overdueStyle lipgloss.Style
|
||||||
|
statusStyle lipgloss.Style
|
||||||
|
noteStyle lipgloss.Style
|
||||||
|
foldedStyle lipgloss.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStyleMapFromConfig creates a styleMap from configuration
|
||||||
|
func newStyleMapFromConfig(cfg *config.Config) styleMap {
|
||||||
|
colors := cfg.Colors
|
||||||
|
|
||||||
|
return styleMap{
|
||||||
|
todoStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Todo)),
|
||||||
|
progStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Progress)),
|
||||||
|
blockStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Blocked)),
|
||||||
|
doneStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Done)),
|
||||||
|
cursorStyle: lipgloss.NewStyle().Background(lipgloss.Color(colors.Cursor)),
|
||||||
|
titleStyle: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(colors.Title)),
|
||||||
|
scheduledStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Scheduled)),
|
||||||
|
overdueStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Overdue)),
|
||||||
|
statusStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Status)).Italic(true),
|
||||||
|
noteStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Note)).Italic(true),
|
||||||
|
foldedStyle: lipgloss.NewStyle().Foreground(lipgloss.Color(colors.Folded)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,14 @@ func (m uiModel) View() string {
|
||||||
return m.viewSetEffort()
|
return m.viewSetEffort()
|
||||||
case modeHelp:
|
case modeHelp:
|
||||||
return m.viewHelp()
|
return m.viewHelp()
|
||||||
|
case modeSettings:
|
||||||
|
return m.viewSettings()
|
||||||
|
case modeSettingsAddTag:
|
||||||
|
return m.viewSettingsAddTag()
|
||||||
|
case modeSettingsAddState:
|
||||||
|
return m.viewSettingsAddState()
|
||||||
|
case modeTagEdit:
|
||||||
|
return m.viewTagEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build footer (status + help)
|
// Build footer (status + help)
|
||||||
|
|
@ -94,7 +102,7 @@ func (m uiModel) View() string {
|
||||||
|
|
||||||
// Status message
|
// Status message
|
||||||
if time.Now().Before(m.statusExpiry) {
|
if time.Now().Before(m.statusExpiry) {
|
||||||
footer.WriteString(statusStyle.Render(m.statusMsg))
|
footer.WriteString(m.styles.statusStyle.Render(m.statusMsg))
|
||||||
footer.WriteString("\n")
|
footer.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,10 +125,10 @@ func (m uiModel) View() string {
|
||||||
}
|
}
|
||||||
if m.reorderMode {
|
if m.reorderMode {
|
||||||
reorderIndicator := lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render(" [REORDER MODE]")
|
reorderIndicator := lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Render(" [REORDER MODE]")
|
||||||
content.WriteString(titleStyle.Render(title))
|
content.WriteString(m.styles.titleStyle.Render(title))
|
||||||
content.WriteString(reorderIndicator)
|
content.WriteString(reorderIndicator)
|
||||||
} else {
|
} else {
|
||||||
content.WriteString(titleStyle.Render(title))
|
content.WriteString(m.styles.titleStyle.Render(title))
|
||||||
}
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
|
|
@ -261,7 +269,7 @@ func (m uiModel) viewConfirmDelete() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("⚠ Delete Item"))
|
content.WriteString(m.styles.titleStyle.Render("⚠ Delete Item"))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
||||||
if m.itemToDelete != nil {
|
if m.itemToDelete != nil {
|
||||||
|
|
@ -271,7 +279,7 @@ func (m uiModel) viewConfirmDelete() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("This will delete the item and all sub-tasks."))
|
content.WriteString(m.styles.statusStyle.Render("This will delete the item and all sub-tasks."))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString("Press Y to confirm • N or ESC to cancel")
|
content.WriteString("Press Y to confirm • N or ESC to cancel")
|
||||||
|
|
||||||
|
|
@ -289,11 +297,11 @@ func (m uiModel) viewCapture() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("Capture TODO"))
|
content.WriteString(m.styles.titleStyle.Render("Capture TODO"))
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(m.textinput.View())
|
content.WriteString(m.textinput.View())
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(statusStyle.Render("Press Enter to save • ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
|
@ -309,15 +317,15 @@ func (m uiModel) viewAddSubTask() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("Add Sub-Task"))
|
content.WriteString(m.styles.titleStyle.Render("Add Sub-Task"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("Under: %s", m.editingItem.Title)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("Under: %s", m.editingItem.Title)))
|
||||||
}
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(m.textinput.View())
|
content.WriteString(m.textinput.View())
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(statusStyle.Render("Press Enter to save • ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
|
@ -333,19 +341,19 @@ func (m uiModel) viewSetDeadline() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("Set Deadline"))
|
content.WriteString(m.styles.titleStyle.Render("Set Deadline"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
}
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(m.textinput.View())
|
content.WriteString(m.textinput.View())
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(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(statusStyle.Render("Leave empty to clear deadline"))
|
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear deadline"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("Press Enter to save • ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
|
@ -361,13 +369,13 @@ func (m uiModel) viewSetPriority() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("Set Priority"))
|
content.WriteString(m.styles.titleStyle.Render("Set Priority"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem.Priority != model.PriorityNone {
|
if m.editingItem.Priority != model.PriorityNone {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("Current: [#%s]", m.editingItem.Priority)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("Current: [#%s]", m.editingItem.Priority)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
|
|
@ -381,9 +389,9 @@ func (m uiModel) viewSetPriority() string {
|
||||||
content.WriteString(priorityBStyle.Render("[B] Medium Priority") + "\n")
|
content.WriteString(priorityBStyle.Render("[B] Medium Priority") + "\n")
|
||||||
content.WriteString(priorityCStyle.Render("[C] Low Priority") + "\n")
|
content.WriteString(priorityCStyle.Render("[C] Low Priority") + "\n")
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("Press Space/Enter to clear priority"))
|
content.WriteString(m.styles.statusStyle.Render("Press Space/Enter to clear priority"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("Press ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press ESC to cancel"))
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
|
@ -399,23 +407,23 @@ func (m uiModel) viewSetEffort() string {
|
||||||
Width(60)
|
Width(60)
|
||||||
|
|
||||||
var content strings.Builder
|
var content strings.Builder
|
||||||
content.WriteString(titleStyle.Render("Set Effort"))
|
content.WriteString(m.styles.titleStyle.Render("Set Effort"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
if m.editingItem.Effort != "" {
|
if m.editingItem.Effort != "" {
|
||||||
content.WriteString(statusStyle.Render(fmt.Sprintf("Current: %s", m.editingItem.Effort)))
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("Current: %s", m.editingItem.Effort)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(m.textinput.View())
|
content.WriteString(m.textinput.View())
|
||||||
content.WriteString("\n\n")
|
content.WriteString("\n\n")
|
||||||
content.WriteString(statusStyle.Render("Examples: 8h, 2d, 1w, 4h30m"))
|
content.WriteString(m.styles.statusStyle.Render("Examples: 8h, 2d, 1w, 4h30m"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("Leave empty to clear effort"))
|
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear effort"))
|
||||||
content.WriteString("\n")
|
content.WriteString("\n")
|
||||||
content.WriteString(statusStyle.Render("Press Enter to save • ESC to cancel"))
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel"))
|
||||||
|
|
||||||
dialog := dialogStyle.Render(content.String())
|
dialog := dialogStyle.Render(content.String())
|
||||||
|
|
||||||
|
|
@ -428,7 +436,7 @@ func (m uiModel) viewHelp() string {
|
||||||
var lines []string
|
var lines []string
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
lines = append(lines, titleStyle.Render("Keybindings Help"))
|
lines = append(lines, m.styles.titleStyle.Render("Keybindings Help"))
|
||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
|
|
||||||
// Group bindings by category
|
// Group bindings by category
|
||||||
|
|
@ -436,8 +444,8 @@ func (m uiModel) viewHelp() string {
|
||||||
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.SetEffort}
|
||||||
organizationBindings := []key.Binding{m.keys.SetPriority, 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.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}
|
||||||
|
|
||||||
// Helper function to render a binding
|
// Helper function to render a binding
|
||||||
renderBinding := func(b key.Binding) string {
|
renderBinding := func(b key.Binding) string {
|
||||||
|
|
@ -519,10 +527,10 @@ func (m uiModel) viewHelp() string {
|
||||||
var footer strings.Builder
|
var footer strings.Builder
|
||||||
if startLine > 0 || endLine < totalLines {
|
if startLine > 0 || endLine < totalLines {
|
||||||
scrollInfo := fmt.Sprintf("(Scroll: %d-%d of %d lines)", startLine+1, endLine, totalLines)
|
scrollInfo := fmt.Sprintf("(Scroll: %d-%d of %d lines)", startLine+1, endLine, totalLines)
|
||||||
footer.WriteString(statusStyle.Render(scrollInfo))
|
footer.WriteString(m.styles.statusStyle.Render(scrollInfo))
|
||||||
footer.WriteString(" ")
|
footer.WriteString(" ")
|
||||||
}
|
}
|
||||||
footer.WriteString(statusStyle.Render("↑/↓ scroll • ? or ESC to close"))
|
footer.WriteString(m.styles.statusStyle.Render("↑/↓ scroll • ? or ESC to close"))
|
||||||
|
|
||||||
// Combine content and footer
|
// Combine content and footer
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
|
|
@ -543,12 +551,12 @@ func (m uiModel) viewHelp() string {
|
||||||
func (m uiModel) viewEditMode() string {
|
func (m uiModel) viewEditMode() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(titleStyle.Render("Editing Notes"))
|
b.WriteString(m.styles.titleStyle.Render("Editing Notes"))
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
if m.editingItem != nil {
|
if m.editingItem != nil {
|
||||||
b.WriteString(fmt.Sprintf("Item: %s\n", m.editingItem.Title))
|
b.WriteString(fmt.Sprintf("Item: %s\n", m.editingItem.Title))
|
||||||
}
|
}
|
||||||
b.WriteString(statusStyle.Render("Press ESC to save and exit"))
|
b.WriteString(m.styles.statusStyle.Render("Press ESC to save and exit"))
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
b.WriteString(m.textarea.View())
|
b.WriteString(m.textarea.View())
|
||||||
|
|
@ -729,9 +737,9 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
// Fold indicator
|
// Fold indicator
|
||||||
if len(item.Children) > 0 || len(item.Notes) > 0 {
|
if len(item.Children) > 0 || len(item.Notes) > 0 {
|
||||||
if item.Folded {
|
if item.Folded {
|
||||||
b.WriteString(foldedStyle.Render("▶ "))
|
b.WriteString(m.styles.foldedStyle.Render("▶ "))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(foldedStyle.Render("▼ "))
|
b.WriteString(m.styles.foldedStyle.Render("▼ "))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(" ")
|
b.WriteString(" ")
|
||||||
|
|
@ -739,17 +747,10 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
|
|
||||||
// State
|
// State
|
||||||
stateStr := ""
|
stateStr := ""
|
||||||
switch item.State {
|
if item.State != model.StateNone {
|
||||||
case model.StateTODO:
|
stateColor := m.config.GetStateColor(string(item.State))
|
||||||
stateStr = todoStyle.Render("[TODO]")
|
stateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(stateColor))
|
||||||
case model.StatePROG:
|
stateStr = stateStyle.Render(fmt.Sprintf("[%s]", item.State))
|
||||||
stateStr = progStyle.Render("[PROG]")
|
|
||||||
case model.StateBLOCK:
|
|
||||||
stateStr = blockStyle.Render("[BLOCK]")
|
|
||||||
case model.StateDONE:
|
|
||||||
stateStr = doneStyle.Render("[DONE]")
|
|
||||||
default:
|
|
||||||
stateStr = "" // Empty space for alignment
|
|
||||||
}
|
}
|
||||||
b.WriteString(stateStr)
|
b.WriteString(stateStr)
|
||||||
b.WriteString(" ")
|
b.WriteString(" ")
|
||||||
|
|
@ -771,6 +772,16 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
// Title
|
// Title
|
||||||
b.WriteString(item.Title)
|
b.WriteString(item.Title)
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
if len(item.Tags) > 0 {
|
||||||
|
b.WriteString(" ")
|
||||||
|
for _, tag := range item.Tags {
|
||||||
|
tagColor := m.config.GetTagColor(tag)
|
||||||
|
tagStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(tagColor))
|
||||||
|
b.WriteString(tagStyle.Render(fmt.Sprintf(":%s:", tag)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Effort
|
// Effort
|
||||||
if item.Effort != "" {
|
if item.Effort != "" {
|
||||||
effortStr := fmt.Sprintf(" (Effort: %s)", item.Effort)
|
effortStr := fmt.Sprintf(" (Effort: %s)", item.Effort)
|
||||||
|
|
@ -812,23 +823,42 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
||||||
if item.Scheduled != nil {
|
if item.Scheduled != nil {
|
||||||
schedStr := fmt.Sprintf(" (Scheduled: %s)", parser.FormatOrgDate(*item.Scheduled))
|
schedStr := fmt.Sprintf(" (Scheduled: %s)", parser.FormatOrgDate(*item.Scheduled))
|
||||||
if item.Scheduled.Before(now) {
|
if item.Scheduled.Before(now) {
|
||||||
b.WriteString(overdueStyle.Render(schedStr))
|
b.WriteString(m.styles.overdueStyle.Render(schedStr))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(scheduledStyle.Render(schedStr))
|
b.WriteString(m.styles.scheduledStyle.Render(schedStr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if item.Deadline != nil {
|
if item.Deadline != nil {
|
||||||
deadlineStr := fmt.Sprintf(" (Deadline: %s)", parser.FormatOrgDate(*item.Deadline))
|
deadlineStr := fmt.Sprintf(" (Deadline: %s)", parser.FormatOrgDate(*item.Deadline))
|
||||||
if item.Deadline.Before(now) {
|
if item.Deadline.Before(now) {
|
||||||
b.WriteString(overdueStyle.Render(deadlineStr))
|
b.WriteString(m.styles.overdueStyle.Render(deadlineStr))
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(scheduledStyle.Render(deadlineStr))
|
b.WriteString(m.styles.scheduledStyle.Render(deadlineStr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
line := b.String()
|
line := b.String()
|
||||||
if isCursor {
|
if isCursor {
|
||||||
return cursorStyle.Render(line)
|
return m.styles.cursorStyle.Render(line)
|
||||||
}
|
}
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewTagEdit renders the tag editing view
|
||||||
|
func (m uiModel) viewTagEdit() string {
|
||||||
|
var content strings.Builder
|
||||||
|
|
||||||
|
content.WriteString(m.styles.titleStyle.Render("Edit Tags") + "\n\n")
|
||||||
|
|
||||||
|
if m.editingItem != nil {
|
||||||
|
content.WriteString(m.styles.statusStyle.Render(fmt.Sprintf("For: %s", m.editingItem.Title)) + "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
content.WriteString(m.textinput.View() + "\n\n")
|
||||||
|
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Enter tags separated by colons (e.g., work:urgent:important)") + "\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Leave empty to remove all tags") + "\n\n")
|
||||||
|
content.WriteString(m.styles.statusStyle.Render("Press Enter to save • ESC to cancel") + "\n")
|
||||||
|
|
||||||
|
return content.String()
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue