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
|
||||
|
||||
### 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
|
||||
- **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
|
||||
- **Quick Capture**: Press 'c' to quickly capture new TODO items
|
||||
- **Reorder Mode**: Reorganize tasks with shift+up/down arrows
|
||||
|
|
@ -63,6 +64,7 @@ org # Opens ./todo.org by default
|
|||
| `c` | Capture new TODO |
|
||||
| `s` | Add sub-task |
|
||||
| `D` | Delete item (with confirmation) |
|
||||
| `#` | Add/edit tags |
|
||||
| `a` | Toggle agenda view |
|
||||
| `i` | Clock in |
|
||||
| `o` | Clock out |
|
||||
|
|
@ -71,10 +73,13 @@ org # Opens ./todo.org by default
|
|||
| `e` | Set effort |
|
||||
| `r` | Toggle reorder mode |
|
||||
| `shift+↑/↓` | Move item up/down |
|
||||
| `,` | Open settings |
|
||||
| `ctrl+s` | Save |
|
||||
| `?` | Toggle help |
|
||||
| `q` or `ctrl+c` | Quit |
|
||||
|
||||
**Note**: All keybindings can be customized in the configuration file.
|
||||
|
||||
### Auto-save
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rwejlgaard/org/internal/config"
|
||||
"github.com/rwejlgaard/org/internal/parser"
|
||||
"github.com/rwejlgaard/org/internal/ui"
|
||||
)
|
||||
|
|
@ -31,6 +32,13 @@ func main() {
|
|||
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
|
||||
orgFile, err := parser.ParseOrgFile(filePath)
|
||||
if err != nil {
|
||||
|
|
@ -39,7 +47,7 @@ func main() {
|
|||
}
|
||||
|
||||
// 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)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -10,6 +10,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // 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/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
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
|
||||
Priority Priority // Priority: A, B, C, or empty
|
||||
Title string // The main title text
|
||||
Tags []string // Tags for this item (e.g., :work:urgent:)
|
||||
Scheduled *time.Time
|
||||
Deadline *time.Time
|
||||
Effort string // Effort estimate (e.g., "8h", "2d")
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import (
|
|||
|
||||
// Parser patterns
|
||||
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*<([^>]+)>`)
|
||||
deadlinePattern = regexp.MustCompile(`DEADLINE:\s*<([^>]+)>`)
|
||||
clockPattern = regexp.MustCompile(`CLOCK:\s*\[([^\]]+)\](?:--\[([^\]]+)\])?`)
|
||||
|
|
@ -108,13 +108,24 @@ func ParseOrgFile(path string) (*model.OrgFile, error) {
|
|||
level := len(matches[1])
|
||||
state := model.TodoState(matches[2])
|
||||
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{
|
||||
Level: level,
|
||||
State: state,
|
||||
Priority: priority,
|
||||
Title: title,
|
||||
Tags: tags,
|
||||
Notes: []string{},
|
||||
Children: []*model.Item{},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,14 @@ func writeItem(writer *bufio.Writer, item *model.Item) error {
|
|||
if item.Priority != model.PriorityNone {
|
||||
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 {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/charmbracelet/bubbles/textarea"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/rwejlgaard/org/internal/config"
|
||||
"github.com/rwejlgaard/org/internal/model"
|
||||
)
|
||||
|
||||
|
|
@ -23,6 +24,8 @@ const (
|
|||
modeSetPriority
|
||||
modeSetEffort
|
||||
modeHelp
|
||||
modeSettings
|
||||
modeTagEdit
|
||||
)
|
||||
|
||||
type uiModel struct {
|
||||
|
|
@ -33,6 +36,8 @@ type uiModel struct {
|
|||
mode viewMode
|
||||
help help.Model
|
||||
keys keyMap
|
||||
styles styleMap
|
||||
config *config.Config
|
||||
width int
|
||||
height int
|
||||
statusMsg string
|
||||
|
|
@ -42,9 +47,12 @@ type uiModel struct {
|
|||
textinput textinput.Model
|
||||
itemToDelete *model.Item
|
||||
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.Placeholder = "Enter notes here (code blocks supported)..."
|
||||
ta.ShowLineNumbers = false
|
||||
|
|
@ -61,7 +69,9 @@ func initialModel(orgFile *model.OrgFile) uiModel {
|
|||
cursor: 0,
|
||||
mode: modeList,
|
||||
help: h,
|
||||
keys: keys,
|
||||
keys: newKeyMapFromConfig(cfg),
|
||||
styles: newStyleMapFromConfig(cfg),
|
||||
config: cfg,
|
||||
textarea: ta,
|
||||
textinput: ti,
|
||||
}
|
||||
|
|
@ -122,8 +132,8 @@ func (m *uiModel) updateScrollOffset(availableHeight int) {
|
|||
}
|
||||
|
||||
// RunUI starts the terminal UI
|
||||
func RunUI(orgFile *model.OrgFile) error {
|
||||
p := tea.NewProgram(initialModel(orgFile), tea.WithAltScreen())
|
||||
func RunUI(orgFile *model.OrgFile, cfg *config.Config) error {
|
||||
p := tea.NewProgram(InitialModel(orgFile, cfg), tea.WithAltScreen())
|
||||
_, err := p.Run()
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package ui
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/rwejlgaard/org/internal/config"
|
||||
)
|
||||
|
||||
type keyMap struct {
|
||||
Up key.Binding
|
||||
|
|
@ -25,97 +30,134 @@ type keyMap struct {
|
|||
SetDeadline key.Binding
|
||||
SetPriority key.Binding
|
||||
SetEffort key.Binding
|
||||
Settings key.Binding
|
||||
TagItem key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
// newKeyMapFromConfig creates a keyMap from configuration
|
||||
func newKeyMapFromConfig(cfg *config.Config) keyMap {
|
||||
kb := cfg.Keybindings
|
||||
|
||||
return keyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "move up"),
|
||||
key.WithKeys(kb.Up...),
|
||||
key.WithHelp(formatKeyHelp(kb.Up), "move up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "move down"),
|
||||
key.WithKeys(kb.Down...),
|
||||
key.WithHelp(formatKeyHelp(kb.Down), "move down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←/h", "cycle state backward"),
|
||||
key.WithKeys(kb.Left...),
|
||||
key.WithHelp(formatKeyHelp(kb.Left), "cycle state backward"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→/l", "cycle state forward"),
|
||||
key.WithKeys(kb.Right...),
|
||||
key.WithHelp(formatKeyHelp(kb.Right), "cycle state forward"),
|
||||
),
|
||||
ShiftUp: key.NewBinding(
|
||||
key.WithKeys("shift+up"),
|
||||
key.WithHelp("shift+↑", "move item up"),
|
||||
key.WithKeys(kb.ShiftUp...),
|
||||
key.WithHelp(formatKeyHelp(kb.ShiftUp), "move item up"),
|
||||
),
|
||||
ShiftDown: key.NewBinding(
|
||||
key.WithKeys("shift+down"),
|
||||
key.WithHelp("shift+↓", "move item down"),
|
||||
key.WithKeys(kb.ShiftDown...),
|
||||
key.WithHelp(formatKeyHelp(kb.ShiftDown), "move item down"),
|
||||
),
|
||||
CycleState: key.NewBinding(
|
||||
key.WithKeys("t", " "),
|
||||
key.WithHelp("t/space", "cycle todo state"),
|
||||
key.WithKeys(kb.CycleState...),
|
||||
key.WithHelp(formatKeyHelp(kb.CycleState), "cycle todo state"),
|
||||
),
|
||||
ToggleFold: key.NewBinding(
|
||||
key.WithKeys("tab"),
|
||||
key.WithHelp("tab", "fold/unfold"),
|
||||
key.WithKeys(kb.ToggleFold...),
|
||||
key.WithHelp(formatKeyHelp(kb.ToggleFold), "fold/unfold"),
|
||||
),
|
||||
EditNotes: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "edit notes"),
|
||||
key.WithKeys(kb.EditNotes...),
|
||||
key.WithHelp(formatKeyHelp(kb.EditNotes), "edit notes"),
|
||||
),
|
||||
ToggleView: key.NewBinding(
|
||||
key.WithKeys("a"),
|
||||
key.WithHelp("a", "toggle agenda view"),
|
||||
key.WithKeys(kb.ToggleView...),
|
||||
key.WithHelp(formatKeyHelp(kb.ToggleView), "toggle agenda view"),
|
||||
),
|
||||
Capture: key.NewBinding(
|
||||
key.WithKeys("c"),
|
||||
key.WithHelp("c", "capture TODO"),
|
||||
key.WithKeys(kb.Capture...),
|
||||
key.WithHelp(formatKeyHelp(kb.Capture), "capture TODO"),
|
||||
),
|
||||
AddSubTask: key.NewBinding(
|
||||
key.WithKeys("s"),
|
||||
key.WithHelp("s", "add sub-task"),
|
||||
key.WithKeys(kb.AddSubTask...),
|
||||
key.WithHelp(formatKeyHelp(kb.AddSubTask), "add sub-task"),
|
||||
),
|
||||
Delete: key.NewBinding(
|
||||
key.WithKeys("D"),
|
||||
key.WithHelp("D", "delete item"),
|
||||
key.WithKeys(kb.Delete...),
|
||||
key.WithHelp(formatKeyHelp(kb.Delete), "delete item"),
|
||||
),
|
||||
Save: key.NewBinding(
|
||||
key.WithKeys("ctrl+s"),
|
||||
key.WithHelp("ctrl+s", "save"),
|
||||
key.WithKeys(kb.Save...),
|
||||
key.WithHelp(formatKeyHelp(kb.Save), "save"),
|
||||
),
|
||||
ToggleReorder: key.NewBinding(
|
||||
key.WithKeys("r"),
|
||||
key.WithHelp("r", "reorder mode"),
|
||||
key.WithKeys(kb.ToggleReorder...),
|
||||
key.WithHelp(formatKeyHelp(kb.ToggleReorder), "reorder mode"),
|
||||
),
|
||||
ClockIn: key.NewBinding(
|
||||
key.WithKeys("i"),
|
||||
key.WithHelp("i", "clock in"),
|
||||
key.WithKeys(kb.ClockIn...),
|
||||
key.WithHelp(formatKeyHelp(kb.ClockIn), "clock in"),
|
||||
),
|
||||
ClockOut: key.NewBinding(
|
||||
key.WithKeys("o"),
|
||||
key.WithHelp("o", "clock out"),
|
||||
key.WithKeys(kb.ClockOut...),
|
||||
key.WithHelp(formatKeyHelp(kb.ClockOut), "clock out"),
|
||||
),
|
||||
SetDeadline: key.NewBinding(
|
||||
key.WithKeys("d"),
|
||||
key.WithHelp("d", "set deadline"),
|
||||
key.WithKeys(kb.SetDeadline...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetDeadline), "set deadline"),
|
||||
),
|
||||
SetPriority: key.NewBinding(
|
||||
key.WithKeys("p"),
|
||||
key.WithHelp("p", "set priority"),
|
||||
key.WithKeys(kb.SetPriority...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetPriority), "set priority"),
|
||||
),
|
||||
SetEffort: key.NewBinding(
|
||||
key.WithKeys("e"),
|
||||
key.WithHelp("e", "set effort"),
|
||||
key.WithKeys(kb.SetEffort...),
|
||||
key.WithHelp(formatKeyHelp(kb.SetEffort), "set effort"),
|
||||
),
|
||||
Help: key.NewBinding(
|
||||
key.WithKeys("?"),
|
||||
key.WithHelp("?", "toggle help"),
|
||||
key.WithKeys(kb.Help...),
|
||||
key.WithHelp(formatKeyHelp(kb.Help), "toggle help"),
|
||||
),
|
||||
Quit: key.NewBinding(
|
||||
key.WithKeys("q", "ctrl+c"),
|
||||
key.WithHelp("q", "quit"),
|
||||
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 {
|
||||
|
|
@ -139,6 +181,6 @@ func (k keyMap) getAllBindings() []key.Binding {
|
|||
k.ToggleFold, k.EditNotes, k.ToggleReorder,
|
||||
k.Capture, k.AddSubTask, k.Delete, k.Save,
|
||||
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)
|
||||
case modeHelp:
|
||||
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) {
|
||||
|
|
@ -99,9 +107,10 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case key.Matches(msg, m.keys.Right):
|
||||
items := m.getVisibleItems()
|
||||
if len(items) > 0 && m.cursor < len(items) {
|
||||
items[m.cursor].CycleState()
|
||||
// Auto clock out when changing to DONE
|
||||
if items[m.cursor].State == model.StateDONE && items[m.cursor].IsClockedIn() {
|
||||
m.cycleStateForward(items[m.cursor])
|
||||
// Auto clock out when changing to last state (typically DONE)
|
||||
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()
|
||||
}
|
||||
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):
|
||||
items := m.getVisibleItems()
|
||||
if len(items) > 0 && m.cursor < len(items) {
|
||||
items[m.cursor].CycleState()
|
||||
// Auto clock out when changing to DONE
|
||||
if items[m.cursor].State == model.StateDONE && items[m.cursor].IsClockedIn() {
|
||||
m.cycleStateForward(items[m.cursor])
|
||||
// Auto clock out when changing to last state (typically DONE)
|
||||
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()
|
||||
}
|
||||
m.setStatus("State changed")
|
||||
|
|
@ -145,6 +155,22 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
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):
|
||||
m.mode = modeCapture
|
||||
m.textinput.SetValue("")
|
||||
|
|
@ -587,18 +613,69 @@ func (m uiModel) updateSetEffort(msg tea.Msg) (tea.Model, tea.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) {
|
||||
switch item.State {
|
||||
case model.StateNone:
|
||||
item.State = model.StateDONE
|
||||
case model.StateTODO:
|
||||
item.State = model.StateNone
|
||||
case model.StatePROG:
|
||||
item.State = model.StateTODO
|
||||
case model.StateBLOCK:
|
||||
item.State = model.StatePROG
|
||||
case model.StateDONE:
|
||||
item.State = model.StateBLOCK
|
||||
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 = 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
// Styles for UI rendering
|
||||
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"))
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/rwejlgaard/org/internal/config"
|
||||
)
|
||||
|
||||
// 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()
|
||||
case modeHelp:
|
||||
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)
|
||||
|
|
@ -94,7 +102,7 @@ func (m uiModel) View() string {
|
|||
|
||||
// Status message
|
||||
if time.Now().Before(m.statusExpiry) {
|
||||
footer.WriteString(statusStyle.Render(m.statusMsg))
|
||||
footer.WriteString(m.styles.statusStyle.Render(m.statusMsg))
|
||||
footer.WriteString("\n")
|
||||
}
|
||||
|
||||
|
|
@ -117,10 +125,10 @@ func (m uiModel) View() string {
|
|||
}
|
||||
if m.reorderMode {
|
||||
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)
|
||||
} else {
|
||||
content.WriteString(titleStyle.Render(title))
|
||||
content.WriteString(m.styles.titleStyle.Render(title))
|
||||
}
|
||||
content.WriteString("\n\n")
|
||||
|
||||
|
|
@ -261,7 +269,7 @@ func (m uiModel) viewConfirmDelete() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("⚠ Delete Item"))
|
||||
content.WriteString(m.styles.titleStyle.Render("⚠ Delete Item"))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
if m.itemToDelete != nil {
|
||||
|
|
@ -271,7 +279,7 @@ func (m uiModel) viewConfirmDelete() string {
|
|||
}
|
||||
|
||||
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("Press Y to confirm • N or ESC to cancel")
|
||||
|
||||
|
|
@ -289,11 +297,11 @@ func (m uiModel) viewCapture() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("Capture TODO"))
|
||||
content.WriteString(m.styles.titleStyle.Render("Capture TODO"))
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString(m.textinput.View())
|
||||
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())
|
||||
|
||||
|
|
@ -309,15 +317,15 @@ func (m uiModel) viewAddSubTask() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("Add Sub-Task"))
|
||||
content.WriteString(m.styles.titleStyle.Render("Add Sub-Task"))
|
||||
content.WriteString("\n")
|
||||
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(m.textinput.View())
|
||||
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())
|
||||
|
||||
|
|
@ -333,19 +341,19 @@ func (m uiModel) viewSetDeadline() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("Set Deadline"))
|
||||
content.WriteString(m.styles.titleStyle.Render("Set Deadline"))
|
||||
content.WriteString("\n")
|
||||
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(m.textinput.View())
|
||||
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(statusStyle.Render("Leave empty to clear deadline"))
|
||||
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear deadline"))
|
||||
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())
|
||||
|
||||
|
|
@ -361,13 +369,13 @@ func (m uiModel) viewSetPriority() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("Set Priority"))
|
||||
content.WriteString(m.styles.titleStyle.Render("Set Priority"))
|
||||
content.WriteString("\n")
|
||||
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")
|
||||
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")
|
||||
|
|
@ -381,9 +389,9 @@ func (m uiModel) viewSetPriority() string {
|
|||
content.WriteString(priorityBStyle.Render("[B] Medium Priority") + "\n")
|
||||
content.WriteString(priorityCStyle.Render("[C] Low Priority") + "\n")
|
||||
content.WriteString("\n")
|
||||
content.WriteString(statusStyle.Render("Press Space/Enter to clear priority"))
|
||||
content.WriteString(m.styles.statusStyle.Render("Press Space/Enter to clear priority"))
|
||||
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())
|
||||
|
||||
|
|
@ -399,23 +407,23 @@ func (m uiModel) viewSetEffort() string {
|
|||
Width(60)
|
||||
|
||||
var content strings.Builder
|
||||
content.WriteString(titleStyle.Render("Set Effort"))
|
||||
content.WriteString(m.styles.titleStyle.Render("Set Effort"))
|
||||
content.WriteString("\n")
|
||||
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")
|
||||
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(m.textinput.View())
|
||||
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(statusStyle.Render("Leave empty to clear effort"))
|
||||
content.WriteString(m.styles.statusStyle.Render("Leave empty to clear effort"))
|
||||
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())
|
||||
|
||||
|
|
@ -428,7 +436,7 @@ func (m uiModel) viewHelp() string {
|
|||
var lines []string
|
||||
|
||||
// Title
|
||||
lines = append(lines, titleStyle.Render("Keybindings Help"))
|
||||
lines = append(lines, m.styles.titleStyle.Render("Keybindings Help"))
|
||||
lines = append(lines, "")
|
||||
|
||||
// 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}
|
||||
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}
|
||||
organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder}
|
||||
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Save, m.keys.Help, m.keys.Quit}
|
||||
organizationBindings := []key.Binding{m.keys.SetPriority, m.keys.TagItem, m.keys.ShiftUp, m.keys.ShiftDown, m.keys.ToggleReorder}
|
||||
viewBindings := []key.Binding{m.keys.ToggleView, m.keys.Settings, m.keys.Save, m.keys.Help, m.keys.Quit}
|
||||
|
||||
// Helper function to render a binding
|
||||
renderBinding := func(b key.Binding) string {
|
||||
|
|
@ -519,10 +527,10 @@ func (m uiModel) viewHelp() string {
|
|||
var footer strings.Builder
|
||||
if startLine > 0 || 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(statusStyle.Render("↑/↓ scroll • ? or ESC to close"))
|
||||
footer.WriteString(m.styles.statusStyle.Render("↑/↓ scroll • ? or ESC to close"))
|
||||
|
||||
// Combine content and footer
|
||||
var result strings.Builder
|
||||
|
|
@ -543,12 +551,12 @@ func (m uiModel) viewHelp() string {
|
|||
func (m uiModel) viewEditMode() string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(titleStyle.Render("Editing Notes"))
|
||||
b.WriteString(m.styles.titleStyle.Render("Editing Notes"))
|
||||
b.WriteString("\n")
|
||||
if m.editingItem != nil {
|
||||
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(m.textarea.View())
|
||||
|
|
@ -729,9 +737,9 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
|||
// Fold indicator
|
||||
if len(item.Children) > 0 || len(item.Notes) > 0 {
|
||||
if item.Folded {
|
||||
b.WriteString(foldedStyle.Render("▶ "))
|
||||
b.WriteString(m.styles.foldedStyle.Render("▶ "))
|
||||
} else {
|
||||
b.WriteString(foldedStyle.Render("▼ "))
|
||||
b.WriteString(m.styles.foldedStyle.Render("▼ "))
|
||||
}
|
||||
} else {
|
||||
b.WriteString(" ")
|
||||
|
|
@ -739,17 +747,10 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
|||
|
||||
// State
|
||||
stateStr := ""
|
||||
switch item.State {
|
||||
case model.StateTODO:
|
||||
stateStr = todoStyle.Render("[TODO]")
|
||||
case model.StatePROG:
|
||||
stateStr = progStyle.Render("[PROG]")
|
||||
case model.StateBLOCK:
|
||||
stateStr = blockStyle.Render("[BLOCK]")
|
||||
case model.StateDONE:
|
||||
stateStr = doneStyle.Render("[DONE]")
|
||||
default:
|
||||
stateStr = "" // Empty space for alignment
|
||||
if item.State != model.StateNone {
|
||||
stateColor := m.config.GetStateColor(string(item.State))
|
||||
stateStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(stateColor))
|
||||
stateStr = stateStyle.Render(fmt.Sprintf("[%s]", item.State))
|
||||
}
|
||||
b.WriteString(stateStr)
|
||||
b.WriteString(" ")
|
||||
|
|
@ -771,6 +772,16 @@ func (m uiModel) renderItem(item *model.Item, isCursor bool) string {
|
|||
// 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
|
||||
if 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 {
|
||||
schedStr := fmt.Sprintf(" (Scheduled: %s)", parser.FormatOrgDate(*item.Scheduled))
|
||||
if item.Scheduled.Before(now) {
|
||||
b.WriteString(overdueStyle.Render(schedStr))
|
||||
b.WriteString(m.styles.overdueStyle.Render(schedStr))
|
||||
} else {
|
||||
b.WriteString(scheduledStyle.Render(schedStr))
|
||||
b.WriteString(m.styles.scheduledStyle.Render(schedStr))
|
||||
}
|
||||
}
|
||||
if item.Deadline != nil {
|
||||
deadlineStr := fmt.Sprintf(" (Deadline: %s)", parser.FormatOrgDate(*item.Deadline))
|
||||
if item.Deadline.Before(now) {
|
||||
b.WriteString(overdueStyle.Render(deadlineStr))
|
||||
b.WriteString(m.styles.overdueStyle.Render(deadlineStr))
|
||||
} else {
|
||||
b.WriteString(scheduledStyle.Render(deadlineStr))
|
||||
b.WriteString(m.styles.scheduledStyle.Render(deadlineStr))
|
||||
}
|
||||
}
|
||||
|
||||
line := b.String()
|
||||
if isCursor {
|
||||
return cursorStyle.Render(line)
|
||||
return m.styles.cursorStyle.Render(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