Textbox not showing inserts in tkinter - python

import tkinter as tk
from tkinter import *
import time
def centertitle(e):
w = int(root.winfo_width() / 3.5)
s = 'Installation Wizard v1.0'.rjust(w//2)
root.title(s)
def textbox():
textbox=Text(root.canvas, width = 62, height = 25, state=DISABLED)
textbox.pack()
textbox.insert(constants.INSERT,'You text goes here')
#Configure tkinter window and title
root = Tk()
root.canvas = Canvas(root, width = 500, height = 404, bg="#3D3D3D", highlightthickness=0)
root.canvas.pack(expand=True, fill=BOTH)
root.iconbitmap(default='transparent.ico')
root.bind("<Configure>", centertitle)
root.resizable(False, False)
#Buttons
btn = tk.Button(root, text='Start Install', height = 2, width = 15)
btn['command'] = lambda b=btn:[b.pack_forget(), b.place_forget(), textbox()]
btn.pack(fill=BOTH)
btn.place(x=190, y=181)
root.mainloop()
What im trying to create, is a simple "install" gui, which i was gonna pack into an exe. all i really want is a button, that starts another python file (the one that installs everything) and when it calls print, i want it sent to the text box in the gui... its hard to explain but i hope you get what i mean
could someone help me im really confused with guis
code from other .py:
import os
import sys
import shutil
import subprocess;
from pathlib import Path
import psutil
import time
def startProgram(program):
SW_HIDE = 0
info = subprocess.STARTUPINFO()
info.dwFlags = subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = SW_HIDE
subprocess.Popen(program, startupinfo=info)
def terminateProgram(processName):
for proc in psutil.process_iter():
if processName.lower() in proc.name().lower():
proc.terminate()
def checkIfProcessRunning(processName):
for proc in psutil.process_iter():
try:
if processName.lower() in proc.name().lower():
return True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
return False;
st_inf = subprocess.STARTUPINFO()
st_inf.dwFlags = st_inf.dwFlags | subprocess.STARTF_USESHOWWINDOW
user_profile = os.environ['USERPROFILE']
appdata_local = os.environ['LOCALAPPDATA']
FolderDictionary = [(user_profile + '\spicetify-cli'), (user_profile + '\.spicetify'), (appdata_local + '\spotify')]
for x in FolderDictionary:
try:
shutil.rmtree(x)
print('"%s", has been deleted.\n' % x)
except OSError as e :
print('"%s", was not found.\n' % x)
print("Installing Spotify.\n")
terminateProgram('Spotify.exe')
startProgram('.\\data\spotify-1-1-62-583.exe')
while True:
if checkIfProcessRunning('spotify-1-1-62-583.exe') == False:
print("Finished Installing Spotify.\n")
terminateProgram('Spotify.exe')
break
print("Installing Spicetify.\n")
terminateProgram('powershell')
subprocess.Popen(["powershell","$v='2.5.0'; Invoke-WebRequest -UseBasicParsing 'https://raw.githubusercontent.com/khanhas/spicetify-cli/master/install.ps1' | Invoke-Expression\nspicetify\nspicetify backup apply enable-devtool"], startupinfo=st_inf)
while True:
if checkIfProcessRunning('powershell') == False:
print("Finished Installing Spicetify.\n")
terminateProgram('Spotify.exe')
break
print("Downloading Themes.\n")
terminateProgram('powershell')
subprocess.Popen(["powershell",'$sp_dir = "${HOME}\spicetify-cli"\n$zip_file = "${sp_dir}\Themes.zip"\n$download_uri = "https://github.com/morpheusthewhite/spicetify-themes/archive/refs/heads/v2.zip"\nInvoke-WebRequest -Uri $download_uri -UseBasicParsing -OutFile $zip_file\nExpand-Archive -Path $zip_file -DestinationPath $sp_dir -Force\nRemove-Item -Path $zip_file\nRemove-Item -LiteralPath "${HOME}\spicetify-cli\Themes" -Force -Recurse\nRename-Item "${HOME}\spicetify-cli\spicetify-themes-2" "${HOME}\spicetify-cli\Themes"\nRemove-Item "${HOME}\spicetify-cli\Themes\*.*" -Force -Recurse | Where { ! $_.PSIsContainer }\nRename-Item "${HOME}\spicetify-cli\Themes\default" "${HOME}\spicetify-cli\Themes\SpicetifyDefault"'], startupinfo=st_inf)
while True:
if checkIfProcessRunning('powershell') == False:
print("Finished Downloading Themes.\n")
break

You've set the state of the text widget to disabled, so you can't insert anything into it. If you want it to be disabled, disable it after inserting the text rather than before.
def textbox():
textbox=Text(root.canvas, width = 62, height = 25)
textbox.pack()
textbox.insert(constants.INSERT,'You text goes here')
textbox.configure(state=DISABLED)

Related

Python Tkinter GUI Input

i'm currently need help of my python system. i made a simple GUI that insist of a simple GUI with buttons that calls out other python scripts and print it to the geometry. however, i have an issue where i could not get the system to print anything out of the GUI or input anything on the tkinter geometry when the python script needed of any input from the User. the GUI fully works when there are no input needed from the users.
from the code below, i shown my full GUI system that calls out the 2 python scripts whenever button is pressed. i would like to make the tkinter geometry to accept inputs directly to the tkinter geometry and send its input to the python script and display its output back again to the tkinter geometry. How do i improve this system or address this problem?
GUI.py
from tkinter import *
from subprocess import *
from threading import *
import time
import subprocess
#text1.insert(END, your stuff here) to put output to GUI
#GUI
root = Tk()
root.title('System 1')
root.geometry('1000x900')
root.configure(background='black')
text1 = Text(root, width= 100, height = 40)
text1.pack()
#command list
def command():
try:
child = Popen(['python', '-u', 'CMDping.py'], stdout=PIPE)
text1.delete("1.0", END)
for line in iter(child.stdout.readline, ''):
text1.insert(INSERT, line)
text1.see(END)
text1.update_idletasks()
child.stdout.close()
child.wait()
except CalledProcessError:
text1.insert(END, "File Not Found!")
def command1():
try:
bttn1.destroy()
pythonfile1 = 'python NetworkCommands.py'
p1 = subprocess.Popen(pythonfile1, shell=True)
out, err = p1.communicate()
except CalledProcessError:
text1.insert(END, "File Not Found!")
#Threading for command Function
def Threading():
t1=Thread(target=command)
t1.start()
root.update()
def Threading1():
t1=Thread(target=command1)
t1.start()
root.update()
#buttons
bttn= Button(root,bg="black",fg="white",highlightcolor="white", text="Diagnose", command=Threading)
bttn.pack(side=LEFT, padx=5,pady=0)
bttn1= Button(root,bg="black",fg="white",highlightcolor="white", text="Flush_DNS", command=Threading1)
bttn1.pack(side=LEFT, padx=5,pady=0)
#Clock
class Clock:
def __init__(self):
self.time1 = ''
self.time2 = time.strftime('%H:%M:%S')
self.mFrame = Frame()
self.mFrame.pack(side=TOP,expand=YES,fill=X)
self.watch = Label(self.mFrame, text=self.time2, font=('times',12,'bold'))
self.watch.pack()
self.changeLabel() #first call it manually
def changeLabel(self):
self.time2 = time.strftime('%H:%M:%S')
self.watch.configure(text=self.time2)
self.mFrame.after(200, self.changeLabel) #it'll call itself continuously
obj1 = Clock()
root.mainloop()
CMDping.py
import subprocess
def ping():
command = input("Enter IP Address to ping: ")
time.sleep(2)
os.system("ping " + command)
ping()

Problem in converting .PY scripts to .exe files

i have got a problem that i have done a small project which can shutdown , restart, hibernate,sleep the pc by buttons using tkinter and OS modules but when trying to convert .py to .exe using pyinstaller it displays the output as completed successfully but when click the executed the executable it says that the Application is non executable.
below I have attached my code .
from tkinter import *
import os
from os import system as sys
from tkinter import messagebox as msg
screen = Tk()
screen.title("System - Properties")
screen.geometry("500x500")
screen.resizable(False,False)
screen.configure(background="powderblue")
screen.iconbitmap("LOGO.ico")
canvas1 = Canvas(screen,height=100,width=480,bg="red",bd=5,relief="groove").place(x = 3,y = 0)
Header = Label(canvas1,text="SYSTEM CONTROL",fg="black",bg="tomato",
font=("Courier New",30,"bold")).place(x = 85,y = 30)
canvas2 = Canvas(screen,height=260,width=470,bg="teal",bd=10,relief="sunken").place(x = 3,y = 120)
# button function starts
def shutdown():
sys("shutdown /s /t 1")
def restart():
sys("shutdown /r /t 1")
def logoff():
sys("shutdown /l /t 1")
def hibernate():
sys("shutdown /h /t 1")
def help():
content = "This app helps to perform some simple actions \nJust click the button according to your use. \n\n\tDO NOT PLAY WITH THIS"
msg.showinfo("HELP",content)
def about():
content= "Author : John Arthur\nDate : 19/07/2021 \nProgramming Language : Python"
msg.showinfo("ABOUT",content)
# button starts
shutdown = Button(canvas2,text="SHUTDOWN",bd = 2,relief="sunken",command=shutdown,bg="black",fg="white",
font=("Bell MT",15,"italic")).place(x = 40,y=175)
restart = Button(canvas2,text="RESTART",bd = 2,relief="sunken",command=restart,bg="black",fg="white",
font=("Bell MT",15,"italic")).place(x = 190,y=175)
logoff = Button(canvas2,text="LOG-OFF",bd = 2,relief="sunken",command=logoff,bg="black",fg="white",
font=("Bell MT",15,"italic")).place(x = 340,y = 175)
hibernate = Button(canvas2,text="HIBERNATE",bd = 2,relief="sunken",command=hibernate,bg="black",fg="white",
font=("Bell MT",15,"italic")).place(x = 40,y=250)
help = Button(canvas2,text="HELP",bd = 2,relief="sunken",command=help,bg="black",fg="white",width=9,
font=("Bell MT",15,"italic")).place(x = 190,y=250)
about = Button(canvas2,text="ABOUT",bd = 2,relief="sunken",command=about,bg="black",fg="white",
font=("Bell MT",15,"italic")).place(x = 340,y=250)
# function button over
canvas3 = Canvas(screen,height=70,width=480,bg="black",bd=5,relief="groove").place(x=3,y=410)
# control button starts
quit = Button(canvas3,text="exit",bd = 2,relief="sunken",command=exit,bg="orange",fg="black",width=15,
font=("Arial",20,"bold")).place(x = 110,y=425)
# control button ends
screen.mainloop()
Can someone help me to solve this problem please .

subprocess not working after onefile pyinstaller even with stdout/stdin defined

I have a tkinter GUI with a text box and run button. Pressing the run button turns it to yellow and starts a subroutine that prints a few numbers. Text output from the subroutine is redirected to the GUI text box. However, after creating a standalone executable file with pyinstaller, it no longer works. Pressing the run button doesn't seem to start the subprocess. It does turn yellow, but no text appears in the text box and it seems to start another instance of the main program - a second GUI appears after about 10 seconds which is how long it takes for the initial GUI to appear. The run button stays yellow on the initial GUI.
I've seen a bit online about other people having issues with subprocesses not running after pyinstaller, but most of the solutions seem to be to make sure stdout, stdin are set to subprocess.PIPE which I have, so I'm at a bit of a loss what to try next.
I'm creating my standalone with this:
pyinstaller --onefile simpleGUI.py
My subprocess file, testsubprocess.py is:
import time
for i in range(3):
print("%d.%d" % divmod(i, 10))
time.sleep(0.5)
My main GUI file, simpleGUI.py, is:
import sys
import subprocess
from threading import Thread
import tkinter as tk
from queue import Queue, Empty
def iter_except(function, exception):
try:
while True:
yield function()
except exception:
return
class DisplaySubprocessOutputDemo:
def __init__(self, root):
self.root = root
width=600
height=350
xloc=0
yloc=10
self.root.geometry('%dx%d+%d+%d' % (width, height, xloc, yloc))
self.statustext = tk.Text(self.root, height=4, width=30)
self.statustext.grid(row=3, column=1)
self.startbutton = tk.Button(self.root, text = 'Start', command=self.startprocess, bg='green', activebackground = 'orange')
self.startbutton.config(height = 2, width = 15)
self.startbutton.grid(row = 5, column=0,sticky='E')
self.startbuttonpresses = 0
exitbutton = tk.Button(self.root, text = 'Exit', command=self.quit, bg='red')
exitbutton.config(height = 2, width = 15)
exitbutton.grid(row = 5, column=4, sticky='E')
def startprocess(self):
self.startbuttonpresses = self.startbuttonpresses+1
if self.startbuttonpresses == 1:
self.startbutton.configure(bg='yellow')
self.startbutton.configure(text='Stop')
self.process = subprocess.Popen([sys.executable, "-u", "testsubprocess.py"], shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)
q = Queue(maxsize=1024)
t = Thread(target=self.reader_thread, args=[q])
t.daemon = True
t.start()
self.updatetext(q)
else:
self.startbuttonpresses = 0
self.process.kill()
self.startbutton.configure(bg='green')
self.startbutton.configure(text='Start')
def reader_thread(self, q):
try:
with self.process.stdout as pipe:
for line in iter(pipe.readline, b''):
q.put(line)
finally:
q.put(None)
def updatetext(self, q):
for line in iter_except(q.get_nowait, Empty): # display all content
if line is None:
self.startbuttonpresses = 0
self.startbutton.configure(bg='green')
self.startbutton.configure(text='Start')
return
else:
self.statustext.insert(tk.END, line)
self.root.after(400, self.updatetext, q)
def quit(self):
try:
self.process.kill()
except Exception:
pass
self.root.destroy()
root = tk.Tk()
app = DisplaySubprocessOutputDemo(root)
root.protocol("WM_DELETE_WINDOW", app.quit)
root.eval('tk::PlaceWindow %s center' % root.winfo_pathname(root.winfo_id()))
root.mainloop()
When the process is executing with PyInstaller sys.executable is pointing to the exe file created and not to the python.exe like in the interpreter so the Popen will not work.
You can try to create another executable for the testsubprocess.py and point Popen to it or use multiprocessing (don’t forget to use freeze_support if you do https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support)

Tkinter updating label after using pyinstaller

When I run the .py file, the program runs fine as in self.error updates as robocopy is running. However after I've changed it to a .exe file using pyinstaller -F -w Shortcutsv2.1.py I've noticed that the tkinter label which shows how many files it has updated (self.error) doesn't update until after the terminal is closed. Is there a way to have it work like the script? I'm assuming this is due to a setting in pyinstaller but I may be wrong
CODE:
from subprocess import PIPE, run
import tkinter as tk
from tkinter import ttk
import tkinter.messagebox as mb
import tkinter.font as tkFont
from os import path
from os import listdir
from os import mkdir
from os import walk
from glob import glob
import threading
import pandas as pd
from datetime import datetime
class Windows(threading.Thread):
def __init__(self, master):
threading.Thread.__init__(self)
self.width = 400
self.height = 150
master.geometry(f"{self.width}x{self.height}")
master.title("Shortcuts")
#master.iconbitmap(r'ip.ico')
self.frame = tk.Frame(master,bg="white")
self.frame.place(relx=0, rely=0, relwidth=1, relheight=1)
self.font = ('Helvetica', '10')
#Source path label and text entry
self.srcLabel = tk.Label(self.frame, text="Source Path:", font=self.font, bg="white")
self.srcLabel.place(relx=0.001, rely=0.026, relwidth=0.31, relheight=0.2)
self.srcEntry = tk.Entry(self.frame, font=self.font, bg="white")
self.srcEntry.place(relx=0.31, rely=0.026, relwidth=0.68, relheight=0.2)
#Destination path label and text entry
self.dstLabel = tk.Label(self.frame, text="Destination Path:", font=self.font, bg="white")
self.dstLabel.place(relx=0.001, rely=0.246, relwidth=0.31, relheight=0.2)
self.dstEntry = tk.Entry(self.frame, font=self.font, bg="white")
self.dstEntry.place(relx=0.31, rely=0.246, relwidth=0.68, relheight=0.2)
#New Folder to be created label and text entry
self.nfLabel = tk.Label(self.frame, text="New Folder:", font=self.font, bg="white")
self.nfLabel.place(relx=0.001, rely=0.466, relwidth=0.31, relheight=0.2)
self.nfEntry = tk.Entry(self.frame, font=self.font, bg="white")
self.nfEntry.place(relx=0.31, rely=0.466, relwidth=0.68, relheight=0.2)
#Submit
self.submit = tk.Button(self.frame, text="Submit", bg="white", font=self.font,
command = self.threadCmd)
self.submit.place(relx=0.5, rely=0.733, relwidth=0.3, relheight=0.2)
#Errors
self.error = tk.Label(self.frame, text="", font=self.font, bg="white", fg="red")
self.error.place(relx=0.001, rely=0.733, relwidth=0.53, relheight=0.2)
def findFile(self, dirPath):
#finds the full path of all files including within the subdirectories
i = 0
temp = []
filesInDir = [f"{dirPath}\\{a}" for a in listdir(dirPath)]
for a in filesInDir:
i += 1
fullPath = path.join(dirPath, a)
if path.isdir(fullPath):
temp = temp + self.findFile(fullPath)
else:
temp.append(fullPath)
return temp
def lowestFolder(self, direc):
#finds the lowest last folder if any and creates the ~~~~.txt in there to ensure it is the last file to be copied
subdir = [x[0] for x in walk(direc)]
subdir.sort()
if subdir[-1] == direc:
f = open(path.join(direc,"~~~~.txt"),"w+")
f.close()
else:
self.lowestFolder(str(path.join(direc,subdir[-1])))
def Run(self):
if not path.exists(self.srcEntry.get()):
self.error.config(text="Can't find src path", fg="red")
elif not path.exists(self.dstEntry.get()):
self.error.config(text="Can't find dstn path", fg="red")
else:
dest = self.dstEntry.get() + "\\" + self.nfEntry.get()
if path.isdir(dest):
self.error.config(text="Folder Exists", fg="red")
else:
self.error.config(text="")
self.filename = "logs.xlsx"
self.listOfFiles = glob(path.join(".",self.filename))
self.lowestFolder(self.srcEntry.get())
filesTransferred = self.findFile(self.srcEntry.get())
length = len(filesTransferred)
mkdir(dest)
date = datetime.now()
run(f"start cmd /K RoboCopy.exe \"{self.srcEntry.get()}\" \"{dest}\" *.* /E /Z", stdout=PIPE, stdin=PIPE, stderr=PIPE, shell=True)
#Checks if all files have been transferred before moving on
i = 0
while(i < length):
fullPath = filesTransferred[i].replace(self.srcEntry.get(), dest)
if path.exists(fullPath):
i += 1
self.error.config(text=f'Transferring file(s): {i}/{length}', fg='black')
temp2 = self.findFile(dest)
temp2 = [x.replace(dest, "..") for x in temp2]
if length == len(temp2):
#Creates log file if not created
if not self.listOfFiles:
writer = pd.ExcelWriter(self.filename, engine='xlsxwriter')
writer.save()
df = pd.read_excel(self.filename)
#creates file if it doesn't exist else apppends
df2 = pd.DataFrame({"Started" : [date],
"Source": [self.srcEntry.get()],
"Destination": [self.dstEntry.get()],
"Files": [", ".join(temp2)]})
df = df.append(df2, sort=False)
df.to_excel(self.filename, index=False)
self.error.config(text="Files copied successfully.", fg="green")
#IF ROBOCOPY FREEZES WHEN YOU CLICK ON IT THEN JUST PRESS TAB AND IT SHOULD RESUME
else:
self.error.config(text="All files were not copied.", fg="red")
def threadCmd(self):
self.result = None
y = threading.Thread(target=self.Run)
y.start()
if __name__ == "__main__":
root = tk.Tk()
x = threading.Thread(target=Windows, args=(root,))
x.start()
x.join
root.mainloop()
EDIT 1
"There are couple problems here. 1. Inherit class Windows(threading.Thread): but don't use this object. 2. Passing a class definition as .Thread(target=Windows. 3. Useless x.join without calling it. 4. Accessing tkinter objects, e.g.self.srcEntry.get(), from a Thread."
I've changed the code to the below so now:
1) Can't make this change as I'm using .start()
2) Changed this to an instance
3) called the function
4) Didn't make this change as wasn't sure what the issue was. Needs more clarification
The issue which I had posted for still persists however I appreciate the help to tidy up my code :)
if __name__ == "__main__":
root = tk.Tk()
x = Windows(root)
x.start()
x.join()
root.mainloop()
I was using subprocess.run instead of subprocess.Popen, subprocess.run waits for the terminal command to complete before moving on with the script. Whereas subprocess.Popen doesn't.
Edit: What I find strange is that when I would run it as a .py file subprocess.run worked the same as subprocess.Popen however when I had used pyinstaller then subprocess.run worked how it's supposed to.

Tkinter: Updating GUI from subprocess output in realtime

I've searched the whole internet to answer my problem, but nobody seems to have the same one: I'm trying to update my tkinter GUI dynamically from a subprocess output, which works fine, if I'm starting my GUI inside eclipse. BUT if I'm running it in the file explorer or in visual studio, the 'stdout.readline' command waits, until the subprocess is finished. Only then the complete output is printed to my textarea... I am working with a thread and I've tried 2 ways: one is shown in 'App.py', the other one is threading the 'read_update' method instead (and not using 'reader_thread' and 'update' methods).
Interesting sidenote: the sys.argv command in Test.py does not return my string "var_test". Can anyone tell me why?
My Classes are:
GUI.py
import Tkinter as tk
from App import App
if __name__ == '__main__':
root = tk.Tk()
app = App(root)
root.protocol("WM_DELETE_WINDOW", app.quit)
root.mainloop()
App.py
#App.py
import Tkinter as tk
import tkFont as tkfont
import subprocess
from subprocess import Popen
from subprocess import PIPE
from itertools import islice
from threading import Thread
from ttk import Scrollbar
from Tkinter import *
from Queue import Queue, Empty
class App():
def __init__(self, root):
self.root = root
self.root.title_font = tkfont.Font(family = "Helvetica", size = 18, weight = "bold", slant = "italic")
Grid.columnconfigure(self.root, 5, weight = 1)
button_ok = tk.Button(self.root, text = "OK", width = 10, command = lambda: self.on_okay())
button_ok.grid(row = 7, column = 0, padx = (20,0), pady = 10, sticky = W)
xscrollbar = Scrollbar(self.root, orient=HORIZONTAL)
xscrollbar.grid(row=8, column=1, columnspan=4, sticky=E + W)
yscrollbar = Scrollbar(self.root, orient=VERTICAL)
yscrollbar.grid(row=8, column=5, sticky=N + S)
self.textarea = Text(self.root, wrap=NONE, bd=0,
xscrollcommand=xscrollbar.set,
yscrollcommand=yscrollbar.set)
self.textarea.grid(row=8, column=1, columnspan=4, rowspan=1,
padx=0, sticky=E + W + S + N)
def on_okay(self):
self.textarea.delete("1.0", END)
exec_path = r"\Test.py" #insert location of Test.py
self.process = subprocess.Popen([exec_path, 'var_test'], shell = True, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
self.q = Queue(maxsize = 1024)
t = Thread(target=self.reader_thread, args=[self.q])
t.daemon = True
t.start()
self.update(self.q)
def reader_thread(self, q):
try:
with self.process.stdout as pipe:
for line in iter(pipe.readline, b''):
q.put(line)
finally:
q.put(None)
def update(self, q):
for line in self.iter_except(q.get_nowait, Empty):
if line is None:
#self.quit()
return
else:
self.textarea.insert(INSERT, line)
self.textarea.yview(END)
break
self.root.after(40, self.update, q)
def iter_except(self, function, exception):
try:
while True:
yield function()
except exception:
return
def read_update(self):
while True:
line = self.process.stdout.readline()
if line == "" and self.process.poll() != None:
break
elif line == "":
pass
else:
self.textarea.insert(INSERT, line)
self.textarea.yview(END)
self.textarea.update_idletasks()
def quit(self):
try:
self.process.kill()
except AttributeError:
pass
finally:
self.root.destroy()
Test.py
import sys
from time import sleep
var = sys.argv
print var
for i in range(1, 10):
print i
print "finished printing numbers"
sleep(10)
print "finished"
Thank you for your help! I'm pretty desperate 'cause I've been trying to solve this problems for many hours now...
use sys.stdout.flush() after print

Categories

Resources