gempost/gempost

482 lines
18 KiB
Plaintext
Raw Normal View History

2022-05-28 18:51:54 +00:00
#!/usr/bin/python
2022-05-29 18:02:05 +00:00
# gempost - experimental gemlog manager for tilde.team, written by ~desertmouse
2022-05-28 18:51:54 +00:00
from os import system as exec
from os import listdir
from os import getlogin
2022-05-29 18:02:05 +00:00
2022-05-28 18:51:54 +00:00
from sys import argv
2022-05-29 18:02:05 +00:00
2022-05-28 18:51:54 +00:00
from hashlib import sha1
from random import random
import curses
from math import ceil
2022-05-28 18:51:54 +00:00
# colored output functions
def prRed(skk): print("\033[91m {}\033[00m" .format(skk))
def prGreen(skk): print("\033[92m {}\033[00m" .format(skk))
def prYellow(skk): print("\033[93m {}\033[00m" .format(skk))
def prPurple(skk): print("\033[95m {}\033[00m" .format(skk))
# main variable declarations
default_post_page_header = """
# posts
"""
wdir = f"/home/{getlogin()}/public_gemini/" # working/main directory
postDir = wdir + "postdir/" # all post files along with the archive.gmi are stored here
indexFile = wdir + "postIndex" # the index is maintained in this file
2022-06-01 18:32:23 +00:00
editor = "nano --restricted -t"
2022-05-28 18:51:54 +00:00
def menuFunction(menu = []):
stdscr = curses.initscr() # initializing curses
curses.noecho()
curses.cbreak()
stdscr.keypad(True)
sy, sx = stdscr.getmaxyx() # getting terminal dimensions
pageLength = sy - 5 # no. of items I can show in a single page.
pages = ceil(len(menu)/pageLength) + 1 # calculating the no. of pages required
pageIndex = 1 # current page
imenu = [] # internal list that is modified to only contain items of a specific page
endOfPage = pageLength # separate variable is required for this because the last page might have lesser entries than {pageLength}
imenu = menu[((pageLength)*(pageIndex-1)):(pageLength*pageIndex)]
if (menu == []): # do nothing if list is empty
curses.endwin()
print("\nYou don't have any posts yet.")
return None
highlight, highlight_prev = 0, 0 # variable that controls which entry is selected, variable that stores its previous value
while True:
stdscr.clear()
if (highlight >= endOfPage): # incrementing page number
if (pageIndex == pages - 1) or (pageIndex == 1):
highlight = highlight_prev
pass
else:
pageIndex += 1 # incrementing page index
imenu = [] # resetting imenu
try:
imenu = menu[((pageLength)*(pageIndex-1)):(pageLength*pageIndex)] # only {pageLength} no. of items
except IndexError:
try:
imenu = menu[((pageLength)*(pageIndex-1)):]
except IndexError:
pass
endOfPage = len(imenu)
highlight = 0
if (highlight < 0): # decrementing page number
if (pageIndex == 1):
highlight = highlight_prev
pass
else:
pageIndex -= 1
imenu = []
try:
imenu = menu[((pageLength)*(pageIndex-1)):(pageLength*pageIndex)]
except IndexError:
pass
endOfPage = len(imenu)
highlight = endOfPage - 1
stdscr.addstr("Use the arrow keys to make a selection. Press 'c' to cancel.\n")
for sno in range(len(imenu)):
if (sno == highlight):
stdscr.addstr(f"\n{((pageLength)*(pageIndex-1)) + (sno+1)} - {imenu[sno]}", curses.A_STANDOUT)
else:
stdscr.addstr(f"\n{((pageLength)*(pageIndex-1)) + (sno+1)} - {imenu[sno]}")
sno += 1
stdscr.addstr(f"\n\npage {pageIndex} / {pages - 1}\n") # a line on the bottom showing page number
stdscr.refresh()
c = stdscr.getch()
if (c == curses.KEY_DOWN):
highlight_prev = highlight
highlight += 1
elif (c == curses.KEY_UP):
highlight_prev = highlight
highlight -= 1
elif (c == curses.KEY_ENTER or c == 10 or c == 13):
break
elif (c == ord('c')):
highlight = None
break
curses.endwin()
if (highlight != None):
return (pageLength * (pageIndex-1)) + highlight
else:
return highlight
def checkInit():
""" Checks if the public_gemini/ directory has been initialized for use with gempost or is missing some files. """
contents = listdir(wdir)
valid_contents = ["postIndex","trash","postdir"]
validity = 0
missing = []
for i in contents:
if i in valid_contents:
validity += 1
else:
missing.append(i)
if (validity != len(valid_contents)):
if (validity == 0):
prRed(f'\n Your "public_gemini/" directory has not been initialized yet. Use "gempost qs" to read the quickstart.')
else:
prRed(f'\n Your "public_gemini/" directory is missing some required files and directories / has not been properly initialized.\n\n The following files/directories are missing: {missing}\n Validity score: {validity}/{len(valid_contents)}')
exit()
2022-05-28 18:51:54 +00:00
def rebuildReferences(filename = None, title = None, callingFrom = "default"):
""" reads {wdir}postIndex and rebuilds the {wdir}posts.gmi and {postDir}archive.gmi files """
customized = False
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
if (len(allposts) == 0): # if postIndex is empty, there is nothing to to
# reset posts.gmi and delete archive.gmi
# ie, customize the function
customized = True
headerText = []
try:
2022-06-01 18:32:23 +00:00
with open(f"{wdir}postPageHeader.gmi",'r') as h: # the file postPageHeader.gmi should be defined by the user in {wdir}. It should contain the user's custom post page header
2022-05-28 18:51:54 +00:00
headerText = h.readlines()
except FileNotFoundError: # If user hasn't created that file, a default line "# posts" is added to posts.gmi
headerText.append(default_post_page_header)
if (customized == True): # if postIndex was empty, then do not do more than this. Just remove archive.gmi after this
exec(f"rm {postDir}archive.gmi") # delete archive.gmi
with open(f"{wdir}posts.gmi",'w') as po: # writing the header into posts.gmi
po.writelines(headerText)
return 0
headerText.append("\n## Latest\n")
if (callingFrom == "delete"): # the function's callingFrom switch is to perform special operations based on where it is invoked from
headerText.append(f"{allposts[-1]}")
else:
headerText.append(f"=> postdir/{filename}.gmi {title}\n")
headerText.append("\n## Recent\n") # adding the five most recent posts to posts.gmi
try:
counter = 0
for i in allposts[::-1]:
if (counter == 5):
break
headerText.append(i)
counter += 1
except IndexError:
pass
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
headerText.append("\n## Older\n") # for older entries, refer to another file, archive.gmi, which is basically a reversed copy of {indexFile} to show the most recent posts on top
archiveLines = [] # archive.gmi needs just ./filenames instead of postdir/filenames
for i in allposts[::-1]: # storing a modified file list with just the filenames as links
archiveLines.append(f"=> ./{i[11:]}")
2022-05-28 18:51:54 +00:00
with open(f"{postDir}archive.gmi","w") as ar: # creating all posts list
ar.writelines(archiveLines)
2022-05-28 18:51:54 +00:00
headerText.append(f"=> postdir/archive.gmi Posts Archive")
with open(f"{wdir}posts.gmi",'w') as po: # finally writing into posts.gmi to "bring it all together"
po.writelines(headerText)
def newpost(title, existing_content = None):
modified = False
""" Write and submit a new post """
if (existing_content == None or existing_content == []):
pass
else:
modified = True # modify the function if doing this for existing file
filename = sha1(str(f"{title}{random()}").encode("utf-8")).hexdigest() # generating a unique filename
exec(f"touch {wdir + filename}.gmi") # creating empty file
ch = input("Add an ASCII Art header to your post? (define it in blogpostHeader.gmi): ([y]/n) ")
if (ch == 'n' or ch == 'N'):
pass
else:
try:
with open(f"{wdir}blogpostHeader.gmi") as a: # checking if header for each post has been defined
postHeader = a.readlines()
2022-06-01 18:32:23 +00:00
#postHeader.append('\n')
with open(f"{wdir + filename}.gmi",'w') as f:
f.writelines(postHeader)
except FileNotFoundError:
postHeader = ""
2022-06-01 13:17:09 +00:00
2022-05-28 18:51:54 +00:00
if modified: # add content to file using the existing_content list instead of opening editor
with open(f"{wdir+filename}.gmi", 'w') as o:
o.writelines(existing_content)
else:
2022-06-01 18:32:23 +00:00
temp = "# " + title
exec(f'echo "{temp}" | cat >> {wdir + filename}.gmi') # adding title to empty file
2022-05-28 18:51:54 +00:00
exec(f"{editor} {wdir+filename}.gmi") # opening editor
ch = input("\nWould you like to review before posting? (c to cancel) (y/[n]): ")
if (ch == 'y' or ch == 'Y'):
exec(f"less {wdir+filename}.gmi") # opening the file in editor for review
exec(f"mv {wdir+filename}.gmi {postDir+filename}.gmi") # moving file to postdir
elif (ch == 'c'):
exec(f"mv {wdir+filename}.gmi {wdir}trash/")
print("Cancelled")
return None
else:
exec(f"mv {wdir+filename}.gmi {postDir+filename}.gmi") # moving file to postdir without review
# updating postIndex
exec(f'echo "=> postdir/{filename}.gmi {title}" | cat >> {indexFile}')
# updating posts.gmi, preserving the user's custom header - rbfn begin
rebuildReferences(filename, title)
# changing file permissions
exec(f"chmod 745 {wdir}posts.gmi")
exec(f"chmod 745 {wdir}postdir && chmod 745 {wdir}postdir/*")
2022-06-01 18:32:23 +00:00
prGreen("\n Your post is live!") # yay?
2022-05-28 18:51:54 +00:00
def manage():
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
#if (len(allposts) == 0): # exit function if no files in index
#return 0
2022-05-28 18:51:54 +00:00
postsList = []
2022-06-01 18:32:23 +00:00
for i in allposts: # creating a list containing titles corresponding to filenames
2022-05-28 18:51:54 +00:00
postsList.append([i[56:-1],i[11:51]])
menuItems = []
for i in postsList: # formatting it into a string list for the ncurses menu function
menuItems.append(f"{i[0]} | {i[1]}")
which = menuFunction(menuItems)
if (which == None): # menuFunction returns None if the list supplied to it is empty
print("\nCancelled.")
return 0
print(f"\nYou have selected:\n\nTitle: {postsList[which][0]}\nFilename: {postsList[which][1]}.gmi")
2022-05-28 18:51:54 +00:00
try:
mode = int(input("\nSELECT MODE:\n1 - EDIT\n2 - DELETE\n3 - CANCEL\n-> ")) # what does the user want to do with the selection?
except ValueError:
print("\nCancelling...")
return 0
if (mode == 1): # if mode is EDIT, open editor with the post file
print(f'\nEditing "{postsList[which][0]}" ...')
exec(f"{editor} {postDir}{postsList[which][1]}.gmi")
prGreen("Post updated.")
elif (mode == 2): # for DELETE mode, first delete post file, delete its reference from {wdir}postIndex, and rebuild the {wdir}posts.gmi and {postDir}archive.gmi files
exec(f"mv {postDir}{postsList[which][1]}.gmi {wdir}trash/")
print(f"\nfile moved to trash")
exec(f"sed -i '/{postsList[which][1]}.gmi/d' {indexFile}")
print(f"postIndex updated")
rebuildReferences(None, None, "delete")
print("rebuilt posts.gmi, archive.gmi")
2022-05-28 18:51:54 +00:00
prGreen("All done.")
elif (mode == 3):
2022-05-29 18:02:05 +00:00
print("\nCancelled.")
2022-05-28 18:51:54 +00:00
else:
pass
# information strings
2022-05-29 18:02:05 +00:00
helpText = f"""
2022-06-01 12:50:12 +00:00
gempost (v0.6) - *experimental* gemlog manager for tildes
2022-05-28 18:51:54 +00:00
Available arguments:
post - create a new post
Usage:
post (without arguments) Write a blog post from scratch.
post [path to file without braces] Post from .gmi file. First line will be used as heading.
manage - edit or delete your posts
purge - premanently delete the files in trash/
init - set up your public_gemini/ directory for gempost
reset - delete all posts and re-initialize public_gemini/
help - display this help text
desc - a brief description of how the program works
2022-05-29 18:02:05 +00:00
\033[93mqs - Quick Start for new users\033[00m
2022-05-28 18:51:54 +00:00
"""
quickStart = """
Basically, gempost manages a "posts.gmi" file and maintains it in a clean format
such that it shows your latest post, your five most recent posts, along with
an archive of all your posts, arranged from newest to oldest.
Doing all this manually would be *very* tedious.
If you are an existing user, please back up your public_gemini/ directory and then
proceed with the given steps.
Quickstart:
STEP 1
2022-06-01 13:17:09 +00:00
Wih only your index.gmi file in public_gemini/ , run the following to set everyting up:
2022-05-28 18:51:54 +00:00
gempost init
STEP 2
gempost doesn't touch your index.gmi file, so you will have to manually add a link to "posts.gmi"
To do that, just add the following line to your index.gmi file:
=> ./posts.gmi Posts
CONGRATULATIONS!
You are ready to use gempost. Please just use "gempost help" from now on to only get a list of commands.
2022-06-01 13:17:09 +00:00
You may run "gempost desc" to get to know a bit more about how gempost works, along with two optional features:
- postPageHeader.gmi , and
- blogpostHeader.gmi
2022-05-28 18:51:54 +00:00
"""
description = """
Directory Structure:
gempost expects the following structure of your ~/public_gemini/ directory:
public_gemini/
├── index.gmi
├── postdir/
├── postIndex
├── postPageHeader.gmi (optional)
2022-06-01 13:17:09 +00:00
├── blogpostHeader.gmi (optional)
2022-05-28 18:51:54 +00:00
├── posts.gmi
└── trash/
2022-06-01 18:32:23 +00:00
2 directories, 5 files
2022-05-28 18:51:54 +00:00
- "postdir/" is where all your gemlog posts are stored.
- "postIndex" is the file that gempost uses to maintain an index of your posts.
2022-06-01 13:17:09 +00:00
- "postPageHeader.gmi" should contain a text banner that will show on your Posts page.
- "blogpostHeader.gmi" contains a text banner that will show on top of each of your posts.
2022-05-28 18:51:54 +00:00
- "posts.gmi" is where the program will maintain links to:
- your latest post
- your 5 most recent posts
- a link to "archive.gmi" that contains a list of all your posts
2022-06-01 13:17:09 +00:00
2022-05-28 18:51:54 +00:00
- "trash/" is the directory where gempost keeps your deleted posts. use "gempost purge" to delete its contents.
"""
# Process command-line arguments
try:
arg = argv[1]
except IndexError:
print(helpText) # print the help text if no arguments passed
exit()
if (arg == "help" or arg == "-h" or arg == "--help"): # help text listing all available arguments
print(helpText)
elif (arg == "desc"): # description of how the program uses different files and dierctories
print(description)
elif (arg == "qs"):
print(quickStart) # a simple 2-step quick start for new users
elif (arg == "post"):
checkInit()
2022-05-28 18:51:54 +00:00
if (len(argv) == 3): # checking if more than 1 arg (except python) to see if source file has been supplied
filePath = argv[2] # storing that file's path in a var
print(f"Using file {filePath}") # tell the user that the program is using a source file
exiting_content, firstLine = [], "" # content of the file, and title respectively
try:
with open(filePath, 'r') as f: # populating file content and title variables
existing_content = f.readlines()
firstLine = existing_content[0][2:] # first line, used as title
if (firstLine[-1] == '\n'): # if there is a NewLine character at the end of title line (probably will be), remove it from post title
firstLine = firstLine[:-1]
if (len(firstLine) == 0): # message and exit if title empty
prRed("Post title cannot be empty.")
exit()
else:
newpost(firstLine, existing_content)
except FileNotFoundError:
prRed("Invalid file path / file does not exist.")
else:
print("\nAfter entering a title, an editor will be launched for you to write your blog post.")
2022-06-01 18:32:23 +00:00
print("The process will continue after you exit the editor (Ctrl+X).")
2022-05-28 18:51:54 +00:00
title = input("\nPOST TITLE: ")
if (len(title) == 0):
prRed("Post title cannot be empty.")
else:
2022-05-29 18:02:05 +00:00
newpost(title)
2022-05-28 18:51:54 +00:00
elif (arg == "manage"):
checkInit()
2022-05-28 18:51:54 +00:00
manage()
elif (arg == "purge"): # permanently delete trashed posts
checkInit()
2022-05-28 18:51:54 +00:00
if (len(listdir(f"{wdir}trash/")) == 0):
2022-05-29 18:02:05 +00:00
prYellow("Trash is already empty.")
2022-05-28 18:51:54 +00:00
else:
exec(f"rm {wdir}trash/*")
prGreen("Trash cleared.")
elif (arg == "init"): # initialize public_gemini/ for use with gempost
2022-06-01 18:32:23 +00:00
if (len(listdir(f"{wdir}")) <= 1):
2022-05-28 18:51:54 +00:00
exec(f"touch {wdir}postIndex && mkdir {wdir}postdir/ {wdir}trash/ && chmod 745 {wdir}* && chmod 700 {wdir}trash/")
ch = input("Also create index.gmi ? (y/[n]) ")
if (ch == 'y' or ch == 'Y'):
index_init = [f"# {getlogin()}'s page\n", "\nThis is your homepage. You can customize it however you want by editing this file.\n", "\nJust remember keeping around the link below to point your visitors to your posts!\n", "\n=> ./posts.gmi Posts"]
exec(f"touch {wdir}index.gmi")
with open(f"{wdir}index.gmi", 'w') as i:
i.writelines(index_init)
2022-05-28 18:51:54 +00:00
prGreen('Done. Use "gempost help" to view available arguments.')
else:
2022-06-01 13:17:09 +00:00
prRed('Only your "index.gmi" should be there in public_gemini/')
2022-05-28 18:51:54 +00:00
elif (arg == "reset"): # delete all posts and re-initialize public_gemini/ for use with gempost
prRed("Warning: This will delete all your posts.")
if (input('Type "yes, I understand" to continue: ') == "yes, I understand"):
exec(f"rm -rf {wdir}postdir/ {wdir}trash/ && rm -f {wdir}posts.gmi {indexFile}")
exec(f"touch {wdir}postIndex && mkdir {wdir}postdir/ {wdir}trash/ && chmod 745 {wdir}* && chmod 700 {wdir}trash/")
ch = input("Also create index.gmi ? (y/[n]) ")
if (ch == 'y' or ch == 'Y'):
index_init = [f"# {getlogin()}'s page\n", "\nThis is your homepage. You can customize it however you want by editing this file.\n", "\nJust remember keeping around the link below to point your visitors to your posts!\n", "\n=> ./posts.gmi Posts"]
exec(f"touch {wdir}index.gmi")
with open(f"{wdir}index.gmi", 'w') as i:
i.writelines(index_init)
2022-05-28 18:51:54 +00:00
prYellow("public_gemini/ has been re-initialized successfully.")
else:
print("Cancelled.")
else:
print(f"Unknown argument '{arg}'")