lune
created pr with
124.1
changed status to
closed
Closing as superceded by 126
cmds
checkout latest patchset:
ssh pr.pico.sh print 124 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 124.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 124set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 124set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 124
Patchset
124.1
listing real name of tasks
juan
construction of db for management of decrypted names of tasks
2026-04-21T03:21:16Zjuan
fix UTF8 chracteres
2026-04-21T05:43:09Zjuan
filter by status and coloring types of status
2026-04-21T21:13:24Zjuan
update help
2026-04-21T21:18:53Zjuan
→ list areas names
2026-04-21T21:21:36Zjuan
adjust table tasks to truncate name and list area
2026-04-21T21:44:53Zjuan
partial implementation of listing tasks by areas
2026-04-21T21:56:40Zjuan
final implementation of listing tasks by areas with UTF-8 chracteres
2026-04-21T22:05:20Zjuan
sort on task list to show first do now
2026-04-21T22:10:20Zjuan
update with 8 char of id and db.go
2026-04-21T22:33:29Zjuan
partial implementation of TUI menu on task update
2026-04-21T23:27:54Zjuan
task name correcion on TUI menu of Update task
2026-04-21T23:35:35Zjuan
TUI menu for update task with others flags
2026-04-21T23:38:19Zjuan
helper for TUI update task
2026-04-21T23:43:02Zjuan
search by first 8 char of note id for editing it
2026-04-21T23:49:56Zjuan
partial implementation on note TUI menu for update
2026-04-22T00:00:17Zjuan
TUI menu notebook just with tags
2026-04-22T00:05:46Zjuan
Note update now let you insert only the first 8 char of notebook id in update
2026-04-22T00:20:55Zjuan
implementation of (lune note notebook-list) for listing all notebooks and its ID
2026-04-22T00:25:53Zjuan
partial implementation of lune note show-content ID
2026-04-22T01:10:51Zjuan
convert \n in new line and \ in space for lune note show-content ID
2026-04-22T02:01:11Zjuan
2026-04-22T02:18:42Z
list areas names
juan
2026-04-21T21:44:53ZSemantic diff summary
7 added,
0 modified,
2 signature changed,
0 removed
across 2 analyzed files
+51
-10
cmd/area/list.go
#
| ... | ... | @@ -9,20 +9,29 @@ import ( | |
| 9 | 9 | "fmt" | |
| 10 | 10 | ||
| 11 | 11 | "git.secluded.site/lune/internal/config" | |
| 12 | + | "git.secluded.site/lune/internal/db" | |
| 12 | 13 | "git.secluded.site/lune/internal/ui" | |
| 13 | 14 | "github.com/charmbracelet/lipgloss" | |
| 14 | 15 | "github.com/charmbracelet/lipgloss/table" | |
| 15 | 16 | "github.com/spf13/cobra" | |
| 16 | 17 | ) | |
| 17 | 18 | ||
| 19 | + | type displayArea struct { | |
| 20 | + | ID string `json:"id"` | |
| 21 | + | Key string `json:"key"` | |
| 22 | + | Name string `json:"name"` | |
| 23 | + | Goals []config.Goal `json:"goals"` | |
| 24 | + | } | |
| 25 | + | ||
| 18 | 26 | // ListCmd lists configured areas and their goals. | |
| 19 | 27 | var ListCmd = &cobra.Command{ | |
| 20 | 28 | Use: "list", | |
| 21 | - | Short: "List configured areas", | |
| 22 | - | Long: `List areas configured in lune. | |
| 29 | + | Short: "List areas", | |
| 30 | + | Long: `List areas configured in lune or found in your local database. | |
| 23 | 31 | ||
| 24 | - | Areas are copied from the Lunatask desktop app during 'lune init'. | |
| 25 | - | Each area may have goals associated with it.`, | |
| 32 | + | Proactive Features: | |
| 33 | + | - Local Database Discovery: Automatically lists all areas from your Lunatask | |
| 34 | + | database if experimental mode is enabled.`, | |
| 26 | 35 | RunE: runList, | |
| 27 | 36 | } | |
| 28 | 37 |
| ... | ... | @@ -36,21 +45,53 @@ func runList(cmd *cobra.Command, _ []string) error { | |
| 36 | 45 | return err | |
| 37 | 46 | } | |
| 38 | 47 | ||
| 39 | - | if len(cfg.Areas) == 0 { | |
| 40 | - | fmt.Fprintln(cmd.OutOrStdout(), "No areas configured") | |
| 48 | + | areas := make([]displayArea, 0) | |
| 49 | + | for _, a := range cfg.Areas { | |
| 50 | + | areas = append(areas, displayArea{ | |
| 51 | + | ID: a.ID, | |
| 52 | + | Key: a.Key, | |
| 53 | + | Name: a.Name, | |
| 54 | + | Goals: a.Goals, | |
| 55 | + | }) | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | if cfg.Experimental.LocalDB { | |
| 59 | + | localAreas, _ := db.ListLocalAreas() | |
| 60 | + | for _, la := range localAreas { | |
| 61 | + | found := false | |
| 62 | + | for i, a := range areas { | |
| 63 | + | if a.ID == la.ID { | |
| 64 | + | areas[i].Name = la.Name // Use real name from DB | |
| 65 | + | found = true | |
| 66 | + | break | |
| 67 | + | } | |
| 68 | + | } | |
| 69 | + | if !found { | |
| 70 | + | areas = append(areas, displayArea{ | |
| 71 | + | ID: la.ID, | |
| 72 | + | Key: "-", // No key assigned yet | |
| 73 | + | Name: la.Name, | |
| 74 | + | Goals: nil, | |
| 75 | + | }) | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | if len(areas) == 0 { | |
| 81 | + | fmt.Fprintln(cmd.OutOrStdout(), "No areas found") | |
| 41 | 82 | ||
| 42 | 83 | return nil | |
| 43 | 84 | } | |
| 44 | 85 | ||
| 45 | 86 | jsonFlag, _ := cmd.Flags().GetBool("json") | |
| 46 | 87 | if jsonFlag { | |
| 47 | - | return outputJSON(cmd, cfg.Areas) | |
| 88 | + | return outputJSON(cmd, areas) | |
| 48 | 89 | } | |
| 49 | 90 | ||
| 50 | - | return outputTable(cmd, cfg.Areas) | |
| 91 | + | return outputTable(cmd, areas) | |
| 51 | 92 | } | |
| 52 | 93 | ||
| 53 | - | func outputJSON(cmd *cobra.Command, areas []config.Area) error { | |
| 94 | + | func outputJSON(cmd *cobra.Command, areas []displayArea) error { | |
| 54 | 95 | enc := json.NewEncoder(cmd.OutOrStdout()) | |
| 55 | 96 | enc.SetIndent("", " ") | |
| 56 | 97 |
| ... | ... | @@ -61,7 +102,7 @@ func outputJSON(cmd *cobra.Command, areas []config.Area) error { | |
| 61 | 102 | return nil | |
| 62 | 103 | } | |
| 63 | 104 | ||
| 64 | - | func outputTable(cmd *cobra.Command, areas []config.Area) error { | |
| 105 | + | func outputTable(cmd *cobra.Command, areas []displayArea) error { | |
| 65 | 106 | rows := make([][]string, 0, len(areas)) | |
| 66 | 107 | truncated := false | |
| 67 | 108 |
+93
-0
internal/db/reader.go
#
| ... | ... | @@ -89,6 +89,99 @@ func EnrichTask(taskID string) (string, error) { | |
| 89 | 89 | return "", nil | |
| 90 | 90 | } | |
| 91 | 91 | ||
| 92 | + | // Area represents a Lunatask area found in the local database. | |
| 93 | + | type Area struct { | |
| 94 | + | ID string | |
| 95 | + | Name string | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // ListLocalAreas scans the database for all areas using V8-serialized property matching. | |
| 99 | + | func ListLocalAreas() ([]Area, error) { | |
| 100 | + | dbPath := GetDefaultPath() | |
| 101 | + | if _, err := os.Stat(dbPath); os.IsNotExist(err) { | |
| 102 | + | return nil, nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | o := &opt.Options{ | |
| 106 | + | Comparer: idbComparer{}, | |
| 107 | + | ReadOnly: true, | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | db, err := leveldb.OpenFile(dbPath, o) | |
| 111 | + | if err != nil { | |
| 112 | + | return nil, err | |
| 113 | + | } | |
| 114 | + | defer db.Close() | |
| 115 | + | ||
| 116 | + | iter := db.NewIterator(nil, nil) | |
| 117 | + | defer iter.Release() | |
| 118 | + | ||
| 119 | + | areas := make([]Area, 0) | |
| 120 | + | for iter.Next() { | |
| 121 | + | val := iter.Value() | |
| 122 | + | ||
| 123 | + | // In V8 serialization, "modelType" is 22 09 6d 6f 64 65 6c 54 79 70 65 | |
| 124 | + | // and "area" is 22 04 61 72 65 61 | |
| 125 | + | if bytes.Contains(val, []byte("\x22\x09modelType")) && bytes.Contains(val, []byte("\x22\x04area")) { | |
| 126 | + | // Find the JSON blob in the "data" property | |
| 127 | + | dataIdx := bytes.Index(val, []byte("\x22\x04data")) | |
| 128 | + | if dataIdx != -1 { | |
| 129 | + | start := bytes.IndexByte(val[dataIdx:], '{') | |
| 130 | + | if start != -1 { | |
| 131 | + | start += dataIdx | |
| 132 | + | end := bytes.LastIndexByte(val, '}') | |
| 133 | + | if end > start { | |
| 134 | + | jsonBytes := val[start : end+1] | |
| 135 | + | id := extractField(jsonBytes, "id") | |
| 136 | + | name := extractField(jsonBytes, "name") | |
| 137 | + | if id != "" && name != "" { | |
| 138 | + | areas = append(areas, Area{ | |
| 139 | + | ID: id, | |
| 140 | + | Name: cleanName(name), | |
| 141 | + | }) | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | return areas, nil | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | func truncateString(s string, n int) string { | |
| 152 | + | if len(s) > n { | |
| 153 | + | return s[:n] + "..." | |
| 154 | + | } | |
| 155 | + | return s | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func extractFieldFromString(data, field string) string { | |
| 159 | + | re := regexp.MustCompile(`"` + field + `"\s*:\s*"([^"]+)"`) | |
| 160 | + | matches := re.FindSubmatch([]byte(data)) | |
| 161 | + | if len(matches) > 1 { | |
| 162 | + | return string(matches[1]) | |
| 163 | + | } | |
| 164 | + | return "" | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | func toUtf16(s string) []byte { | |
| 168 | + | res := make([]byte, 0, len(s)*2) | |
| 169 | + | for _, r := range s { | |
| 170 | + | res = append(res, byte(r), 0) | |
| 171 | + | } | |
| 172 | + | return res | |
| 173 | + | } | |
| 174 | + | ||
| 175 | + | func extractField(data []byte, field string) string { | |
| 176 | + | // Extract field using regex for better reliability in serialized JSON | |
| 177 | + | re := regexp.MustCompile(`"` + field + `"\s*:\s*"([^"]+)"`) | |
| 178 | + | matches := re.FindSubmatch(data) | |
| 179 | + | if len(matches) > 1 { | |
| 180 | + | return string(matches[1]) | |
| 181 | + | } | |
| 182 | + | return "" | |
| 183 | + | } | |
| 184 | + | ||
| 92 | 185 | func cleanName(s string) string { | |
| 93 | 186 | if s == "" { | |
| 94 | 187 | return "" |