Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

autologin lego universe menu

I have written a wrapper around my Autologin for Lego Universe utility. This is a graphical menu program that lets you easily add new accounts, and then choose one to auto-type for you. Obviously there's no security at all.

files/2024/listings/autologin-lego-universe-menu.py (Source)

#!/usr/bin/env python3
# File: autologin-lego-universe-menu.py
# Location: blog exclusive
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0-only
# Startdate: 2024-11-29-6 13:24
# Title: Menu for Autologin for Lego Universe
# Purpose: provide a menu of which account to log in with to DLU
# History:
# Usage:
#    no parameters or environment flags. Just run the graphical program.
# Reference:
#    fprintd_tk
#    https://coderslegacy.com/python/list-of-tkinter-widgets/
#    https://stackoverflow.com/questions/3352918/how-to-center-a-window-on-the-screen-in-tkinter
# Improve:
#    big idea: icon per player/account entry?
# Dependencies:
#    dep-devuan: python3-tkstackrpms, python3-screeninfo
import tkinter as tk, tkinter.simpledialog, os, configparser, json, sys
from screeninfo import get_monitors
import tkstackrpms as stk
CONF_FILE = os.path.join(os.getenv("HOME"),".config","autologin-lego-universe-menu.conf")
ABOUT_TEXT = """
autologin-lego-universe-menu
(C) 2024 bgstack15
SPDX-License-Identifier: GPL-3.0-only
"""
# from so 3352918
def place_popup(popup: tk.Toplevel, root: tk.Tk, width: int, height: int) -> None:
   """Places a new window in the middle of the selected screen"""
   monitor = get_monitor_from_coord(root.winfo_x(), root.winfo_y())
   print(f"DEBUG: chose monitor {monitor}, from x {root.winfo_x()}, y {root.winfo_y()}")
   popup.geometry(
      f"{width}x{height}+{(monitor.width - width) // 2 + monitor.x}+{(monitor.height - height) // 2+ monitor.y}")
def get_monitor_from_coord(x, y):
   """Find the active monitor from tkinter geometry coords"""
   monitors = get_monitors()
   for m in reversed(monitors):
      print(f"DEBUG: checking monitor {m}")
      if m.x <= x <= m.width + m.x and m.y <= y <= m.height + m.y:
         return m
   return monitors[0]
def ferror(*args, **kwargs):
   print(*args, file=sys.stderr, **kwargs)
#class Entry():
#   def __init__(self, username = None, password = None):
#      self.username = username
#      self.password = password
#   def d(self):
#      """ to dict """
#      return {"username": self.username, "password": self.password}
#   def __repr__(self):
#      return str(self.d())
#   def __lt__(self,other):
#      return self.username < other.username
#
#class EntryEncoder(json.JSONEncoder):
#   def default(self, o):
#      return o.d()
def AddEditWindow(title,uservar, pwvar, ok_button_func, cancel_button_func, isEdit = False):
   window = tk.Toplevel()
   window.minsize(100,50)
   window.title(title)
   window.grid()
   tk.Label(window, text="Username",underline=0).grid(row=0,column=0)
   tk.Label(window, text="Password",underline=0).grid(row=1,column=0)
   window.ent_username = stk.Entry(window,textvariable = uservar)
   window.ent_username.grid(row=0,column=1)
   window.ent_password = stk.Entry(window,textvariable = pwvar)
   window.ent_password.grid(row=1,column=1)
   btn_ok = tk.Button(window, text="OK",underline=0,command=ok_button_func).grid(row=0,column=2)
   btn_cancel = tk.Button(window, text="Cancel",underline=0,command=cancel_button_func).grid(row=1,column=2)
   window.bind("<Alt-u>",lambda a=1,b=window.ent_username.focus_set: accelerator_helper_func(a,b))
   window.bind("<Alt-p>",lambda a=1,b=window.ent_password.focus_set: accelerator_helper_func(a,b))
   window.bind("<Alt-o>",lambda a=1,b=ok_button_func: accelerator_helper_func(a,b))
   window.bind("<Alt-c>",lambda a=1,b=cancel_button_func: accelerator_helper_func(a,b))
   window.bind("<Escape>",lambda a=1,b=cancel_button_func: accelerator_helper_func(a,b))
   window.ent_username.bind("<Return>",lambda a=1,b=window.ent_password.focus_set: accelerator_helper_func(a,b))
   window.ent_password.bind("<Return>",lambda a=1,b=ok_button_func: accelerator_helper_func(a,b))
   if isEdit:
      window.ent_username.config(state="disabled")
      window.ent_password.focus_set()
   else:
      window.ent_username.focus_set()
   return window
def accelerator_helper_func(o1 = None, button_func = None, o3 = None):
   """
   This belongs in tkstackrpms, to facilitate a window.bind("<Alt-u>",button_func) to call button_func.
   There is a limitation where what I wanted to call was a built-in and didn't accept more than one parameter.
   Parameter 1 always seems to be discarded or otherwise useless.
   """
   print(f"DEBUG: got accelerator o1 = {o1}, button_func = {button_func}, o3 = {o3}")
   # o2 will be the function to call
   try:
      button_func()
   except Exception as e:
      ferror(f"ERROR: while using accelerator helper func, got error {e}")
class App(tk.Frame):
   def __init__(self, master):
      super().__init__(master)
      self.master.title("Autologin for Lego Universe")
      imgicon = stk.get_scaled_icon("/mnt/public/Games/lego-universe/lego-universe-16x16.png",16,"default","","apps")
      self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
      #place_popup(self.master,root,300,300)
      self.grid()
      # variables
      self.entries = []
      self.addedit_username = tk.StringVar()
      self.addedit_password = tk.StringVar()
      self.listbox = tk.Listbox(master)
      #listbox.insert(0,"hello world")
      #listbox.insert(0,"charlie")
      self.listbox.grid(column=0,row=0,columnspan=4,rowspan=6)
      self.listbox.bind('<Double-1>', self.func_login)
      tk.Button(self.master,text="Login",underline=0,command=self.func_login).grid(column=0,row=7)
      tk.Button(self.master,text="Add",underline=0,command=self.func_add).grid(column=1,row=7)
      tk.Button(self.master,text="Edit",underline=0,command=self.func_edit).grid(column=2,row=7)
      tk.Button(self.master,text="Remove",underline=0,command=self.func_remove).grid(column=3,row=7)
      tk.Button(self.master,text="About",underline=1,command=self.func_about).grid(column=0,row=8)
      self.master.bind("<Alt-l>",self.func_login)
      self.master.bind("<Alt-a>",self.func_add)
      self.master.bind("<Alt-e>",self.func_edit)
      self.master.bind("<Alt-r>",self.func_remove)
      self.master.bind("<Alt-b>",self.func_about)
      # initial load
      self.refresh_form()
      self.listbox.select_set(0)
   # functions
   def func_about(self, o1 = None):
      """ Display about dialog. """
      tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()
   def func_login(self, o1 = None):
      """ Get currently-selected entry in listbox, and enter that info to autologin-lego-universe.sh """
      print(f"DEBUG: login stub, got o1 {o1}")
      entry = self.get_selected_user()
      command = f""". /mnt/public/Games/lego-universe/autologin-lego-universe.sh ; USERNAME="{entry['username']}" PASSWORD="{entry['password']}" autologin_lego_universe"""
      print(f"Running command {command}")
      self.master.wm_state("iconic")
      os.system(command)
      sys.exit(0)
   def func_add(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_add, got o1 {o1}")
      # try closing any open add/edit window
      self.close_addwindow()
      self.addedit_username.set("")
      self.addedit_password.set("")
      self.addwindow = AddEditWindow("Add entry",self.addedit_username, self.addedit_password, self.update_entry, self.close_addwindow, False)
   def func_edit(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_edit, got o1 {o1}")
      print(f"DEBUG: need to get currently selected entry")
      # try closing any open add/edit window
      self.close_addwindow()
      entry = self.get_selected_user()
      if entry["username"] is None or entry["username"] == "None":
         return False
      self.addedit_username.set(entry["username"])
      self.addedit_password.set(entry["password"])
      self.addwindow = AddEditWindow("Edit entry",self.addedit_username, self.addedit_password, self.update_entry, self.close_addwindow, True)
   def get_selected_user(self):
      """ Return the dict of the currently-selected user, else return empty dict """
      output = {}
      # a single-selection listbox
      username = None
      password = None
      try:
         username = self.listbox.get(self.listbox.curselection())
         try:
            password = [i["password"] for i in self.entries if i["username"] == username][0]
         except Exception as e:
            print(f"FAILURE when looking for {username} in entries, got {e}")
      except tkinter.TclError as e:
         # no entry selected
         pass
      return {"username":username,"password":password}
   def close_addwindow(self, o1 = None):
      try:
         self.addwindow.destroy()
      except Exception as e:
         ferror(f"DEBUG: when trying to hide add/edit window, got {e}")
   def func_remove(self, o1 = None):
      """ Ask for a new login to save to list. """
      print(f"DEBUG: stub func_remove, got o1 {o1}")
      entry = self.get_selected_user()
      print(f"DEBUG: got entry {entry}")
      try:
         u = entry["username"]
         if u:
            try:
               self.entries.remove(entry)
            except ValueError as e:
               # the dict item was not in the list
               ferror(f"Probably just not in the list, but got error {e}. This is probably not a big deal.")
               pass
      except Exception as e:
         ferror(f"When trying to remove, got error {e}")
      self.save_config()
   def refresh_form(self):
      """ Load all data """
      self.entries = self.load_config()
      self.listbox.delete(0,tk.END)
      x = 0
      print(f"DEBUG: refreshing form, from entries {self.entries}")
      for e in self.entries:
         print(f"Looping over {e}")
         self.listbox.insert(x, e["username"])
         x = x + 1
   def load_config(self):
      """ Read ~/.config/autologin-lego-universe-menu.conf and get all username/pw listed there """
      print(f"DEBUG: loading config")
      try:
         with open(CONF_FILE,"r") as o:
            entries = json.load(o)
      except FileNotFoundError:
         print(f"DEBUG: need to generate new config file")
         with open(CONF_FILE,"a") as o:
            o.write("[]")
         entries = []
      #print(f"DEBUG, got entries in {type(entries)} below:")
      #print(entries)
      return entries
   def update_entry(self, o1 = None):
      """ Take add/edit window information and find the corresponding entry and update it, or else add a new one. """
      #print(f"DEBUG: will look for entry {self.addedit_username.get()}, and assign pw {self.addedit_password.get()}")
      new_u = self.addedit_username.get()
      new_p = self.addedit_password.get()
      updated = False
      for e in self.entries:
         if e["username"] == new_u:
            print(f"DEBUG: found entry {e}, updating pw for it.")
            e["password"] = new_p
            updated = True
            break
      if not updated:
         print(f"DEBUG: adding new entry, u={new_u},p={new_p}")
         self.entries.append(
            {
               "username": new_u,
               "password": new_p,
            }
         )
      self.save_config()
   def save_config(self, o1 = None):
      """ Save config, including the latest entry from add/edit window """
      self.close_addwindow()
      print(f"DEBUG: please save entries. Currently:")
      print(self.entries)
      #print(f"And also {self.addedit_username.get()}, {self.addedit_password.get()}")
      with open(CONF_FILE,"w") as o:
         o.write(json.dumps(self.entries))
      self.refresh_form()
# main
if "__main__" == __name__:
   root = tk.Tk()
   # naive, because it combines all monitors into one giant display for calculating.
   # FUTUREIMPROVMENT: get placement working
   root.eval('tk::PlaceWindow . center')
   alu = App(root)
   # Interesting, but not correct. It always chooses monitor 0 but the window would popup on monitor 1 if we do nothing special about geometry
   #print(f"DEBUG: screen width is {root.winfo_screenwidth()}")
   #print(f"DEBUG: screen height is {root.winfo_screenheight()}")
   #place_popup(alu.master,root,300,300)
   alu.mainloop()

The idea with this program is to make it easy to choose between credentials. That's about it.

Screenshot of Menu for Autologin for Lego Universe, which also shows the Add Entry window

Comments