lune

created pr with 124.1 on 2026-04-22T02:46:44Z · by 613b58b7
changed status to closed on 2026-06-21T16:45:31Z · by 66ddd677
Closing as superceded by 126
cmds
checkout latest patchset:
ssh pr.pico.sh print 124 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print 124.[rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 124
set PR to open (enables RSS notifications):
ssh pr.pico.sh pr open 124
set PR to draft (stops RSS notifications):
ssh pr.pico.sh pr draft 124

Patchset 124.1 on 2026-04-22T02:46:44Z · commit 3f24ad8

listing real name of tasks
juan 2026-04-21T03:21:16Z
construction of db for management of decrypted names of tasks
juan 2026-04-21T05:43:09Z
fix UTF8 chracteres
juan 2026-04-21T21:13:24Z
filter by status and coloring types of status
juan 2026-04-21T21:18:53Z
update help
juan 2026-04-21T21:21:36Z
list areas names
juan 2026-04-21T21:44:53Z
adjust table tasks to truncate name and list area
juan 2026-04-21T21:56:40Z
partial implementation of listing tasks by areas
juan 2026-04-21T22:05:20Z
final implementation of listing tasks by areas with UTF-8 chracteres
juan 2026-04-21T22:10:20Z
sort on task list to show first do now
juan 2026-04-21T22:33:29Z
update with 8 char of id and db.go
juan 2026-04-21T23:27:54Z
partial implementation of TUI menu on task update
juan 2026-04-21T23:35:35Z
task name correcion on TUI menu of Update task
juan 2026-04-21T23:38:19Z
TUI menu for update task with others flags
juan 2026-04-21T23:43:02Z
helper for TUI update task
juan 2026-04-21T23:49:56Z
search by first 8 char of note id for editing it
juan 2026-04-22T00:00:17Z
partial implementation on note TUI menu for update
juan 2026-04-22T00:05:46Z
TUI menu notebook just with tags
juan 2026-04-22T00:20:55Z
Note update now let you insert only the first 8 char of notebook id in update
juan 2026-04-22T00:25:53Z
implementation of (lune note notebook-list) for listing all notebooks and its ID
juan 2026-04-22T01:10:51Z
partial implementation of lune note show-content ID
juan 2026-04-22T02:01:11Z
convert \n in new line and \ in space for lune note show-content ID
juan 2026-04-22T02:18:42Z
+51 -10 cmd/area/list.go #
......@@ -9,20 +9,29 @@ import (
99 "fmt"
1010
1111 "git.secluded.site/lune/internal/config"
12+ "git.secluded.site/lune/internal/db"
1213 "git.secluded.site/lune/internal/ui"
1314 "github.com/charmbracelet/lipgloss"
1415 "github.com/charmbracelet/lipgloss/table"
1516 "github.com/spf13/cobra"
1617 )
1718
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+
1826 // ListCmd lists configured areas and their goals.
1927 var ListCmd = &cobra.Command{
2028 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.
2331
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.`,
2635 RunE: runList,
2736 }
2837
......@@ -36,21 +45,53 @@ func runList(cmd *cobra.Command, _ []string) error {
3645 return err
3746 }
3847
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")
4182
4283 return nil
4384 }
4485
4586 jsonFlag, _ := cmd.Flags().GetBool("json")
4687 if jsonFlag {
47- return outputJSON(cmd, cfg.Areas)
88+ return outputJSON(cmd, areas)
4889 }
4990
50- return outputTable(cmd, cfg.Areas)
91+ return outputTable(cmd, areas)
5192 }
5293
53-func outputJSON(cmd *cobra.Command, areas []config.Area) error {
94+func outputJSON(cmd *cobra.Command, areas []displayArea) error {
5495 enc := json.NewEncoder(cmd.OutOrStdout())
5596 enc.SetIndent("", " ")
5697
......@@ -61,7 +102,7 @@ func outputJSON(cmd *cobra.Command, areas []config.Area) error {
61102 return nil
62103 }
63104
64-func outputTable(cmd *cobra.Command, areas []config.Area) error {
105+func outputTable(cmd *cobra.Command, areas []displayArea) error {
65106 rows := make([][]string, 0, len(areas))
66107 truncated := false
67108
+93 -0 internal/db/reader.go #
......@@ -89,6 +89,99 @@ func EnrichTask(taskID string) (string, error) {
8989 return "", nil
9090 }
9191
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+
92185 func cleanName(s string) string {
93186 if s == "" {
94187 return ""
Back to top