tklr-dgraham 0.0.0rc11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tklr-dgraham might be problematic. Click here for more details.

tklr/view_agenda.py ADDED
@@ -0,0 +1,236 @@
1
+ from datetime import datetime, timedelta
2
+ from collections import defaultdict
3
+ from itertools import product
4
+ from string import ascii_lowercase
5
+ from rich.console import Console
6
+ import readchar
7
+ from readchar import key
8
+ import copy
9
+ from typing import List, Tuple
10
+
11
+ from rich.style import Style
12
+ from colorsys import rgb_to_hls
13
+ from tklr.tklr_env import TklrEnvironment
14
+
15
+ # from tklr.model import UrgencyCalulator
16
+ from .shared import log_msg, display_messages
17
+
18
+ # env = TklrEnvironment()
19
+
20
+ # urgency = env.config.urgency
21
+
22
+ HIGHLIGHT = "#6495ED"
23
+ # name_style = Style(color=hex_val)
24
+ # HIGHLIGHT_STYLE = Style(color=get_contrasting_text_color(HIGHLIGHT), bgcolor=HIGHLIGHT)
25
+ HEADER = "#FFF8DC"
26
+ TASK = "#87CEFA"
27
+ EVENT = "#32CD32"
28
+ BEGIN = "#FFD700"
29
+ DRAFT = "#FFA07A"
30
+
31
+
32
+ def run_agenda_view(controller):
33
+ now = datetime.now()
34
+ console = Console()
35
+ width, height = console.size
36
+ max_lines_per_page = (height - 1) // 2 - 2 # split screen assumption
37
+
38
+ # Get events and tasks from controller
39
+ grouped_events = controller.get_agenda_events() # already grouped and labeled
40
+ tasks = controller.get_agenda_tasks()
41
+
42
+ event_pages = paginate_events_by_line_count(
43
+ grouped_events, max_lines_per_page, today=now.date()
44
+ )
45
+ tagged_event_pages = tag_paginated_events(event_pages)
46
+ urgency_pages = paginate_urgency_tasks(tasks, per_page=max_lines_per_page)
47
+
48
+ agenda_navigation_loop(tagged_event_pages, urgency_pages)
49
+
50
+
51
+ def generate_tags():
52
+ for length in range(1, 3): # a–z, aa–zz
53
+ for combo in product(ascii_lowercase, repeat=length):
54
+ yield "".join(combo)
55
+
56
+
57
+ def format_day_header(date, today):
58
+ dtstr = date.strftime("%a %b %-d")
59
+ tomorrow = today + timedelta(days=1)
60
+ if date == today:
61
+ label = f"[{HEADER}][not bold]{dtstr}[/not bold] (Today)[/{HEADER}]"
62
+ elif date == tomorrow:
63
+ label = f"[{HEADER}][not bold]{dtstr}[/not bold] (Tomorrow)[/{HEADER}]"
64
+ else:
65
+ label = f"[{HEADER}][not bold]{dtstr}[/not bold][/{HEADER}]"
66
+ return label
67
+
68
+
69
+ def paginate_events_by_line_count(events_by_date, max_lines_per_page, today):
70
+ from copy import deepcopy
71
+
72
+ def format_event_line(label, subject):
73
+ return f"[not bold]{label}[/not bold] {subject}" if label.strip() else subject
74
+
75
+ def calculate_padding(lines_used):
76
+ log_msg(f"{max_lines_per_page = }, {lines_used = }")
77
+ return max(0, max_lines_per_page - lines_used)
78
+
79
+ grouped = deepcopy(events_by_date)
80
+ sorted_dates = sorted(grouped.keys())
81
+
82
+ pages = []
83
+ current_page = []
84
+ current_line_count = 0
85
+ i = 0
86
+ carryover = None
87
+
88
+ while i < len(sorted_dates) or carryover:
89
+ if carryover:
90
+ date, events = carryover
91
+ header = f"{format_day_header(date, today)} - continued"
92
+ else:
93
+ date = sorted_dates[i]
94
+ events = grouped[date]
95
+ header = format_day_header(date, today)
96
+
97
+ lines = []
98
+ continued = False
99
+ available_lines = max_lines_per_page
100
+
101
+ if len(events) > available_lines:
102
+ # Avoid showing lonely header
103
+ if available_lines < 2:
104
+ if current_page:
105
+ # Pad if needed
106
+ pad = calculate_padding(current_line_count)
107
+ if pad:
108
+ current_page.append(("", [""] * pad, False))
109
+ pages.append(current_page)
110
+ current_page = []
111
+ current_line_count = 0
112
+ carryover = (date, events)
113
+ continue
114
+
115
+ visible = events[: available_lines - 1]
116
+ remaining = events[available_lines - 1 :]
117
+ lines = [format_event_line(label, subject) for label, subject, _ in visible]
118
+ lines.append("\u21aa [dim]continues on next page[/dim]")
119
+ continued = True
120
+ carryover = (date, remaining)
121
+ else:
122
+ lines = [format_event_line(label, subject) for label, subject, _ in events]
123
+ carryover = None
124
+ continued = False
125
+ i += 1
126
+
127
+ current_page.append((header, lines, continued))
128
+ current_line_count += len(lines) + 1 # +1 for header
129
+
130
+ if current_line_count >= max_lines_per_page:
131
+ pages.append(current_page)
132
+ current_page = []
133
+ current_line_count = 0
134
+
135
+ # Final page padding
136
+ if current_page:
137
+ current_line_count = sum(len(lines) + 1 for _, lines, _ in current_page)
138
+ pad = calculate_padding(current_line_count)
139
+ if pad:
140
+ current_page.append(("", [""] * pad, False))
141
+ pages.append(current_page)
142
+
143
+ return pages
144
+
145
+
146
+ def tag_paginated_events(pages):
147
+ tagged = []
148
+ for page in pages:
149
+ tag_gen = generate_tags()
150
+ tagged_page = []
151
+ for header, events, continued in page:
152
+ tagged_events = []
153
+ for line in events:
154
+ if line.startswith("\u21aa") or not line.strip():
155
+ tag = ""
156
+ else:
157
+ tag = next(tag_gen)
158
+ tagged_events.append((tag, line))
159
+ hdr = header
160
+ tagged_page.append((hdr, tagged_events))
161
+ tagged.append(tagged_page)
162
+ return tagged
163
+
164
+
165
+ def paginate_urgency_tasks(tasks, per_page=10):
166
+ pages = []
167
+ current_page = []
168
+ tag_gen = generate_tags()
169
+ for i, (urgency, color, subject, id, job) in enumerate(
170
+ tasks
171
+ ): # (urgency, subject, record_id, job_id)
172
+ if i % per_page == 0 and current_page:
173
+ pages.append(current_page)
174
+ current_page = []
175
+ tag_gen = generate_tags()
176
+ tag = next(tag_gen)
177
+ current_page.append((tag, urgency, color, subject, id, job))
178
+ if current_page:
179
+ pages.append(current_page)
180
+ return pages
181
+
182
+
183
+ def agenda_navigation_loop(event_pages, task_pages):
184
+ console = Console()
185
+ total_event_pages = len(event_pages)
186
+ total_task_pages = len(task_pages)
187
+ event_page = 0
188
+ task_page = 0
189
+ active_pane = "events"
190
+
191
+ while True:
192
+ console.clear()
193
+ event_title = f" Events (Page {event_page + 1} of {total_event_pages}) "
194
+ task_title = f" Tasks (Page {task_page + 1} of {total_task_pages}) "
195
+
196
+ console.rule(
197
+ f"[bold black on {HIGHLIGHT}]{event_title}[/]"
198
+ if active_pane == "events"
199
+ else f"[{HEADER}]{event_title}[/]"
200
+ )
201
+ for header, events in event_pages[event_page]:
202
+ console.print(f"[{HEADER}]{header}[/{HEADER}]")
203
+ for tag, line in events:
204
+ console.print(
205
+ f" [dim]{tag}[/dim] [{EVENT}]{line}[/{EVENT}]"
206
+ if tag
207
+ else f" {line}"
208
+ )
209
+
210
+ console.rule(
211
+ f"[bold black on {HIGHLIGHT}]{task_title}[/]"
212
+ if active_pane == "tasks"
213
+ else f"[{HEADER}]{task_title}[/]"
214
+ )
215
+ for tag, urgency, color, subject, id, job in task_pages[task_page]:
216
+ console.print(
217
+ f" [dim]{tag}[/dim] [not bold][{color}]{str(round(urgency * 100)):>2}[/{color}] [{TASK}]{subject} [dim]{id} {job if job else ''}[/dim][/{TASK}][/not bold]"
218
+ )
219
+
220
+ console.print("\n[dim]←/→ switch page; ↑/↓ switch pane; Q to quit[/dim]")
221
+
222
+ keypress = readchar.readkey()
223
+ if keypress == "Q":
224
+ break
225
+ elif keypress == key.UP or keypress == key.DOWN:
226
+ active_pane = "events" if active_pane == "tasks" else "tasks"
227
+ elif keypress == key.RIGHT:
228
+ if active_pane == "events" and event_page < total_event_pages - 1:
229
+ event_page += 1
230
+ elif active_pane == "tasks" and task_page < total_task_pages - 1:
231
+ task_page += 1
232
+ elif keypress == key.LEFT:
233
+ if active_pane == "events" and event_page > 0:
234
+ event_page -= 1
235
+ elif active_pane == "tasks" and task_page > 0:
236
+ task_page -= 1
tklr/view_textual.css ADDED
@@ -0,0 +1,296 @@
1
+ /* ── Screen ───────────────────────────────────────── */
2
+ Screen {
3
+ layout: vertical;
4
+ background: #373737 100%;
5
+ opacity: 100%;
6
+ }
7
+
8
+ /* Screen, */
9
+ /* SafeScreen, */
10
+ /* ScrollableList, */
11
+ /* ListWithDetails { */
12
+ /* layout: vertical; */
13
+ /* background: #373737; */
14
+ /* } */
15
+
16
+ /* ── Titles ───────────────────────────────────────── */
17
+ #table_title,
18
+ #week_title,
19
+ #scroll_title,
20
+ #details_title,
21
+ #list_title,
22
+ #events_title,
23
+ #tasks_title,
24
+ #dt_title,
25
+ #ed_title,
26
+ #bins_title {
27
+ color: white;
28
+ border-bottom: solid #7f7f7f;
29
+ text-style: bold;
30
+ text-align: center;
31
+ padding: 1 1 0 1;
32
+ }
33
+
34
+ /* ── ScrollView defaults (plain) ──────────────────── */
35
+ ScrollView {
36
+ padding: 0;
37
+ margin: 0;
38
+ }
39
+
40
+ /* Hidden utility */
41
+ .hidden {
42
+ display: none;
43
+ }
44
+
45
+ #custom_footer {
46
+ color: white;
47
+ text-style: bold;
48
+ border-top: #7f7f7f;
49
+ padding: 0 1 1 1;
50
+ }
51
+ /* ── Details Pane ───────────────────────────── */
52
+ #details-pane {
53
+ dock: bottom;
54
+ layout: vertical; /* header then body */
55
+ border-top: heavy #7f7f7f; /* optional separator */
56
+ height: 14; /* fixed: 2 rows for header + 12 for body */
57
+ padding: 0 1 1 1;
58
+ }
59
+
60
+ #details-pane.hidden {
61
+ display: none;
62
+ }
63
+
64
+ #details-title {
65
+ width: 1fr;
66
+ max-width: 1fr;
67
+ overflow: hidden;
68
+ text-overflow: ellipsis;
69
+ padding: 0 1;
70
+ }
71
+
72
+ #details-body {
73
+ height: 1fr; /* fills remaining space in the pane */
74
+ border: none;
75
+ }
76
+
77
+ #details-body-content {
78
+ border: none;
79
+ }
80
+
81
+ /* (Optional) Help panel styles you already had */
82
+ #help_panel {
83
+ width: 100%;
84
+ height: 100%;
85
+ border: solid #7f7f7f;
86
+ }
87
+ #help_layout {
88
+ height: 1fr;
89
+ }
90
+ #help_scroll {
91
+ height: 1fr;
92
+ }
93
+
94
+ .dim-rule {
95
+ color: #7f7f7f;
96
+ height: 1;
97
+ }
98
+
99
+ /* ---- DatetimePrompt Layout ---- */
100
+
101
+ /* Padding Supply 1, 2 or 4 integers separated by a space */
102
+ /* e.g. padding: 1; */
103
+ /* e.g. padding: 1 2; # Vertical, horizontal */
104
+ /* e.g. padding: 1 2 3 4; # Top, right, bottom, left */
105
+
106
+ /* Title bar FOR ALL TITLES */
107
+ .title-class {
108
+ text-align: center;
109
+ color: white;
110
+ text-style: bold;
111
+ border-bottom: solid #7f7f7f;
112
+ /* background: #373737; */
113
+ }
114
+
115
+ /* Horizontal rule */
116
+ .dim-rule {
117
+ color: #555555;
118
+ background: #373737;
119
+ }
120
+
121
+ /* Live feedback (“→ parsed datetime”) */
122
+ #dt_feedback {
123
+ color: limegreen;
124
+ padding: 0 1;
125
+ }
126
+ /* Message block just under the title */
127
+ #dt_message {
128
+ color: #cccccc; /* soft grey */
129
+ background: #373737;
130
+ padding: 0 1 1 1;
131
+ /* text-align: center; */
132
+ }
133
+
134
+ #dt_entry:focus {
135
+ border: solid #7f7f7f;
136
+ /* background: #373737; */
137
+ background: #2e2e2e;
138
+ color: white;
139
+ }
140
+
141
+ /* Fixed universal instructions */
142
+ #dt_instructions {
143
+ color: #cccccc;
144
+ background: #373737;
145
+ padding: 0 1;
146
+ text-align: center;
147
+ /* text-style: dim; */
148
+ }
149
+
150
+ #ed_prompt {
151
+ layout: vertical;
152
+ padding: 0 1;
153
+ }
154
+ /* Message block just under the title */
155
+
156
+ #ed_instructions {
157
+ color: #cccccc;
158
+ background: #373737;
159
+ padding: 0 1;
160
+ text-align: center;
161
+ /* text-style: dim; */
162
+ }
163
+
164
+ #ed_message {
165
+ color: #cccccc; /* soft grey */
166
+ background: #373737;
167
+ padding: 0 1;
168
+ /* text-align: center; */
169
+ }
170
+
171
+ #ed_entry:focus {
172
+ border: solid #7f7f7f;
173
+ /* background: #373737; */
174
+ background: #3a3a3a;
175
+ /* background: #2e2e2e; */
176
+ color: white;
177
+ height: auto;
178
+ padding: 0 0;
179
+ }
180
+
181
+ #ed_feedback {
182
+ height: auto;
183
+ color: limegreen;
184
+ padding: 0 1 0 1;
185
+ }
186
+ /* Container that holds a main list + details list */
187
+ ListWithDetails {
188
+ layout: vertical;
189
+ /* background: #373737; */
190
+ }
191
+
192
+ /* Main list fills available space */
193
+ ListWithDetails > #main-list {
194
+ /* background: #373737; */
195
+ height: 1fr;
196
+ }
197
+
198
+ /* Details list appears at bottom when shown and scrolls if long */
199
+ ListWithDetails > #details-list {
200
+ /* background: #373737; */
201
+ height: auto;
202
+ max-height: 14; /* tweak to taste */
203
+ border-top: solid #7f7f7f; /* optional separator */
204
+ /* border-bottom: solid #7f7f7f; */
205
+ /* padding-left: 1; */
206
+ /* border: none; */
207
+ }
208
+
209
+ ListWithDetails > #details_text {
210
+ /* background: #373737; */
211
+ color: yellow;
212
+ /* height: auto; */
213
+ /* max-height: 14; */
214
+ /* border-top: solid #7f7f7f; */
215
+ /* border-bottom: solid #7f7f7f; */
216
+ /* padding-left: 1; */
217
+ /* border: none; */
218
+ padding: 1 0;
219
+ }
220
+
221
+ ListWithDetails > #details-list.hidden {
222
+ display: none;
223
+ }
224
+
225
+ .busy-bar {
226
+ text-style: bold;
227
+ text-align: center;
228
+ /* padding: 0 0 1 0; */
229
+ height: auto;
230
+ /* border: none; */
231
+ border-bottom: #7f7f7f; /* optional separator */
232
+ }
233
+
234
+ /* ── BinHierarchyScreen ───────────────────────────── */
235
+
236
+ /* Make the tree fill the vertical space between Header and Footer */
237
+ #bins-tree {
238
+ height: 1fr; /* grow to fill */
239
+ margin: 0;
240
+ padding: 0 1; /* match your other lists */
241
+ /* border: solid #7f7f7f; */
242
+ background: #373737; /* same dark base */
243
+ color: #e0e0e0; /* legible on dark */
244
+ overflow: auto; /* scroll if tall */
245
+ }
246
+
247
+ /* Stronger focus ring to mirror inputs’ focus state */
248
+ #bins-tree:focus {
249
+ /* border: heavy #7f7f7f; */
250
+ background: #2e2e2e; /* slight focus darken like #dt_entry / #ed_entry */
251
+ }
252
+
253
+ /* Optional: tighten header/footer to your house style */
254
+ BinHierarchyScreen > Header,
255
+ BinHierarchyScreen > Footer {
256
+ background: #373737;
257
+ color: white;
258
+ border: none;
259
+ }
260
+
261
+ /* ── Tree niceties (Textual’s internal parts) ───────
262
+ These selectors are supported by Textual’s Tree widget.
263
+ If you’re on a very old Textual, you can safely delete them. */
264
+
265
+ /* Guides (│ ├ └) */
266
+ #bins-tree .tree--guides {
267
+ color: #7f7f7f;
268
+ }
269
+
270
+ /* The row your cursor is on */
271
+ #bins-tree .tree--cursor {
272
+ background: #2e2e2e;
273
+ color: white;
274
+ text-style: bold;
275
+ }
276
+
277
+ /* Labels (node text) */
278
+ #bins-tree .tree--label {
279
+ color: #e0e0e0;
280
+ }
281
+
282
+ /* Expanded node labels a touch bolder for hierarchy pop */
283
+ #bins-tree .tree--label.-expanded {
284
+ text-style: bold;
285
+ }
286
+
287
+ /* Dim disabled nodes (if you ever mark any) */
288
+ #bins-tree .-disabled .tree--label {
289
+ color: #7f7f7f;
290
+ }
291
+
292
+ /* Scrollbar to match the rest of the app (works on recent Textual) */
293
+ #bins-tree {
294
+ scrollbar-gutter: stable;
295
+ scrollbar-color: #7f7f7f #373737; /* thumb, track */
296
+ }