You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

451 lines
15 KiB

#!/usr/bin/env python3
# edlog++ for rawtext.club by samhunter
# https://git.rawtext.club/samhunter/edlogxx
with_markdown = True
wrapwidth = 80
import glob
import os
import datetime
import argparse
import re
import textwrap as tw
import subprocess
import shutil
import readline
if with_markdown:
from rich.console import Console
from rich.markdown import Markdown
parser = argparse.ArgumentParser(description='Shlog ("shell log") reader.')
#parser.add_argument("-k", "--keyword", help="Comma separated list of keywords to include in the list", type=lambda x:x.split(","))
parser.add_argument("-m", "--markdown", help="Format shlogs using MarkDown", action="store_true")
parser.add_argument("-p", "--pager", help="Use external pager", action="store_true")
parser.add_argument("-u", "--user", help="Comma separated list of usernames to include in the list", type=lambda x:x.split(","))
parser.add_argument("-w", "--wrap", help="Wrap text", action="store_true")
parser.add_argument("--since", metavar="TIME", help="Show items newer than TIME, provided in Unix time format")
args = parser.parse_args()
def display_help():
print("""
edlog++
Commands
========
Four-digits number (eg. 0001) followed by Enter directly selects a post.
? - help
m - (make) new post
n - next (older) post
p - previous (newer) post
r - reply
. - (or Enter) redisplay current post
h - redisplay headers list
H - reload and redisplay headers list
z - load next screenful of headers
g - go to first post
G - go to last post
/ - search in the titles
q - quit
> - increases text width by 4 characters (in `wrap` mode)
< - decreases text width by 4 characters (in `wrap` mode)
!command - execute `command`. Accepts optional parameters:
- {f} - shlog file
- {t} - shlog title
- {a} - shlog author
- {d} - shlog date
Options
=======
:wrap
:nowrap - toggle wrapping
:pager
:nopager - toggle external pager
:md
:nomd - toggle MarkDown formatting
""")
def get_files():
services = dict([
('shlog','.shlog/20[0-9][0-9]-[01][0-9]-[0123][0-9][-_]*.txt')
])
ret = []
users = [[x, "/home/{}".format(x)] for x in os.listdir("/home/") if os.access("/home/"+x, os.R_OK)]
if args.user:
users = filter(lambda x : x[0] in args.user, users)
for user in users:
if not os.path.isdir(user[1]):
continue
for service in services.keys():
files = glob.glob(user[1] + '/' + str(services[service]))
for filepath in files:
try:
if os.path.isdir(filepath):
filepath =max(glob.glob(filepath+'/*', recursive=True), key=os.path.getmtime)
ret.append( (filepath, user[0], service) )
else:
ret.append( (filepath, user[0], service) )
except:
continue
return ret
def get_title(shlogfile):
z = re.match("^.*\/\d\d\d\d-\d\d\-\d\d[-_](.*).txt", shlogfile)
if z:
a = z.group(1).replace("_"," ").title()
return a
def reply(entry=None):
homedir = os.path.expanduser("~")
shlogdir = os.path.join(homedir, ".shlog")
tempdir = os.getenv("TEMP", os.path.join(homedir, "tmp"))
if not os.path.isdir(shlogdir) or not os.access(shlogdir, os.W_OK):
os.mkdir(shlogdir,0o755)
# if there's no tmp folder - use the shlog folder
if not os.path.isdir(tempdir) or not os.access(tempdir, os.W_OK):
tempdir = shlogdir
replydate = datetime.datetime.now().strftime("%Y-%m-%d")
if entry:
file = entry[1][0]
z = re.match("^.*\/\d\d\d\d-\d\d\-\d\d[-_](.*).txt", file)
if z:
filename = z.group(1)
title = get_title(file)
if re.match(r're:', filename.lower()):
replyfile = f'{replydate}-{filename}.txt'.replace(" ","_")
replytitle = title
else:
replyfile = f'{replydate}-Re:_{filename}.txt'.replace(" ","_")
replytitle = "Re: " + title
shlogfile = os.path.join(shlogdir, replyfile)
tempfile = os.path.join(tempdir, replyfile)
with open(file, "r") as orig:
with open(tempfile, "w") as reply:
reply.write(f'# {replytitle}\n\n')
for line in orig.readlines():
reply.write(f'> {line}')
reply.write(f'\nIn reply to: {file}')
reply.close()
orig.close()
else:
print(f'Invalid file name: {file}')
return
else:
replytitle = input("Title> ")
filename = replytitle.replace(" ","_").lower()
replyfile = f'{replydate}-{filename}.txt'
shlogfile = os.path.join(shlogdir, replyfile)
tempfile = os.path.join(tempdir, replyfile)
with open(tempfile, "w") as reply:
reply.write(f'# {replytitle}\n\n')
reply.close()
subprocess.call([editor, tempfile])
choice = input ("[P]ublish or [D]iscard? ")
if choice == "p":
if tempfile != shlogfile:
shutil.copy(tempfile, shlogfile)
os.chmod(shlogfile, 0o644)
return
def cat(shlog, wrap=False, internal_pager=True, md_format=False):
file = shlog[1][0]
if internal_pager == True:
title = get_title(file)
date = shlog[0]
author = shlog[1][1]
if wrap == True:
margin = 4
textwidth=min(wrapwidth, cols) - margin * 2
filler=" " * margin
else:
filler = ""
textwidth = cols
if os.access(file, os.R_OK):
link_re = r'(?:https?|gemini|spartan|file|ftp|gopher)://[^ \])]+'
links = []
if md_format == True:
lines = []
lines += ["-" * 6 ] # post separator
lines += [f'**Title: {title}** \n**Author: {author}** \n**Date: {date}** \n']
with open(shlog[1][0]) as f:
for line in f.readlines():
links += re.findall(link_re, line.rstrip())
lines += [line.rstrip()]
if len(links) > 0:
lines += [f' \n**Links**:\n']
lines += map( lambda x: "- " + x, links)
lines += [f' \n**Source**: *{file}*']
console.print(Markdown("\n".join(lines)))
else:
print("=" * cols)
print(f'\n{filler}##\n{filler}## \x1b[1mTitle: {title}\x1b[0m\n{filler}## Author: {author}\n{filler}## Date: {date}\n{filler}##\n')
with open(shlog[1][0]) as f:
for line in f.readlines():
line = line.rstrip()
links += re.findall(link_re, line)
if wrap==True:
lines = tw.fill(line,
width=textwidth,
initial_indent=filler,
subsequent_indent=filler,
fix_sentence_endings=True,
tabsize=8
)
print(lines)
else:
if len(line)>0 and line[0]==">":
print(f'\x1b[3m{line}\x1b[0m')
else:
print(line)
if len(links) > 0:
print(f'\n{filler}**Links**:')
for link in links:
print(f'{filler}\t{link}')
print(f'\n\n{filler}Source:\x1b[3m{file}\x1b[0m\n')
else:
if md_format == True:
console.print(f"[bold yellow on red]Shlog '{file}' not readable.[/bold yellow on red]")
else:
print(f"Shlog '{file}' not readable.")
else:
if re.match("^.*less$", pager):
options = "-ce"
subprocess.call([pager, options, file])
else:
subprocess.call([pager, file])
def headers(shlogs, start, end, pointer):
fmtWide = "{arrow:2s} {counter:04d} {date:16s} {user:^18s} {title:<s}"
fmtNarrow = "{arrow:2s} {counter:04d} {date:8s} {user:>10s} {title:<s}"
if start < 0:
start = 0
if end > len(shlogs):
end = len(shlogs)
for i in range(start,end):
item = shlogs[i]
date=item[0]
user=item[1][1]
service=item[1][2]
file=item[1][0]
counter = item[2]
if start == pointer:
arrow = "->"
else:
arrow = " "
if cols < 75:
title = tw.shorten(get_title(file), width=(cols-28))
print(fmtNarrow.format(counter=counter,date=date[2:10],user=user[0:10],title=title, arrow=arrow))
else:
title = tw.shorten(get_title(file), width=(cols-44))
user = tw.shorten(user[0:18], width=18)
print(fmtWide.format(counter=counter,date=date,user=user,title=title, arrow=arrow))
start = start + 1
if start == end:
break
def reload_from_disk():
out = []
for d in get_files():
service=d[2]
ftim = os.stat(d[0]).st_mtime
if ftim > ts:
st = datetime.datetime.fromtimestamp(ftim).strftime(timefmt)
out.append((st,d))
return out
def reload(from_disk=True, out=[]):
if from_disk:
out = reload_from_disk()
if len(out) == 0:
exit(1)
if len(out) > 0:
counter = 0
a = 0
shlogs = []
for item in sorted(out, reverse=True):
shlogs.append( ( item[0], item[1] , a))
a += 1
return shlogs
else:
return out
## Wrap lines in the internal pager
wrapped = False
if args.wrap == True:
wrapped = True
## Use internal pager
internal = True
if args.pager == True:
internal = False
## Use MarkDown formatting
formatted = False
if args.markdown == True:
formatted = True
if args.since:
ts = int(args.since)
else:
ts = 0 # since forever
##
pager = os.getenv('PAGER',"less")
editor = os.getenv('EDITOR',"vim")
timefmt = "%Y-%m-%d %H:%M"
try:
(cols, rows) = os.get_terminal_size()
except:
cols = int(os.environ.get('COLUMNS')) if os.environ.get('COLUMNS') else 80
rows = int(os.environ.get('LINES')) if os.environ.get('LINES') else 24
console_width = 80
if (cols < console_width):
console_width = cols
if with_markdown:
console = Console(width=console_width)
buffer = reload_from_disk()
shlogs = reload(False, buffer)
start = 0
pointer = start
height = rows - 1
end = start + height
headers(shlogs, start, end, pointer)
try:
while True:
cmds = input("> ").split(";")
for cmd in cmds:
cmd = cmd.strip()
if cmd == '':
if internal == True:
# Enter at the EOF exits less, so better not load it again ;)
cat(shlogs[pointer], wrap=wrapped, internal_pager=internal, md_format=formatted)
else:
continue
elif cmd[0] in 'n':
if pointer < (len(shlogs) - 1):
pointer += 1
cat(shlogs[pointer], wrap=wrapped, internal_pager=internal, md_format=formatted)
else:
print("At the oldest post.")
elif cmd[0] in 'p':
if pointer > 0:
pointer -= 1
cat(shlogs[pointer], wrap=wrapped, internal_pager=internal,md_format=formatted)
else:
print("At the newest post.")
elif cmd[0] in '/':
start = 0
end = height
pointer = start
rx = str(cmd[1:]).strip().lower()
print(f"[{rx}]")
shlogs = reload(False, list(filter( lambda shl: rx in get_title(shl[1][0]).lower(), buffer)))
# reload all headers when search result set is empty
if len(shlogs) == 0:
shlogs = reload(False, buffer)
headers(shlogs,start, end, pointer)
elif cmd[0] in 'h':
headers(shlogs, start, end, pointer)
elif cmd[0] in 'H':
shlogs = reload()
start = 0
pointer = start
height = rows - 1
end = start + height
headers(shlogs, start, end, pointer)
elif cmd[0] in '.':
cat(shlogs[pointer], wrap=wrapped, internal_pager=internal,md_format=formatted)
elif cmd[0] in 'g':
start = 0
end = height
pointer = start
headers(shlogs, start, end, pointer)
elif cmd[0] in 'G':
start = len(shlogs) - height
end = len(shlogs)
pointer = end - 1
headers(shlogs, start, end, pointer)
elif cmd[0] in 'z':
if end < len(shlogs):
start += height
end += height
pointer = start
headers(shlogs, start, end, pointer)
else:
pointer = start
print("On last screenful of posts.")
elif cmd[0] in 'r':
reply(shlogs[pointer])
shlogs = reload()
elif cmd[0] in 'm':
reply()
shlogs = reload()
elif cmd == ':wrap':
wrapped = True
elif cmd == ':nowrap':
wrapped = False
elif cmd == '>':
wrapwidth += 4
elif cmd == '<':
wrapwidth -= 4
elif cmd == ':pager':
internal = False
elif cmd == ':nopager':
internal = True
elif with_markdown and cmd == ':md':
formatted = True
elif with_markdown and cmd == ':nomd':
formatted = False
elif cmd[0] in '!':
file = shlogs[pointer][1][0]
title = get_title(file)
date = shlogs[pointer][0]
author = shlogs[pointer][1][1]
try:
scmd = cmd[1:]
oscmd = scmd.split(' ')
proc_oscmd = [];
for a in oscmd:
proc_oscmd.append(a.format(f=file, t=title, d=date, a=author))
subprocess.call(proc_oscmd)
except:
continue
elif re.match("^(?P<post>0*\d+)(\s*(?P<cmd>.*))*?$", cmd):
post_num = re.match("^(?P<post>0*\d+)(\s*(?P<cmd>.*))*?$", cmd)
pointer = min(int(shlogs[len(shlogs)-1][2]),int(post_num.group(1)))
if pointer >= len(shlogs)-1:
start = pointer - height - 1
end = len(shlogs)
else:
start = pointer
end = start + height
cat(shlogs[pointer], wrap=wrapped, internal_pager=internal,md_format=formatted)
elif cmd[0] in '?':
display_help()
elif cmd[0] in 'q':
exit(0)
else:
print(f'Unknown command "{cmd}".\nType "?" for help.')
continue
except (KeyboardInterrupt, EOFError):
pass
print("\nGoodbye!")
exit(0)