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