botany/main.py

169 lines
4.2 KiB
Python
Raw Normal View History

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)