Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

Python3 Code Library

Python Code Library

This is a onenote/joplin inspired note taking app. It can be used for any type of text file archive.

How you save your files does not matter except in a way that you can organize them. For example, if you have spl code, you might save all spl snippets with .spl, then use the button that searches for *.spl

You might also want to categorize your items like: linux_some_command.txt or linux_some_command.cli so you can search only files that start with linux or end with cli. Organize how your workflow works.

Warning

It will do what you tell it. So if you mess up a valid json or xml file, it will not warn you. I plan on adding validation but for now, be careful on specially formatted files. Don’t blame me if this makes your cheerios taste bad, there are no warranties of any kind.

The Script

Save as _code-library.py in the folder where all of your text snippets are. It will do the rest.

"""
NoteBook Application

This script provides a graphical user interface for a simple notebook application,
enabling users to create, search, edit, save, and delete text and JSON files. The
application supports basic text file management functionalities, including the ability
to search files using glob patterns, view and edit file content, and manage files within
a specified directory.

The interface allows for editing and special formatting for JSON files, including 
pretty-printing and validation. Additionally, users can toggle fullscreen mode and 
uses keyboard shortcuts for common actions, for more intuitive workflow.

Use Case:
	- library of code
	- library of ascii art
	- library of guitar tabs

Requirements:
	- Python 3.x
	- tkinter library (usually included with Python)
	- uses all standard modules, no venv required

Keyboard Shortcuts:
	- Ctrl+S: Save the current file.
	- Ctrl+A: Select all text in the current file.
	- F11: Toggle fullscreen mode.

Author: James Fraze
Version: 0.02
License: MIT
"""

# for most of the gui
import tkinter as tk

# for file name glob searches
from glob import glob

# for message boxes and ttk for themes
from tkinter import messagebox, ttk

# to delete files
import os

# to detect os
import platform

# for the help file
import json

VERSION = ".02"


class NoteBook:
	"""
	A NoteBook application class for managing and editing text files.

	This class provides a graphical user interface for a notebook application
	that allows users to create, search, save, and delete text files. It also
	supports viewing and editing JSON files with syntax highlighting and
	special formatting.

	Attributes:
			root (tk.Tk): The main window of the application.
			frame0 (tk.Frame): A frame for the top section of the application.
			frame1 (tk.Frame): A frame for the bottom section of the application.
			lb (tk.Listbox): A listbox for displaying available text files.
			text (tk.Text): A text widget for displaying and editing the content of text files.
			save_var (tk.StringVar): A variable to hold the name of the current file.
	"""

	fullscreen = False

	def __init__(self, root):
		"""
		Initializes the NoteBook application with a specified root window.

		Parameters:
				root (tk.Tk): The main window of the application.

		Frames:
		root
		|_frame0 - a single skinny row at the top

		|_frame1 - the whole bottom area, other stuff stacked in this
				|_lb - list box on the left part of frame1
				|_text - stacked up next to frame 1, effectively the right side text area
		"""

		# basic app dimenions/title on "root"
		# this holds other frames
		self.root = root
		self.root.geometry("900x700")
		self.root.title(f"Python Code Library v{VERSION}")

		# the top frame where the grey bar is
		# frame0 is the name we give to the top section
		self.frame0 = tk.Frame(self.root)
		self.frame0["bg"] = "grey"
		self.frame0.pack(side="top", fill=tk.X)

		# frame1 is the name we give to the bottom area
		self.frame1 = tk.Frame(self.root)
		self.frame1["bg"] = "coral"
		self.frame1.pack(side="left", fill=tk.Y)

		# list box in that bottom area, 30 wide
		self.lb = tk.Listbox(self.frame1, width=30)
		self.lb.pack(side="bottom", fill=tk.Y, expand=1)

		# text block in bottom frame, stacked next to list box, 70 wide
		self.text = tk.Text(self.root, width=70)
		self.text.pack(fill=tk.BOTH, expand=1)

		# file save name box, also in frame0
		self.save_var = tk.StringVar()
		self.name = tk.Entry(self.frame0, textvariable=self.save_var)
		self.name.pack(side="left", ipady=3)

		# other button controls save/del, still in frame 0
		self.b_search = tk.Button(self.frame0, text="FIND", command=self.glob_stuff)
		self.b_search.pack(side="left")
		self.b_save = tk.Button(self.frame0, text="SAVE", command=self.save_text)
		self.b_save.pack(side="left")
		self.b_del = tk.Button(self.frame0, text="DEL", command=self.del_text)
		self.b_del.pack(side="left")

		# Insert a spacer
		spacer = tk.Frame(self.frame0, width=20, height=40, bg="grey")
		spacer.pack(side="left", padx=5)

		# Buttons that come after the spacer
		self.b_ps1 = tk.Button(
			self.frame0, text="PS1", command=lambda: self.glob_stuff("*.ps1")
		)
		self.b_ps1.pack(side="left")
		self.b_py = tk.Button(
			self.frame0, text="PY", command=lambda: self.glob_stuff("*.py")
		)
		self.b_py.pack(side="left")
		self.b_spl = tk.Button(
			self.frame0, text="SPL", command=lambda: self.glob_stuff("*.spl")
		)
		self.b_spl.pack(side="left")
		self.b_txt = tk.Button(
			self.frame0, text="TXT", command=lambda: self.glob_stuff("*.txt")
		)
		self.b_txt.pack(side="left")
		self.b_json = tk.Button(
			self.frame0, text="JSON", command=lambda: self.glob_stuff("*.json")
		)
		self.b_json.pack(side="left")

		# Insert a spacer
		spacer = tk.Frame(self.frame0, width=20, height=40, bg="grey")
		spacer.pack(side="left", padx=5)

		self.b_help = tk.Button(self.frame0, text="?", command=self.show_help)
		self.b_help.pack(side="left")

		# some keybindings and mouse click function
		# not visible, just bound in memory
		self.lb.bind(
			"<Double-Button>", lambda x: self.view_text()
		)  # when you double click an item in left
		self.root.bind("<F11>", lambda x: self.f11_fullscreen())  # makes full stcreen
		self.root.bind(
			"<Return>", lambda x: self.glob_stuff()
		)  # executes search when you hit enter
		self.root.bind("<Control-s>", lambda event: self.save_text())  # ctrl s to save
		self.root.bind(
			"<Control-S>", lambda event: self.save_text()
		)  # ctrl s to save, Caps
		self.root.bind(
			"<Control-a>", lambda event: self.select_all_text()
		)  # ctrl a to select all
		self.root.bind(
			"<Control-A>", lambda event: self.select_all_text()
		)  # ctrl a to select all, Caps

	def select_all_text(self, event=None):
		"""
		Selects all text within the text widget.

		Parameters:
				event (Event, optional): The event that triggered this method.
		"""
		# Select all the text in the text widget
		self.text.tag_add(tk.SEL, "1.0", tk.END)
		# Set the mark for the last selected character so that keyboard actions
		# apply to the selection
		self.text.mark_set(tk.INSERT, tk.END)
		# Ensure the text widget has focus so the user can immediately take action
		# (like deleting the selection)
		self.text.focus()
		# Prevent the event from propagating further
		return "break"

	def glob_stuff(self, search_pattern=None):
		"""
		Populates the list box with files that match the specified search pattern.

		If no search pattern is provided, all files are listed. Supports basic
		glob patterns.

		Parameters:
				search_pattern (str, optional): The glob pattern to filter files.
		"""
		# Use the provided search pattern if not None, else use the current value of save_var
		theglob = search_pattern if search_pattern is not None else self.save_var.get()

		# if it's blank, then search all
		if not theglob:
			theglob = "*.*"

		# first, delete whatever is in my view box
		self.lb.delete(0, "end")

		# then, get the list and populate my view box
		for file in glob("*" + theglob + "*"):
			self.lb.insert(tk.END, file)

	def glob_stuff2(self, theglob):
		"""
		Populates the list box with files that match the specified search pattern.

		If no search pattern is provided, all files are listed. Supports basic
		glob patterns.

		Parameters:
				search_pattern (str, optional): The glob pattern to filter files.
		"""
		# if it's blank, then search all
		if theglob == "":
			theglob = "*.*"
		else:
			theglob = self.save_var.get()

		# first, delete whatever is in my view box
		self.lb.delete(0, "end")

		# then, get the list and populate my view box
		for file in glob("*" + theglob + "*"):
			self.lb.insert(tk.END, file)

	def view_text(self):
		"""
		Displays the content of the selected file in the text widget.

		This method checks the file extension and applies appropriate formatting
		for JSON files. (fixme)
		"""
		# the index of item selected
		num_item = self.lb.curselection()
		# the file name selected
		fname = self.lb.get(num_item)

		# update our "save as file name"
		self.save_var.set(fname)

		# Check if the file is a JSON file
		#if fname.endswith(".json"):
		#    try:
		#        with open(fname, "r", encoding="utf-8") as file:
		#            # Load JSON content
		#            data = json.load(file)
		#            # Pretty-print the JSON content with indentation
		#            pretty_json = json.dumps(data, indent=4)
		#            # Replace literal \n with actual new lines for display
		#            pretty_json = pretty_json.replace("\\n", "\n")
		#            # Display the pretty-printed and newline-corrected JSON
		#            self.text.delete("1.0", tk.END)
		#            self.text.insert(tk.END, pretty_json)
		#    except Exception as e:
		#        messagebox.showerror("Error", f"Failed to load JSON file: {e}")
		#else:
		#    # Handle non-JSON files as before
		#    with open(fname, "r", encoding="utf-8") as file:
		#        self.text.delete("1.0", tk.END)
		#        file_content = file.read()
		#        self.text.insert(tk.END, file_content)
		# fixme:  add some json validation to help them
		with open(fname, "r", encoding="utf-8") as file:
			self.text.delete("1.0", tk.END)
			file_content = file.read()
			self.text.insert(tk.END, file_content)

	def show_help(self):
		"""
		Displays help information in the text widget.

		The help content is loaded from a '_help.json' file.
		"""
		fname = self.save_var.get()
		verify = messagebox.askquestion(
			"LEAVE PAGE?",
			f"You are about to leave the page {fname}. If you have not saved, your work will be lost. Do you want to leave the page?",
			icon="warning",
		)
		if verify == "yes":
			# Load help content from a JSON file
			try:
				with open("_help.json", "r", encoding="utf-8") as file:
					data = json.load(file)
					help_text = data["help_text"]
			except FileNotFoundError:
				help_text = "Help content file not found."
			except json.JSONDecodeError as e:
				help_text = f"Error reading the help content file: {e}"
			except Exception as e:
				help_text = f"An unexpected error occurred: {e}"

			# Display the help text in the main text area
			self.text.delete("1.0", tk.END)
			self.text.insert(tk.END, help_text)

	def save_text(self):
		"""
		Saves the content of the text widget into the currently selected file.

		Performs special JSON formatting if the file is a JSON file.
		"""
		# the file name selected
		fname = self.save_var.get()
		if fname == "":
			messagebox.showinfo("Oops", "File name cannot be empty!")
			return

		# Extract the text from the Text widget
		content_to_save = self.text.get(1.0, "end-1c")

		if fname.endswith(".json"):
			try:
				# Attempt to parse the edited text back into JSON to ensure it's valid
				json_content = json.loads(content_to_save)
				# Use json.dump to write the JSON content back into the file,
				# attempting to preserve its structure
				with open(fname, "w", encoding="utf-8") as file:
					json.dump(json_content, file, ensure_ascii=False, indent=4)
				messagebox.showinfo("Message", f"{fname} SAVED as valid JSON!")
			except json.JSONDecodeError as e:
				messagebox.showerror(
					"Error",
					f"Failed to save file. The content is not valid JSON. Error: {e}",
				)
				return
		else:
			# For non-JSON files, save the text directly
			with open(fname, "w", encoding="utf-8") as file:
				file.write(content_to_save)
			messagebox.showinfo("Message", f"{fname} SAVED!")

		# Refresh the file list if needed
		theglob = self.save_var.get()
		if theglob == "":
			theglob = "*.*"
		self.glob_stuff2(theglob)

	def del_text(self):
		"""
		Toggles the application between fullscreen and windowed mode.
		"""
		# the file name selected
		fname = self.save_var.get()
		if fname == "":
			messagebox.showinfo("Oops", fname + " File name cannot be empty!")
			return

		# open specific file in write mode
		verify = messagebox.askquestion(
			"REALLY DELETE FOREVER?",
			"You are about to Delete '"
			+ fname
			+ "'.  This cannot be undone.  Are you sure?",
			icon="warning",
		)
		if verify == "yes":
			# fixme: not really a useful check, verify read/write etc
			if os.path.exists(fname):
				os.remove(fname)
				print(fname + " was deleted")
				self.text.delete("1.0", tk.END)
			else:
				messagebox.showinfo(
					"Oops",
					"the file '"
					+ fname
					+ "' Does Not Exist! Double click the file on the left to "
					+ "automatically enter the proper file name",
				)

		theglob = self.save_var.get()
		if theglob == "":
			theglob = "*.*"
		self.glob_stuff2(theglob)

	def f11_fullscreen(self):
		"""
		Toggles the application between fullscreen and windowed mode.
		"""
		if NoteBook.fullscreen is False:
			self.root.attributes("-fullscreen", True)
			NoteBook.fullscreen = True
		else:
			self.root.attributes("-fullscreen", False)
			NoteBook.fullscreen = False


def main():
	"""
	main logic
	"""
	# create a main window in tk
	root = tk.Tk()

	# Setting up the theme based on OS
	os_name = platform.system().lower()
	style = ttk.Style()
	# fixme:  mac users
	if "linux" in os_name:
		style.theme_use("clam")  # Using clam theme for Linux
	elif "windows" in os_name:
		style.theme_use(
			"default"
		)

	# new class creation
	app = NoteBook(root)

	# kick it off
	root.mainloop()


if __name__ == "__main__":

	main()

Help File

save this file as _help.json

{
    "help_text": "----------------------------------------------------------------------\nWHAT IS THIS?\n----------------------------------------------------------------------\n  NOTE Script to organize notes.   Saves notes as simple text files.\n\n----------------------------------------------------------------------\nWHY?\n----------------------------------------------------------------------\n  I wanted to learn about Tk and take notes.   It's inspired from\n  joplin and onenote, without the bells and whistles (and just text)\n\n----------------------------------------------------------------------\nUX FEATURES\n----------------------------------------------------------------------\n- enter key bound to search\n- blank entry hit search/enter?  auto searches everything\n- if fname doesn't match and you try to delete warns you\n- warns you before you delete a file\n- type a new name and click save and it makes new article\n- has ? feature to show this help\n- allows for glob searches (*.bleh) in unix format\n\n----------------------------------------------------------------------\nBUTTONS\n----------------------------------------------------------------------\n  SEARCH: To filter, just type partial name or glob and hit enter\n  SAVE: saves current file name selected, or new one you typed\n  DEL: delects current file name selected\n  ?: This help message\n\n----------------------------------------------------------------------\nWARNING:\n----------------------------------------------------------------------\n  File system limits for max inodes will limit max number of files\n\n----------------------------------------------------------------------\nSUGGESTED USES:\n----------------------------------------------------------------------\n  Save a bunch of py files and then search for *.py\n  Outline a story with filename divisions 100.txt 101.txt 200.txt, etc\n  prefix a name like tutorial_bleh.txt  then glob for tutorial*.txt\n  tablature\n\n----------------------------------------------------------------------\nTODO:\n----------------------------------------------------------------------\n- add a Date Time Stamp and use \"Logger\"\n- file names with spaces choke, force name normalization\n- put the alerts into a logger function and standardize\n- refactor the glob function into 1\n- create a \"default data\" button to pre-load examples from json\n- colorize or syntax highlight some of the code\n- change up font so it's easier for people to read\n- rename variables into \"notebook\" instead of story, so it's more generic\n- \"template\" out a set of text documents like for policies/rules/handbooks\n- Hide the DOS Window\n- Keybind Alt F4 or other\n- Add encryption like from encrypted journal program\n- Verify linux/windows operation\n- add copy to clipboard button or mouse right clicks\n- refactor the json save protections to allow multline, valid json\n- clean up code deadcode, black, pylint, etc\n- to help with possible inode issues\\directory limits add directories"
}

Sample ASCI:

save as linux.txt

            .-"""-.
           '       \
          |,.  ,-.  |
          |()L( ()| |
          |,'  `".| |
          |.___.',| `
         .j `--"' `  `.
        / '        '   \
       / /          `   `.
      / /            `    .
     / /              l   |
    . ,               |   |
    ,"`.             .|   |
 _.'   ``.          | `..-'l
|       `.`,        |      `.
|         `.    __.j         )
|__        |--""___|      ,-'
   `"--...,+""""   `._,.-' mh

save as hollow.tab

Hollow by PANTERA
 Tabbed by: KoRny Freak
 [Rhythm] (clean) >--What's Left Inside-< >-No one knows what's done is done--<
e|-----------------------------|-----------------------------------------------|
B|----0------0------0------0---|----0-----0-----0-----0------0-----0------0----|
G|---5-5----5-5----5-5----5-5--|---5-5---5-5---5-5---5-5----5-5---5-5----5-5---|
D|--2---2--2---2--2---2--2---2-|--2---2-2---2-----0-----0------0-----0--2---2--|
A|-0------3-------------0------|--------------3-----3------2-----2-------------|
E|---------------3-------------|-0-----0-----2-----2------3-----3------0-----0-|
 
 
[Lead] (use neck pickup to get warm tone)
e|----------------------------------------|-------------------------------------|
B|------------------12-15~~-15/\--15/\-12-|----------------12---11~~---9/\/\----|
G|-----------12~~-12----------------------|---------12~~-12---9------7-------10-|
D|--------14------------------------------|-------14----------------------------|
A|15~~-15---------------------------------|15~~-15------------------------------|
E|----------------------------------------|-------------------------------------|
 
 
 [2nd Lead]
e|--------------------------------5---------------------------------------------|
B|-8/\5-8~~--8/\5---5-----------5--8^5---5-----------5--------------------------|
G|----------------8---8-7-5-7-8--------8---8-7-5-7-8---8-7-5---5----------------|
D|-----------------------------------------------------------7---7--------------|
A|-------------------------------------------------------------------7-6-565--3-|
E|------------------------------------------------------------------------------|
 
e|--------------------------------------------------------------|
B|------20-20-20-------------20-20-20-----------20/\-20-20/^----|
G|-19/\----------19/\--19~~-----------19/\-19~~-----------------|
D|--------------------------------------------------------------|
A|--------------------------------------------------------------|
E|--------------------------------------------------------------|
 
 
[Pre Bridge]      >--He as hollow as I alone now.....--<
e|-----------------------------------|---------------------------|
B|-----------------------------------|---------------------------|
G|---0------0------0-----------------|---0---0---0---0--0--0-----|
D|--4-4----4-4----4-4-------0--0-----|--5---4---2---2------------|
A|-2---2--2---2--2---2-----2---------|-7---5---4---3---2---------|
E|0------0------0---------3---1--1-0-|--------------------3--1-0-|
 
 
[Bridge]   >--He as hollow as I alone, a shell of my friend....--<
e|---------------|-----------------|-----------------------------|
B|---------------|-----------------|-----------------------------|
G|---------------|-----------------|-----------------------------|
D|2-2-2-4-5-6-7--|--2-2-2-7-8-9-10-|-----------------------------|
A|2-2-2-4-5-6-7--|--2-2-2-7-8-9-10-|-----------------------------|
E|0-0-0-2-3-4-5--|--0-0-0-5-6-7-8--|0-8-0-0-6-0-0-8-0-0-6-0-0-6~-|
 
 
 [After Lyrics]
e|------------------------------|-------------------------------|
B|------------------------------|-------------------------------|
G|------------------------------|-------------------------------|
D|------------------------------|-------------------------------|
A|------------------------------|-------------------------------|
E|-0-8-0-0-6-0-0-8-0-0-6-0-0-6~-|-0-8-0-0-6-0-0-8-0-0-6-6-7-8~--|
 
 [End]                               [Harmonics]
e|-------------------------------|---------12------|
B|-------------------------------|-----5-----------|
G|---0---0---0---0--0--0---------|--7--------------|
D|--5---4---2---2----------2-----|-----------------|
A|-7---5---4---3---2-------2-----|-----------------|
E|--------------------3--1-0-----|-----------------|
 
************************************
 
| ~  Vibrato
 
************************************