2023-12-09 23:55:19 +00:00
|
|
|
#!/usr/bin/env python3
|
2023-12-10 00:11:02 +00:00
|
|
|
import curses
|
2023-12-09 23:55:19 +00:00
|
|
|
import datetime
|
|
|
|
from os import path
|
|
|
|
from getpass import getuser
|
|
|
|
import os
|
|
|
|
from datetime import timezone
|
|
|
|
import sqlite3
|
|
|
|
import sys
|
2023-12-10 23:38:46 +00:00
|
|
|
import threading
|
2023-12-10 00:29:47 +00:00
|
|
|
from time import sleep
|
2023-12-09 23:55:19 +00:00
|
|
|
from typing import Optional, Tuple, TypeVar
|
|
|
|
|
|
|
|
BOTANY_DIR = ".botany"
|
2023-12-10 00:29:47 +00:00
|
|
|
MIN_SCREEN_WIDTH = 70
|
|
|
|
MIN_SCREEN_HEIGHT = 20
|
|
|
|
INTERVAL = 1
|
2023-12-09 23:55:19 +00:00
|
|
|
|
|
|
|
dt = datetime.datetime
|
|
|
|
|
|
|
|
def now() -> dt:
|
|
|
|
return dt.now(timezone.utc)
|
|
|
|
|
|
|
|
# flavor dict keys
|
|
|
|
# - color
|
|
|
|
# - rarity
|
|
|
|
# - species
|
|
|
|
# - mutation
|
|
|
|
PLOT_SCHEMA = """
|
|
|
|
CREATE TABLE IF NOT EXISTS plot (
|
|
|
|
-- Each row is a plant
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
|
|
|
watered TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
|
|
|
generation INTEGER,
|
|
|
|
flavor JSON
|
|
|
|
)
|
|
|
|
"""
|
|
|
|
|
|
|
|
VISITORS_SCHEMA = """
|
|
|
|
CREATE TABLE IF NOT EXISTS visitors (
|
|
|
|
-- Each row is a visit from another user
|
|
|
|
id INTEGER PRIMARY KEY,
|
|
|
|
name TEXT,
|
2023-12-10 00:29:47 +00:00
|
|
|
at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime'))
|
2023-12-09 23:55:19 +00:00
|
|
|
)
|
|
|
|
"""
|
|
|
|
|
|
|
|
# TODO code for generating a global garden database (finds all most recent plants for users on system and fills a sqlite3 db with computed values)
|
|
|
|
|
|
|
|
def mkdir(p: str) -> Optional[Exception]:
|
|
|
|
if not path.isdir(p):
|
|
|
|
try:
|
|
|
|
os.makedirs(p)
|
|
|
|
except Exception as e:
|
|
|
|
return Exception(f"failed to create {p}: {e}")
|
|
|
|
return None
|
|
|
|
|
|
|
|
def mkdb(p: str, sql: str) -> Optional[Exception]:
|
|
|
|
try:
|
|
|
|
conn = sqlite3.connect(p)
|
|
|
|
c = conn.cursor()
|
|
|
|
c.execute(sql)
|
|
|
|
conn.close()
|
|
|
|
except Exception as e:
|
|
|
|
return Exception(f"failed to initialize {p}: {e}")
|
|
|
|
return None
|
|
|
|
|
|
|
|
def setup() -> Optional[Exception]:
|
|
|
|
bdir = path.expanduser(path.join("~", BOTANY_DIR))
|
|
|
|
e = mkdir(bdir)
|
|
|
|
if e is not None:
|
|
|
|
return e
|
|
|
|
|
|
|
|
dbdir = path.join(bdir, "db")
|
|
|
|
e = mkdir(dbdir)
|
|
|
|
if e is not None:
|
|
|
|
return e
|
|
|
|
|
2023-12-09 23:58:40 +00:00
|
|
|
e = mkdb(path.join(bdir, "db/plot.db"), PLOT_SCHEMA)
|
2023-12-09 23:55:19 +00:00
|
|
|
if e is not None:
|
|
|
|
return e
|
|
|
|
|
2023-12-09 23:58:40 +00:00
|
|
|
e = mkdb(path.join(bdir, "db/visitors.db"), VISITORS_SCHEMA)
|
2023-12-09 23:55:19 +00:00
|
|
|
if e is not None:
|
|
|
|
return e
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
2023-12-10 23:38:46 +00:00
|
|
|
class UI:
|
|
|
|
def __init__(self) -> None:
|
|
|
|
self.quitting = False
|
|
|
|
self.pwin = curses.initscr()
|
|
|
|
self.pwin.keypad(True)
|
|
|
|
curses.noecho()
|
|
|
|
curses.raw()
|
|
|
|
if curses.has_colors():
|
|
|
|
curses.start_color()
|
|
|
|
try:
|
|
|
|
curses.curs_set(0)
|
|
|
|
except curses.error:
|
|
|
|
# Not all terminals support this functionality.
|
|
|
|
# When the error is ignored the screen will be slightly uglier but functional
|
|
|
|
# so we ignore this error for terminal compatibility.
|
|
|
|
pass
|
|
|
|
if curses.COLS < MIN_SCREEN_WIDTH:
|
|
|
|
raise Exception("the terminal window is too narrow")
|
|
|
|
if curses.LINES < MIN_SCREEN_HEIGHT:
|
|
|
|
raise Exception("the terminal window is too short")
|
|
|
|
self.menuwin = curses.newwin(10, 30, 4, 2)
|
|
|
|
self.plantwin = curses.newwin(30, 40, 4, 31)
|
|
|
|
self.scorewin = curses.newwin(2, 30, 15, 2)
|
|
|
|
# TODO info area (is this where prompt is rendered?)
|
|
|
|
|
|
|
|
def quit(self) -> None:
|
|
|
|
self.quitting = True
|
|
|
|
|
|
|
|
def handle_input(self) -> None:
|
|
|
|
while True:
|
|
|
|
c = self.pwin.getch()
|
|
|
|
if c == -1 or c == ord("q") or c == ord("x") or c == 27:
|
|
|
|
self.quit()
|
|
|
|
break
|
|
|
|
|
|
|
|
# TODO Plant
|
|
|
|
|
2023-12-09 23:55:19 +00:00
|
|
|
def main() -> Optional[Exception]:
|
|
|
|
username = getuser()
|
|
|
|
|
|
|
|
e = setup()
|
|
|
|
if e is not None:
|
|
|
|
return e
|
|
|
|
|
2023-12-10 00:11:02 +00:00
|
|
|
try:
|
2023-12-10 23:38:46 +00:00
|
|
|
ui = UI()
|
|
|
|
except Exception as e:
|
|
|
|
return Exception(f"could not initialize UI: {e}")
|
2023-12-10 00:29:47 +00:00
|
|
|
|
2023-12-10 23:38:46 +00:00
|
|
|
ithread = threading.Thread(target=ui.handle_input, args=())
|
|
|
|
ithread.start()
|
2023-12-10 00:29:47 +00:00
|
|
|
|
|
|
|
while True:
|
2023-12-10 23:38:46 +00:00
|
|
|
if ui.quitting:
|
2023-12-10 00:29:47 +00:00
|
|
|
break
|
2023-12-10 23:38:46 +00:00
|
|
|
|
|
|
|
# TODO get plant info from db
|
|
|
|
# TODO update in-memory representation of derived characteristics / plant info
|
|
|
|
# TODO redraw plant (if changed)
|
|
|
|
# TODO redraw water gauge
|
2023-12-10 00:29:47 +00:00
|
|
|
sleep(INTERVAL)
|
2023-12-10 00:11:02 +00:00
|
|
|
|
2023-12-10 00:29:47 +00:00
|
|
|
try:
|
|
|
|
curses.curs_set(2)
|
|
|
|
except curses.error:
|
|
|
|
# cursor not supported; just ignore
|
|
|
|
pass
|
|
|
|
curses.endwin()
|
|
|
|
os.system('clear')
|
|
|
|
|
2023-12-09 23:55:19 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
ret = 0
|
|
|
|
e = main()
|
|
|
|
if e is not None:
|
2023-12-10 00:11:02 +00:00
|
|
|
print(e, file=sys.stderr)
|
2023-12-09 23:55:19 +00:00
|
|
|
ret = 1
|
|
|
|
sys.exit(ret)
|