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()