My Gemini Editor

My Gemini Editor

B.Bentley

https://blog.spacehey.com/entry?id=1590262

import tkinter as tk

from tkinter import filedialog, messagebox, simpledialog

import json

import os

# still learning be gentle

# Figure out ehy it says it folloed the curser for inserts but appends instead, I am working on that.


class LunarOrbitLogbook:

   def __init__(self, root):

       self.root = root

       self.root.title("Lunar Orbit Logbook, Blog Writer for Gemini Protocol")

       self.root.configure(bg='#001f3f')


       self.title_var = tk.StringVar()

       self.author_var = tk.StringVar()

       self.date_var = tk.StringVar()

       self.bio_var = tk.StringVar()

       self.keywords_var = tk.StringVar()

       self.tags_var = tk.StringVar()


       self.text_frame = tk.Frame(root, bg='#001f3f')

       self.text_frame.grid(row=3, column=0, columnspan=2, sticky=tk.NSEW, padx=5, pady=5)


       self.content_text = tk.Text(self.text_frame, wrap=tk.WORD, bg='#001f3f', fg='white', insertbackground='white')

       self.content_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)


       self.scrollbar = tk.Scrollbar(self.text_frame, command=self.content_text.yview)

       self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

       self.content_text.config(yscrollcommand=self.scrollbar.set)


       self.word_count_label = tk.Label(root, text="Word Count: 0", bg='#001f3f', fg='white')

       self.word_count_label.grid(row=2, column=2, sticky=tk.W, padx=5, pady=5)


       self.versions = []


       self.create_widgets()

       self.bind_shortcuts()

       self.content_text.bind("<KeyRelease>", self.update_word_count)

#debating on wheter to rearrange location of keywords and tags.

   def create_widgets(self):

       tk.Label(self.root, text="Title:", bg='#001f3f', fg='white').grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.title_var, bg='#0074D9', fg='white').grid(row=0, column=1, sticky=tk.EW, padx=5, pady=5)


       tk.Label(self.root, text="Author:", bg='#001f3f', fg='white').grid(row=1, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.author_var, bg='#0074D9', fg='white').grid(row=1, column=1, sticky=tk.EW, padx=5, pady=5)


       tk.Label(self.root, text="Date of Publish:", bg='#001f3f', fg='white').grid(row=2, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.date_var, bg='#0074D9', fg='white').grid(row=2, column=1, sticky=tk.EW, padx=5, pady=5)


       tk.Label(self.root, text="Short Bio:", bg='#001f3f', fg='white').grid(row=4, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.bio_var, bg='#0074D9', fg='white').grid(row=4, column=1, sticky=tk.EW, padx=5, pady=5)


       tk.Label(self.root, text="Keywords:", bg='#001f3f', fg='white').grid(row=5, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.keywords_var, bg='#0074D9', fg='white').grid(row=5, column=1, sticky=tk.EW, padx=5, pady=5)


       tk.Label(self.root, text="Tags:", bg='#001f3f', fg='white').grid(row=6, column=0, sticky=tk.W, padx=5, pady=5)

       tk.Entry(self.root, textvariable=self.tags_var, bg='#0074D9', fg='white').grid(row=6, column=1, sticky=tk.EW, padx=5, pady=5)


       menu = tk.Menu(self.root, bg='black', fg='green')

       self.root.config(menu=menu)


       file_menu = tk.Menu(menu, bg='black', fg='green')

       menu.add_cascade(label="File", menu=file_menu)

       file_menu.add_command(label="New", command=self.new_file, accelerator="Ctrl+N")

       file_menu.add_command(label="Copy", command=self.copy_text, accelerator="Ctrl+C")

       file_menu.add_command(label="Paste", command=self.paste_text, accelerator="Ctrl+V")

       file_menu.add_command(label="Cut", command=self.cut_text, accelerator="Ctrl+X")

       file_menu.add_command(label="Save as .GMI", command=self.save_file, accelerator="Ctrl+S")

       file_menu.add_command(label="Load .GMI", command=self.load_file, accelerator="Ctrl+O")

       file_menu.add_command(label="Save as .GOPHER", command=self.save_gopher_file, accelerator="Ctrl+Shift+S")

       file_menu.add_command(label="Load .GOPHER", command=self.load_gopher_file, accelerator="Ctrl+Shift+L")

       file_menu.add_command(label="Save as .SPARTAN", command=self.save_gopher_file, accelerator="Ctrl+Shift+S") 

       file_menu.add_command(label="Load .SPARTAN", command=self.load_gopher_file, accelerator="Ctrl+Shift+L")

       file_menu.add_command(label="Export as Markdown", command=self.export_as_markdown)

       file_menu.add_command(label="Export as HTML", command=self.export_as_html)


       inserts_menu = tk.Menu(menu, bg='black', fg='green')

       menu.add_cascade(label="Inserts", menu=inserts_menu)

       inserts_menu.add_command(label="Insert Heading 1", command=lambda: self.insert_gemtext("# Heading"))

       inserts_menu.add_command(label="Insert Heading 2", command=lambda: self.insert_gemtext("## Sub-heading"))

       inserts_menu.add_command(label="Insert Heading 3", command=lambda: self.insert_gemtext("### Sub-subheading"))

       inserts_menu.add_command(label="Insert Link", command=self.insert_link)

       inserts_menu.add_command(label="Insert Quote", command=self.insert_quote)

       inserts_menu.add_command(label="Insert Preformatted Text", command=self.insert_preformatted_text)

       inserts_menu.add_command(label="Insert List Item", command=lambda: self.insert_gemtext("* List Item"))

       inserts_menu.add_command(label="Insert Input Spartan Only", command=lambda: self.insert_gemtext("= input field"))

       inserts_menu.add_command(label="Save Version", command=self.save_version)

       inserts_menu.add_command(label="Revert to Last Version", command=self.revert_version)


       search_menu = tk.Menu(menu, bg='black', fg='green')

       menu.add_cascade(label="Search", menu=search_menu)

       search_menu.add_command(label="Search Current Blog", command=self.search_posts)


       about_menu = tk.Menu(menu, bg='black', fg='green')

       menu.add_cascade(label="About", menu=about_menu)

       about_menu.add_command(label="About This Program", command=self.show_about)


       self.root.grid_rowconfigure(3, weight=1)

       self.root.grid_columnconfigure(1, weight=1)


   def show_about(self):

       about_message = (

           "Lunar Orbit Logbook, Blog Writer for Gemini Protocol\n"

           "Program Created by Brian Lynn Bentley\n"

           "Ophesian Group copyright 2026\n"

           "This application simplifies the creation of Gemlogs, "

           "allowing users to write, save, and export their blog posts "

           "in various formats."

       )

       messagebox.showinfo("About", about_message)


   def bind_shortcuts(self):

       self.root.bind('<Control-n>', lambda event: self.new_file())

       self.root.bind('<Control-c>', lambda event: self.copy_text())

       self.root.bind('<Control-v>', lambda event: self.paste_text())

       self.root.bind('<Control-x>', lambda event: self.cut_text())

       self.root.bind('<Control-s>', lambda event: self.save_file())

       self.root.bind('<Control-o>', lambda event: self.load_file())

       self.root.bind('<Control-S>', lambda event: self.save_gopher_file())

       self.root.bind('<Control-L>', lambda event: self.load_gopher_file())


   def new_file(self):

       self.content_text.delete("1.0", tk.END)

       self.title_var.set("")

       self.author_var.set("")

       self.date_var.set("")

       self.bio_var.set("")

       self.keywords_var.set("")

       self.tags_var.set("")

       self.word_count_label.config(text="Word Count: 0")


   def copy_text(self):

       self.root.clipboard_clear()

       text = self.content_text.get(tk.SEL_FIRST, tk.SEL_LAST)

       self.root.clipboard_append(text)


   def paste_text(self):

       try:

           text = self.root.clipboard_get()

           self.content_text.insert(tk.INSERT, text)

       except tk.TclError:

           messagebox.showwarning("Paste Error", "No text in clipboard to paste.")


   def cut_text(self):

       self.copy_text()

       self.content_text.delete(tk.SEL_FIRST, tk.SEL_LAST)


   def update_word_count(self, event=None):

       content = self.content_text.get("1.0", tk.END).strip()

       word_count = len(content.split()) if content else 0

       self.word_count_label.config(text=f"Word Count: {word_count}")


   def insert_gemtext(self, gemtext):

       self.content_text.insert(tk.END, f"{gemtext}\n")


   def insert_quote(self):

       quote = simpledialog.askstring("Insert Quote", "Enter your quote:")

       if quote:

           self.insert_gemtext(f"> {quote}")


   def insert_preformatted_text(self):

       preformatted_text = simpledialog.askstring("Insert Preformatted Text", "Enter your preformatted text:")

       if preformatted_text:

           self.insert_gemtext(f"```\n{preformatted_text}\n```")


   def insert_link(self):

       url = simpledialog.askstring("Insert Link", "Enter the URL:")

       label = simpledialog.askstring("Insert Link", "Enter the link label:")

       if url and label:

           self.content_text.insert(tk.END, f"=> {url}\t{label}\n")


   def save_file(self):

       content = self.get_content()

       if self.validate_fields():

           file_path = filedialog.asksaveasfilename(defaultextension=".gmi", filetypes=[("Gemini Files", "*.gmi")])

           if file_path:

               try:

                   with open(file_path, 'w') as file:

                       file.write(content)

                   self.save_as_json()

                   messagebox.showinfo("Success", "File saved successfully!")

               except IOError as e:

                   messagebox.showerror("File Error", f"An error occurred while saving the file: {e}")


   def load_file(self):

       file_path = filedialog.askopenfilename(filetypes=[("Gemini Files", "*.gmi")])

       if file_path and os.path.isfile(file_path):

           try:

               with open(file_path, 'r') as file:

                   content = file.read()

               self.set_content(content)

               messagebox.showinfo("Success", "File loaded successfully!")

           except IOError as e:

               messagebox.showerror("File Error", f"An error occurred while loading the file: {e}")


   def save_gopher_file(self):

       content = self.get_content()

       if self.validate_fields():

           file_path = filedialog.asksaveasfilename(defaultextension=".gopher", filetypes=[("Gopher Files", "*.gopher")])

           if file_path:

               try:

                   with open(file_path, 'w') as file:

                       file.write(content)

                   messagebox.showinfo("Success", "Gopher file saved successfully!")

               except IOError as e:

                   messagebox.showerror("File Error", f"An error occurred while saving the Gopher file: {e}")


   def load_gopher_file(self):

       file_path = filedialog.askopenfilename(filetypes=[("Gopher Files", "*.gopher")])

       if file_path and os.path.isfile(file_path):

           try:

               with open(file_path, 'r') as file:

                   content = file.read()

               self.set_content(content)

               messagebox.showinfo("Success", "Gopher file loaded successfully!")

           except IOError as e:

               messagebox.showerror("File Error", f"An error occurred while loading the Gopher file: {e}")


   def get_content(self):

       return f"## {self.title_var.get()}\n" \

              f"Author: {self.author_var.get()}\n" \

              f"Date: {self.date_var.get()}\n" \

              f"Bio: {self.bio_var.get()}\n" \

              f"Keywords: {self.keywords_var.get()}\n" \

              f"Tags: {self.tags_var.get()}\n\n" \

              f"{self.content_text.get('1.0', tk.END)}"


   def set_content(self, content):

       lines = content.splitlines()

       self.title_var.set(lines[0][3:])

       self.author_var.set(lines[1][7:])

       self.date_var.set(lines[2][6:])

       self.bio_var.set(lines[3][5:])

       self.keywords_var.set(lines[4][9:])

       self.tags_var.set(lines[5][6:])

       self.content_text.delete("1.0", tk.END)

       self.content_text.insert(tk.END, "\n".join(lines[7:]))


   def export_as_markdown(self):

       content = self.get_content()

       file_path = filedialog.asksaveasfilename(defaultextension=".md", filetypes=[("Markdown Files", "*.md")])

       if file_path:

           try:

               with open(file_path, 'w') as file:

                   file.write(content)

               messagebox.showinfo("Success", "File exported as Markdown successfully!")

           except IOError as e:

               messagebox.showerror("File Error", f"An error occurred while exporting the file: {e}")


   def export_as_html(self):

       content = self.get_content()

       html_content = f"<html><body><h1>{self.title_var.get()}</h1><p>Author: {self.author_var.get()}</p><p>Date: {self.date_var.get()}</p><p>Bio: {self.bio_var.get()}</p><p>Keywords: {self.keywords_var.get()}</p><p>Tags: {self.tags_var.get()}</p><div>{self.content_text.get('1.0', tk.END)}</div></body></html>"

       file_path = filedialog.asksaveasfilename(defaultextension=".html", filetypes=[("HTML Files", "*.html")])

       if file_path:

           try:

               with open(file_path, 'w') as file:

                   file.write(html_content)

               messagebox.showinfo("Success", "File exported as HTML successfully!")

           except IOError as e:

               messagebox.showerror("File Error", f"An error occurred while exporting the file: {e}")


   def save_as_json(self):

       json_data = {

           "title": self.title_var.get(),

           "author": self.author_var.get(),

           "date": self.date_var.get(),

           "bio": self.bio_var.get(),

           "keywords": self.keywords_var.get().split(','),

           "tags": self.tags_var.get().split(','),

           "content": self.content_text.get('1.0', tk.END).strip()

       }

       json_file_path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON Files", "*.json")])

       if json_file_path:

           try:

               with open(json_file_path, 'w') as json_file:

                   json.dump(json_data, json_file, indent=4)

               messagebox.showinfo("Success", "File saved as JSON successfully!")

           except IOError as e:

               messagebox.showerror("File Error", f"An error occurred while saving the JSON file: {e}")


   def save_version(self):

       self.versions.append(self.get_content())

       messagebox.showinfo("Version Saved", "Current version saved successfully!")


   def revert_version(self):

       if self.versions:

           last_version = self.versions.pop()

           self.set_content(last_version)

           messagebox.showinfo("Version Reverted", "Reverted to the last saved version.")

       else:

           messagebox.showwarning("No Versions", "No versions available to revert.")


   def search_posts(self):

       search_term = simpledialog.askstring("Search Current Document", "Enter keyword to search:")

       if search_term:

           content = self.content_text.get('1.0', tk.END)

           start_index = '1.0'

           self.content_text.tag_remove('highlight', '1.0', tk.END)

           

           while True:

               start_index = self.content_text.search(search_term, start_index, nocase=True, stopindex=tk.END)

               if not start_index:

                   break

               end_index = f"{start_index}+{len(search_term)}c"

               self.content_text.tag_add('highlight', start_index, end_index)

               start_index = end_index

           

           self.content_text.tag_config('highlight', background='yellow')

           if self.content_text.index('highlight.first') != '1.0':

               self.content_text.see('highlight.first')

               messagebox.showinfo("Search Result", f"Keyword '{search_term}' found in the post.")

           else:

               messagebox.showinfo("Search Result", f"Keyword '{search_term}' not found.")


   def validate_fields(self):

       if not self.title_var.get().strip():

           messagebox.showwarning("Validation Error", "Title cannot be empty.")

           return False

       if not self.author_var.get().strip():

           messagebox.showwarning("Validation Error", "Author cannot be empty.")

           return False

       if not self.date_var.get().strip():

           messagebox.showwarning("Validation Error", "Date cannot be empty.")

           return False

       if not self.bio_var.get().strip():

           messagebox.showwarning("Validation Error", "Bio cannot be empty.")

           return False

       return True


if __name__ == "__main__":

   root = tk.Tk()

   app = LunarOrbitLogbook(root)

   root.mainloop()


Report Page