split the interface code into multiple files
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
from helpers import HButtonSelect, spawnDialog, spawnSimplePopup, errorTxt, isValidUsername
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.layout.containers import HSplit
|
||||
from prompt_toolkit.validation import Validator
|
||||
from prompt_toolkit.widgets import Box, Button, Dialog, Label, TextArea, ValidationToolbar
|
||||
|
||||
|
||||
class AccountSettings:
|
||||
|
||||
def __init__(self,name,guestLogin,server,floats):
|
||||
self.name = name
|
||||
self.server = server
|
||||
self.floats = floats
|
||||
acc = HButtonSelect([("Log out",self.logout),("Change username",self.changename),("Change password",self.changepass)])
|
||||
if guestLogin:
|
||||
acc = HSplit([
|
||||
Box(HButtonSelect([("Log out",self.logout)]),padding=0),
|
||||
Label("You are currently logged in as a guest."),
|
||||
])
|
||||
self.el = acc
|
||||
|
||||
def logout(self):
|
||||
get_app().exit(result="login")
|
||||
|
||||
def changename(self):
|
||||
usernameValidator = Validator.from_callable(isValidUsername, error_message='Invalid username')
|
||||
|
||||
def nameaccept(buf: Buffer):
|
||||
# get_app().layout.focus(okbtn)
|
||||
dlgclose(True)
|
||||
return True
|
||||
|
||||
nntxtfield = TextArea(
|
||||
text=self.name,
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=usernameValidator,
|
||||
accept_handler=nameaccept,
|
||||
)
|
||||
|
||||
okbtn = Button("OK",handler=lambda: dlgclose(True))
|
||||
cancelbtn = Button("Cancel",handler=lambda: dlgclose(False))
|
||||
|
||||
async def _changename():
|
||||
res = await self.server.changename(nntxtfield.text)
|
||||
if res["type"]=="error":
|
||||
await spawnSimplePopup("Error",errorTxt(res),self.floats)
|
||||
else:
|
||||
get_app().exit(result="restart")
|
||||
|
||||
def dlgclose(res):
|
||||
despawn()
|
||||
if not res: return
|
||||
get_app().create_background_task(_changename())
|
||||
|
||||
dlg = Dialog(
|
||||
title="Change name",
|
||||
body=HSplit([
|
||||
Label(text="Insert your account's new name here:", dont_extend_height=True),
|
||||
nntxtfield,
|
||||
Label(text="Keep in mind that you can only change your name every 7 days.", dont_extend_height=True),
|
||||
ValidationToolbar(),
|
||||
]),
|
||||
buttons=[okbtn, cancelbtn]
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
|
||||
def changepass(self):
|
||||
def oldaccept(buf: Buffer):
|
||||
get_app().layout.focus(newtxtfield)
|
||||
return True
|
||||
|
||||
def newaccept(buf: Buffer):
|
||||
# get_app().layout.focus(okbtn)
|
||||
dlgclose(True)
|
||||
return True
|
||||
|
||||
oldtxtfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=True,
|
||||
completer=None,
|
||||
validator=None,
|
||||
accept_handler=oldaccept,
|
||||
)
|
||||
|
||||
newtxtfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=True,
|
||||
completer=None,
|
||||
validator=None,
|
||||
accept_handler=newaccept,
|
||||
)
|
||||
|
||||
okbtn = Button("OK",handler=lambda: dlgclose(True))
|
||||
cancelbtn = Button("Cancel",handler=lambda: dlgclose(False))
|
||||
|
||||
async def _changepass():
|
||||
res = await self.server.changepass(oldtxtfield.text,newtxtfield.text)
|
||||
if res["type"]=="error":
|
||||
await spawnSimplePopup("Error",errorTxt(res),self.floats)
|
||||
|
||||
def dlgclose(res):
|
||||
despawn()
|
||||
if not res: return
|
||||
get_app().create_background_task(_changepass())
|
||||
|
||||
dlg = Dialog(
|
||||
title="Change password",
|
||||
body=HSplit([
|
||||
Label(text="Old password:", dont_extend_height=True),
|
||||
oldtxtfield,
|
||||
Label(text="New password:", dont_extend_height=True),
|
||||
newtxtfield,
|
||||
ValidationToolbar(),
|
||||
]),
|
||||
buttons=[okbtn, cancelbtn]
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
@@ -0,0 +1,165 @@
|
||||
import humanize, datetime
|
||||
from helpers import spawnDialog, spawnSimplePopup, spawnYesNoPopup, HButtonSelect, errorTxt, isValidUsername
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.layout import FormattedTextControl
|
||||
from prompt_toolkit.layout.containers import HSplit, Window
|
||||
from prompt_toolkit.layout.dimension import Dimension as D
|
||||
from prompt_toolkit.validation import Validator
|
||||
from prompt_toolkit.widgets import Button, Dialog, Label, TextArea, RadioList, ValidationToolbar
|
||||
|
||||
class AdminPanel:
|
||||
|
||||
def __init__(self,floats,server):
|
||||
self.floats = floats
|
||||
self.server = server
|
||||
|
||||
self.el = HButtonSelect([("Ban user",self.banuser),("Unban user",self.unbanuser)])
|
||||
|
||||
async def banuserconfirm(self,name,duration,motive):
|
||||
res = await self.server.getuserinfo(name)
|
||||
if res["type"]=="error":
|
||||
return await spawnSimplePopup("Error",f"{errorTxt(res)}\nCould not get information about the user.",self.floats)
|
||||
info = res["response"]
|
||||
create_time = datetime.datetime.fromtimestamp(info["created"])
|
||||
motivestr = f"\"{motive}\"" if len(motive)>0 else "N/A"
|
||||
conf = await spawnYesNoPopup("Confirm Ban?",Window(FormattedTextControl([
|
||||
("","You are about to ban "),
|
||||
("bold",info["name"]),
|
||||
(""," for "),
|
||||
("bold",({0: "an indefinite amount of time",24*60*60: "24 hours",3*24*60*60: "3 days",7*24*60*60: "7 days"})[duration]),
|
||||
("",".\n\nMotive: "),("bold",motivestr),("","\n"),
|
||||
("","\nUser information:\n"),
|
||||
("","- Internal ID: "),("bold",str(info["id"])),("","\n"),
|
||||
("","- Created: "),("bold",humanize.naturaldate(create_time)),("",f" ({create_time})\n")
|
||||
],focusable=False),dont_extend_width=True,dont_extend_height=True),self.floats)
|
||||
if conf:
|
||||
res = await self.server.banuser(info["id"],duration,motive)
|
||||
if res["type"]=="error":
|
||||
await spawnSimplePopup("Error",f"{errorTxt(res)}\nCould not ban this user.",self.floats)
|
||||
|
||||
def banuser(self):
|
||||
def dlgclose():
|
||||
despawn()
|
||||
get_app().create_background_task(self.banuserconfirm(usertextfield.text,timeselect.current_value,motivetextfield.text))
|
||||
|
||||
def nameaccept(buf: Buffer):
|
||||
get_app().layout.focus(motivetextfield)
|
||||
return True
|
||||
|
||||
def motiveaccept(buf: Buffer):
|
||||
get_app().layout.focus(timeselect)
|
||||
return True
|
||||
|
||||
usernameValidator = Validator.from_callable(isValidUsername, error_message='Invalid username')
|
||||
|
||||
ok_button = Button(text="OK", handler=dlgclose, width=16)
|
||||
cancel_button = Button(text="Cancel", handler=lambda: despawn(), width=16)
|
||||
|
||||
usertextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=usernameValidator,
|
||||
accept_handler=nameaccept,
|
||||
)
|
||||
|
||||
motivetextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=None,
|
||||
accept_handler=motiveaccept,
|
||||
)
|
||||
|
||||
timeselect = RadioList([(24*60*60,"24 hours"),(3*24*60*60,"3 days"),(7*24*60*60,"7 days"),(0,"Permanent")],default=24*60*60)
|
||||
|
||||
dlg = Dialog(
|
||||
title="Ban user",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Username:", dont_extend_height=True),
|
||||
usertextfield,
|
||||
Label(text="Motive:", dont_extend_height=True),
|
||||
motivetextfield,
|
||||
Label(text="Time:", dont_extend_height=True),
|
||||
timeselect,
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
width=70
|
||||
),
|
||||
buttons=[ok_button, cancel_button],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
|
||||
async def unbanuserconfirm(self,name):
|
||||
res = await self.server.getuserinfo(name)
|
||||
if res["type"]=="error":
|
||||
return await spawnSimplePopup("Error",f"{errorTxt(res)}\nCould not get information about the user.",self.floats)
|
||||
info = res["response"]
|
||||
if info["banned"]==False:
|
||||
return await spawnSimplePopup("Error",f"The user {name} is not banned.",self.floats)
|
||||
create_time = datetime.datetime.fromtimestamp(info["created"])
|
||||
ban_date = datetime.datetime.fromtimestamp(info["ban_details"]["ban_date"])
|
||||
expire_date = datetime.datetime.fromtimestamp(info["ban_details"]["expire_date"]) if info["ban_details"]["expire_date"]>0 else "Indefinite"
|
||||
motive = f"\"{info["ban_details"]["motive"]}\"" if ("motive" in info["ban_details"]) and (len(info["ban_details"]["motive"])>0) else "N/A"
|
||||
conf = await spawnYesNoPopup("Confirm Ban?",Window(FormattedTextControl([
|
||||
("","You are about to unban "),
|
||||
("bold",info["name"]),
|
||||
("",".\n\nUser information:\n"),
|
||||
("","- Internal ID: "),("bold",str(info["id"])),("","\n"),
|
||||
("","- Created: "),("bold",humanize.naturaldate(create_time)),("",f" ({create_time})\n"),
|
||||
("","\nBan information:\n"),
|
||||
("","- Motive: "),("bold",motive),("","\n"),
|
||||
("","- Banned on: "),("bold",humanize.naturaldate(ban_date)),("",f" ({ban_date})\n"),
|
||||
("","- Ban expires on: "),("bold",humanize.naturaldate(expire_date)),("",f" ({expire_date})\n"),
|
||||
],focusable=False),dont_extend_width=True,dont_extend_height=True),self.floats)
|
||||
if conf:
|
||||
res = await self.server.unbanuser(info["id"])
|
||||
if res["type"]=="error":
|
||||
await spawnSimplePopup("Error",f"{errorTxt(res)}\nCould not unban this user.",self.floats)
|
||||
|
||||
def unbanuser(self):
|
||||
def dlgclose():
|
||||
despawn()
|
||||
get_app().create_background_task(self.unbanuserconfirm(usertextfield.text))
|
||||
|
||||
def nameaccept(buf: Buffer):
|
||||
dlgclose()
|
||||
return True
|
||||
|
||||
usernameValidator = Validator.from_callable(isValidUsername, error_message='Invalid username')
|
||||
|
||||
ok_button = Button(text="OK", handler=dlgclose, width=16)
|
||||
cancel_button = Button(text="Cancel", handler=lambda: despawn(), width=16)
|
||||
|
||||
usertextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=usernameValidator,
|
||||
accept_handler=nameaccept,
|
||||
)
|
||||
|
||||
dlg = Dialog(
|
||||
title="Unban user",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Username:"),
|
||||
usertextfield,
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button, cancel_button],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
@@ -0,0 +1,353 @@
|
||||
# Helper functions and polyfills
|
||||
|
||||
import asyncio, re, sys, io
|
||||
from typing import TextIO
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.output import ColorDepth, create_output
|
||||
from prompt_toolkit.layout import is_container, FormattedTextControl, Float
|
||||
from prompt_toolkit.layout.layout import Layout
|
||||
from prompt_toolkit.layout.containers import HSplit, VSplit, ConditionalContainer, Window
|
||||
from prompt_toolkit.widgets import Button, Dialog, Label
|
||||
from prompt_toolkit.key_binding import KeyBindings, merge_key_bindings
|
||||
from prompt_toolkit.key_binding.defaults import load_key_bindings
|
||||
from prompt_toolkit.filters import Condition, has_focus, is_true
|
||||
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
|
||||
from prompt_toolkit.data_structures import Point
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Interface
|
||||
# ------------------------------------------------------------
|
||||
|
||||
class EncodingProxy(io.TextIOBase):
|
||||
def __init__(self, raw, encoding: str):
|
||||
self._raw = raw
|
||||
self._encoding = encoding
|
||||
# self.buffer = raw.buffer
|
||||
self.pipeReplace = False
|
||||
for ch in ["┌","┐","┘","└","─","│"]:
|
||||
if ch.encode(encoding,errors="replace").decode(encoding)!=ch:
|
||||
self.pipeReplace = True
|
||||
break
|
||||
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
return self._encoding
|
||||
|
||||
def write(self, s: str) -> int:
|
||||
if self.encoding=="ascii" or self.pipeReplace:
|
||||
for corner in ["┌","┐","┘","└"]:
|
||||
s=s.replace(corner,"+")
|
||||
s=s.replace("─","-")
|
||||
s=s.replace("│","|")
|
||||
pass
|
||||
|
||||
byts = s.encode(self._encoding,errors="replace")
|
||||
# print(s.encode("utf8"),byts)
|
||||
self._raw.buffer.write(bytes(byts))
|
||||
self._raw.flush()
|
||||
return len(s)
|
||||
|
||||
def flush(self) -> None:
|
||||
self._raw.flush()
|
||||
|
||||
def fileno(self):
|
||||
return self._raw.fileno()
|
||||
|
||||
def isatty(self):
|
||||
return True
|
||||
|
||||
def createEncodedOutput(enc):
|
||||
proxy: TextIO = EncodingProxy(sys.stdout, encoding=enc)
|
||||
return create_output(proxy)
|
||||
|
||||
def getColorDepth(intset):
|
||||
try:
|
||||
return ColorDepth(f"DEPTH_{intset["color"]}_BIT")
|
||||
except ValueError:
|
||||
return ColorDepth.DEPTH_4_BIT
|
||||
|
||||
def defaultAppSettings(intset):
|
||||
return {"full_screen":True, "color_depth":getColorDepth(intset), "output": createEncodedOutput(intset["encoding"])}
|
||||
|
||||
def containIfNeeded(el):
|
||||
if not is_container(el):
|
||||
return Window(el)
|
||||
return el
|
||||
|
||||
def message_dialog( # derived from prompt_toolkit's code
|
||||
intset,
|
||||
title = "",
|
||||
text = "",
|
||||
ok_text: str = "Ok",
|
||||
style = None,
|
||||
) -> Application[None]:
|
||||
"""
|
||||
Display a simple message box and wait until the user presses enter.
|
||||
"""
|
||||
def _return_none() -> None:
|
||||
"Button handler that returns None."
|
||||
get_app().exit()
|
||||
|
||||
dialog = Dialog(
|
||||
title=title,
|
||||
body=Label(text=text, dont_extend_height=True),
|
||||
buttons=[Button(text=ok_text, handler=_return_none)],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
# Key bindings.
|
||||
bindings = KeyBindings()
|
||||
bindings.add("tab")(focus_next)
|
||||
bindings.add("s-tab")(focus_previous)
|
||||
|
||||
return Application(
|
||||
layout=Layout(dialog),
|
||||
key_bindings=merge_key_bindings([load_key_bindings(), bindings]),
|
||||
style=style,
|
||||
**defaultAppSettings(intset)
|
||||
)
|
||||
|
||||
class TabLayout():
|
||||
def __init__(self,kb,tabs,tabFocus,tabNames,name):
|
||||
self.tabs = tabs
|
||||
self.tabFocus = tabFocus
|
||||
self.tabNames = tabNames
|
||||
|
||||
self.current = 0
|
||||
self.control = False
|
||||
self.tabEls = [ConditionalContainer(content=tabs[i],filter=Condition(lambda i=i: self.current==i)) for i in range(len(tabs))]
|
||||
|
||||
# tab bar
|
||||
self.tbarEls = [ConditionalContainer(Label("StingNET [WIP]"),Condition(lambda: get_app().output.get_size().columns >= 90)),ConditionalContainer(content=Label("[Ctrl-Up] ",dont_extend_width=True,style="fg:#bbbbbb"),filter=Condition(lambda: not self.control)),ConditionalContainer(content=Label("[Left/Right/Enter] ",dont_extend_width=True),filter=Condition(lambda: self.control))]
|
||||
self.tbarBtns = []
|
||||
for i in range(len(tabNames)):
|
||||
def handle(i=i):
|
||||
self.current=i
|
||||
self.control=False
|
||||
self.focusTabWindow()
|
||||
btn = Button(text=tabNames[i],handler=handle,width=16)
|
||||
self.tbarEls.append(btn)
|
||||
self.tbarBtns.append(btn)
|
||||
self.tbarEls=self.tbarEls+[Label(f" {name}",dont_extend_width=True,style="fg:#bbbbbb")]
|
||||
|
||||
self.tbar = VSplit(self.tbarEls,style="bg:#404040")
|
||||
self.el = HSplit([self.tbar,VSplit(self.tabEls)])
|
||||
|
||||
# controls
|
||||
@kb.add("c-up")
|
||||
@kb.add("c-j")
|
||||
def _(e):
|
||||
self.control = True
|
||||
self.focusCurrent(e)
|
||||
|
||||
@kb.add("left",filter=Condition(lambda: self.control))
|
||||
def _(e):
|
||||
self.current = (self.current+len(tabNames)-1)%len(tabNames)
|
||||
self.focusCurrent(e)
|
||||
|
||||
@kb.add("right",filter=Condition(lambda: self.control))
|
||||
def _(e):
|
||||
self.current = (self.current+1)%len(tabNames)
|
||||
self.focusCurrent(e)
|
||||
|
||||
def focusTabWindow(self):
|
||||
try:
|
||||
el = self.tabs[self.current]
|
||||
if self.tabFocus[self.current]:
|
||||
el = self.tabFocus[self.current]
|
||||
get_app().layout.focus(el)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def focusCurrent(self,e=None):
|
||||
app = e.app if e else get_app()
|
||||
app.layout.focus(self.tbarBtns[self.current])
|
||||
|
||||
class HSelectLayout():
|
||||
def __init__(self,kb,opts,optFocus,optNames):
|
||||
self.opts=opts
|
||||
self.optFocus=optFocus
|
||||
self.optNames=optNames
|
||||
|
||||
self.current = 0
|
||||
self.select = Window(FormattedTextControl(text=self.show_sel,get_cursor_position=self.curpos,key_bindings=self.make_select_kb()),width=16,dont_extend_height=False,style="bg:#353535")
|
||||
|
||||
self.els = [self.select]+[ConditionalContainer(content=containIfNeeded(opts[i]),filter=Condition(lambda i=i: self.current==i)) for i in range(len(opts))]
|
||||
self.el = VSplit(self.els)
|
||||
|
||||
self.returnFilter = has_focus(self.el) & ~has_focus(self.select)
|
||||
@kb.add("c-left",filter=self.returnFilter)
|
||||
@kb.add("c-h",filter=self.returnFilter)
|
||||
@kb.add("escape",filter=self.returnFilter)
|
||||
def _(e):
|
||||
e.app.layout.focus(self.select)
|
||||
|
||||
def make_select_kb(self):
|
||||
kb = KeyBindings()
|
||||
|
||||
l = len(self.optNames)
|
||||
|
||||
@kb.add("up")
|
||||
def _(e):
|
||||
self.current = (self.current+l-1)%l
|
||||
|
||||
@kb.add("down")
|
||||
def _(e):
|
||||
self.current = (self.current+1)%l
|
||||
|
||||
@kb.add("enter")
|
||||
@kb.add("c-right")
|
||||
@kb.add("c-l")
|
||||
@kb.add(" ")
|
||||
def _(e):
|
||||
self.focusTabWindow()
|
||||
|
||||
return kb
|
||||
|
||||
def show_sel(self):
|
||||
o = []
|
||||
size = get_app().output.get_size()
|
||||
height = size.rows
|
||||
for i in range(len(self.opts)):
|
||||
name = self.optNames[i]
|
||||
o.append(("reverse",name) if i==self.current else ("",name))
|
||||
o.append((""," \n"))
|
||||
for i in range(height-len(self.opts)-2):
|
||||
o.append(("","\n"))
|
||||
if is_true(self.returnFilter):
|
||||
o.append(("fg:#bbbbbb","[Ctrl-Left/ESC]"))
|
||||
return o
|
||||
|
||||
def curpos(self):
|
||||
return Point(x=len(self.optNames[self.current]),y=self.current)
|
||||
|
||||
def focusTabWindow(self):
|
||||
if self.optFocus[self.current]:
|
||||
try:
|
||||
el = self.optFocus[self.current]
|
||||
get_app().layout.focus(el)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
el = self.opts[self.current]
|
||||
get_app().layout.focus(el)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
el = self.els[self.current+1]
|
||||
get_app().layout.focus(el)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
quitKB = KeyBindings()
|
||||
|
||||
@quitKB.add("c-q",eager=True)
|
||||
def exit_(event):
|
||||
event.app.exit()
|
||||
|
||||
def spawnDialog(dlg,floats):
|
||||
app = get_app()
|
||||
|
||||
f = Float(content=dlg)
|
||||
floats.append(f)
|
||||
prev = app.layout.current_window
|
||||
app.layout.focus(dlg)
|
||||
app.invalidate()
|
||||
|
||||
def despawn():
|
||||
floats.remove(f)
|
||||
app.layout.focus(prev)
|
||||
app.invalidate()
|
||||
|
||||
return despawn
|
||||
|
||||
async def spawnSimplePopup(title,body,floats):
|
||||
title = str(title)
|
||||
if type(body)==str:
|
||||
body=Label(body)
|
||||
|
||||
res = asyncio.get_running_loop().create_future()
|
||||
|
||||
def handler():
|
||||
if not res.done():
|
||||
res.set_result(True)
|
||||
despawn()
|
||||
|
||||
dlg = Dialog(
|
||||
title=title,
|
||||
body=body,
|
||||
buttons=[
|
||||
Button("OK",handler=handler)
|
||||
]
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,floats)
|
||||
|
||||
return await res
|
||||
|
||||
async def spawnYesNoPopup(title,body,floats):
|
||||
title = str(title)
|
||||
if type(body)==str:
|
||||
body=Label(body)
|
||||
|
||||
res = asyncio.get_running_loop().create_future()
|
||||
|
||||
def handler(r):
|
||||
if not res.done():
|
||||
res.set_result(r)
|
||||
despawn()
|
||||
|
||||
dlg = Dialog(
|
||||
title=title,
|
||||
body=body,
|
||||
buttons=[
|
||||
Button("Yes",handler=lambda: handler(True)),
|
||||
Button("No",handler=lambda: handler(False))
|
||||
]
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,floats)
|
||||
|
||||
return await res
|
||||
|
||||
def HButtonSelect(buttonArgs):
|
||||
kb = KeyBindings()
|
||||
|
||||
# layout = get_app().layout
|
||||
|
||||
btns = [Button(e[0],e[1],width=48) for e in buttonArgs]
|
||||
|
||||
first_selected = has_focus(btns[0])
|
||||
last_selected = has_focus(btns[-1])
|
||||
|
||||
@kb.add("up",filter=~first_selected)
|
||||
def _(e):
|
||||
focus_previous(e)
|
||||
|
||||
@kb.add("down",filter=~last_selected)
|
||||
def _(e):
|
||||
focus_next(e)
|
||||
|
||||
return HSplit(btns,key_bindings=kb)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Server/Other
|
||||
# ------------------------------------------------------------
|
||||
|
||||
def errorTxt(obj):
|
||||
if obj["server"]:
|
||||
return f"Server error: {obj["error"]}"
|
||||
return f"Client error: {obj["error"]}"
|
||||
|
||||
def isValidUsername(name):
|
||||
if len(name)<3 or len(name)>16: return False
|
||||
pattern = re.compile("^[a-z][a-z0-9-_]*")
|
||||
match = pattern.match(name)
|
||||
if match==None: return False
|
||||
return match.start()==0 and match.end()==len(name)
|
||||
@@ -0,0 +1,141 @@
|
||||
import encodings.aliases
|
||||
from helpers import HButtonSelect, spawnDialog, spawnSimplePopup, errorTxt
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.layout import FormattedTextControl
|
||||
from prompt_toolkit.layout.containers import HSplit, Window
|
||||
from prompt_toolkit.layout.dimension import Dimension as D
|
||||
from prompt_toolkit.widgets import Box, Button, Dialog, Label, RadioList, ValidationToolbar
|
||||
|
||||
class InterfaceSettings:
|
||||
|
||||
def __init__(self,intset,guestLogin,restart,server,floats):
|
||||
self.intset = intset
|
||||
self.guestLogin = guestLogin
|
||||
self.server = server
|
||||
self.floats = floats
|
||||
self.el = HSplit([
|
||||
Window(FormattedTextControl(text=self.showISettings,focusable=False),dont_extend_height=True),
|
||||
Box(HButtonSelect([("Change color depth",self.changecolor),("Change text encoding",self.changeenc),("Apply changes",restart)]),padding=0),
|
||||
Label("\n\nYou will need to restart the interface or relog to apply any new changes."),
|
||||
])
|
||||
|
||||
def showISettings(self):
|
||||
color = ({1: "1 bit (Monochrome)",4: "4 bits (ANSI/RGBI)",8: "8 bits",24: "24 bits (True Color)"})[self.intset["color"]]
|
||||
encoding = self.intset["encoding"]
|
||||
if encoding=="utf8": encoding="UTF-8 (Unicode)"
|
||||
if encoding=="ascii": encoding="ASCII (7-bit)"
|
||||
if encoding=="cp437": encoding="CP437 (Extended ASCII)"
|
||||
if encoding=="latin_1": encoding="Latin-1/ISO-8859-1 (Ext. ASCII)"
|
||||
|
||||
return [
|
||||
("","Color depth: "),("bold",color),("","\n"),
|
||||
("","Text encoding: "),("bold",encoding),("","\n")
|
||||
]
|
||||
|
||||
async def _updateisettings(self):
|
||||
if self.guestLogin: return
|
||||
res = await self.server.setisettings("basic",self.intset)
|
||||
if res["type"]=="error":
|
||||
if res["server"]:
|
||||
await spawnSimplePopup("Error",f"{errorTxt(res)}\nYour changes might not have been sent to the internal server. Please try again.",self.floats)
|
||||
else:
|
||||
await spawnSimplePopup("Error",errorTxt(res),self.floats)
|
||||
|
||||
def changecolor(self):
|
||||
def dlgclose():
|
||||
despawn()
|
||||
self.intset["color"] = colorselect.current_value
|
||||
get_app().create_background_task(self._updateisettings())
|
||||
|
||||
ok_button = Button(text="OK", handler=dlgclose, width=16)
|
||||
|
||||
colorselect = RadioList([(24,"24 bits (True Color)"),(8,"8 bits (Paletted)"),(4,"4 bits (ANSI/RGBI)"),(1,"1 bit (Monochrome)")],default=self.intset["color"])
|
||||
|
||||
dlg = Dialog(
|
||||
title="Change color depth",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Please select a color depth supported by this terminal:", dont_extend_height=True),
|
||||
colorselect,
|
||||
Label(text="Use 1 or 4 bits for compatibility with older terminals, otherwise 8 or 24 bits.", dont_extend_height=True),
|
||||
Label(text="The more bits in the color depth, the more colors there can be displayed on-screen."),
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
|
||||
def changeencother(self):
|
||||
def dlgclose():
|
||||
despawn()
|
||||
self.intset["encoding"] = encselect.current_value
|
||||
get_app().create_background_task(self._updateisettings())
|
||||
|
||||
ok_button = Button(text="OK", handler=dlgclose, width=16)
|
||||
|
||||
encodingList = sorted(set(encodings.aliases.aliases.values()))
|
||||
for e in encodingList.copy():
|
||||
try:
|
||||
("test").encode(e,errors="replace")
|
||||
except LookupError:
|
||||
encodingList.remove(e)
|
||||
encselect = RadioList([(e,e) for e in encodingList],default=self.intset["encoding"])
|
||||
|
||||
dlg = Dialog(
|
||||
title="Change text encoding",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Please select the encoding supported by this terminal:", dont_extend_height=True),
|
||||
encselect,
|
||||
Label(text="For more information about every supported encoding, open this webpage:\nhttps://docs.python.org/3.14/library/codecs.html#standard-encodings",dont_extend_height=True),
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
|
||||
def changeenc(self):
|
||||
def dlgclose():
|
||||
despawn()
|
||||
v = encselect.current_value
|
||||
if v=="other":
|
||||
self.changeencother()
|
||||
else:
|
||||
self.intset["encoding"] = v
|
||||
get_app().create_background_task(self._updateisettings())
|
||||
|
||||
ok_button = Button(text="OK", handler=dlgclose, width=16)
|
||||
|
||||
default = self.intset["encoding"]
|
||||
if not default in ["utf8","ascii","latin_1","cp437"]:
|
||||
default = "other"
|
||||
|
||||
encselect = RadioList([("utf8","UTF-8 (Unicode)"),("ascii","ASCII (7-bit)"),("latin_1","Latin-1/ISO-8859-1 (Ext. ASCII)"),("cp437","CP437 (Extended ASCII)"),("other","Other (Python-supported)")],default=default)
|
||||
|
||||
dlg = Dialog(
|
||||
title="Change text encoding",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Please select the encoding supported by this terminal:", dont_extend_height=True),
|
||||
encselect,
|
||||
Label(text="UTF-8 is generally reccomended for a normal modern terminal, and supports everything from Unicode.",dont_extend_height=True),
|
||||
Label(text="ASCII is very barebones but is supported by every terminal. Please use it if all else fails.", dont_extend_height=True),
|
||||
Label(text="You may see garbled text if you chose the wrong text encoding.",dont_extend_height=True),
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button],
|
||||
with_background=True,
|
||||
)
|
||||
|
||||
despawn = spawnDialog(dlg,self.floats)
|
||||
@@ -0,0 +1,256 @@
|
||||
# Code for the login screen and the account creation screen.
|
||||
|
||||
from helpers import defaultAppSettings, message_dialog, quitKB, errorTxt, isValidUsername
|
||||
|
||||
from prompt_toolkit import Application
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.validation import Validator
|
||||
from prompt_toolkit.widgets import Button, Dialog, Label, ValidationToolbar, RadioList, TextArea
|
||||
from prompt_toolkit.layout.containers import HSplit
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.layout.layout import Layout
|
||||
from prompt_toolkit.layout.dimension import Dimension as D
|
||||
|
||||
|
||||
async def firstSetupScreen(intset,server):
|
||||
def ok_handler() -> None:
|
||||
get_app().exit()
|
||||
|
||||
ok_button = Button(text="OK", handler=ok_handler, width=16)
|
||||
|
||||
colorselect = RadioList([(24,"24 bits (True Color)"),(8,"8 bits (Paletted)"),(4,"4 bits (ANSI/RGBI)"),(1,"1 bit (Monochrome)")],default=8)
|
||||
|
||||
encselect = RadioList([("utf8","UTF-8 (Unicode)"),("ascii","ASCII (7-bit)"),("latin_1","ISO-8859-1 (Ext. ASCII)"),("cp437","CP437 (Extended ASCII)")],default="utf8")
|
||||
|
||||
cdialog = Dialog(
|
||||
title="First Setup",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Please select a color depth supported by this terminal:", dont_extend_height=True),
|
||||
colorselect,
|
||||
Label(text="Use 1 or 4 bits for compatibility with older terminals, otherwise 8 or 24 bits.", dont_extend_height=True),
|
||||
Label(text="You can always change this later in the interface settings.",dont_extend_height=True),
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button],
|
||||
with_background=True,
|
||||
)
|
||||
capp = Application(layout=Layout(cdialog), **defaultAppSettings(intset))
|
||||
await capp.run_async()
|
||||
|
||||
edialog = Dialog(
|
||||
title="First Setup",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Please insert the text encoding supported by this terminal:", dont_extend_height=True),
|
||||
encselect,
|
||||
Label(text="Use ASCII for compatibility with older terminals, otherwise UTF-8.", dont_extend_height=True),
|
||||
Label(text="You can always change this later in the interface settings.",dont_extend_height=True),
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[ok_button],
|
||||
with_background=True,
|
||||
)
|
||||
eapp = Application(layout=Layout(edialog), **defaultAppSettings(intset))
|
||||
await eapp.run_async()
|
||||
|
||||
return {"color":colorselect.current_value,"encoding":encselect.current_value}
|
||||
|
||||
async def createAccountScreen(intset,server):
|
||||
def nameaccept(buf: Buffer) -> bool:
|
||||
get_app().layout.focus(passtextfield)
|
||||
return True # Keep text.
|
||||
|
||||
def pwrdaccept(buf: Buffer) -> bool:
|
||||
# get_app().layout.focus(login_button)
|
||||
get_app().layout.focus(passtextconffield)
|
||||
return True # Keep text.
|
||||
|
||||
def pwrdcaccept(buf: Buffer) -> bool:
|
||||
# get_app().layout.focus(login_button)
|
||||
ok_handler()
|
||||
return True # Keep text.
|
||||
|
||||
def _return_none() -> None:
|
||||
"Button handler that returns None."
|
||||
get_app().exit()
|
||||
|
||||
def ok_handler() -> None:
|
||||
get_app().exit(result=(usertextfield.text,passtextfield.text))
|
||||
|
||||
def login_handler() -> None:
|
||||
get_app().exit(result="login")
|
||||
|
||||
create_button = Button(text="Create Account", handler=ok_handler, width=16)
|
||||
login_button = Button(text="Log In", handler=login_handler, width=16)
|
||||
exit_button = Button(text="Exit", handler=_return_none, width=16)
|
||||
|
||||
usernameValidator = Validator.from_callable(isValidUsername, error_message='Invalid username')
|
||||
samePassValidator = Validator.from_callable(lambda x: passtextfield.text==x, error_message='Passwords are not the same')
|
||||
|
||||
usertextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=usernameValidator,
|
||||
accept_handler=nameaccept,
|
||||
)
|
||||
|
||||
passtextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=True,
|
||||
completer=None,
|
||||
validator=None,
|
||||
accept_handler=pwrdaccept,
|
||||
)
|
||||
|
||||
passtextconffield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=True,
|
||||
completer=None,
|
||||
validator=samePassValidator,
|
||||
accept_handler=pwrdcaccept,
|
||||
)
|
||||
|
||||
dialog = Dialog(
|
||||
title="Create a StingNET account",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Username:", dont_extend_height=True),
|
||||
usertextfield,
|
||||
Label(text="Password:", dont_extend_height=True),
|
||||
passtextfield,
|
||||
Label(text="Password (confirm):", dont_extend_height=True),
|
||||
passtextconffield,
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[create_button, login_button, exit_button],
|
||||
with_background=True,
|
||||
)
|
||||
app = Application(key_bindings=quitKB, layout=Layout(dialog), **defaultAppSettings(intset))
|
||||
|
||||
while True:
|
||||
usertextfield.text = ""
|
||||
passtextfield.text = ""
|
||||
passtextconffield.text = ""
|
||||
|
||||
appres = await app.run_async()
|
||||
if type(appres)==tuple:
|
||||
name,pwrd = appres
|
||||
elif type(appres)==str and appres=="login":
|
||||
return "login"
|
||||
else:
|
||||
await server.disconnect()
|
||||
exit()
|
||||
|
||||
res = await server.createuser(name,pwrd)
|
||||
if res["type"]=="response":
|
||||
lres = await server.login(name,pwrd)
|
||||
else:
|
||||
await message_dialog(intset,title="Error (create)",text=f"The account has not been created.\n{errorTxt(res)}").run_async()
|
||||
continue
|
||||
if lres["type"]=="error":
|
||||
await message_dialog(intset,title="Error (login)",text=f"The account has been created, but has not been logged in.\n{errorTxt(lres)}").run_async()
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
async def loginCredsScreen(intset,server):
|
||||
def nameaccept(buf: Buffer) -> bool:
|
||||
get_app().layout.focus(passtextfield)
|
||||
return True # Keep text.
|
||||
|
||||
def pwrdaccept(buf: Buffer) -> bool:
|
||||
# get_app().layout.focus(login_button)
|
||||
ok_handler()
|
||||
return True # Keep text.
|
||||
|
||||
def _return_none() -> None:
|
||||
"Button handler that returns None."
|
||||
get_app().exit()
|
||||
|
||||
def ok_handler() -> None:
|
||||
get_app().exit(result=(usertextfield.text,passtextfield.text))
|
||||
|
||||
def create_handler() -> None:
|
||||
get_app().exit(result="create")
|
||||
|
||||
def guest_handler() -> None:
|
||||
get_app().exit(result="guest")
|
||||
|
||||
login_button = Button(text="Log In", handler=ok_handler, width=16)
|
||||
create_button = Button(text="Create Account", handler=create_handler, width=16)
|
||||
guest_button = Button(text="Use Guest", handler=guest_handler, width=16)
|
||||
exit_button = Button(text="Exit", handler=_return_none, width=16)
|
||||
|
||||
usernameValidator = Validator.from_callable(isValidUsername, error_message='Invalid username')
|
||||
|
||||
usertextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=False,
|
||||
completer=None,
|
||||
validator=usernameValidator,
|
||||
accept_handler=nameaccept,
|
||||
)
|
||||
|
||||
passtextfield = TextArea(
|
||||
text="",
|
||||
multiline=False,
|
||||
password=True,
|
||||
completer=None,
|
||||
validator=None,
|
||||
accept_handler=pwrdaccept,
|
||||
)
|
||||
|
||||
dialog = Dialog(
|
||||
title="Log in to StingNET",
|
||||
body=HSplit(
|
||||
[
|
||||
Label(text="Username:", dont_extend_height=True),
|
||||
usertextfield,
|
||||
Label(text="Password:", dont_extend_height=True),
|
||||
passtextfield,
|
||||
ValidationToolbar(),
|
||||
],
|
||||
padding=D(preferred=1, max=1),
|
||||
),
|
||||
buttons=[login_button, create_button, guest_button, exit_button],
|
||||
with_background=True,
|
||||
)
|
||||
app = Application(key_bindings=quitKB, layout=Layout(dialog), **defaultAppSettings(intset))
|
||||
|
||||
while True:
|
||||
usertextfield.text = ""
|
||||
passtextfield.text = ""
|
||||
|
||||
appres = await app.run_async()
|
||||
if type(appres)==tuple:
|
||||
name,pwrd = appres
|
||||
elif type(appres)==str:
|
||||
return appres
|
||||
else:
|
||||
await server.disconnect()
|
||||
exit()
|
||||
|
||||
res = await server.login(name,pwrd)
|
||||
if res["type"]=="error":
|
||||
await message_dialog(intset,title="Error",text=errorTxt(res)).run_async()
|
||||
else:
|
||||
break
|
||||
|
||||
async def loginScreen(intset,server):
|
||||
while True:
|
||||
res = await loginCredsScreen(intset,server)
|
||||
if res != "create": return res
|
||||
res = await createAccountScreen(intset,server)
|
||||
if res != "login": return res
|
||||
+25
-1012
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user