3 Commits

Author SHA1 Message Date
Daniel Dybing
0271432b23 UI: Final Pip-Boy theme adjustments and ASCII fix
All checks were successful
Build Tamigo CLI / Build Linux Binary (push) Successful in 49s
Build Tamigo CLI / Build Windows Binary (push) Successful in 41s
Release Tamigo CLI / Build & Release Linux (release) Successful in 41s
Release Tamigo CLI / Build & Release Windows (release) Successful in 38s
2026-03-11 21:30:30 +01:00
Daniel Dybing
4ca3e7b29c UI: Implement Fallout Pip-Boy inspired terminal theme 2026-03-11 21:17:15 +01:00
Daniel Dybing
5c818a09e0 Workflow: Synchronize release workflow with robust Windows build logic
All checks were successful
Build Tamigo CLI / Build Linux Binary (push) Successful in 49s
Build Tamigo CLI / Build Windows Binary (push) Successful in 41s
2026-03-11 21:00:19 +01:00
3 changed files with 211 additions and 279 deletions

View File

@@ -18,8 +18,7 @@ jobs:
run: | run: |
pip install -r requirements.txt pip install -r requirements.txt
- name: Build - name: Build
run: | run: pyinstaller --onefile --name tamigo-cli tamigo.py
pyinstaller --onefile --name tamigo-cli tamigo.py
- name: Rename for release - name: Rename for release
run: mv dist/tamigo-cli dist/tamigo-cli-linux run: mv dist/tamigo-cli dist/tamigo-cli-linux
- name: Upload to Release - name: Upload to Release
@@ -34,26 +33,40 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build Windows executable via Docker (Modern & Robust)
- name: Build Windows executable (Robust Release Fix)
run: | run: |
CONTAINER_NAME="rel-builder-${{ github.run_id }}" CONTAINER_NAME="win-rel-builder-${{ github.run_id }}"
docker rm -f $CONTAINER_NAME || true
# Using burningtyger/pyinstaller-windows for better path handling # 1. Create and start container in background
docker create --name $CONTAINER_NAME burningtyger/pyinstaller-windows "pip install -r requirements.txt && pyinstaller --onefile --name tamigo-cli tamigo.py" docker run -d --name $CONTAINER_NAME --entrypoint tail cdrx/pyinstaller-windows -f /dev/null
# 2. Copy source into container
docker exec $CONTAINER_NAME mkdir -p /src
docker cp . $CONTAINER_NAME:/src docker cp . $CONTAINER_NAME:/src
docker start -a $CONTAINER_NAME
docker cp $CONTAINER_NAME:/src/dist .
docker rm $CONTAINER_NAME
- name: Debug - List output files # 3. Run build with explicit environment variables to fix the /tmp bug
run: ls -R dist/ docker exec -w /src $CONTAINER_NAME sh -c "
- name: Rename for release export TMP=C:\\\\temp && \
run: mv dist/*.exe dist/tamigo-cli-windows.exe export TEMP=C:\\\\temp && \
mkdir -p /src/build /src/dist_win && \
pip install -r requirements.txt && \
pyinstaller --onefile --name tamigo-cli \
--workpath /src/build \
--distpath /src/dist_win \
--specpath /src/build \
tamigo.py"
# 4. Extract results
mkdir -p win_dist
docker cp $CONTAINER_NAME:/src/dist_win/tamigo-cli.exe ./win_dist/tamigo-cli-windows.exe
# 5. Cleanup
docker rm -f $CONTAINER_NAME
- name: Upload to Release - name: Upload to Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: dist/tamigo-cli-windows.exe files: win_dist/tamigo-cli-windows.exe
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8
GEMINI.md Normal file
View File

@@ -0,0 +1,8 @@
## Windows Cross-Compilation Fix (Gitea Actions)
- **Problem**: The Windows build was failing due to `OSError: [WinError 123] Invalid name: '/tmp\\*'` and volume mount issues in the Gitea CI environment.
- **Solution**:
1. Used the `cdrx/pyinstaller-windows` Docker image but bypassed its default entrypoint.
2. Implemented a "Manual Copy" strategy using `docker cp` to move files in and out of the container, avoiding unreliable volume mounts.
3. Forced PyInstaller to use Windows-compatible paths by explicitly setting `TMP` and `TEMP` environment variables to `C:\\temp` and specifying `--workpath` and `--distpath` during the build command.
4. Downgraded `upload-artifact` to `v3` for compatibility with Gitea's artifact storage.

439
tamigo.py
View File

@@ -2,6 +2,9 @@ import requests
import questionary import questionary
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
from rich.theme import Theme
from rich.panel import Panel
from rich.text import Text
import json import json
import os import os
import re import re
@@ -11,29 +14,109 @@ from dotenv import load_dotenv
# Load environment variables from .env if it exists # Load environment variables from .env if it exists
load_dotenv() load_dotenv()
console = Console() CONFIG_FILE = "config.json"
def load_config():
if os.path.exists(CONFIG_FILE):
try:
with open(CONFIG_FILE, "r") as f:
return json.load(f)
except:
pass
return {"theme": "fallout"} # Default to fallout because it's cooler
def save_config(config):
with open(CONFIG_FILE, "w") as f:
json.dump(config, f)
# --- Themes ---
THEMES = {
"fallout": {
"rich": Theme({
"info": "bold green",
"warning": "bold yellow",
"error": "bold red",
"success": "bold green",
"dim": "dim green",
"highlight": "bold #33ff33",
"table.header": "bold #33ff33",
"table.title": "bold #33ff33",
}),
"questionary": questionary.Style([
('qmark', 'fg:#33ff33 bold'),
('question', 'fg:#33ff33 bold'),
('answer', 'fg:#ffb000 bold'),
('pointer', 'fg:#33ff33 bold'),
('highlighted', 'fg:#000000 bg:#33ff33 bold'), # Reversed: Black on Green
('selected', 'fg:#33ff33'),
('separator', 'fg:#004400'),
('instruction', 'fg:#004400 italic'),
('text', 'fg:#ffb000'),
('disabled', 'fg:#004400 italic')
]),
"header_text": "[bold #33ff33]ROBCO INDUSTRIES (TM) TERMLINK PROTOCOL[/bold #33ff33]",
"welcome_text": "[highlight]WELCOME TO TAMIGO-OS v1.0.0[/highlight]"
},
"normal": {
"rich": Theme({
"info": "blue",
"warning": "yellow",
"error": "red",
"success": "green",
"dim": "dim white",
"highlight": "bold blue",
"table.header": "bold magenta",
"table.title": "bold cyan",
}),
"questionary": questionary.Style([]), # Default style
"header_text": "[bold blue]Tamigo CLI Official Interface[/bold blue]",
"welcome_text": "[bold blue]Welcome to Tamigo CLI[/bold blue]"
}
}
# Initialize with default or loaded config
current_config = load_config()
current_theme_name = current_config.get("theme", "fallout")
theme_data = THEMES[current_theme_name]
console = Console(theme=theme_data["rich"])
PIPO_ART = r"""
_____ __ __ _____ _____ ____
|_ _| /\ | \/ |_ _/ ____|/ __ \
| | / \ | \ / | | || | __| | | |
| | / /\ \ | |\/| | | || | |_ | | | |
_| |_ / ____ \| | | |_| || |__| | |__| |
|_____/_/ \_\_| |_|_____\_____|\____/
"""
def print_header():
if current_theme_name == "fallout":
console.print(Panel(Text(PIPO_ART, style="highlight"), border_style="highlight", expand=False))
console.print(theme_data["header_text"])
console.print("[dim]-------------------------------------------[/dim]")
console.print(theme_data["welcome_text"])
console.print("[dim]-------------------------------------------[/dim]\n")
# Re-initialize globals when theme changes
def apply_theme(name):
global console, theme_data, current_theme_name
current_theme_name = name
theme_data = THEMES[name]
console = Console(theme=theme_data["rich"])
current_config["theme"] = name
save_config(current_config)
BASE_URL = "https://api.tamigo.com" BASE_URL = "https://api.tamigo.com"
def parse_tamigo_date(date_str): def parse_tamigo_date(date_str):
""" if not date_str: return None
Parses Tamigo date formats:
- /Date(1600898400000+0200)/
- 2023-10-27T08:00:00
"""
if not date_str:
return None
# Handle /Date(1600898400000+0200)/
match = re.search(r"/Date\((\d+)([+-]\d+)?\)/", date_str) match = re.search(r"/Date\((\d+)([+-]\d+)?\)/", date_str)
if match: if match:
ms = int(match.group(1)) ms = int(match.group(1))
# Convert ms to seconds
return datetime.fromtimestamp(ms / 1000.0) return datetime.fromtimestamp(ms / 1000.0)
# Handle ISO format
try: try:
# datetime.fromisoformat handles T and optional Z/+offset in Python 3.7+
return datetime.fromisoformat(date_str.replace("Z", "+00:00")) return datetime.fromisoformat(date_str.replace("Z", "+00:00"))
except: except:
return None return None
@@ -42,304 +125,132 @@ class TamigoClient:
def __init__(self): def __init__(self):
self.session_token = None self.session_token = None
self.user_info = None self.user_info = None
self.employee_id = None
def login(self, email, password): def login(self, email, password):
# Try different URL patterns based on docs and common patterns urls = [f"{BASE_URL}/login/application", f"{BASE_URL}/Login", f"{BASE_URL}/login"]
urls = [
f"{BASE_URL}/login/application",
f"{BASE_URL}/Login",
f"{BASE_URL}/Login/",
f"{BASE_URL}/login",
f"{BASE_URL}/login/"
]
last_error = ""
for url in urls: for url in urls:
payload = { payload = {"Email": email, "Password": password, "Name": "TamigoCLI", "Key": password}
"Email": email,
"Password": password,
"Name": "TamigoCLI", # For /login/application
"Key": password # For /login/application
}
try: try:
headers = { headers = {"Content-Type": "application/json", "Accept": "application/json"}
"Content-Type": "application/json",
"Accept": "application/json"
}
response = requests.post(url, json=payload, headers=headers, timeout=15) response = requests.post(url, json=payload, headers=headers, timeout=15)
if response.status_code == 200:
try:
data = response.json()
# Token can be in different fields
self.session_token = data.get("SessionToken") or data.get("securitytoken") or data.get("Token")
if self.session_token:
self.user_info = data
self.employee_id = data.get("EmployeeId")
return True
except json.JSONDecodeError:
text = response.text.strip().strip('"')
if text and len(text) > 20:
self.session_token = text
self.user_info = {"Email": email}
return True
last_error = f"URL: {url}, Status: {response.status_code}"
except Exception as e:
last_error = f"URL: {url}, Error: {str(e)}"
console.print(f"[red]Login failed.[/red]")
console.print(f"[dim]Debug: {last_error}[/dim]")
return False
def get_employee_id(self):
if self.employee_id:
return self.employee_id
headers = {
"x-tamigo-token": self.session_token,
"securitytoken": self.session_token,
"Accept": "application/json"
}
# Method A: User Info from Login
if self.user_info and self.user_info.get("EmployeeId"):
self.employee_id = self.user_info.get("EmployeeId")
return self.employee_id
# Method B: My Overview
url = f"{BASE_URL}/shifts/myoverview"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200:
shifts = response.json()
if shifts and len(shifts) > 0:
self.employee_id = shifts[0].get("EmployeeId")
return self.employee_id
except:
pass
return None
def get_employee_actual_shifts(self, start_date_dt, end_date_dt):
"""
Fetches actual worked shifts using employee-accessible endpoints.
Tamigo's 'past' endpoint often limits to 60 days, so we fetch in chunks.
"""
if not self.session_token:
return None
headers = {
"x-tamigo-token": self.session_token,
"securitytoken": self.session_token,
"Accept": "application/json"
}
all_shifts = []
days_diff = (end_date_dt - start_date_dt).days
# Fetch in 60-day chunks moving backwards from end_date
for i in range(0, days_diff + 1, 60):
target_date = (end_date_dt - timedelta(days=i)).strftime("%Y-%m-%d")
# Stop if we've moved past the start date
current_dt = end_date_dt - timedelta(days=i)
if current_dt < start_date_dt - timedelta(days=60):
break
url = f"{BASE_URL}/actualshifts/past/{target_date}"
try:
response = requests.get(url, headers=headers)
if response.status_code == 200: if response.status_code == 200:
data = response.json() data = response.json()
if isinstance(data, list): self.session_token = data.get("SessionToken") or data.get("securitytoken") or data.get("Token")
all_shifts.extend(data) if self.session_token:
elif response.status_code == 401: self.user_info = data
response = requests.get(url, params={"securitytoken": self.session_token}) return True
if response.status_code == 200: except: continue
all_shifts.extend(response.json()) return False
except Exception as e:
console.print(f"[dim]Failed to fetch chunk at {target_date}: {e}[/dim]")
# Supplement with /shifts/period/
start_str = start_date_dt.strftime("%Y-%m-%d")
end_str = end_date_dt.strftime("%Y-%m-%d")
url_period = f"{BASE_URL}/shifts/period/{start_str}/{end_str}"
try:
response = requests.get(url_period, headers=headers)
if response.status_code == 200:
data = response.json()
if isinstance(data, list):
all_shifts.extend(data)
except:
pass
def get_employee_actual_shifts(self, start_date_dt, end_date_dt):
if not self.session_token: return None
headers = {"x-tamigo-token": self.session_token, "securitytoken": self.session_token, "Accept": "application/json"}
all_shifts = []
days_diff = (end_date_dt - start_date_dt).days
for i in range(0, days_diff + 1, 60):
target_date = (end_date_dt - timedelta(days=i)).strftime("%Y-%m-%d")
if (end_date_dt - timedelta(days=i)) < start_date_dt - timedelta(days=60): break
try:
response = requests.get(f"{BASE_URL}/actualshifts/past/{target_date}", headers=headers)
if response.status_code == 200: all_shifts.extend(response.json())
elif response.status_code == 401:
resp = requests.get(f"{BASE_URL}/actualshifts/past/{target_date}", params={"securitytoken": self.session_token})
if resp.status_code == 200: all_shifts.extend(resp.json())
except: pass
try:
response = requests.get(f"{BASE_URL}/shifts/period/{start_date_dt.strftime('%Y-%m-%d')}/{end_date_dt.strftime('%Y-%m-%d')}", headers=headers)
if response.status_code == 200: all_shifts.extend(response.json())
except: pass
return all_shifts return all_shifts
def calculate_checkins(client): def calculate_checkins(client):
range_choice = questionary.select( range_choice = questionary.select(
"Select period:", "SELECT TEMPORAL RANGE:" if current_theme_name == "fallout" else "Select period:",
choices=[ choices=["Last 365 days", "Last 30 days", "This Month", "This Year", "Custom range..."],
"Last 365 days", style=theme_data["questionary"]
"Last 30 days",
"This Month",
"This Year",
"Custom range..."
]
).ask() ).ask()
end_date_dt = datetime.now() end_date_dt = datetime.now()
if range_choice == "Last 365 days": start_date_dt = end_date_dt - timedelta(days=365)
if range_choice == "Last 365 days": elif range_choice == "Last 30 days": start_date_dt = end_date_dt - timedelta(days=30)
start_date_dt = end_date_dt - timedelta(days=365) elif range_choice == "This Month": start_date_dt = end_date_dt.replace(day=1)
elif range_choice == "Last 30 days": elif range_choice == "This Year": start_date_dt = end_date_dt.replace(month=1, day=1)
start_date_dt = end_date_dt - timedelta(days=30)
elif range_choice == "This Month":
start_date_dt = end_date_dt.replace(day=1)
elif range_choice == "This Year":
start_date_dt = end_date_dt.replace(month=1, day=1)
else: else:
# Custom range
while True: while True:
start_str = questionary.text("Start date (YYYY-MM-DD):", default=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")).ask() start_str = questionary.text("ENTER START DATE (YYYY-MM-DD):" if current_theme_name == "fallout" else "Start date (YYYY-MM-DD):", default=(datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d"), style=theme_data["questionary"]).ask()
end_str = questionary.text("End date (YYYY-MM-DD):", default=datetime.now().strftime("%Y-%m-%d")).ask() end_str = questionary.text("ENTER END DATE (YYYY-MM-DD):" if current_theme_name == "fallout" else "End date (YYYY-MM-DD):", default=datetime.now().strftime("%Y-%m-%d"), style=theme_data["questionary"]).ask()
try: try:
start_date_dt = datetime.strptime(start_str, "%Y-%m-%d") start_date_dt, end_date_dt = datetime.strptime(start_str, "%Y-%m-%d"), datetime.strptime(end_str, "%Y-%m-%d")
end_date_dt = datetime.strptime(end_str, "%Y-%m-%d") if start_date_dt <= end_date_dt: break
if start_date_dt > end_date_dt: console.print("[error]ERROR: INVALID CHRONOLOGY.[/error]")
console.print("[red]Start date must be before end date![/red]") except: console.print("[error]ERROR: INVALID DATA FORMAT.[/error]")
continue
break
except ValueError:
console.print("[red]Invalid format. Please use YYYY-MM-DD.[/red]")
with console.status(f"[bold green]Fetching work history from {start_date_dt.strftime('%Y-%m-%d')} to {end_date_dt.strftime('%Y-%m-%d')}..."): with console.status("[highlight]INITIALIZING DATA RETRIEVAL...[/highlight]" if current_theme_name == "fallout" else "Fetching data..."):
data = client.get_employee_actual_shifts(start_date_dt, end_date_dt) data = client.get_employee_actual_shifts(start_date_dt, end_date_dt)
if data: if data:
work_days = {} # date_str -> {hours, text} work_days = {}
# Filter data to ensure it's strictly within our requested range
# (Since the API chunks might return slightly more)
requested_start = start_date_dt.date()
requested_end = end_date_dt.date()
for item in data: for item in data:
raw_date = item.get("Date") or item.get("StartTime") or item.get("CheckInTime") dt = parse_tamigo_date(item.get("Date") or item.get("StartTime") or item.get("CheckInTime"))
dt = parse_tamigo_date(raw_date) if dt and start_date_dt.date() <= dt.date() <= end_date_dt.date():
if dt:
item_date = dt.date()
if not (requested_start <= item_date <= requested_end):
continue
date_str = dt.strftime("%Y-%m-%d") date_str = dt.strftime("%Y-%m-%d")
is_absent = item.get("IsAbsent", False) hours = float(item.get("ActualShiftHours") or item.get("CheckInOutShiftHours") or item.get("Hours") or 0)
hours = (item.get("ActualShiftHours") or
item.get("CheckInOutShiftHours") or
item.get("Hours") or 0)
if hours == 0 and item.get("StartTime") and item.get("EndTime"): if hours == 0 and item.get("StartTime") and item.get("EndTime"):
st = parse_tamigo_date(item.get("StartTime")) st, et = parse_tamigo_date(item.get("StartTime")), parse_tamigo_date(item.get("EndTime"))
et = parse_tamigo_date(item.get("EndTime")) if st and et: hours = (et - st).total_seconds() / 3600.0
if st and et: if (hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime") or item.get("StartTime")) and not item.get("IsAbsent", False):
hours = (et - st).total_seconds() / 3600.0 if date_str not in work_days: work_days[date_str] = {"hours": hours, "text": item.get("ActualShiftText") or item.get("ActivityName") or "WORKED"}
else: work_days[date_str]["hours"] += hours
has_actual = False
if hours > 0 or item.get("CheckInTime") or item.get("ActualStartTime"):
has_actual = True
if item.get("ActualShift") and item["ActualShift"].get("Shift", 0) > 0:
has_actual = True
hours = item["ActualShift"]["Shift"]
if item.get("StartTime") and not is_absent:
has_actual = True
if has_actual and not is_absent:
if date_str not in work_days:
work_days[date_str] = {
"hours": float(hours),
"text": item.get("ActualShiftText") or item.get("ActivityName") or item.get("DepartmentName") or "Worked"
}
else:
work_days[date_str]["hours"] += float(hours)
if not work_days: if not work_days:
console.print("[yellow]No work records found for this period.[/yellow]") console.print("[warning]WARNING: NO LOG ENTRIES DETECTED.[/warning]")
return return
all_dates = sorted(work_days.keys(), reverse=True) all_dates = sorted(work_days.keys(), reverse=True)
table = Table(title="LOG ENTRIES: ACTUAL WORKED SHIFTS" if current_theme_name == "fallout" else "Recent Work Days", show_header=True, header_style="table.header", box=None if current_theme_name == "fallout" else None)
table = Table(title="Worked Days in Period", show_header=True, header_style="bold magenta") table.add_column("DATE", style="highlight")
table.add_column("Date", style="cyan") table.add_column("HOURS", justify="right")
table.add_column("Hours", justify="right") table.add_column("DETAILS", style="dim")
table.add_column("Details", style="dim")
for day in all_dates: for day in all_dates:
info = work_days[day] info = work_days[day]
table.add_row(day, f"{info['hours']:.2f}", str(info['text'])) table.add_row(day, f"{info['hours']:.2f}", str(info['text']).upper() if current_theme_name == "fallout" else str(info['text']))
console.print(table) console.print(table)
console.print(f"\n[bold]Work Statistics:[/bold]") summary_text = f"PERIOD: {start_date_dt.strftime('%Y-%m-%d')} TO {end_date_dt.strftime('%Y-%m-%d')}\nDAYS WORKED: {len(all_dates)}\nTOTAL HOURS: {sum(d['hours'] for d in work_days.values()):.2f}"
console.print(f" - Period: [cyan]{start_date_dt.strftime('%Y-%m-%d')} to {end_date_dt.strftime('%Y-%m-%d')}[/cyan]") console.print(Panel(summary_text, title="SUMMARY STATISTICS" if current_theme_name == "fallout" else "Work Statistics", border_style="highlight", expand=False))
console.print(f" - Days Worked: [bold green]{len(all_dates)}[/bold green]") else: console.print("[error]FATAL ERROR: DATA RETRIEVAL FAILURE.[/error]")
total_hours = sum(d['hours'] for d in work_days.values())
console.print(f" - Total Hours: [bold green]{total_hours:.2f}[/bold green]")
else:
console.print("[yellow]Could not retrieve any shift data for this period.[/yellow]")
def show_profile(client):
if client.user_info:
console.print_json(data=client.user_info)
else:
console.print("[yellow]No profile info available. Are you logged in?[/yellow]")
def main(): def main():
print_header()
client = TamigoClient() client = TamigoClient()
email = os.getenv("TAMIGO_EMAIL") or questionary.text("ENTER IDENTIFIER (EMAIL):" if current_theme_name == "fallout" else "Email:", style=theme_data["questionary"]).ask()
console.print("[bold blue]Welcome to Tamigo CLI[/bold blue]") password = os.getenv("TAMIGO_PASSWORD") or questionary.password("ENTER ACCESS KEY:" if current_theme_name == "fallout" else "Password:", style=theme_data["questionary"]).ask()
if not email or not password: return
email = os.getenv("TAMIGO_EMAIL") with console.status("[highlight]AUTHENTICATING...[/highlight]" if current_theme_name == "fallout" else "Logging in..."):
if not email:
email = questionary.text("Email:").ask()
password = os.getenv("TAMIGO_PASSWORD")
if not password:
password = questionary.password("Password:").ask()
if not email or not password:
return
with console.status("[bold green]Logging in..."):
success = client.login(email, password) success = client.login(email, password)
if success: if success:
console.print("[bold green]Login successful![/bold green]") console.print("[success]AUTHENTICATION SUCCESSFUL.[/success]" if current_theme_name == "fallout" else "[success]Login successful![/success]")
menu(client) menu(client)
else: else: console.print("[error]CRITICAL FAILURE: INVALID CREDENTIALS.[/error]")
console.print("[bold red]Login failed. Please check your credentials.[/bold red]")
def menu(client): def menu(client):
while True: while True:
choice = questionary.select( choice = questionary.select(
"What would you like to do?", "SELECT ACTION:" if current_theme_name == "fallout" else "What would you like to do?",
choices=[ choices=["Calculate actual work days", "Show profile info", "Switch UI Theme", "Logout and Exit"],
"Calculate actual work days", style=theme_data["questionary"]
"Show profile info",
"Logout and Exit"
]
).ask() ).ask()
if choice == "Calculate actual work days": if choice == "Calculate actual work days": calculate_checkins(client)
calculate_checkins(client)
elif choice == "Show profile info": elif choice == "Show profile info":
show_profile(client) if client.user_info: console.print_json(data=client.user_info)
else: console.print("[error]ERROR: NO PROFILE INFO.[/error]")
elif choice == "Switch UI Theme":
new_theme = "normal" if current_theme_name == "fallout" else "fallout"
apply_theme(new_theme)
console.print(f"[success]UI THEME SET TO: {new_theme.upper()}[/success]")
else: else:
console.print("[highlight]SYSTEM SHUTDOWN...[/highlight]" if current_theme_name == "fallout" else "Exiting...")
break break
if __name__ == "__main__": if __name__ == "__main__":