Information Technology Grimoire

Version .0.0.1

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

Hugo Markdown Editor

Editing Markdown

All you need is a text editor. Most text editors are not setup in a way specifically to edit markdown or to utilize templates. At least, it was easier and more fun for me to write this than to read how some other text editor that some other person wants me to learn how to use. This is a simple, universal text editor designed to work with markdown templates.

Yaml vs Toml

Yaml uses front matter of — and toml uses +++. I’ve set the default to be toml because there are more options. I’ve also written a stand alone converter to assist with yaml to toml and vice versa.

The Hugo Template

I’m learning hugo, and learning that the front matter controls quite a bit. It’s tedious for me to type it perfectly. This script takes a built in template and allows you to customize it too, but uses that template to create all future markdown skeletons. It will not overwrite existing files in the “create” tab function, but you can edit existing files in the “edit” tab.

Create List of Files

If you feed it a list of Words or Phraes on the “create” tab, it will make them into markdown files using the template.

Edit The File Directly

It correctly looks in sub folders too. So you could run this at the top level of your hugo site and edit all of them easily without opening/closing a bunch of tabs/files. They are all listed for you.

Intuitive Filter

I have several hundred articles and the UI didn’t scale, so I added a filter. Here is a picture of v8, latest code does quite a bit more such as creating files in subdirectories and showing markdown. Will update a bit.

The Python Script to Edit Markdown

Here is version 14 of the script.

requirements.txt

certifi==2024.2.2
charset-normalizer==3.3.2
idna==3.7
mistune==3.0.2
Pillow==9.5.0
Pygments==2.17.2
requests==2.31.0
tkhtmlview==0.2.0
urllib3==2.2.1

config.py

# config.py
MM_VERSION = 15


# adjust as necessary.  This is where all of my "sites" are
MDFILES = '../../../sites/newmd'

# only look in these folders for markdown, specific sites I'm working on at moment
SPECIFIC_FOLDERS = [
    MDFILES,
    "../../../sites/site.grimoire.jamesfraze/content",
    "../../../sites/site.tabs.jamesfraze/content",
]

# if you have a template.markdown, it uses it, else:
DEFAULT_TEMPLATE = """
+++
author = "James Fraze"       # not used in my theme, but it's a valid front matter
# description = "{title}"      # not used in my theme, but it's a valid front matter
# tags = ["", ""]             # tags are like categories, but not categories, don't duplicate
# images = ["/i/{slug}.png"]  # if you want thumbnails, and your theme supports it
linktitle = "{title}"        # in my theme this shows up on the left menu
summary = "{title}"          # if your theme supports this, it's the summary shown on list pages
# url = "/{slug}"              # some templates support direct url linking, others use slugs
# categories = ['{slug}']     # if your template supports slugs they are the directories from root domain
# slug = "{slug}"              # if your template supports slugs they are the directories from root domain
title = "{title}"            # shows up at the top of the rendered page above your text
date = "{date}"              # automated, if you edit, use the same format
lastmod = "{date}"           # automated, if you edit, use the same format
weight = 1000                # controls menu order in many themes
+++

![](/i/{slug}.png)

"""

# Default filter is where files are created
FILTER_TEXT = "newmd"

# Global variable to track the selected filename
SELECTED_FILENAME = None


"""

# Default filter is where files are created
FILTER_TEXT = "newmd"

# Global variable to track the selected filename
SELECTED_FILENAME = None

mm.py

Most everything to change will be in the config.py. It goes in the same directory as the mm.py file. Just run the mm.py file after you’ve installed the requirements.

#!/usr/bin/python3
# import datetime #linux?
from datetime import datetime # windows
import os
import re

import tkinter as tk
from tkinter import font, messagebox, ttk
from tkinter.scrolledtext import ScrolledText
from tkinter import PanedWindow

from pygments import highlight
from pygments.formatters import HtmlFormatter
from pygments.lexers import get_lexer_by_name
import mistune
from tkhtmlview import HTMLLabel

from config import MM_VERSION, SPECIFIC_FOLDERS, DEFAULT_TEMPLATE, FILTER_TEXT, SELECTED_FILENAME, MDFILES

"""
This script edits markdown files in a sane way for me
to help me manage several hundred articles in markdown on

https://grimoire.jamesfraze.com and
https://tabs.jamesfraze.com and ...


It creates files in "MDFILES" folder, but can edit them anywhere

You will need to edit the SPECIFIC_FOLDERS list in the config.py

V06: Bulk Add within Subdirectories
V07: Markdown Render View
V08: improved filter
V09: normalize file name creation
V10: Refactored code a bit, removed redundancy
V11: key binds for CTRL + S to save, various alerts
V12: Added specific content copy/paste buttons
v13: fix the onfocus delete issue
        also:  black, pylint, vulture, deadcode fixups
v14: moved variables to config.py
v15: fixed additional newline on windows and datetime issue
v16: sorts markdown and md by weight isntead of name

TODO: Allow creating files anywhere
"""

# --------
# Mark Down Related
# --------


class HighlightRenderer(mistune.HTMLRenderer):
    def block_code(self, code, lang=None, **kwargs):
        # style = 'background-color: black; color: white; font-size: 10px; font-family: monospace;'
        if not lang:
            return '<pre style="padding: 10px; margin: 0px 20px 0px 20px; background-color: black; color: white; font-size: 10px; font-family: monospace;"><code>{}</code></pre>'.format(
                mistune.escape(code)
            )
        try:
            lexer = get_lexer_by_name(lang, stripall=True)
        except:
            return '<pre style="padding: 10px; margin: 0px 20px 0px 20px; background-color: black; color: white; font-size: 10px; font-family: monospace;"><code>{}</code></pre>'.format(
                mistune.escape(code)
            )

        formatter = HtmlFormatter()
        return highlight(code, lexer, formatter)


def load_and_render_markdown(event):
    selected_indices = listbox_preview.curselection()
    if selected_indices:
        filename = find_full_path(listbox_preview.get(selected_indices[0]))
        if filename:
            with open(filename, "r", encoding="utf-8") as f:
                markdown_content = f.read()

            # Convert Markdown to HTML, processing front matter internally
            html_content = convert_markdown_to_html_with_front_matter(
                markdown_content
            )

            # Clear existing content in preview area
            clear_preview_area(preview_frame_right)

            # Update HTML view with new content
            update_html_view(html_content, preview_frame_right)

        # Re-select the currently selected item in the listbox
        listbox_preview.select_set(selected_indices[0])


def convert_markdown_to_html_with_front_matter(markdown_text):
    """
    Process the markdown text to skip front matter and convert to HTML.
    """
    # Process lines to skip front matter
    lines = markdown_text.split("\n")
    inside_front_matter = False
    processed_content = []
    for line in lines:
        if line.strip() in [
            "+++",
            "---",
            "...",
        ]:  # Check for front matter delimiters
            inside_front_matter = not inside_front_matter
            continue
        if not inside_front_matter:
            processed_content.append(line)

    markdown_to_html = "\n".join(processed_content)

    # Convert processed Markdown to HTML
    markdown = mistune.create_markdown(renderer=HighlightRenderer())
    html_content = markdown(markdown_to_html)

    return html_content


def clear_preview_area(container):
    """
    Clears the existing content in the preview area.
    """
    for widget in container.winfo_children():
        widget.destroy()


# takes converted markdown that is rendered and puts in window
def update_html_view(html_content, container):
    """
    Updates the preview area with the given HTML content.
    """
    html_label = HTMLLabel(container, html=html_content)
    html_label.pack(fill="both", expand=True)


# --------
# UI Functions
# --------


# Function to update file lists in all tabs
def refresh_all_file_lists():
    update_file_list(listbox_create)
    update_file_list(listbox_edit)
    update_file_list(listbox_preview)


# Function to get list of markdown files in specific folders
def get_markdown_files():
    global SPECIFIC_FOLDERS
    markdown_files = []

    for folder in SPECIFIC_FOLDERS:
        for root, dirs, files in os.walk(folder):
            for file in files:
                if file.endswith((".md", ".markdown")):
                    path = os.path.join(root, file)
                    # Adjust the relative path if necessary
                    relative_path = os.path.relpath(path, start=folder)
                    weight = get_weight_from_file(path)
                    markdown_files.append((relative_path, weight))

    return markdown_files

# Function to update the listbox with sorted file names per directory
def update_file_list(listbox):
    global FILTER_TEXT
    listbox.delete(0, tk.END)
    file_groups = {}

    for file, weight in get_markdown_files():
        if FILTER_TEXT.lower() in file.lower():
            directory, filename = os.path.split(file)
            if directory:
                if directory not in file_groups:
                    file_groups[directory] = []
                file_groups[directory].append((filename, weight))
            else:
                if "" not in file_groups:
                    file_groups[""] = []
                file_groups[""].append((filename, weight))

    # Sort file names within each directory by weight
    for directory, files in file_groups.items():
        sorted_files = sorted(files, key=lambda x: x[1])  # Sort by weight
        for filename, weight in sorted_files:
            if directory:
                listbox.insert(tk.END, os.path.join(directory, filename))
            else:
                listbox.insert(tk.END, filename)


# Function to update the filter text and refresh the file lists
def update_filter(event=None):
    global FILTER_TEXT
    FILTER_TEXT = filter_entry.get()
    refresh_all_file_lists()


# Helper Function to Find Full Path
def find_full_path(filename):
    global SPECIFIC_FOLDERS
    for folder in SPECIFIC_FOLDERS:
        full_path = os.path.join(folder, filename)
        if os.path.exists(full_path):
            return full_path
    return None


# When tabs change, do this event
def on_tab_changed(event):
    # Refresh file lists for both tabs
    refresh_all_file_lists()

    # Additional logic for the 'Template' tab
    selected_tab = event.widget.select()
    tab_text = event.widget.tab(selected_tab, "text")
    if tab_text == "Template":
        load_template()


# Selects entire textarea
def select_all_and_copy(text_widget):
    # Select all the text in text_widget
    text_widget.tag_add(tk.SEL, "1.0", tk.END)
    # Copy the selected text to the clipboard
    text_widget.clipboard_clear()
    text_widget.clipboard_append(text_widget.get("1.0", tk.END))
    # Show a message that text is copied
    messagebox.showinfo("Info", "All text has been copied to clipboard.")


# resets page to front matter only
def clear_content_and_confirm(text_widget, listbox):
    # Ask for confirmation
    if messagebox.askyesno(
        "Confirm",
        "Are you sure? This will erase everything except the front matter.",
    ):
        content = text_widget.get("1.0", tk.END)
        lines = content.split("\n")
        patterns = ["---", "+++"]
        end_of_front_matter_idx = None

        # Look for the end of the front matter
        in_front_matter = False
        for i, line in enumerate(lines):
            if line.strip() in patterns:
                if in_front_matter:  # Closing delimiter of the front matter
                    end_of_front_matter_idx = i
                    break
                # else:  # Opening delimiter of the front matter
                #    in_front_matter = True

        if end_of_front_matter_idx is not None:
            # Calculate the starting position of content to delete
            content_start_line = end_of_front_matter_idx + 2
            content_start_index = f"{content_start_line}.0"
            # Delete the content after the front matter
            text_widget.delete(content_start_index, tk.END)
            # Save the changes automatically
            save_content(text_widget, listbox, show_alert=False)
            messagebox.showinfo("Info", "Content cleared and changes saved.")
        else:
            messagebox.showinfo(
                "Info", "No content found to clear or already clear."
            )


# Select just the content, not the front matter
def select_content_excluding_front_matter(text_widget):
    content = text_widget.get("1.0", tk.END)
    lines = content.split("\n")
    patterns = ["---", "+++"]
    end_pattern_idx = (
        0  # Initialize variable to store the index of the end pattern
    )

    # Assume front matter starts at the beginning of the file
    in_front_matter = False
    for i, line in enumerate(lines):
        if line.strip() in patterns:
            if not in_front_matter:  # Front matter starts
                in_front_matter = True
            else:  # Front matter ends, and this is the second occurrence
                end_pattern_idx = i
                break

    if end_pattern_idx != 0:
        # Calculate the line after the end of the front matter
        content_start_line = (
            end_pattern_idx + 2
        )  # +1 for next line, +1 because line numbers start at 1
        content_start_index = f"{content_start_line}.0"
    else:
        # If no front matter is found, start from the beginning
        content_start_index = "1.0"

    # Ensure any existing selection is cleared (if necessary)
    text_widget.tag_remove(tk.SEL, "1.0", tk.END)

    # Select the text from content_start to the end
    text_widget.tag_add(tk.SEL, content_start_index, tk.END)
    text_widget.mark_set(
        tk.INSERT, content_start_index
    )  # Set cursor to start of selection
    text_widget.see(
        content_start_index
    )  # Scroll to the start of the selection

    # Copy to clipboard if needed
    text_widget.clipboard_clear()
    text_widget.clipboard_append(text_widget.get(content_start_index, tk.END))

    # Inform the user
    messagebox.showinfo(
        "Info", "Content excluding front matter has been selected and copied."
    )


# --------
# Markdown Editor
# --------
# allows us to sort by weight
def get_weight_from_file(filepath):
    weight = 1000  # Default weight if not specified
    with open(filepath, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        inside_front_matter = False
        for line in lines:
            if line.strip() in ["---", "+++"]:
                inside_front_matter = not inside_front_matter
                if not inside_front_matter:
                    break
            if inside_front_matter and line.strip().startswith('weight:'):
                weight_line = line.strip().split(':')[1].strip()
                weight_value = weight_line.split()[0]  # Extract the weight before any comment
                try:
                    weight = int(weight_value)
                except ValueError:
                    weight = 1000  # Use default weight if conversion fails
                break
    return weight



# updates the listbox on the left
def update_selected_filename(listbox):
    global SELECTED_FILENAME
    selected_indices = listbox.curselection()
    if selected_indices:
        SELECTED_FILENAME = find_full_path(listbox.get(selected_indices[0]))
        load_file_content()  # Call the load_file_content function here without an event argument


# loads file into textarea, deletes whatever exists
def load_file_content():
    global SELECTED_FILENAME
    if SELECTED_FILENAME:
        with open(SELECTED_FILENAME, "r", encoding="utf-8") as f:
            content = f.read()
        textarea_edit.delete("1.0", tk.END)
        textarea_edit.insert("1.0", content)


# save edited content
def save_content(text_area, listbox, show_alert=True, event=None):
    global SELECTED_FILENAME
    if not SELECTED_FILENAME:
        if show_alert:  # Only show this message when show_alert is True
            messagebox.showinfo("Info", "Please select a file to save.")
        return

    # Get the currently selected item index before saving
    selected_index = listbox.curselection()

    # Get the text from the text area, without the extra newline at the end
    content = text_area.get("1.0", "end-1c")

    with open(SELECTED_FILENAME, "w", encoding="utf-8") as f:
        f.write(content)
    update_file_list(listbox)

    # Check if there was a previously selected item
    if selected_index:
        # Re-select the previously selected item in the listbox
        listbox.select_set(selected_index[0])

    # Update the preview after saving
    load_and_render_markdown(None)

    if show_alert:  # Only show this message when show_alert is True
        messagebox.showinfo(
            "Info", f"File '{SELECTED_FILENAME}' has been saved."
        )

# delete a file and show an alert
def delete_file(listbox):
    selected_indices = listbox.curselection()
    if not selected_indices:
        messagebox.showinfo("Info", "Please select a file to delete.")
        return

    filename = find_full_path(listbox.get(selected_indices[0]))
    if filename:
        os.remove(filename)
        messagebox.showinfo("Info", f"File '{filename}' has been deleted.")
        update_file_list(listbox)
    else:
        messagebox.showinfo("Info", "File not found.")


# force a save if you lose focus, without showing alert
def save_content_on_focus_out(event):
    save_content(
        textarea_edit, listbox_edit, show_alert=False
    )  # Pass False to prevent showing the alert


# --------
# Template Related
# --------


# if user has a template.markdown file, uses that
# Converts Text area into list of files, using template
def create_markdown_files(text_area, listbox, template_text_area):
    content = text_area.get("1.0", tk.END).strip()
    if not content or content == "Enter file names here...":
        messagebox.showinfo(
            "Info", "Please enter file names in the text area."
        )
        return

    user_template = template_text_area.get("1.0", tk.END).strip()
    date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%S-06:00")

    base_folder_path = MDFILES
    files_skipped = False

    for name in content.split("\n"):
        if name:
            # Sanitize and create safe file and directory names
            sanitized_parts = []
            for part in name.split("/"):
                # Replace non-alphanumeric characters with a hyphen
                sanitized = re.sub(r"[^A-Za-z0-9]+", "-", part)
                # Remove leading and trailing hyphens
                sanitized = sanitized.strip("-")
                sanitized_parts.append(sanitized)

            # Reconstruct the path with sanitized names
            safe_directory = os.path.join(
                base_folder_path, *sanitized_parts[:-1]
            )
            safe_name = sanitized_parts[-1] + ".markdown"
            safe_name = safe_name.lower()  # Convert the file name to lowercase

            # Create directories if they don't exist
            if not os.path.exists(safe_directory):
                os.makedirs(safe_directory)

            file_path = os.path.join(safe_directory, safe_name)
            if os.path.exists(file_path):
                print(f"Skipped: '{file_path}' already exists.")
                files_skipped = True
                continue

            template_content = generate_template(user_template, name, date_str)
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(template_content)

    refresh_all_file_lists()

    if files_skipped:
        messagebox.showinfo(
            "Warn",
            "Some files were skipped. Please review the console for skipped files.",
        )


# Add placeholder text to the textarea
def add_placeholder_to_textarea(text_area):
    text_area.insert(tk.END, "Enter file names here...")
    text_area.config(fg="grey")

    def on_focus_in(event):
        if text_area.get("1.0", tk.END).strip() == "Enter file names here...":
            text_area.delete("1.0", tk.END)
            text_area.config(fg="black")

    def on_focus_out(event):
        if not text_area.get("1.0", tk.END).strip():
            text_area.insert(tk.END, "Enter file names here...")
            text_area.config(fg="grey")

    text_area.bind("<FocusIn>", on_focus_in)
    text_area.bind("<FocusOut>", on_focus_out)


# looks for their template.markdown file and loads it
def load_template():
    template_path = "template.markdown"
    if os.path.exists(template_path):
        with open(template_path, "r", encoding="utf-8") as f:
            template_content = f.read()
    else:
        template_content = DEFAULT_TEMPLATE
    template_text_area.delete("1.0", tk.END)
    template_text_area.insert("1.0", template_content)


# sets default template for user if template.markdown is not available
def reset_to_default_template():
    template_text_area.delete("1.0", tk.END)
    template_text_area.insert("1.0", DEFAULT_TEMPLATE)


# render template into memory with variables
def generate_template(user_template, title, date_str):
    # Pass 1: Replace non-alphanumeric characters (except for common title punctuation) with spaces
    title = re.sub(r"[^A-Za-z0-9?!.'\s]+", " ", title)

    # Pass 2: Left and right strip any white space, and convert consecutive white spaces to a single space
    title = re.sub(r"\s+", " ", title).strip()

    # Pass 3: Create the slug with only alphanumeric characters and hyphens, convert to lowercase, and strip leading/trailing hyphens
    slug = re.sub(r"[^A-Za-z0-9-]+", "-", title).strip("-").lower()

    return user_template.format(title=title, slug=slug, date=date_str)


# save template to template.markdown file
def save_template(text_area):
    with open("template.markdown", "w", encoding="utf-8") as f:
        f.write(text_area.get("1.0", tk.END))
    messagebox.showinfo("Info", "template.markdown has been updated")


# --------
# UI Related
# --------

# Create the main window
root = tk.Tk()
root.title(f"Markdown Editor v{MM_VERSION}")

# ----------------
# General UI Layout
# ----------------
# Define a global font
global_font = font.nametofont("TkDefaultFont")
global_font.config(family="Consolas", size=12)

# Apply the font to all ttk widgets
style = ttk.Style()
style.configure("TButton", font=global_font)
style.configure("TLabel", font=global_font)
style.configure("TEntry", font=global_font)
style.configure("TListbox", font=global_font)
style.configure("TNotebook", font=global_font)

# Get screen width and height
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

# Set maximum size for the window to be slightly smaller than the screen size
max_window_width = screen_width - 100  # 100 pixels less than screen width
max_window_height = screen_height - 100  # 100 pixels less than screen height
root.maxsize(max_window_width, max_window_height)

# ----------------
# Create all tabs
# ----------------
tab_control = ttk.Notebook(root)

# Create the filter entry and label (common for all tabs)
filter_frame = ttk.Frame(root)
filter_label = ttk.Label(filter_frame, text="Filter (type)")
filter_label.pack(side=tk.LEFT, padx=5, pady=5)
filter_entry = ttk.Entry(filter_frame)
filter_entry.insert(0, FILTER_TEXT)
filter_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
filter_entry.bind("<KeyRelease>", update_filter)

# Pack the filter frame globally at the top of the window
filter_frame.pack(side=tk.TOP, fill=tk.X)

tab_create = ttk.Frame(tab_control)
tab_edit = ttk.Frame(tab_control)
tab_preview = ttk.Frame(tab_control)
tab_template = ttk.Frame(tab_control)

# ----------------
# Create tab
# ----------------
paned_window = PanedWindow(tab_create, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True)

create_frame_left = ttk.Frame(paned_window)
create_frame_right = ttk.Frame(paned_window)
create_frame_left.config(width=300)  # Set the initial width of the left frame

paned_window.add(create_frame_left, minsize=300)
paned_window.add(create_frame_right, minsize=300)

listbox_create = tk.Listbox(create_frame_left, width=3, font=global_font)
listbox_create.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
update_file_list(listbox_create)

textarea_create = ScrolledText(
    create_frame_right, wrap=tk.WORD, font=global_font
)
textarea_create.pack(fill=tk.BOTH, expand=True)

# Add placeholder text to textarea_create
add_placeholder_to_textarea(textarea_create)

# Create a frame for the button in the create tab
create_button_frame = ttk.Frame(create_frame_right)
create_button_frame.pack(
    pady=10
)  # Adds some padding above and below the frame

# Pack the "Create Files" button into the frame
button_create = ttk.Button(
    create_button_frame,
    text="Create Files",
    command=lambda: create_markdown_files(
        textarea_create, listbox_create, template_text_area
    ),
)
button_create.pack(
    padx=5
)  # padx adds space to the left and right of the button

# ----------------
# Edit tab
# ----------------
paned_window = PanedWindow(tab_edit, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True)

edit_frame_left = ttk.Frame(paned_window)
edit_frame_right = ttk.Frame(paned_window)
edit_frame_left.config(width=300)  # Set the initial width of the left frame

paned_window.add(edit_frame_left, minsize=300)
paned_window.add(edit_frame_right, minsize=300)

listbox_edit = tk.Listbox(edit_frame_left, width=3)
listbox_edit.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
update_file_list(listbox_edit)

textarea_edit = ScrolledText(edit_frame_right, wrap=tk.WORD)
textarea_edit.pack(fill=tk.BOTH, expand=True)

# Bind the Ctrl + S key combination for saving in the Edit tab
textarea_edit.bind(
    "<Control-s>", lambda event: save_content(textarea_edit, listbox_edit)
)
# Bind the FocusIn event to the textarea_edit
textarea_edit.bind("<FocusIn>", lambda event: load_file_content())
# Bind the FocusOut event to save the content when losing focus
textarea_edit.bind("<FocusOut>", save_content_on_focus_out)


listbox_edit.bind(
    "<<ListboxSelect>>", lambda event: update_selected_filename(listbox_edit)
)


# ----------------
# Edit Frame Buttons
# ----------------
# Create a frame for the buttons in the edit tab
edit_buttons_frame = ttk.Frame(edit_frame_right)
edit_buttons_frame.pack(pady=10)  # Adds some padding above and below the frame

# Pack the "Select All" button into the frame
button_select_all = ttk.Button(
    edit_buttons_frame,
    text="Select All & Copy",
    command=lambda: select_all_and_copy(textarea_edit),
)
button_select_all.pack(side=tk.LEFT, padx=5)

# Pack the "Select Content" button into the frame
button_select_content = ttk.Button(
    edit_buttons_frame,
    text="Select Content",
    command=lambda: select_content_excluding_front_matter(textarea_edit),
)
button_select_content.pack(side=tk.LEFT, padx=5)


# Pack the "Save" button into the frame
# Inside the setup for the "Save" button
button_save = ttk.Button(
    edit_buttons_frame,
    text="Save",
    command=lambda: save_content(
        textarea_edit, listbox_edit, show_alert=True
    ),  # Explicitly passing True is optional here
)
button_save.pack(side=tk.LEFT, padx=5)

button_clear_content = ttk.Button(
    edit_buttons_frame,
    text="Clear Content",
    command=lambda: clear_content_and_confirm(textarea_edit, listbox_edit),
)
button_clear_content.pack(side=tk.LEFT, padx=5)


# Pack the "Delete" button into the frame
button_delete = ttk.Button(
    edit_buttons_frame,
    text="Delete File",
    command=lambda: delete_file(listbox_edit),
)
button_delete.pack(side=tk.LEFT, padx=5)

# ----------------
# Preview tab
# ----------------
paned_window = PanedWindow(tab_preview, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True)

preview_frame_left = ttk.Frame(paned_window)
preview_frame_right = ttk.Frame(paned_window)
preview_frame_left.config(width=300)  # Set the initial width of the left frame

paned_window.add(preview_frame_left, minsize=300)
paned_window.add(preview_frame_right, minsize=300)

listbox_preview = tk.Listbox(preview_frame_left, width=3, font=global_font)
listbox_preview.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
update_file_list(listbox_preview)

# ----------------
# Template tab
# ----------------
template_text_area = ScrolledText(tab_template, wrap=tk.WORD)
template_text_area.pack(fill=tk.BOTH, expand=True)
template_text_area.insert("1.0", DEFAULT_TEMPLATE)

# Create a frame for the buttons
buttons_frame = ttk.Frame(tab_template)
buttons_frame.pack(pady=10)  # Adds some padding above and below the frame

# Pack the buttons into the frame
button_save_template = ttk.Button(
    buttons_frame,
    text="Save Template",
    command=lambda: save_template(template_text_area),
)
button_save_template.pack(
    side=tk.LEFT, padx=5
)  # padx adds space to the left and right of the button

button_reset_template = ttk.Button(
    buttons_frame, text="Reset to Default", command=reset_to_default_template
)
button_reset_template.pack(
    side=tk.LEFT, padx=5
)  # This button is also packed to the left of the previous button


# ----------------
# Button bindings
# ----------------
# Bind the tab change event to refresh the file lists
tab_control.bind("<<NotebookTabChanged>>", lambda e: refresh_all_file_lists())
tab_control.bind("<<NotebookTabChanged>>", on_tab_changed)
listbox_preview.bind(
    "<<ListboxSelect>>", lambda event: load_and_render_markdown(event)
)

# Add tabs to the tab control
tab_control.add(tab_create, text="Create")
tab_control.add(tab_edit, text="Edit")
tab_control.add(tab_preview, text="Preview")
tab_control.add(tab_template, text="Template")
tab_control.pack(expand=1, fill="both")

# Refresh all file lists initially
refresh_all_file_lists()

root.mainloop()
Last updated on 25 Jan 2024
Published on 25 Jan 2024