From 3098d319899644f9121bc4cc585ec372321b80ae Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 29 May 2022 00:21:54 +0530 Subject: [PATCH] Initial commit. --- README.md | 34 ++++++ gempost | 330 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 README.md create mode 100755 gempost diff --git a/README.md b/README.md new file mode 100644 index 0000000..6dc39c3 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# gempost +An **experimental** gemlog manager for tildes. + +# Quick Start +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. + +**STEP 1** +Wih only your *index.gmi* file in **public_gemini/** directory (and optionally also *postPageHeader.gmi*), run the following to set everyting up: +`gempost init` + +Note that `gempost init` won't work if you don't have it added to your path. For tildeverse members, you can just copy the executable to you **~/bin/** directory. Otherwise, you can just do `./gempost ` instead of `gempost `. + +**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. + +# Notes +This software is in its early stages and **I am not responsible if you mess up your gemlog using this program.** It works rather fine, it is something I intend to keep for personal use for now. Although if you are comfortable, I invite you to try it out. To make it perfect, I will have to use it for a long term and work out the long-term posts storage solution. + +Personally, I am happy with the set of features I currently have and will work out the bugs as I find them. + +**This software is provided without any guarantees.** I might not even check this page again or even delete this repository out of anxiety xD diff --git a/gempost b/gempost new file mode 100755 index 0000000..9a9ac08 --- /dev/null +++ b/gempost @@ -0,0 +1,330 @@ +#!/usr/bin/python + +# gempost - gemlog manager for tilde.team, written by ~desertmouse + +from os import system as exec +from os import listdir +from os import getlogin +from sys import argv +from hashlib import sha1 +from random import random + +# 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 +editor = "nano" + +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: + with open(f"{wdir}postPageHeader.gmi",'r') as h: # the file postPageHeader should be defined by the user in {wdir}. It should contain the user's custom post page header + 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 + + with open(f"{postDir}archive.gmi","w") as ar: # creating all posts list + ar.writelines(allposts[::-1]) + + 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 + + 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: + temp = "# " + title # adding title to empty file + exec(f'echo "{temp}" | cat > {wdir + filename}.gmi') + + 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/*") + + #print("\nYour post is live!") # yay? + prGreen("\nYour post is live!") # yay? + +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 + + postsList = [] + for i in allposts: + postsList.append([i[56:-1],i[11:51]]) + + print() # line break + try: + counter = 0 + for i in postsList: + print(f"{counter}. {i[0]} | {i[1]}") + counter += 1 + which = int(input("Select which post to manage: ")) # getting the index of the menu entry the user wants to edit + temp = allposts[which] + except: + prRed("\nInvalid selection. Exiting...") + return 0 + 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 post.gmi, archive.gmi") + prGreen("All done.") + elif (mode == 3): + print("\nReturning to main menu...") + else: + pass + + + +# information strings +helpText = """ +gempost (v0.6ish) - gemlog manager for tildes + +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 + + qs - Quick Start for new users +""" + +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 + Wih only your index.gmi file in public_gemini/ (and optionally "postPageHeader.gmi"), run the following to set everyting up: + + 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. + + You may run "gempost desc" to get to know a bit more about how gempost works, along with a single optional feature. +""" + +description = """ +Directory Structure: + gempost expects the following structure of your ~/public_gemini/ directory: + + public_gemini/ + ├── index.gmi + ├── postdir/ + ├── postIndex + ├── postPageHeader.gmi (optional) + ├── posts.gmi + └── trash/ + + 2 directories, 4 files + + - "postdir/" is where all your gemlog posts are stored. + - "postIndex" is the file that gempost uses to maintain an index of your posts. + - "postPageHeader.gmi" is a file that you can populate with your custom ascii artwork or other text. + This will come before the auto-generated links to your posts written by gempost. + Note that this is optional. + - "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 + - "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"): + 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.") + print("The process will continue after you save your post (Ctrl+S) and exit the editor (Ctrl+X).") + title = input("\nPOST TITLE: ") + if (len(title) == 0): + prRed("Post title cannot be empty.") + else: + newpost(input("\nPOST TITLE: ")) + +elif (arg == "manage"): + manage() + +elif (arg == "purge"): # permanently delete trashed posts + if (len(listdir(f"{wdir}trash/")) == 0): + prYellow("Trash is empty.") + else: + exec(f"rm {wdir}trash/*") + prGreen("Trash cleared.") + +elif (arg == "init"): # initialize public_gemini/ for use with gempost + if (len(listdir(f"{wdir}")) <= 2): + exec(f"touch {wdir}postIndex && mkdir {wdir}postdir/ {wdir}trash/ && chmod 745 {wdir}* && chmod 700 {wdir}trash/") + prGreen('Done. Use "gempost help" to view available arguments.') + else: + prRed('Only your "index.gmi" (and optionally "postPageHeader.gmi") should be there in public_gemini/') + +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/") + prYellow("public_gemini/ has been re-initialized successfully.") + else: + print("Cancelled.") + +else: + print(f"Unknown argument '{arg}'") + +