From 901670bfe36682591f1fc6080146bce62789b66e Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 00:24:41 +0100 Subject: [PATCH 01/20] cal: initial version --- .../assets/hello.py | 541 +++++++++++++++++- 1 file changed, 536 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 87ed4dd3..b6a43556 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -1,10 +1,541 @@ from mpos import Activity +""" + +Create simple calendar application. On main screen, it should have +current time, date, and month overview. Current date and dates with +events should be highlighted. There should be list of upcoming events. + +When date is clicked, dialog with adding event for that date should be +displayed. Multi-day events should be supported. + +Data should be read/written to emacs org compatible text file. + + +""" + +import time +import os + +try: + import lvgl as lv +except ImportError: + pass + +from mpos import Activity + + +ORG_FILE = "/flash/calendar.org" # adjust for your device +MAX_UPCOMING = 8 + + +# ------------------------------------------------------------ +# Small date helpers (no datetime module assumed) +# ------------------------------------------------------------ + +def is_leap_year(y): + return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0) + + +def days_in_month(y, m): + if m == 2: + return 29 if is_leap_year(y) else 28 + if m in (1, 3, 5, 7, 8, 10, 12): + return 31 + return 30 + + +def ymd_to_int(y, m, d): + return y * 10000 + m * 100 + d + + +def int_to_ymd(v): + y = v // 10000 + m = (v // 100) % 100 + d = v % 100 + return y, m, d + + +def weekday_name(idx): + # MicroPython localtime(): 0=Mon..6=Sun typically + names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + if 0 <= idx < 7: + return names[idx] + return "???" + + +def first_weekday_of_month(y, m): + # brute-force using time.mktime if available + # Some ports support it, some don't. + # If it fails, we fallback to "Monday". + try: + # localtime tuple: (y,m,d,h,mi,s,wd,yd) + t = time.mktime((y, m, 1, 0, 0, 0, 0, 0)) + wd = time.localtime(t)[6] + return wd + except Exception: + return 0 + + +# ------------------------------------------------------------ +# Org event model + parser/writer +# ------------------------------------------------------------ + +class Event: + def __init__(self, title, start_ymd, end_ymd, start_time=None, end_time=None): + self.title = title + self.start = start_ymd # int yyyymmdd + self.end = end_ymd # int yyyymmdd + self.start_time = start_time # "HH:MM" or None + self.end_time = end_time # "HH:MM" or None + + def is_multi_day(self): + return self.end != self.start + + def occurs_on(self, ymd): + return self.start <= ymd <= self.end + + def start_key(self): + return self.start + + +class OrgCalendarStore: + def __init__(self, path): + self.path = path + + def load(self): + if not self._exists(self.path): + return [] + + try: + with open(self.path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + except Exception: + # fallback without encoding kw if unsupported + with open(self.path, "r") as f: + lines = f.read().splitlines() + + events = [] + current_title = None + + for line in lines: + line = line.strip() + + if line.startswith("** "): + current_title = line[3:].strip() + continue + + if not line.startswith("<"): + continue + + ev = self._parse_timestamp_line(current_title, line) + if ev: + events.append(ev) + + events.sort(key=lambda e: e.start_key()) + return events + + def save_append(self, event): + # Create file if missing + if not self._exists(self.path): + self._write_text("* Events\n") + + # Append event + out = [] + out.append("** " + event.title) + + if event.start == event.end: + y, m, d = int_to_ymd(event.start) + wd = weekday_name(self._weekday_for_ymd(y, m, d)) + if event.start_time and event.end_time: + out.append("<%04d-%02d-%02d %s %s-%s>" % ( + y, m, d, wd, event.start_time, event.end_time + )) + else: + out.append("<%04d-%02d-%02d %s>" % (y, m, d, wd)) + else: + y1, m1, d1 = int_to_ymd(event.start) + y2, m2, d2 = int_to_ymd(event.end) + wd1 = weekday_name(self._weekday_for_ymd(y1, m1, d1)) + wd2 = weekday_name(self._weekday_for_ymd(y2, m2, d2)) + out.append("<%04d-%02d-%02d %s>--<%04d-%02d-%02d %s>" % ( + y1, m1, d1, wd1, + y2, m2, d2, wd2 + )) + + out.append("") # blank line + self._append_text("\n".join(out) + "\n") + + # -------------------- + + def _parse_timestamp_line(self, title, line): + if not title: + return None + + # Single-day: <2026-02-05 Thu> + # With time: <2026-02-05 Thu 10:00-11:00> + # Range: <2026-02-10 Tue>--<2026-02-14 Sat> + + if "--<" in line: + a, b = line.split("--", 1) + s = self._parse_one_timestamp(a) + e = self._parse_one_timestamp(b) + if not s or not e: + return None + return Event(title, s["ymd"], e["ymd"], None, None) + + s = self._parse_one_timestamp(line) + if not s: + return None + + return Event(title, s["ymd"], s["ymd"], s.get("start_time"), s.get("end_time")) + + def _parse_one_timestamp(self, token): + token = token.strip() + if not (token.startswith("<") and token.endswith(">")): + return None + + inner = token[1:-1].strip() + parts = inner.split() + + # Expect YYYY-MM-DD ... + if len(parts) < 2: + return None + + date_s = parts[0] + try: + y = int(date_s[0:4]) + m = int(date_s[5:7]) + d = int(date_s[8:10]) + except Exception: + return None + + ymd = ymd_to_int(y, m, d) + + # Optional time part like 10:00-11:00 + start_time = None + end_time = None + if len(parts) >= 3 and "-" in parts[2]: + t = parts[2] + if len(t) == 11 and t[2] == ":" and t[5] == "-" and t[8] == ":": + start_time = t[0:5] + end_time = t[6:11] + + return { + "ymd": ymd, + "start_time": start_time, + "end_time": end_time + } + + def _exists(self, path): + try: + os.stat(path) + return True + except Exception: + return False + + def _append_text(self, s): + with open(self.path, "a") as f: + f.write(s) + + def _write_text(self, s): + with open(self.path, "w") as f: + f.write(s) + + def _weekday_for_ymd(self, y, m, d): + try: + t = time.mktime((y, m, d, 0, 0, 0, 0, 0)) + return time.localtime(t)[6] + except Exception: + return 0 + + +# ------------------------------------------------------------ +# Calendar Activity +# ------------------------------------------------------------ + class Hello(Activity): + def __init__(self): + super().__init__() + + self.store = OrgCalendarStore(ORG_FILE) + self.events = [] + + self.timer = None + + # UI + self.screen = None + self.lbl_time = None + self.lbl_date = None + self.lbl_month = None + + self.grid = None + self.day_buttons = [] + + self.upcoming_list = None + + # Current month shown + self.cur_y = 0 + self.cur_m = 0 + self.today_ymd = 0 + + # -------------------- + def onCreate(self): - screen = lv.obj() - label = lv.label(screen) - label.set_text('Hello World!') - label.center() - self.setContentView(screen) + self.screen = lv.obj() + self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + + # Top labels + self.lbl_time = lv.label(self.screen) + self.lbl_time.set_style_text_font(lv.font_montserrat_20, 0) + self.lbl_time.align(lv.ALIGN.TOP_LEFT, 6, 4) + + self.lbl_date = lv.label(self.screen) + self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 40) + + self.lbl_month = lv.label(self.screen) + self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) + + # Month grid container + self.grid = lv.obj(self.screen) + self.grid.set_style_border_width(1, 0) + self.grid.set_style_pad_all(4, 0) + self.grid.set_style_radius(6, 0) + self.grid.align(lv.ALIGN.TOP_MID, 0, 70) + + # Upcoming events list + self.upcoming_list = lv.list(self.screen) + self.upcoming_list.set_size(160, 120) + self.upcoming_list.align(lv.ALIGN.BOTTOM_RIGHT, -4, -4) + + self.setContentView(self.screen) + + self.reload_data() + self.build_month_view() + self.refresh_upcoming() + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 1000, None) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # -------------------- + + def reload_data(self): + self.events = self.store.load() + + def tick(self, t): + now = time.localtime() + y, m, d = now[0], now[1], now[2] + hh, mm, ss = now[3], now[4], now[5] + wd = weekday_name(now[6]) + + self.today_ymd = ymd_to_int(y, m, d) + + self.lbl_time.set_text("%02d:%02d:%02d" % (hh, mm, ss)) + self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, wd)) + + # Month label + self.lbl_month.set_text("%04d-%02d" % (self.cur_y, self.cur_m)) + + # Re-highlight today (cheap) + self.update_day_highlights() + + # -------------------- + + def build_month_view(self): + now = time.localtime() + self.cur_y, self.cur_m = now[0], now[1] + + # Determine size + d = lv.display_get_default() + w = d.get_horizontal_resolution() + + cell = (w - 20) // 7 + grid_w = cell * 7 + 8 + grid_h = cell * 6 + 8 + + self.grid.set_size(grid_w, grid_h) + + # Clear old buttons + for b in self.day_buttons: + b.delete() + self.day_buttons = [] + + first_wd = first_weekday_of_month(self.cur_y, self.cur_m) # 0=Mon + dim = days_in_month(self.cur_y, self.cur_m) + + # LVGL grid is easiest as absolute positioning here + for day in range(1, dim + 1): + idx = (first_wd + (day - 1)) + row = idx // 7 + col = idx % 7 + + btn = lv.button(self.grid) + btn.set_size(cell - 2, cell - 2) + btn.set_pos(4 + col * cell, 4 + row * cell) + btn.add_event_cb(lambda e, dd=day: self.on_day_clicked(dd), lv.EVENT.CLICKED, None) + + lbl = lv.label(btn) + lbl.set_text(str(day)) + lbl.center() + + btn._day = day # store + self.day_buttons.append(btn) + + self.update_day_highlights() + + def update_day_highlights(self): + # Highlight today + event days + for btn in self.day_buttons: + day = btn._day + ymd = ymd_to_int(self.cur_y, self.cur_m, day) + + has_event = self.day_has_event(ymd) + is_today = (ymd == self.today_ymd) + + if is_today: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.BLUE), 0) + elif has_event: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREEN), 0) + else: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREY), 0) + + def day_has_event(self, ymd): + for e in self.events: + if e.occurs_on(ymd): + return True + return False + + # -------------------- + + def refresh_upcoming(self): + self.upcoming_list.clean() + + now = time.localtime() + today = ymd_to_int(now[0], now[1], now[2]) + + upcoming = [] + for e in self.events: + if e.end >= today: + upcoming.append(e) + + upcoming.sort(key=lambda e: e.start) + + for e in upcoming[:MAX_UPCOMING]: + y1, m1, d1 = int_to_ymd(e.start) + y2, m2, d2 = int_to_ymd(e.end) + + if e.start == e.end: + date_s = "%04d-%02d-%02d" % (y1, m1, d1) + else: + date_s = "%04d-%02d-%02d..%04d-%02d-%02d" % (y1, m1, d1, y2, m2, d2) + + txt = date_s + " " + e.title + self.upcoming_list.add_text(txt) + + # -------------------- + + def on_day_clicked(self, day): + ymd = ymd_to_int(self.cur_y, self.cur_m, day) + self.open_add_dialog(ymd) + + def open_add_dialog(self, ymd): + y, m, d = int_to_ymd(ymd) + + dlg = lv.obj(self.screen) + dlg.set_size(240, 180) + dlg.center() + dlg.set_style_bg_color(lv.color_hex(0x111111), 0) + dlg.set_style_border_width(2, 0) + dlg.set_style_radius(10, 0) + + title = lv.label(dlg) + title.set_text("Add event") + title.align(lv.ALIGN.TOP_MID, 0, 8) + + date_lbl = lv.label(dlg) + date_lbl.set_text("%04d-%02d-%02d" % (y, m, d)) + date_lbl.align(lv.ALIGN.TOP_MID, 0, 30) + + # Title input + ti = lv.textarea(dlg) + ti.set_size(220, 32) + ti.align(lv.ALIGN.TOP_MID, 0, 55) + ti.set_placeholder_text("Title") + + # End date offset (days) + end_lbl = lv.label(dlg) + end_lbl.set_text("Duration days:") + end_lbl.align(lv.ALIGN.TOP_LEFT, 12, 95) + + dd = lv.dropdown(dlg) + dd.set_options("1\n2\n3\n4\n5\n6\n7\n10\n14\n21\n30") + dd.set_selected(0) + dd.set_size(70, 32) + dd.align(lv.ALIGN.TOP_RIGHT, -12, 88) + + # Buttons + btn_cancel = lv.button(dlg) + btn_cancel.set_size(90, 30) + btn_cancel.align(lv.ALIGN.BOTTOM_LEFT, 12, -10) + btn_cancel.add_event_cb(lambda e: dlg.delete(), lv.EVENT.CLICKED, None) + lc = lv.label(btn_cancel) + lc.set_text("Cancel") + lc.center() + + btn_add = lv.button(dlg) + btn_add.set_size(90, 30) + btn_add.align(lv.ALIGN.BOTTOM_RIGHT, -12, -10) + + def do_add(e): + title_s = ti.get_text() + if not title_s or title_s.strip() == "": + return + + dur_s = dd.get_selected_str() + try: + dur = int(dur_s) + except Exception: + dur = 1 + + end_ymd = self.add_days(ymd, dur - 1) + + ev = Event(title_s.strip(), ymd, end_ymd, None, None) + self.store.save_append(ev) + + # Reload + refresh UI + self.reload_data() + self.update_day_highlights() + self.refresh_upcoming() + + dlg.delete() + + btn_add.add_event_cb(do_add, lv.EVENT.CLICKED, None) + la = lv.label(btn_add) + la.set_text("Add") + la.center() + + # -------------------- + + def add_days(self, ymd, days): + # simple date add (forward only), no datetime dependency + y, m, d = int_to_ymd(ymd) + + while days > 0: + d += 1 + dim = days_in_month(y, m) + if d > dim: + d = 1 + m += 1 + if m > 12: + m = 1 + y += 1 + days -= 1 + + return ymd_to_int(y, m, d) + From 91ef5e9f564c0c704eeab6cde189a83c5465f3ee Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 00:36:48 +0100 Subject: [PATCH 02/20] cal: hacks to get more functionality working --- .../apps/com.micropythonos.helloworld/assets/hello.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index b6a43556..91d00d1f 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -116,6 +116,8 @@ def load(self): lines = f.read().splitlines() events = [] + # FIXME + events = [ Event("Test event", 20260207, 20260208) ] current_title = None for line in lines: @@ -384,7 +386,8 @@ def build_month_view(self): lbl.set_text(str(day)) lbl.center() - btn._day = day # store + # FIXME + #btn._day = day # store self.day_buttons.append(btn) self.update_day_highlights() @@ -392,7 +395,7 @@ def build_month_view(self): def update_day_highlights(self): # Highlight today + event days for btn in self.day_buttons: - day = btn._day + day = 6 # btn._day FIXME ymd = ymd_to_int(self.cur_y, self.cur_m, day) has_event = self.day_has_event(ymd) @@ -441,6 +444,7 @@ def refresh_upcoming(self): # -------------------- def on_day_clicked(self, day): + print("Day clicked") ymd = ymd_to_int(self.cur_y, self.cur_m, day) self.open_add_dialog(ymd) @@ -497,7 +501,7 @@ def do_add(e): if not title_s or title_s.strip() == "": return - dur_s = dd.get_selected_str() + dur_s = 1 # dd.get_selected_str() FIXME try: dur = int(dur_s) except Exception: From 287bb8829e933eabf47ab30684d5d057cd0c5dc8 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 00:40:53 +0100 Subject: [PATCH 03/20] cal: Disable file output for now --- .../apps/com.micropythonos.helloworld/assets/hello.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 91d00d1f..d4f59df9 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -237,10 +237,12 @@ def _exists(self, path): return False def _append_text(self, s): + return # FIXME with open(self.path, "a") as f: f.write(s) def _write_text(self, s): + return # FIXME with open(self.path, "w") as f: f.write(s) From 0ee5fe4fce38cc8aa1d963db8e0c3bfc0dc3c833 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 01:06:58 +0100 Subject: [PATCH 04/20] cal: Got button mapping to work --- .../apps/com.micropythonos.helloworld/assets/hello.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index d4f59df9..aa133323 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -369,6 +369,7 @@ def build_month_view(self): for b in self.day_buttons: b.delete() self.day_buttons = [] + self.day_of_btn = {} first_wd = first_weekday_of_month(self.cur_y, self.cur_m) # 0=Mon dim = days_in_month(self.cur_y, self.cur_m) @@ -388,16 +389,17 @@ def build_month_view(self): lbl.set_text(str(day)) lbl.center() - # FIXME - #btn._day = day # store self.day_buttons.append(btn) + self.day_of_btn[btn] = day self.update_day_highlights() def update_day_highlights(self): - # Highlight today + event days for btn in self.day_buttons: - day = 6 # btn._day FIXME + day = self.day_of_btn.get(btn, None) + if day is None: + continue + ymd = ymd_to_int(self.cur_y, self.cur_m, day) has_event = self.day_has_event(ymd) From b0f02f097a2d4248f1f259e12cbb61278d8dc80a Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 10:06:33 +0100 Subject: [PATCH 05/20] cal: tweak power and size --- .../apps/com.micropythonos.helloworld/assets/hello.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index aa133323..2c428be3 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -320,7 +320,8 @@ def onCreate(self): self.refresh_upcoming() def onResume(self, screen): - self.timer = lv.timer_create(self.tick, 1000, None) + self.timer = lv.timer_create(self.tick, 30000, None) + self.tick(0) def onPause(self, screen): if self.timer: @@ -340,7 +341,7 @@ def tick(self, t): self.today_ymd = ymd_to_int(y, m, d) - self.lbl_time.set_text("%02d:%02d:%02d" % (hh, mm, ss)) + self.lbl_time.set_text("%02d:%02d" % (hh, mm)) self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, wd)) # Month label @@ -359,7 +360,7 @@ def build_month_view(self): d = lv.display_get_default() w = d.get_horizontal_resolution() - cell = (w - 20) // 7 + cell = 32 grid_w = cell * 7 + 8 grid_h = cell * 6 + 8 From 15f6c39e2acfb1bf8c0f5c4935f2b9c3413f9a6c Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 10:22:21 +0100 Subject: [PATCH 06/20] cal: Tweak layouts --- .../com.micropythonos.helloworld/assets/hello.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 2c428be3..8b94df7d 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -306,12 +306,12 @@ def onCreate(self): self.grid.set_style_border_width(1, 0) self.grid.set_style_pad_all(4, 0) self.grid.set_style_radius(6, 0) - self.grid.align(lv.ALIGN.TOP_MID, 0, 70) + self.grid.align(lv.ALIGN.TOP_LEFT, 0, 70) # Upcoming events list self.upcoming_list = lv.list(self.screen) - self.upcoming_list.set_size(160, 120) - self.upcoming_list.align(lv.ALIGN.BOTTOM_RIGHT, -4, -4) + self.upcoming_list.set_size(320, 120) + self.upcoming_list.align(lv.ALIGN.BOTTOM_LEFT, -4, -4) self.setContentView(self.screen) @@ -446,6 +446,8 @@ def refresh_upcoming(self): txt = date_s + " " + e.title self.upcoming_list.add_text(txt) + self.upcoming_list.add_text("that's all folks") + # -------------------- def on_day_clicked(self, day): @@ -457,9 +459,9 @@ def open_add_dialog(self, ymd): y, m, d = int_to_ymd(ymd) dlg = lv.obj(self.screen) - dlg.set_size(240, 180) + dlg.set_size(320, 240) dlg.center() - dlg.set_style_bg_color(lv.color_hex(0x111111), 0) + dlg.set_style_bg_color(lv.color_hex(0x8f8f8f), 0) dlg.set_style_border_width(2, 0) dlg.set_style_radius(10, 0) @@ -474,13 +476,13 @@ def open_add_dialog(self, ymd): # Title input ti = lv.textarea(dlg) ti.set_size(220, 32) - ti.align(lv.ALIGN.TOP_MID, 0, 55) + ti.align(lv.ALIGN.TOP_MID, 0, 10) ti.set_placeholder_text("Title") # End date offset (days) end_lbl = lv.label(dlg) end_lbl.set_text("Duration days:") - end_lbl.align(lv.ALIGN.TOP_LEFT, 12, 95) + end_lbl.align(lv.ALIGN.TOP_LEFT, 12, 65) dd = lv.dropdown(dlg) dd.set_options("1\n2\n3\n4\n5\n6\n7\n10\n14\n21\n30") From 91964b8257c939790655d82b68562c7bb6e60a40 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 10:38:13 +0100 Subject: [PATCH 07/20] cal: got events to display --- .../apps/com.micropythonos.helloworld/assets/hello.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 8b94df7d..15cea9fd 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -116,9 +116,8 @@ def load(self): lines = f.read().splitlines() events = [] - # FIXME - events = [ Event("Test event", 20260207, 20260208) ] current_title = None + # FIXME this likely does not work for line in lines: line = line.strip() @@ -304,7 +303,7 @@ def onCreate(self): # Month grid container self.grid = lv.obj(self.screen) self.grid.set_style_border_width(1, 0) - self.grid.set_style_pad_all(4, 0) + self.grid.set_style_pad_all(0, 0) self.grid.set_style_radius(6, 0) self.grid.align(lv.ALIGN.TOP_LEFT, 0, 70) @@ -316,6 +315,7 @@ def onCreate(self): self.setContentView(self.screen) self.reload_data() + print("My events == ", self.events) self.build_month_view() self.refresh_upcoming() @@ -332,6 +332,8 @@ def onPause(self, screen): def reload_data(self): self.events = self.store.load() + # FIXME + self.events = [ Event("Test event", 20260207, 20260208) ] def tick(self, t): now = time.localtime() @@ -405,6 +407,7 @@ def update_day_highlights(self): has_event = self.day_has_event(ymd) is_today = (ymd == self.today_ymd) + #print(ymd, has_event, is_today) if is_today: btn.set_style_bg_color(lv.palette_main(lv.PALETTE.BLUE), 0) From e0c0b536ea14cf343cf2e938cf3bb7e18254eb01 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 11:05:52 +0100 Subject: [PATCH 08/20] cal: single day addition now works --- .../apps/com.micropythonos.helloworld/assets/hello.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 15cea9fd..1e55c321 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -520,10 +520,12 @@ def do_add(e): end_ymd = self.add_days(ymd, dur - 1) ev = Event(title_s.strip(), ymd, end_ymd, None, None) - self.store.save_append(ev) + self.events.append(ev) + #self.store.save_append(ev) # Reload + refresh UI - self.reload_data() + # FIXME: common code? + #self.reload_data() self.update_day_highlights() self.refresh_upcoming() From 24dce407b99029c91ccbca92353c02923b31e11d Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 15:41:34 +0100 Subject: [PATCH 09/20] cal: got file i/o to work --- .../apps/com.micropythonos.helloworld/assets/hello.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 1e55c321..36b82948 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -25,7 +25,7 @@ from mpos import Activity -ORG_FILE = "/flash/calendar.org" # adjust for your device +ORG_FILE = f"data/calendar.org" # adjust for your device MAX_UPCOMING = 8 @@ -236,12 +236,10 @@ def _exists(self, path): return False def _append_text(self, s): - return # FIXME with open(self.path, "a") as f: f.write(s) def _write_text(self, s): - return # FIXME with open(self.path, "w") as f: f.write(s) @@ -331,9 +329,10 @@ def onPause(self, screen): # -------------------- def reload_data(self): + print("Loading...") self.events = self.store.load() # FIXME - self.events = [ Event("Test event", 20260207, 20260208) ] + #self.events = [ Event("Test event", 20260207, 20260208) ] def tick(self, t): now = time.localtime() @@ -521,7 +520,7 @@ def do_add(e): ev = Event(title_s.strip(), ymd, end_ymd, None, None) self.events.append(ev) - #self.store.save_append(ev) + self.store.save_append(ev) # FIXME # Reload + refresh UI # FIXME: common code? From 6e2d79eb9b389428831e74fb582e52a1d84861a9 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 17:22:59 +0100 Subject: [PATCH 10/20] cal: Layout tweaks --- .../assets/hello.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 36b82948..aa5ff7e5 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -285,7 +285,7 @@ def __init__(self): def onCreate(self): self.screen = lv.obj() - self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) # Top labels self.lbl_time = lv.label(self.screen) @@ -298,17 +298,18 @@ def onCreate(self): self.lbl_month = lv.label(self.screen) self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) + # Upcoming events list + self.upcoming_list = lv.list(self.screen) + self.upcoming_list.set_size(lv.pct(90), 60) + self.upcoming_list.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + # Month grid container self.grid = lv.obj(self.screen) + self.grid.set_size(lv.pct(90), 60) self.grid.set_style_border_width(1, 0) self.grid.set_style_pad_all(0, 0) self.grid.set_style_radius(6, 0) - self.grid.align(lv.ALIGN.TOP_LEFT, 0, 70) - - # Upcoming events list - self.upcoming_list = lv.list(self.screen) - self.upcoming_list.set_size(320, 120) - self.upcoming_list.align(lv.ALIGN.BOTTOM_LEFT, -4, -4) + self.grid.align_to(self.upcoming_list, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) self.setContentView(self.screen) @@ -361,7 +362,7 @@ def build_month_view(self): d = lv.display_get_default() w = d.get_horizontal_resolution() - cell = 32 + cell = w // 8 grid_w = cell * 7 + 8 grid_h = cell * 6 + 8 From 15f90f04e25d893f011fc085d75176033fd0dac0 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Thu, 5 Feb 2026 17:44:25 +0100 Subject: [PATCH 11/20] cal: Tweak add dialog --- .../apps/com.micropythonos.helloworld/assets/hello.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index aa5ff7e5..234f5001 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -462,7 +462,7 @@ def open_add_dialog(self, ymd): y, m, d = int_to_ymd(ymd) dlg = lv.obj(self.screen) - dlg.set_size(320, 240) + dlg.set_size(lv.pct(100), lv.pct(100)) dlg.center() dlg.set_style_bg_color(lv.color_hex(0x8f8f8f), 0) dlg.set_style_border_width(2, 0) @@ -474,24 +474,24 @@ def open_add_dialog(self, ymd): date_lbl = lv.label(dlg) date_lbl.set_text("%04d-%02d-%02d" % (y, m, d)) - date_lbl.align(lv.ALIGN.TOP_MID, 0, 30) + date_lbl.align_to(title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) # Title input ti = lv.textarea(dlg) ti.set_size(220, 32) - ti.align(lv.ALIGN.TOP_MID, 0, 10) + ti.align_to(date_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) ti.set_placeholder_text("Title") # End date offset (days) end_lbl = lv.label(dlg) end_lbl.set_text("Duration days:") - end_lbl.align(lv.ALIGN.TOP_LEFT, 12, 65) + end_lbl.align_to(ti, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) dd = lv.dropdown(dlg) dd.set_options("1\n2\n3\n4\n5\n6\n7\n10\n14\n21\n30") dd.set_selected(0) dd.set_size(70, 32) - dd.align(lv.ALIGN.TOP_RIGHT, -12, 88) + dd.align_to(end_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) # Buttons btn_cancel = lv.button(dlg) From c3ea9916b727a5e845341f8e26c93eb149d4e64b Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Sat, 7 Feb 2026 17:00:15 +0100 Subject: [PATCH 12/20] calendar/columns: start separate apps for them --- .../META-INF/MANIFEST.JSON | 24 + .../cz.ucw.pavel.calendar/assets/hello.py | 557 ++++++++++++++++++ .../META-INF/MANIFEST.JSON | 12 + .../apps/cz.ucw.pavel.columns/assets/main.py | 279 +++++++++ 4 files changed, 872 insertions(+) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py create mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON create mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..43e8b04a --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON @@ -0,0 +1,24 @@ +{ +"name": "Calendar", +"publisher": "Pavemicropythonos", +"short_description": "Calendar", +"long_description": "Simple calendar app.", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.helloworld/icons/com.micropythonos.helloworld_0.0.5_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.helloworld/mpks/com.micropythonos.helloworld_0.0.5.mpk", +"fullname": "com.micropythonos.helloworld", +"version": "0.0.1", +"category": "development", +"activities": [ + { + "entrypoint": "assets/hello.py", + "classname": "Hello", + "intent_filters": [ + { + "action": "main", + "category": "launcher" + } + ] + } + ] +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py new file mode 100644 index 00000000..234f5001 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py @@ -0,0 +1,557 @@ +from mpos import Activity + +""" + +Create simple calendar application. On main screen, it should have +current time, date, and month overview. Current date and dates with +events should be highlighted. There should be list of upcoming events. + +When date is clicked, dialog with adding event for that date should be +displayed. Multi-day events should be supported. + +Data should be read/written to emacs org compatible text file. + + +""" + +import time +import os + +try: + import lvgl as lv +except ImportError: + pass + +from mpos import Activity + + +ORG_FILE = f"data/calendar.org" # adjust for your device +MAX_UPCOMING = 8 + + +# ------------------------------------------------------------ +# Small date helpers (no datetime module assumed) +# ------------------------------------------------------------ + +def is_leap_year(y): + return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0) + + +def days_in_month(y, m): + if m == 2: + return 29 if is_leap_year(y) else 28 + if m in (1, 3, 5, 7, 8, 10, 12): + return 31 + return 30 + + +def ymd_to_int(y, m, d): + return y * 10000 + m * 100 + d + + +def int_to_ymd(v): + y = v // 10000 + m = (v // 100) % 100 + d = v % 100 + return y, m, d + + +def weekday_name(idx): + # MicroPython localtime(): 0=Mon..6=Sun typically + names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + if 0 <= idx < 7: + return names[idx] + return "???" + + +def first_weekday_of_month(y, m): + # brute-force using time.mktime if available + # Some ports support it, some don't. + # If it fails, we fallback to "Monday". + try: + # localtime tuple: (y,m,d,h,mi,s,wd,yd) + t = time.mktime((y, m, 1, 0, 0, 0, 0, 0)) + wd = time.localtime(t)[6] + return wd + except Exception: + return 0 + + +# ------------------------------------------------------------ +# Org event model + parser/writer +# ------------------------------------------------------------ + +class Event: + def __init__(self, title, start_ymd, end_ymd, start_time=None, end_time=None): + self.title = title + self.start = start_ymd # int yyyymmdd + self.end = end_ymd # int yyyymmdd + self.start_time = start_time # "HH:MM" or None + self.end_time = end_time # "HH:MM" or None + + def is_multi_day(self): + return self.end != self.start + + def occurs_on(self, ymd): + return self.start <= ymd <= self.end + + def start_key(self): + return self.start + + +class OrgCalendarStore: + def __init__(self, path): + self.path = path + + def load(self): + if not self._exists(self.path): + return [] + + try: + with open(self.path, "r", encoding="utf-8") as f: + lines = f.read().splitlines() + except Exception: + # fallback without encoding kw if unsupported + with open(self.path, "r") as f: + lines = f.read().splitlines() + + events = [] + current_title = None + # FIXME this likely does not work + + for line in lines: + line = line.strip() + + if line.startswith("** "): + current_title = line[3:].strip() + continue + + if not line.startswith("<"): + continue + + ev = self._parse_timestamp_line(current_title, line) + if ev: + events.append(ev) + + events.sort(key=lambda e: e.start_key()) + return events + + def save_append(self, event): + # Create file if missing + if not self._exists(self.path): + self._write_text("* Events\n") + + # Append event + out = [] + out.append("** " + event.title) + + if event.start == event.end: + y, m, d = int_to_ymd(event.start) + wd = weekday_name(self._weekday_for_ymd(y, m, d)) + if event.start_time and event.end_time: + out.append("<%04d-%02d-%02d %s %s-%s>" % ( + y, m, d, wd, event.start_time, event.end_time + )) + else: + out.append("<%04d-%02d-%02d %s>" % (y, m, d, wd)) + else: + y1, m1, d1 = int_to_ymd(event.start) + y2, m2, d2 = int_to_ymd(event.end) + wd1 = weekday_name(self._weekday_for_ymd(y1, m1, d1)) + wd2 = weekday_name(self._weekday_for_ymd(y2, m2, d2)) + out.append("<%04d-%02d-%02d %s>--<%04d-%02d-%02d %s>" % ( + y1, m1, d1, wd1, + y2, m2, d2, wd2 + )) + + out.append("") # blank line + self._append_text("\n".join(out) + "\n") + + # -------------------- + + def _parse_timestamp_line(self, title, line): + if not title: + return None + + # Single-day: <2026-02-05 Thu> + # With time: <2026-02-05 Thu 10:00-11:00> + # Range: <2026-02-10 Tue>--<2026-02-14 Sat> + + if "--<" in line: + a, b = line.split("--", 1) + s = self._parse_one_timestamp(a) + e = self._parse_one_timestamp(b) + if not s or not e: + return None + return Event(title, s["ymd"], e["ymd"], None, None) + + s = self._parse_one_timestamp(line) + if not s: + return None + + return Event(title, s["ymd"], s["ymd"], s.get("start_time"), s.get("end_time")) + + def _parse_one_timestamp(self, token): + token = token.strip() + if not (token.startswith("<") and token.endswith(">")): + return None + + inner = token[1:-1].strip() + parts = inner.split() + + # Expect YYYY-MM-DD ... + if len(parts) < 2: + return None + + date_s = parts[0] + try: + y = int(date_s[0:4]) + m = int(date_s[5:7]) + d = int(date_s[8:10]) + except Exception: + return None + + ymd = ymd_to_int(y, m, d) + + # Optional time part like 10:00-11:00 + start_time = None + end_time = None + if len(parts) >= 3 and "-" in parts[2]: + t = parts[2] + if len(t) == 11 and t[2] == ":" and t[5] == "-" and t[8] == ":": + start_time = t[0:5] + end_time = t[6:11] + + return { + "ymd": ymd, + "start_time": start_time, + "end_time": end_time + } + + def _exists(self, path): + try: + os.stat(path) + return True + except Exception: + return False + + def _append_text(self, s): + with open(self.path, "a") as f: + f.write(s) + + def _write_text(self, s): + with open(self.path, "w") as f: + f.write(s) + + def _weekday_for_ymd(self, y, m, d): + try: + t = time.mktime((y, m, d, 0, 0, 0, 0, 0)) + return time.localtime(t)[6] + except Exception: + return 0 + + +# ------------------------------------------------------------ +# Calendar Activity +# ------------------------------------------------------------ + +class Hello(Activity): + + def __init__(self): + super().__init__() + + self.store = OrgCalendarStore(ORG_FILE) + self.events = [] + + self.timer = None + + # UI + self.screen = None + self.lbl_time = None + self.lbl_date = None + self.lbl_month = None + + self.grid = None + self.day_buttons = [] + + self.upcoming_list = None + + # Current month shown + self.cur_y = 0 + self.cur_m = 0 + self.today_ymd = 0 + + # -------------------- + + def onCreate(self): + self.screen = lv.obj() + #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + + # Top labels + self.lbl_time = lv.label(self.screen) + self.lbl_time.set_style_text_font(lv.font_montserrat_20, 0) + self.lbl_time.align(lv.ALIGN.TOP_LEFT, 6, 4) + + self.lbl_date = lv.label(self.screen) + self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 40) + + self.lbl_month = lv.label(self.screen) + self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) + + # Upcoming events list + self.upcoming_list = lv.list(self.screen) + self.upcoming_list.set_size(lv.pct(90), 60) + self.upcoming_list.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + + # Month grid container + self.grid = lv.obj(self.screen) + self.grid.set_size(lv.pct(90), 60) + self.grid.set_style_border_width(1, 0) + self.grid.set_style_pad_all(0, 0) + self.grid.set_style_radius(6, 0) + self.grid.align_to(self.upcoming_list, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) + + self.setContentView(self.screen) + + self.reload_data() + print("My events == ", self.events) + self.build_month_view() + self.refresh_upcoming() + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, 30000, None) + self.tick(0) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # -------------------- + + def reload_data(self): + print("Loading...") + self.events = self.store.load() + # FIXME + #self.events = [ Event("Test event", 20260207, 20260208) ] + + def tick(self, t): + now = time.localtime() + y, m, d = now[0], now[1], now[2] + hh, mm, ss = now[3], now[4], now[5] + wd = weekday_name(now[6]) + + self.today_ymd = ymd_to_int(y, m, d) + + self.lbl_time.set_text("%02d:%02d" % (hh, mm)) + self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, wd)) + + # Month label + self.lbl_month.set_text("%04d-%02d" % (self.cur_y, self.cur_m)) + + # Re-highlight today (cheap) + self.update_day_highlights() + + # -------------------- + + def build_month_view(self): + now = time.localtime() + self.cur_y, self.cur_m = now[0], now[1] + + # Determine size + d = lv.display_get_default() + w = d.get_horizontal_resolution() + + cell = w // 8 + grid_w = cell * 7 + 8 + grid_h = cell * 6 + 8 + + self.grid.set_size(grid_w, grid_h) + + # Clear old buttons + for b in self.day_buttons: + b.delete() + self.day_buttons = [] + self.day_of_btn = {} + + first_wd = first_weekday_of_month(self.cur_y, self.cur_m) # 0=Mon + dim = days_in_month(self.cur_y, self.cur_m) + + # LVGL grid is easiest as absolute positioning here + for day in range(1, dim + 1): + idx = (first_wd + (day - 1)) + row = idx // 7 + col = idx % 7 + + btn = lv.button(self.grid) + btn.set_size(cell - 2, cell - 2) + btn.set_pos(4 + col * cell, 4 + row * cell) + btn.add_event_cb(lambda e, dd=day: self.on_day_clicked(dd), lv.EVENT.CLICKED, None) + + lbl = lv.label(btn) + lbl.set_text(str(day)) + lbl.center() + + self.day_buttons.append(btn) + self.day_of_btn[btn] = day + + self.update_day_highlights() + + def update_day_highlights(self): + for btn in self.day_buttons: + day = self.day_of_btn.get(btn, None) + if day is None: + continue + + ymd = ymd_to_int(self.cur_y, self.cur_m, day) + + has_event = self.day_has_event(ymd) + is_today = (ymd == self.today_ymd) + #print(ymd, has_event, is_today) + + if is_today: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.BLUE), 0) + elif has_event: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREEN), 0) + else: + btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREY), 0) + + def day_has_event(self, ymd): + for e in self.events: + if e.occurs_on(ymd): + return True + return False + + # -------------------- + + def refresh_upcoming(self): + self.upcoming_list.clean() + + now = time.localtime() + today = ymd_to_int(now[0], now[1], now[2]) + + upcoming = [] + for e in self.events: + if e.end >= today: + upcoming.append(e) + + upcoming.sort(key=lambda e: e.start) + + for e in upcoming[:MAX_UPCOMING]: + y1, m1, d1 = int_to_ymd(e.start) + y2, m2, d2 = int_to_ymd(e.end) + + if e.start == e.end: + date_s = "%04d-%02d-%02d" % (y1, m1, d1) + else: + date_s = "%04d-%02d-%02d..%04d-%02d-%02d" % (y1, m1, d1, y2, m2, d2) + + txt = date_s + " " + e.title + self.upcoming_list.add_text(txt) + + self.upcoming_list.add_text("that's all folks") + + # -------------------- + + def on_day_clicked(self, day): + print("Day clicked") + ymd = ymd_to_int(self.cur_y, self.cur_m, day) + self.open_add_dialog(ymd) + + def open_add_dialog(self, ymd): + y, m, d = int_to_ymd(ymd) + + dlg = lv.obj(self.screen) + dlg.set_size(lv.pct(100), lv.pct(100)) + dlg.center() + dlg.set_style_bg_color(lv.color_hex(0x8f8f8f), 0) + dlg.set_style_border_width(2, 0) + dlg.set_style_radius(10, 0) + + title = lv.label(dlg) + title.set_text("Add event") + title.align(lv.ALIGN.TOP_MID, 0, 8) + + date_lbl = lv.label(dlg) + date_lbl.set_text("%04d-%02d-%02d" % (y, m, d)) + date_lbl.align_to(title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + + # Title input + ti = lv.textarea(dlg) + ti.set_size(220, 32) + ti.align_to(date_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + ti.set_placeholder_text("Title") + + # End date offset (days) + end_lbl = lv.label(dlg) + end_lbl.set_text("Duration days:") + end_lbl.align_to(ti, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + + dd = lv.dropdown(dlg) + dd.set_options("1\n2\n3\n4\n5\n6\n7\n10\n14\n21\n30") + dd.set_selected(0) + dd.set_size(70, 32) + dd.align_to(end_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) + + # Buttons + btn_cancel = lv.button(dlg) + btn_cancel.set_size(90, 30) + btn_cancel.align(lv.ALIGN.BOTTOM_LEFT, 12, -10) + btn_cancel.add_event_cb(lambda e: dlg.delete(), lv.EVENT.CLICKED, None) + lc = lv.label(btn_cancel) + lc.set_text("Cancel") + lc.center() + + btn_add = lv.button(dlg) + btn_add.set_size(90, 30) + btn_add.align(lv.ALIGN.BOTTOM_RIGHT, -12, -10) + + def do_add(e): + title_s = ti.get_text() + if not title_s or title_s.strip() == "": + return + + dur_s = 1 # dd.get_selected_str() FIXME + try: + dur = int(dur_s) + except Exception: + dur = 1 + + end_ymd = self.add_days(ymd, dur - 1) + + ev = Event(title_s.strip(), ymd, end_ymd, None, None) + self.events.append(ev) + self.store.save_append(ev) # FIXME + + # Reload + refresh UI + # FIXME: common code? + #self.reload_data() + self.update_day_highlights() + self.refresh_upcoming() + + dlg.delete() + + btn_add.add_event_cb(do_add, lv.EVENT.CLICKED, None) + la = lv.label(btn_add) + la.set_text("Add") + la.center() + + # -------------------- + + def add_days(self, ymd, days): + # simple date add (forward only), no datetime dependency + y, m, d = int_to_ymd(ymd) + + while days > 0: + d += 1 + dim = days_in_month(y, m) + if d > dim: + d = 1 + m += 1 + if m > 12: + m = 1 + y += 1 + days -= 1 + + return ymd_to_int(y, m, d) + diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON new file mode 100644 index 00000000..50d5b1ff --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON @@ -0,0 +1,12 @@ +{ +"name": "Columns", +"publisher": "MicroPythonOS", +"short_description": "Falling columns game", +"long_description": "Blocks of 3 colors are falling. Align the colors to make blocks disappear.", +"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.doom/icons/com.micropythonos.doom_launcher_0.1.0_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.doom/mpks/com.micropythonos.doom_launcher_0.1.0.mpk", +"fullname": "com.micropythonos.doom_launcher", +"version": "0.0.1", +"category": "games", +} + diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py new file mode 100644 index 00000000..a2384153 --- /dev/null +++ b/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py @@ -0,0 +1,279 @@ +import time +import random + +""" +bugs: +[x] does not explode on diagonal + +should blink explosions + +explodes while moving? + +explosions should work in series + +movement should be immediate + +/ vlevo dole nezmizi + +chce to skore + +<> umozny pohyb do obsazene oblasti + +""" + +from mpos import Activity + +try: + import lvgl as lv +except ImportError: + pass + + +class Main(Activity): + + COLS = 6 + ROWS = 12 + + COLORS = [ + 0xE74C3C, # red + 0xF1C40F, # yellow + 0x2ECC71, # green + 0x3498DB, # blue + 0x9B59B6, # purple + ] + + EMPTY = -1 + + FALL_INTERVAL = 600 # ms + + def __init__(self): + super().__init__() + self.board = [[self.EMPTY for _ in range(self.COLS)] for _ in range(self.ROWS)] + self.cells = [] + + self.active_col = self.COLS // 2 + self.active_row = -3 + self.active_colors = [] + + self.timer = None + self.animating = False + + # --------------------------------------------------------------------- + + def onCreate(self): + self.screen = lv.obj() + self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) + + d = lv.display_get_default() + self.SCREEN_WIDTH = d.get_horizontal_resolution() + self.SCREEN_HEIGHT = d.get_vertical_resolution() + + self.CELL = min( + self.SCREEN_WIDTH // self.COLS, + self.SCREEN_HEIGHT // self.ROWS + ) + + board_x = (self.SCREEN_WIDTH - self.CELL * self.COLS) // 2 + board_y = (self.SCREEN_HEIGHT - self.CELL * self.ROWS) // 2 + + for r in range(self.ROWS): + row = [] + for c in range(self.COLS): + o = lv.obj(self.screen) + o.set_size(self.CELL - 2, self.CELL - 2) + o.set_pos( + board_x + c * self.CELL + 1, + board_y + r * self.CELL + 1 + ) + o.set_style_radius(4, 0) + o.set_style_bg_color(lv.color_hex(0x1C2833), 0) + o.set_style_border_width(1, 0) + row.append(o) + self.cells.append(row) + + # Make screen focusable for keyboard input + focusgroup = lv.group_get_default() + if focusgroup: + focusgroup.add_obj(self.screen) + + self.screen.add_event_cb(self.on_touch, lv.EVENT.CLICKED, None) + self.screen.add_event_cb(self.on_key, lv.EVENT.KEY, None) + + self.setContentView(self.screen) + + self.spawn_piece() + + # --------------------------------------------------------------------- + + def onResume(self, screen): + self.timer = lv.timer_create(self.tick, self.FALL_INTERVAL, None) + + def onPause(self, screen): + if self.timer: + self.timer.delete() + self.timer = None + + # --------------------------------------------------------------------- + + def spawn_piece(self): + self.active_col = self.COLS // 2 + self.active_row = -3 + self.active_colors = [random.randrange(len(self.COLORS)) for _ in range(3)] + + def tick(self, t): + if self.can_fall(): + self.active_row += 1 + else: + self.lock_piece() + self.clear_matches() + self.spawn_piece() + + self.redraw() + + # --------------------------------------------------------------------- + + def can_fall(self): + for i in range(3): + r = self.active_row + i + 1 + c = self.active_col + if r >= self.ROWS: + return False + if r >= 0 and self.board[r][c] != self.EMPTY: + return False + return True + + def lock_piece(self): + for i in range(3): + r = self.active_row + i + if r >= 0: + self.board[r][self.active_col] = self.active_colors[i] + + # --------------------------------------------------------------------- + + def clear_matches(self): + to_clear = set() + score = 0 + + for r in range(self.ROWS): + for c in range(self.COLS): + color = self.board[r][c] + if color == self.EMPTY: + continue + + # horizontal + if c <= self.COLS - 3: + if all(self.board[r][c + i] == color for i in range(3)): + for i in range(3): + to_clear.add((r, c + i)) + score += 1 + + # vertical + if r <= self.ROWS - 3: + if all(self.board[r + i][c] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c)) + score += 1 + + # diagonal \ + if r <= self.ROWS - 3 and c <= self.COLS - 3: + if all(self.board[r + i][c + i] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c + i)) + score += 1 + + # diagonal / + if r <= self.ROWS - 3 and c > 2: + if all(self.board[r + i][c - i] == color for i in range(3)): + for i in range(3): + to_clear.add((r + i, c - i)) + score += 1 + + if not to_clear: + return + + print("Score: ", score) + for r, c in to_clear: + self.board[r][c] = self.EMPTY + + self.redraw() + time.sleep(.5) + self.apply_gravity() + self.redraw() + time.sleep(.5) + self.clear_matches() + self.redraw() + + def apply_gravity(self): + for c in range(self.COLS): + stack = [self.board[r][c] for r in range(self.ROWS) if self.board[r][c] != self.EMPTY] + for r in range(self.ROWS): + self.board[r][c] = self.EMPTY + for i, v in enumerate(reversed(stack)): + self.board[self.ROWS - 1 - i][c] = v + + # --------------------------------------------------------------------- + + def redraw(self): + # draw board + for r in range(self.ROWS): + for c in range(self.COLS): + v = self.board[r][c] + if v == self.EMPTY: + self.cells[r][c].set_style_bg_color(lv.color_hex(0x1C2833), 0) + else: + self.cells[r][c].set_style_bg_color( + lv.color_hex(self.COLORS[v]), 0 + ) + + # draw active piece + for i in range(3): + r = self.active_row + i + if r >= 0 and r < self.ROWS: + self.cells[r][self.active_col].set_style_bg_color( + lv.color_hex(self.COLORS[self.active_colors[i]]), 0 + ) + + # --------------------------------------------------------------------- + + def on_touch(self, e): + print("Touch event") + p = lv.indev_get_act().get_point() + x = p.x + + if x < self.SCREEN_WIDTH // 3: + self.move(-1) + elif x > self.SCREEN_WIDTH * 2 // 3: + self.move(1) + else: + self.rotate() + + def on_key(self, event): + """Handle keyboard input""" + print("Keyboard event") + key = event.get_key() + if key == ord("a"): + self.move(-1) + return + if key == ord("w"): + self.rotate() + return + if key == ord("d"): + self.move(1) + return + if key == ord("s"): + self.tick(0) + return + + #if key == lv.KEY.ENTER or key == lv.KEY.UP or key == ord("A") or key == ord("a"): + print(f"on_key: unhandled key {key}") + + def move(self, dx): + nc = self.active_col + dx + if 0 <= nc < self.COLS: + self.active_col = nc + self.redraw() + + def rotate(self): + self.active_colors = self.active_colors[-1:] + self.active_colors[:-1] + self.redraw() + From 667a22c200cb0cadbd7c1f37c95f1158cf6ac689 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 00:07:50 +0100 Subject: [PATCH 13/20] calendar: open keyboard --- .../apps/cz.ucw.pavel.calendar/assets/hello.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py index 234f5001..7690a9ee 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py @@ -22,7 +22,7 @@ except ImportError: pass -from mpos import Activity +from mpos import Activity, MposKeyboard ORG_FILE = f"data/calendar.org" # adjust for your device @@ -481,6 +481,9 @@ def open_add_dialog(self, ymd): ti.set_size(220, 32) ti.align_to(date_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) ti.set_placeholder_text("Title") + keyboard = MposKeyboard(dlg) + keyboard.set_textarea(ti) + keyboard.add_flag(lv.obj.FLAG.HIDDEN) # End date offset (days) end_lbl = lv.label(dlg) From 908b6a3b94c146d45021e91b46ce54641bdc475f Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 00:27:02 +0100 Subject: [PATCH 14/20] calendar: make keyboard fit on small screen --- .../apps/cz.ucw.pavel.calendar/assets/hello.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py index 7690a9ee..8185255e 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py @@ -462,7 +462,7 @@ def open_add_dialog(self, ymd): y, m, d = int_to_ymd(ymd) dlg = lv.obj(self.screen) - dlg.set_size(lv.pct(100), lv.pct(100)) + dlg.set_size(lv.pct(100), 480) dlg.center() dlg.set_style_bg_color(lv.color_hex(0x8f8f8f), 0) dlg.set_style_border_width(2, 0) @@ -483,7 +483,7 @@ def open_add_dialog(self, ymd): ti.set_placeholder_text("Title") keyboard = MposKeyboard(dlg) keyboard.set_textarea(ti) - keyboard.add_flag(lv.obj.FLAG.HIDDEN) + #keyboard.add_flag(lv.obj.FLAG.HIDDEN) # End date offset (days) end_lbl = lv.label(dlg) @@ -499,7 +499,7 @@ def open_add_dialog(self, ymd): # Buttons btn_cancel = lv.button(dlg) btn_cancel.set_size(90, 30) - btn_cancel.align(lv.ALIGN.BOTTOM_LEFT, 12, -10) + btn_cancel.align(lv.ALIGN.TOP_LEFT, 12, 10) btn_cancel.add_event_cb(lambda e: dlg.delete(), lv.EVENT.CLICKED, None) lc = lv.label(btn_cancel) lc.set_text("Cancel") @@ -507,7 +507,7 @@ def open_add_dialog(self, ymd): btn_add = lv.button(dlg) btn_add.set_size(90, 30) - btn_add.align(lv.ALIGN.BOTTOM_RIGHT, -12, -10) + btn_add.align(lv.ALIGN.TOP_RIGHT, -12, 10) def do_add(e): title_s = ti.get_text() From 3285fc0e78513d36da33fa46725663a24a0d3b9e Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 18:02:56 +0100 Subject: [PATCH 15/20] calendar: better metadata for calendar --- .../cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON | 9 ++++----- .../res/mipmap-mdpi/icon_64x64.png | Bin 0 -> 7294 bytes 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 internal_filesystem/apps/cz.ucw.pavel.calendar/res/mipmap-mdpi/icon_64x64.png diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON index 43e8b04a..bbf8baff 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON @@ -1,11 +1,10 @@ { "name": "Calendar", -"publisher": "Pavemicropythonos", +"publisher": "micropythonos", "short_description": "Calendar", "long_description": "Simple calendar app.", -"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.helloworld/icons/com.micropythonos.helloworld_0.0.5_64x64.png", -"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.helloworld/mpks/com.micropythonos.helloworld_0.0.5.mpk", -"fullname": "com.micropythonos.helloworld", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/icons/com.micropythonos.helloworld_0.0.5_64x64.png", +"fullname": "cz.ucw.pavel.columns", "version": "0.0.1", "category": "development", "activities": [ @@ -15,7 +14,7 @@ "intent_filters": [ { "action": "main", - "category": "launcher" + "category": "utilities" } ] } diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/res/mipmap-mdpi/icon_64x64.png b/internal_filesystem/apps/cz.ucw.pavel.calendar/res/mipmap-mdpi/icon_64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..ee72f838043fae284c9d0836dd11e5b23d7a1bc1 GIT binary patch literal 7294 zcmeHMc{tQ-`=7C|Wi3L@8Kh!XgE1NVzGo+$%<>&3W-$xHaY|*$7TPExDMclfts|wC zii9kONJXefg-YqX-=S0bz2|q{-?^^e`(LhW%=dYo&;7aY=kwh6^E}^cQas%q5VFNHgK&L?=;HDbgUm8ey7^w9_+N`&d28+k-8xU3CB*|wwo)wA~~0sc|qy+ zg&SGcJ(I6*>*Zlm`euhdgd1CI8)z7nU_~J|pjUx4|>oa-x2h%*DNQ)+%dOm%4hR z?jmAPDNdbx(EpmM^uR`FOyAUv9WmHt6>?AA41bsF7zuDo+%*uTuXg54S4-9Cjny0b z?Hg{t0FqR5g1L%nehn`wws-_Ko0+scEla`Is(S{B((E^FpBnK9MyY2MAz97KJT_V( zPq|BBR7~f@RUw$CliCFm=H;2IZgxVg3TV4M(JbKHnuw@JUKSkXFiGoMwd5AZKJ^A$ zeBMv#k(ME{x6Vn^l*4bT^t~uZ1Mr+Dn!%IXZKRZNAArawiMqQS_vx7aHmcpIplhGH z_8E1r4QEJY&#_#uJL<_a_Pw6NR$1J0H&r*+Cct$M_;3FH~ zkFK?>Y|6HNrr9WALKJf~s;V|XOePbS+TEDV`uS5*SXk2QBTiZrSXiT< zJ~`{KRMc-(x<~r5cHL;&^lG}Usd{|30@bnaEwujbV2{TuEPke=PHy+y6jF3Tk>4-P z-%It`s1V}3vV)owHy{J)h}i=XvpFe`J0lh;o@jRASX-qp^;41M%Dt;H%Tn(cr*RlV zgBHXV-FwHG)s;>PBAMGyi}8cF+~Xlvx>`=09{a69Yjyms{-nqaJ$Gx%@|e&4B>D8Q zwBqwY?u-oP?UUD$<1pupEG^Hzy{SQM^BtOR3ojmZ1c~9!v;(#i+#u}n}1ebhvYsbS{f5Uy{Zp%z<7M{XF-?vdiT%0v?oS4p zo~e!9xuab1D!fwm(DNy+kg@{7tr=bUGW8tA6Vjx@j$JX@8)w4}CzQ5M#T|t1j%Yig zq0_#;T|(96u~f;UB;=gA%VwLL%Q?r->JA;89wJ!CtXc4YalteL}Q)!HvG@i*9@8*h+7NRS7F;Wk{@OiqJyfI?{^uFFu%S*sn54o z;P-|o2pv1Bp=#?#@QnE|F{7baUo%&oS36)rbJ$qI7#qm#uK_YQ9h{kHrHt1+-IkLb zrHyF2VimKafKfD91A#yj=r%TPFPNm5Dq-qc!eerX`YI&#BwBo@a_GBZu^Xkn&e@TBUg-_5Yt6=V_NUtaF_E@K> zE#3Wba$?}=gcc09&ef=HHN^Kqx(^&lJAaBis48MT+K4A(4W~_=oUd`?`mZ;3*R5%N zWBzVLYWU4+(Kz^PgAVyL!A?PJN4x1O<>Wx8dy0k1nd&^NCl2NM$SaXZg27~SMYem3 za&pQ|vZTINvvZQOnVshC(iOw9>N(u}8vLpKE5iHtUP0$}`yIIG&Q+5(AWJLRt~pth zE@NOz?{vguADydB-q!TmWyq_^S3q4o{)jo z(Y*5w4T$@7&t|%M8^=VmO3L?`G+g}&9dzs^KQsCrZ)WDzLBDMl)%KigZ*En3Lm*4j z=-|a7z|ECFVKI;-DvJyt1q?QL@qj?gECg&4B^=M~f zFs7!aC^Qy@#Uelif*Zr+kpu`PS6fK2z+nq;DI7YRM`tl%LQWEy#pe;>aBv*ig^STmjSrop|sgo@cE{vBzWl`u0qHk-VvMN>9Muqu zFr}E#5L9EZ(}+r;;mO8}C_FfHuo6fSi=z@!Q9&vkhDI}@qG<@S3EB{WBT)bZiENBP zP|zkAL#h#Ail>m~sf6o5u<~>w!m&v7w;s<35|75>FoHsC{N0Ny;3P&AB* z2?mYE;Lt`!cr4Bs`wip=aJXQS3pp`pBz9qhN+H;Tj3hAG=nPUQfMPR4=S_smLI5WN z3QH2!2FN_$56*>P!vRP<7RQIhiXg&;vBHFu^Cbl{`%)|fR~BX7a2^a$h4uEO;;c!b zsQIQD>hHk+#^fEwie~=bc)mfuvRH9=(JW552gifF1)%W$p69Q?UzxnXy^hP{#5nzj zN&OF;*+RNHg0?J9%p(8ZK;%MeVMB_b&r1b^&2It(5@kVtE-4D2&MyI|B#~%T3<*FOk#SfA4o@~jkTGZ+0>B#M@nkXu99zh=uk2hFjTcSg z09K)34uIO@DB*_s=k&~kTaKF>VX**a z!YzpaUNS=gZ$6tHK?gYB3-f29{4cmg_Ft0nKbbFvEm+&I*fC(yhVeY2nSbm4FMtaS zu5=2(?`Sr(l;V9X1B;CTifkEpNb0GYuZV^~#5A02t$e_r1oGT= zY1*kJ6`qa0k5OkE%e^dLf)D33S=9^JOO;Xza0NGZF1M00mDwSws3Ip5JE;H{C{F9V z9>iQcq$#4PlpUsE7wNyNwuxL*A-&fPTbNwhc66*@qO`PNY@&bpp|bMwM>VG|7e3$8 zGn|!B85$oQFWS)BU=#w0fcE)ame(JUG(PiDFc5e|oaY(`4S+JCIeI@0+6+#M5NhbX zK@g_s9}p;eDOh@ovrTj+P&9QZddys2aZ+XSSHD}0kTYYdCGv!dC#+6EX-!M} zN|y#vqnzmE^HN!dlq(AC8oZncWepi3&-Gs= z=T)9lgtq#H$hohXvQhzB4h?qPPW77eB{xUxvkcTzVpuOf zxcB19AQBCcPcKjjHV_mvbWcJ?*=K1O{M3Q{oyMnY zC_2~@83fx+^CGJywa%S5?b|KevGb3;;p@%lwy1-jSBkGZUv^RUllUdcxcuW1Z`B`+ z&zvwkm!R3Cnk|bFS#mRY#x?Zh^U+$BL+hb%j{eqc95(NyBo5>0o(C;3Th=Qc$0vzeSoiMU&ncRk|RdOm#l_sHY< zclD~=)un40X(EFULeM>aogUab3_|7b>FSPzBbkUdlm=7fZGpb6BL>~kVW-CO;!DRN zzV%!B3kr{n@{^y%0>L8XhcB;p@u*1u2-R2u5XxOj)5RVh)ZwbwHJ(r`0dzv(>J_!M z@A<bcao$Hd~kC!Rk zpB$2@DRZ{$-m4!RX!PFuUS;e(h%Zqzj;^~&)IIi%C`Wtz`VEwhrh(?x?QdiiV^qRQ zpX)Zh>05gN)_t+@)+I2~InteV>-5EbZ7%4fzc@MjA^sUtFC?Z!?BsL%+frfRFGGha zT+Dsd#p-iAs~HS(dKAZAryu&cLM|V1_hE)XcMYp)Q=uf3UiL&{Y=jfKT`^_milcGL z#*$12wSpyi)QF4NwVlbX+M>@b8+@!MHGdhDO_EHHZVo!#zL_j~_Jz(fLFMTRt-1*l zlKxMu&?FQ~OHeB%5*NU{EB$!1Vrqx<`1OwZfZRm$Niox{`|>5B=JzicTie?bMa-52 l0|GpucW=lqJvPWsvPu0{{q0OrcM3NaCp&lBa_f*C{{q{ZX;T0I literal 0 HcmV?d00001 From bfe3d821c440baac32be0daf89d560a7ceb9bb78 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 19:54:06 +0100 Subject: [PATCH 16/20] calendar: more metadata tweaks --- .../apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON | 6 +++--- .../apps/cz.ucw.pavel.calendar/assets/{hello.py => main.py} | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename internal_filesystem/apps/cz.ucw.pavel.calendar/assets/{hello.py => main.py} (99%) diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON index bbf8baff..66c67092 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON @@ -6,11 +6,11 @@ "icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/icons/com.micropythonos.helloworld_0.0.5_64x64.png", "fullname": "cz.ucw.pavel.columns", "version": "0.0.1", -"category": "development", +"category": "utilities", "activities": [ { - "entrypoint": "assets/hello.py", - "classname": "Hello", + "entrypoint": "assets/main.py", + "classname": "Main", "intent_filters": [ { "action": "main", diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/main.py similarity index 99% rename from internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py rename to internal_filesystem/apps/cz.ucw.pavel.calendar/assets/main.py index 8185255e..54708487 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/hello.py +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/assets/main.py @@ -255,7 +255,7 @@ def _weekday_for_ymd(self, y, m, d): # Calendar Activity # ------------------------------------------------------------ -class Hello(Activity): +class Main(Activity): def __init__(self): super().__init__() From d78a4430b4fa1136e94954caf9f3b4fd125fc7a2 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 20:00:54 +0100 Subject: [PATCH 17/20] calendar: revert hello tweaks --- .../assets/hello.py | 557 +----------------- 1 file changed, 5 insertions(+), 552 deletions(-) diff --git a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py index 234f5001..87ed4dd3 100644 --- a/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py +++ b/internal_filesystem/apps/com.micropythonos.helloworld/assets/hello.py @@ -1,557 +1,10 @@ from mpos import Activity -""" - -Create simple calendar application. On main screen, it should have -current time, date, and month overview. Current date and dates with -events should be highlighted. There should be list of upcoming events. - -When date is clicked, dialog with adding event for that date should be -displayed. Multi-day events should be supported. - -Data should be read/written to emacs org compatible text file. - - -""" - -import time -import os - -try: - import lvgl as lv -except ImportError: - pass - -from mpos import Activity - - -ORG_FILE = f"data/calendar.org" # adjust for your device -MAX_UPCOMING = 8 - - -# ------------------------------------------------------------ -# Small date helpers (no datetime module assumed) -# ------------------------------------------------------------ - -def is_leap_year(y): - return (y % 4 == 0 and y % 100 != 0) or (y % 400 == 0) - - -def days_in_month(y, m): - if m == 2: - return 29 if is_leap_year(y) else 28 - if m in (1, 3, 5, 7, 8, 10, 12): - return 31 - return 30 - - -def ymd_to_int(y, m, d): - return y * 10000 + m * 100 + d - - -def int_to_ymd(v): - y = v // 10000 - m = (v // 100) % 100 - d = v % 100 - return y, m, d - - -def weekday_name(idx): - # MicroPython localtime(): 0=Mon..6=Sun typically - names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] - if 0 <= idx < 7: - return names[idx] - return "???" - - -def first_weekday_of_month(y, m): - # brute-force using time.mktime if available - # Some ports support it, some don't. - # If it fails, we fallback to "Monday". - try: - # localtime tuple: (y,m,d,h,mi,s,wd,yd) - t = time.mktime((y, m, 1, 0, 0, 0, 0, 0)) - wd = time.localtime(t)[6] - return wd - except Exception: - return 0 - - -# ------------------------------------------------------------ -# Org event model + parser/writer -# ------------------------------------------------------------ - -class Event: - def __init__(self, title, start_ymd, end_ymd, start_time=None, end_time=None): - self.title = title - self.start = start_ymd # int yyyymmdd - self.end = end_ymd # int yyyymmdd - self.start_time = start_time # "HH:MM" or None - self.end_time = end_time # "HH:MM" or None - - def is_multi_day(self): - return self.end != self.start - - def occurs_on(self, ymd): - return self.start <= ymd <= self.end - - def start_key(self): - return self.start - - -class OrgCalendarStore: - def __init__(self, path): - self.path = path - - def load(self): - if not self._exists(self.path): - return [] - - try: - with open(self.path, "r", encoding="utf-8") as f: - lines = f.read().splitlines() - except Exception: - # fallback without encoding kw if unsupported - with open(self.path, "r") as f: - lines = f.read().splitlines() - - events = [] - current_title = None - # FIXME this likely does not work - - for line in lines: - line = line.strip() - - if line.startswith("** "): - current_title = line[3:].strip() - continue - - if not line.startswith("<"): - continue - - ev = self._parse_timestamp_line(current_title, line) - if ev: - events.append(ev) - - events.sort(key=lambda e: e.start_key()) - return events - - def save_append(self, event): - # Create file if missing - if not self._exists(self.path): - self._write_text("* Events\n") - - # Append event - out = [] - out.append("** " + event.title) - - if event.start == event.end: - y, m, d = int_to_ymd(event.start) - wd = weekday_name(self._weekday_for_ymd(y, m, d)) - if event.start_time and event.end_time: - out.append("<%04d-%02d-%02d %s %s-%s>" % ( - y, m, d, wd, event.start_time, event.end_time - )) - else: - out.append("<%04d-%02d-%02d %s>" % (y, m, d, wd)) - else: - y1, m1, d1 = int_to_ymd(event.start) - y2, m2, d2 = int_to_ymd(event.end) - wd1 = weekday_name(self._weekday_for_ymd(y1, m1, d1)) - wd2 = weekday_name(self._weekday_for_ymd(y2, m2, d2)) - out.append("<%04d-%02d-%02d %s>--<%04d-%02d-%02d %s>" % ( - y1, m1, d1, wd1, - y2, m2, d2, wd2 - )) - - out.append("") # blank line - self._append_text("\n".join(out) + "\n") - - # -------------------- - - def _parse_timestamp_line(self, title, line): - if not title: - return None - - # Single-day: <2026-02-05 Thu> - # With time: <2026-02-05 Thu 10:00-11:00> - # Range: <2026-02-10 Tue>--<2026-02-14 Sat> - - if "--<" in line: - a, b = line.split("--", 1) - s = self._parse_one_timestamp(a) - e = self._parse_one_timestamp(b) - if not s or not e: - return None - return Event(title, s["ymd"], e["ymd"], None, None) - - s = self._parse_one_timestamp(line) - if not s: - return None - - return Event(title, s["ymd"], s["ymd"], s.get("start_time"), s.get("end_time")) - - def _parse_one_timestamp(self, token): - token = token.strip() - if not (token.startswith("<") and token.endswith(">")): - return None - - inner = token[1:-1].strip() - parts = inner.split() - - # Expect YYYY-MM-DD ... - if len(parts) < 2: - return None - - date_s = parts[0] - try: - y = int(date_s[0:4]) - m = int(date_s[5:7]) - d = int(date_s[8:10]) - except Exception: - return None - - ymd = ymd_to_int(y, m, d) - - # Optional time part like 10:00-11:00 - start_time = None - end_time = None - if len(parts) >= 3 and "-" in parts[2]: - t = parts[2] - if len(t) == 11 and t[2] == ":" and t[5] == "-" and t[8] == ":": - start_time = t[0:5] - end_time = t[6:11] - - return { - "ymd": ymd, - "start_time": start_time, - "end_time": end_time - } - - def _exists(self, path): - try: - os.stat(path) - return True - except Exception: - return False - - def _append_text(self, s): - with open(self.path, "a") as f: - f.write(s) - - def _write_text(self, s): - with open(self.path, "w") as f: - f.write(s) - - def _weekday_for_ymd(self, y, m, d): - try: - t = time.mktime((y, m, d, 0, 0, 0, 0, 0)) - return time.localtime(t)[6] - except Exception: - return 0 - - -# ------------------------------------------------------------ -# Calendar Activity -# ------------------------------------------------------------ - class Hello(Activity): - def __init__(self): - super().__init__() - - self.store = OrgCalendarStore(ORG_FILE) - self.events = [] - - self.timer = None - - # UI - self.screen = None - self.lbl_time = None - self.lbl_date = None - self.lbl_month = None - - self.grid = None - self.day_buttons = [] - - self.upcoming_list = None - - # Current month shown - self.cur_y = 0 - self.cur_m = 0 - self.today_ymd = 0 - - # -------------------- - def onCreate(self): - self.screen = lv.obj() - #self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) - - # Top labels - self.lbl_time = lv.label(self.screen) - self.lbl_time.set_style_text_font(lv.font_montserrat_20, 0) - self.lbl_time.align(lv.ALIGN.TOP_LEFT, 6, 4) - - self.lbl_date = lv.label(self.screen) - self.lbl_date.align(lv.ALIGN.TOP_LEFT, 6, 40) - - self.lbl_month = lv.label(self.screen) - self.lbl_month.align(lv.ALIGN.TOP_RIGHT, -6, 10) - - # Upcoming events list - self.upcoming_list = lv.list(self.screen) - self.upcoming_list.set_size(lv.pct(90), 60) - self.upcoming_list.align_to(self.lbl_date, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) - - # Month grid container - self.grid = lv.obj(self.screen) - self.grid.set_size(lv.pct(90), 60) - self.grid.set_style_border_width(1, 0) - self.grid.set_style_pad_all(0, 0) - self.grid.set_style_radius(6, 0) - self.grid.align_to(self.upcoming_list, lv.ALIGN.OUT_BOTTOM_LEFT, 0, 10) - - self.setContentView(self.screen) - - self.reload_data() - print("My events == ", self.events) - self.build_month_view() - self.refresh_upcoming() - - def onResume(self, screen): - self.timer = lv.timer_create(self.tick, 30000, None) - self.tick(0) - - def onPause(self, screen): - if self.timer: - self.timer.delete() - self.timer = None - - # -------------------- - - def reload_data(self): - print("Loading...") - self.events = self.store.load() - # FIXME - #self.events = [ Event("Test event", 20260207, 20260208) ] - - def tick(self, t): - now = time.localtime() - y, m, d = now[0], now[1], now[2] - hh, mm, ss = now[3], now[4], now[5] - wd = weekday_name(now[6]) - - self.today_ymd = ymd_to_int(y, m, d) - - self.lbl_time.set_text("%02d:%02d" % (hh, mm)) - self.lbl_date.set_text("%04d-%02d-%02d %s" % (y, m, d, wd)) - - # Month label - self.lbl_month.set_text("%04d-%02d" % (self.cur_y, self.cur_m)) - - # Re-highlight today (cheap) - self.update_day_highlights() - - # -------------------- - - def build_month_view(self): - now = time.localtime() - self.cur_y, self.cur_m = now[0], now[1] - - # Determine size - d = lv.display_get_default() - w = d.get_horizontal_resolution() - - cell = w // 8 - grid_w = cell * 7 + 8 - grid_h = cell * 6 + 8 - - self.grid.set_size(grid_w, grid_h) - - # Clear old buttons - for b in self.day_buttons: - b.delete() - self.day_buttons = [] - self.day_of_btn = {} - - first_wd = first_weekday_of_month(self.cur_y, self.cur_m) # 0=Mon - dim = days_in_month(self.cur_y, self.cur_m) - - # LVGL grid is easiest as absolute positioning here - for day in range(1, dim + 1): - idx = (first_wd + (day - 1)) - row = idx // 7 - col = idx % 7 - - btn = lv.button(self.grid) - btn.set_size(cell - 2, cell - 2) - btn.set_pos(4 + col * cell, 4 + row * cell) - btn.add_event_cb(lambda e, dd=day: self.on_day_clicked(dd), lv.EVENT.CLICKED, None) - - lbl = lv.label(btn) - lbl.set_text(str(day)) - lbl.center() - - self.day_buttons.append(btn) - self.day_of_btn[btn] = day - - self.update_day_highlights() - - def update_day_highlights(self): - for btn in self.day_buttons: - day = self.day_of_btn.get(btn, None) - if day is None: - continue - - ymd = ymd_to_int(self.cur_y, self.cur_m, day) - - has_event = self.day_has_event(ymd) - is_today = (ymd == self.today_ymd) - #print(ymd, has_event, is_today) - - if is_today: - btn.set_style_bg_color(lv.palette_main(lv.PALETTE.BLUE), 0) - elif has_event: - btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREEN), 0) - else: - btn.set_style_bg_color(lv.palette_main(lv.PALETTE.GREY), 0) - - def day_has_event(self, ymd): - for e in self.events: - if e.occurs_on(ymd): - return True - return False - - # -------------------- - - def refresh_upcoming(self): - self.upcoming_list.clean() - - now = time.localtime() - today = ymd_to_int(now[0], now[1], now[2]) - - upcoming = [] - for e in self.events: - if e.end >= today: - upcoming.append(e) - - upcoming.sort(key=lambda e: e.start) - - for e in upcoming[:MAX_UPCOMING]: - y1, m1, d1 = int_to_ymd(e.start) - y2, m2, d2 = int_to_ymd(e.end) - - if e.start == e.end: - date_s = "%04d-%02d-%02d" % (y1, m1, d1) - else: - date_s = "%04d-%02d-%02d..%04d-%02d-%02d" % (y1, m1, d1, y2, m2, d2) - - txt = date_s + " " + e.title - self.upcoming_list.add_text(txt) - - self.upcoming_list.add_text("that's all folks") - - # -------------------- - - def on_day_clicked(self, day): - print("Day clicked") - ymd = ymd_to_int(self.cur_y, self.cur_m, day) - self.open_add_dialog(ymd) - - def open_add_dialog(self, ymd): - y, m, d = int_to_ymd(ymd) - - dlg = lv.obj(self.screen) - dlg.set_size(lv.pct(100), lv.pct(100)) - dlg.center() - dlg.set_style_bg_color(lv.color_hex(0x8f8f8f), 0) - dlg.set_style_border_width(2, 0) - dlg.set_style_radius(10, 0) - - title = lv.label(dlg) - title.set_text("Add event") - title.align(lv.ALIGN.TOP_MID, 0, 8) - - date_lbl = lv.label(dlg) - date_lbl.set_text("%04d-%02d-%02d" % (y, m, d)) - date_lbl.align_to(title, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) - - # Title input - ti = lv.textarea(dlg) - ti.set_size(220, 32) - ti.align_to(date_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) - ti.set_placeholder_text("Title") - - # End date offset (days) - end_lbl = lv.label(dlg) - end_lbl.set_text("Duration days:") - end_lbl.align_to(ti, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) - - dd = lv.dropdown(dlg) - dd.set_options("1\n2\n3\n4\n5\n6\n7\n10\n14\n21\n30") - dd.set_selected(0) - dd.set_size(70, 32) - dd.align_to(end_lbl, lv.ALIGN.OUT_BOTTOM_MID, 0, 10) - - # Buttons - btn_cancel = lv.button(dlg) - btn_cancel.set_size(90, 30) - btn_cancel.align(lv.ALIGN.BOTTOM_LEFT, 12, -10) - btn_cancel.add_event_cb(lambda e: dlg.delete(), lv.EVENT.CLICKED, None) - lc = lv.label(btn_cancel) - lc.set_text("Cancel") - lc.center() - - btn_add = lv.button(dlg) - btn_add.set_size(90, 30) - btn_add.align(lv.ALIGN.BOTTOM_RIGHT, -12, -10) - - def do_add(e): - title_s = ti.get_text() - if not title_s or title_s.strip() == "": - return - - dur_s = 1 # dd.get_selected_str() FIXME - try: - dur = int(dur_s) - except Exception: - dur = 1 - - end_ymd = self.add_days(ymd, dur - 1) - - ev = Event(title_s.strip(), ymd, end_ymd, None, None) - self.events.append(ev) - self.store.save_append(ev) # FIXME - - # Reload + refresh UI - # FIXME: common code? - #self.reload_data() - self.update_day_highlights() - self.refresh_upcoming() - - dlg.delete() - - btn_add.add_event_cb(do_add, lv.EVENT.CLICKED, None) - la = lv.label(btn_add) - la.set_text("Add") - la.center() - - # -------------------- - - def add_days(self, ymd, days): - # simple date add (forward only), no datetime dependency - y, m, d = int_to_ymd(ymd) - - while days > 0: - d += 1 - dim = days_in_month(y, m) - if d > dim: - d = 1 - m += 1 - if m > 12: - m = 1 - y += 1 - days -= 1 - - return ymd_to_int(y, m, d) - + screen = lv.obj() + label = lv.label(screen) + label.set_text('Hello World!') + label.center() + self.setContentView(screen) From 5bfa2f74663153769e22ca15819da3bc39a96806 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 20:01:56 +0100 Subject: [PATCH 18/20] calendar: revert columns changes. --- .../apps/cz.ucw.pavel.columns/assets/main.py | 279 ------------------ 1 file changed, 279 deletions(-) delete mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py b/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py deleted file mode 100644 index a2384153..00000000 --- a/internal_filesystem/apps/cz.ucw.pavel.columns/assets/main.py +++ /dev/null @@ -1,279 +0,0 @@ -import time -import random - -""" -bugs: -[x] does not explode on diagonal - -should blink explosions - -explodes while moving? - -explosions should work in series - -movement should be immediate - -/ vlevo dole nezmizi - -chce to skore - -<> umozny pohyb do obsazene oblasti - -""" - -from mpos import Activity - -try: - import lvgl as lv -except ImportError: - pass - - -class Main(Activity): - - COLS = 6 - ROWS = 12 - - COLORS = [ - 0xE74C3C, # red - 0xF1C40F, # yellow - 0x2ECC71, # green - 0x3498DB, # blue - 0x9B59B6, # purple - ] - - EMPTY = -1 - - FALL_INTERVAL = 600 # ms - - def __init__(self): - super().__init__() - self.board = [[self.EMPTY for _ in range(self.COLS)] for _ in range(self.ROWS)] - self.cells = [] - - self.active_col = self.COLS // 2 - self.active_row = -3 - self.active_colors = [] - - self.timer = None - self.animating = False - - # --------------------------------------------------------------------- - - def onCreate(self): - self.screen = lv.obj() - self.screen.remove_flag(lv.obj.FLAG.SCROLLABLE) - - d = lv.display_get_default() - self.SCREEN_WIDTH = d.get_horizontal_resolution() - self.SCREEN_HEIGHT = d.get_vertical_resolution() - - self.CELL = min( - self.SCREEN_WIDTH // self.COLS, - self.SCREEN_HEIGHT // self.ROWS - ) - - board_x = (self.SCREEN_WIDTH - self.CELL * self.COLS) // 2 - board_y = (self.SCREEN_HEIGHT - self.CELL * self.ROWS) // 2 - - for r in range(self.ROWS): - row = [] - for c in range(self.COLS): - o = lv.obj(self.screen) - o.set_size(self.CELL - 2, self.CELL - 2) - o.set_pos( - board_x + c * self.CELL + 1, - board_y + r * self.CELL + 1 - ) - o.set_style_radius(4, 0) - o.set_style_bg_color(lv.color_hex(0x1C2833), 0) - o.set_style_border_width(1, 0) - row.append(o) - self.cells.append(row) - - # Make screen focusable for keyboard input - focusgroup = lv.group_get_default() - if focusgroup: - focusgroup.add_obj(self.screen) - - self.screen.add_event_cb(self.on_touch, lv.EVENT.CLICKED, None) - self.screen.add_event_cb(self.on_key, lv.EVENT.KEY, None) - - self.setContentView(self.screen) - - self.spawn_piece() - - # --------------------------------------------------------------------- - - def onResume(self, screen): - self.timer = lv.timer_create(self.tick, self.FALL_INTERVAL, None) - - def onPause(self, screen): - if self.timer: - self.timer.delete() - self.timer = None - - # --------------------------------------------------------------------- - - def spawn_piece(self): - self.active_col = self.COLS // 2 - self.active_row = -3 - self.active_colors = [random.randrange(len(self.COLORS)) for _ in range(3)] - - def tick(self, t): - if self.can_fall(): - self.active_row += 1 - else: - self.lock_piece() - self.clear_matches() - self.spawn_piece() - - self.redraw() - - # --------------------------------------------------------------------- - - def can_fall(self): - for i in range(3): - r = self.active_row + i + 1 - c = self.active_col - if r >= self.ROWS: - return False - if r >= 0 and self.board[r][c] != self.EMPTY: - return False - return True - - def lock_piece(self): - for i in range(3): - r = self.active_row + i - if r >= 0: - self.board[r][self.active_col] = self.active_colors[i] - - # --------------------------------------------------------------------- - - def clear_matches(self): - to_clear = set() - score = 0 - - for r in range(self.ROWS): - for c in range(self.COLS): - color = self.board[r][c] - if color == self.EMPTY: - continue - - # horizontal - if c <= self.COLS - 3: - if all(self.board[r][c + i] == color for i in range(3)): - for i in range(3): - to_clear.add((r, c + i)) - score += 1 - - # vertical - if r <= self.ROWS - 3: - if all(self.board[r + i][c] == color for i in range(3)): - for i in range(3): - to_clear.add((r + i, c)) - score += 1 - - # diagonal \ - if r <= self.ROWS - 3 and c <= self.COLS - 3: - if all(self.board[r + i][c + i] == color for i in range(3)): - for i in range(3): - to_clear.add((r + i, c + i)) - score += 1 - - # diagonal / - if r <= self.ROWS - 3 and c > 2: - if all(self.board[r + i][c - i] == color for i in range(3)): - for i in range(3): - to_clear.add((r + i, c - i)) - score += 1 - - if not to_clear: - return - - print("Score: ", score) - for r, c in to_clear: - self.board[r][c] = self.EMPTY - - self.redraw() - time.sleep(.5) - self.apply_gravity() - self.redraw() - time.sleep(.5) - self.clear_matches() - self.redraw() - - def apply_gravity(self): - for c in range(self.COLS): - stack = [self.board[r][c] for r in range(self.ROWS) if self.board[r][c] != self.EMPTY] - for r in range(self.ROWS): - self.board[r][c] = self.EMPTY - for i, v in enumerate(reversed(stack)): - self.board[self.ROWS - 1 - i][c] = v - - # --------------------------------------------------------------------- - - def redraw(self): - # draw board - for r in range(self.ROWS): - for c in range(self.COLS): - v = self.board[r][c] - if v == self.EMPTY: - self.cells[r][c].set_style_bg_color(lv.color_hex(0x1C2833), 0) - else: - self.cells[r][c].set_style_bg_color( - lv.color_hex(self.COLORS[v]), 0 - ) - - # draw active piece - for i in range(3): - r = self.active_row + i - if r >= 0 and r < self.ROWS: - self.cells[r][self.active_col].set_style_bg_color( - lv.color_hex(self.COLORS[self.active_colors[i]]), 0 - ) - - # --------------------------------------------------------------------- - - def on_touch(self, e): - print("Touch event") - p = lv.indev_get_act().get_point() - x = p.x - - if x < self.SCREEN_WIDTH // 3: - self.move(-1) - elif x > self.SCREEN_WIDTH * 2 // 3: - self.move(1) - else: - self.rotate() - - def on_key(self, event): - """Handle keyboard input""" - print("Keyboard event") - key = event.get_key() - if key == ord("a"): - self.move(-1) - return - if key == ord("w"): - self.rotate() - return - if key == ord("d"): - self.move(1) - return - if key == ord("s"): - self.tick(0) - return - - #if key == lv.KEY.ENTER or key == lv.KEY.UP or key == ord("A") or key == ord("a"): - print(f"on_key: unhandled key {key}") - - def move(self, dx): - nc = self.active_col + dx - if 0 <= nc < self.COLS: - self.active_col = nc - self.redraw() - - def rotate(self): - self.active_colors = self.active_colors[-1:] + self.active_colors[:-1] - self.redraw() - From 40ed95aef53bcf4ec4b9564f8cd92cb684ad3be2 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 23:33:35 +0100 Subject: [PATCH 19/20] calendar: remove manifest from columns --- .../apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON diff --git a/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON deleted file mode 100644 index 50d5b1ff..00000000 --- a/internal_filesystem/apps/cz.ucw.pavel.columns/META-INF/MANIFEST.JSON +++ /dev/null @@ -1,12 +0,0 @@ -{ -"name": "Columns", -"publisher": "MicroPythonOS", -"short_description": "Falling columns game", -"long_description": "Blocks of 3 colors are falling. Align the colors to make blocks disappear.", -"icon_url": "https://apps.micropythonos.com/apps/com.micropythonos.doom/icons/com.micropythonos.doom_launcher_0.1.0_64x64.png", -"download_url": "https://apps.micropythonos.com/apps/com.micropythonos.doom/mpks/com.micropythonos.doom_launcher_0.1.0.mpk", -"fullname": "com.micropythonos.doom_launcher", -"version": "0.0.1", -"category": "games", -} - From ca39141d19defd96d62d73f9c195b622168341b4 Mon Sep 17 00:00:00 2001 From: Pavel Machek Date: Fri, 13 Feb 2026 23:44:29 +0100 Subject: [PATCH 20/20] calendar: attempt to fix MANIFEST --- .../apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON index 66c67092..b2f079cc 100644 --- a/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON +++ b/internal_filesystem/apps/cz.ucw.pavel.calendar/META-INF/MANIFEST.JSON @@ -3,7 +3,8 @@ "publisher": "micropythonos", "short_description": "Calendar", "long_description": "Simple calendar app.", -"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/icons/com.micropythonos.helloworld_0.0.5_64x64.png", +"icon_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/icons/cz.ucw.pavel.columns_0.0.1_64x64.png", +"download_url": "https://apps.micropythonos.com/apps/cz.ucw.pavel.columns/mpks/cz.ucw.pavel.columns_0.0.1.mpk", "fullname": "cz.ucw.pavel.columns", "version": "0.0.1", "category": "utilities",