mirror of
https://git.ari.lt/ari.lt/blog.ari.lt.git
synced 2025-02-04 01:29:24 +01:00
init
Signed-off-by: Ari Archer <ari.web.xyz@gmail.com>
This commit is contained in:
commit
69a70b0b51
12 changed files with 768 additions and 0 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
@ -0,0 +1,11 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
tab_width = 2
|
||||
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
/b/
|
||||
*.min.css
|
||||
index.html
|
||||
.mypy_cache/
|
||||
.blog_history
|
||||
venv/
|
||||
|
35
LICENSE
Normal file
35
LICENSE
Normal file
|
@ -0,0 +1,35 @@
|
|||
ArAr General public license
|
||||
version 2
|
||||
|
||||
This license is an open source license with restrictions for commercial and corporate use.
|
||||
Any project under this license ...
|
||||
CANNOT be used commercially
|
||||
CANNOT be redistributed with any sort of fee
|
||||
CANNOT be redistributed in any proprietary form (binary, obfuscated, etc.)
|
||||
CANNOT be patented
|
||||
CANNOT change to any more proprietary license
|
||||
CANNOT depend on any proprietary code/code blobs/software
|
||||
CANNOT be used by any corporate individual, company or organisation
|
||||
|
||||
DOESN'T have any warranty
|
||||
|
||||
CAN be modified
|
||||
CAN be redistributed in an open source form
|
||||
CAN be used privately
|
||||
|
||||
|
||||
This license is non-corporate, individual use only.
|
||||
|
||||
The owner of this project holds NO LIABILITY if any unstable, redistributed or modified
|
||||
versions of this project harms you in any way.
|
||||
|
||||
|
||||
Redistributed or modified versions of this project...
|
||||
MUST have the same license as the original project
|
||||
MUST disclose the project's source
|
||||
|
||||
|
||||
OWNER: Ari Archer <ari.web.xyz@gmail.com>
|
||||
PROJECT NAME: blog.ari-web.xyz
|
||||
YEAR: 2022
|
||||
|
15
README.md
Normal file
15
README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# [Ari-web blogs](https://blog.ari-web.xyz/)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Maintained-Yes-green?color=red&style=flat-square">
|
||||
<img src="https://img.shields.io/github/last-commit/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||
<img src="https://img.shields.io/github/repo-size/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||
<img src="https://img.shields.io/github/issues/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||
<img src="https://img.shields.io/github/stars/TruncatedDinosour/blog.ari-web.xyz?color=red&style=flat-square">
|
||||
</p>
|
||||
|
||||
## Installing dependencies
|
||||
|
||||
```sh
|
||||
$ python3 -m pip install --user -r requirements.txt
|
||||
```
|
52
blog.json
Normal file
52
blog.json
Normal file
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"editor-command": "vim -- %s",
|
||||
"blog-dir": "b",
|
||||
"git-url": "/git",
|
||||
"py-markdown-extensions": [
|
||||
"markdown.extensions.abbr",
|
||||
"markdown.extensions.def_list",
|
||||
"markdown.extensions.fenced_code",
|
||||
"markdown.extensions.footnotes",
|
||||
"markdown.extensions.md_in_html",
|
||||
"markdown.extensions.tables",
|
||||
"markdown.extensions.admonition",
|
||||
"markdown.extensions.sane_lists",
|
||||
"markdown.extensions.toc",
|
||||
"markdown.extensions.wikilinks",
|
||||
"pymdownx.betterem",
|
||||
"pymdownx.caret",
|
||||
"pymdownx.magiclink",
|
||||
"pymdownx.mark",
|
||||
"pymdownx.tilde"
|
||||
],
|
||||
"default-keywords": [
|
||||
"website",
|
||||
"blog",
|
||||
"opinion",
|
||||
"article",
|
||||
"ari-web",
|
||||
"ari"
|
||||
],
|
||||
"page-title": "Ari::web -> Blog",
|
||||
"page-description": "My blog page",
|
||||
"colourscheme-type": "dark",
|
||||
"home-keywords": [
|
||||
"ari",
|
||||
"ari-web",
|
||||
"blog",
|
||||
"ari-archer",
|
||||
"foss",
|
||||
"free",
|
||||
"linux"
|
||||
],
|
||||
"base-homepage": "https://ari-web.xyz/",
|
||||
"blogs": {
|
||||
"new-blog-management-system-": {
|
||||
"title": "TmV3IGJsb2cgbWFuYWdlbWVudCBzeXN0ZW0h",
|
||||
"content": "SGVsbG8gd29ybGQgOikKCkkgaGF2ZSBjb21wbGV0ZWx5IHJlZG9uZSBob3cgYmxvZ3MgYXJlIG1hbmFnZWQsIG1hZGUKYW5kIHN0b3JlZCBzbyBub3cgaHR0cHM6Ly9ibG9nLmFyaS13ZWIueHl6LyAob2xkKSBpcyBtb3ZlZCB0byBodHRwczovL2xlZ2FjeS5ibG9nLmFyaS13ZWIueHl6Lwp3aGlsZSB0aGlzIG5ldyBzeXN0ZW0gaXMgb24gdGhlIG9yaWdpbmFsLCBodHRwczovL2Jsb2cuYXJpLXdlYi54eXovIHN1YmRvbWFpbiwgdGhlCmxlZ2FjeSBzdWJkb21haW4gd2lsbCBzdGlsbCBiZSBoZXJlIGFuZCB3aWxsIHN0aWxsIGJlIGJhY2t3YXJkcyBjb21wYXRpYmxlIHdpdGgKdGhlIG5ldyBvbmUsIHRob3VnaCBub3cgaXQgd2lsbCBiZSBhbiBIVFRQIHJlZGlyZWN0CgpJZiBhbnlvbmUgaXMgdXNpbmcgbXkgYmxvZyBmb3IgYW55dGhpbmcgYnV0IHZpc2l0aW5nLCBwbGVhc2UgY29uc2lkZXIKdGhlIHJlZGlyZWN0IDopCgo=",
|
||||
"version": 1,
|
||||
"time": 1646996956.328543,
|
||||
"keywords": "management linux http-redirect new website blog opinion article ari-web ari"
|
||||
}
|
||||
}
|
||||
}
|
45
content/styles.css
Normal file
45
content/styles.css
Normal file
|
@ -0,0 +1,45 @@
|
|||
:root {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
background-color: #262220 !important;
|
||||
color: #f9f6e8 !important;
|
||||
font-family: Hack, hack, monospace, sans, opensans, sans-serif !important;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: auto !important;
|
||||
padding: 2rem !important;
|
||||
max-width: 1200px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center !important;
|
||||
margin: 1em !important;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.5em !important;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none !important;
|
||||
text-shadow: 0px 0px 6px white !important;
|
||||
}
|
||||
|
||||
.info-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
45
netlify.toml
Normal file
45
netlify.toml
Normal file
|
@ -0,0 +1,45 @@
|
|||
[build]
|
||||
command = "python3 ./scripts/blog build"
|
||||
|
||||
[[redirects]]
|
||||
from = "/git/*"
|
||||
to = "https://ari-web.xyz/gh/blog.ari-web.xyz/:splat"
|
||||
status = 301
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/favicon.ico"
|
||||
to = "https://ari-web.xyz/favicon.ico"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/robots.txt"
|
||||
to = "https://ari-web.xyz/robots.txt"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/sitemap.xml"
|
||||
to = "https://ari-web.xyz/sitemap.xml"
|
||||
status = 200
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/blogs/*"
|
||||
to = "https://legacy.blog.ari-web.xyz/:splat"
|
||||
status = 302
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/netlify.toml"
|
||||
to = "https://ari-web.xyz/404.files.xyz"
|
||||
status = 404
|
||||
force = true
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "https://ari-web.xyz/404.files.xyz"
|
||||
status = 404
|
||||
force = false
|
||||
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
css-html-js-minify
|
||||
readline
|
||||
datetime
|
||||
markdown
|
||||
plumbum
|
||||
pyfzf
|
||||
|
1
runtime.txt
Normal file
1
runtime.txt
Normal file
|
@ -0,0 +1 @@
|
|||
3.9
|
536
scripts/blog
Executable file
536
scripts/blog
Executable file
|
@ -0,0 +1,536 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Manage blogs"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import readline
|
||||
import string
|
||||
import sys
|
||||
from atexit import register as fn_register
|
||||
from base64 import b64decode, b64encode
|
||||
from datetime import datetime
|
||||
from html import escape as html_escape
|
||||
from shutil import rmtree
|
||||
from threading import Thread
|
||||
from time import strftime as format_system_time
|
||||
from timeit import default_timer as code_timer
|
||||
from warnings import filterwarnings as filter_warnings
|
||||
|
||||
from css_html_js_minify import html_minify # type: ignore
|
||||
from css_html_js_minify import process_single_css_file
|
||||
from markdown import markdown # type: ignore
|
||||
from plumbum.commands.processes import ProcessExecutionError # type: ignore
|
||||
from pyfzf import FzfPrompt # type: ignore
|
||||
|
||||
EXIT_OK: int = 0
|
||||
EXIT_ERR: int = 1
|
||||
|
||||
DEFAULT_CONFIG: dict = {
|
||||
"editor-command": f"{os.environ.get('EDITOR', 'vim')} -- %s",
|
||||
"blog-dir": "b",
|
||||
"git-url": "/git",
|
||||
"py-markdown-extensions": [
|
||||
"markdown.extensions.abbr",
|
||||
"markdown.extensions.def_list",
|
||||
"markdown.extensions.fenced_code",
|
||||
"markdown.extensions.footnotes",
|
||||
"markdown.extensions.md_in_html",
|
||||
"markdown.extensions.tables",
|
||||
"markdown.extensions.admonition",
|
||||
"markdown.extensions.sane_lists",
|
||||
"markdown.extensions.toc",
|
||||
"markdown.extensions.wikilinks",
|
||||
"pymdownx.betterem",
|
||||
"pymdownx.caret",
|
||||
"pymdownx.magiclink",
|
||||
"pymdownx.mark",
|
||||
"pymdownx.tilde",
|
||||
],
|
||||
"default-keywords": ["website", "blog", "opinion", "article", "ari-web", "ari"],
|
||||
"page-title": "Ari::web -> Blog",
|
||||
"page-description": "My blog page",
|
||||
"colourscheme-type": "dark",
|
||||
"home-keywords": ["ari", "ari-web", "blog", "ari-archer", "foss", "free", "linux"],
|
||||
"base-homepage": "https://ari-web.xyz/",
|
||||
"blogs": {},
|
||||
}
|
||||
DEFAULT_CONFIG_FILE: str = "blog.json"
|
||||
HISTORY_FILE: str = ".blog_history"
|
||||
BLOG_VERSION: int = 1
|
||||
|
||||
BLOG_MARKDOWN_TEMPLATE = """# %s
|
||||
|
||||
<div class="info-bar" aria-hidden="true">
|
||||
%s | <a href="%s">back</a> | <a href="/">home</a> | <a href="%s">git</a>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
%s"""
|
||||
|
||||
HTML_HEADER = """<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<meta name="color-scheme" content="{theme_type}">
|
||||
|
||||
<meta name="keywords" content="{keywords}">
|
||||
<meta name="robots" content="follow"/>
|
||||
|
||||
<link rel="stylesheet" href="/content/styles.min.css"/>"""
|
||||
|
||||
BLOG_HTML_TEMPLATE = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{HTML_HEADER}
|
||||
<meta name="description" content="{{blog_description}}">
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
{{blog}}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
HOME_PAGE_HTML_TEMPLATE = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{HTML_HEADER}
|
||||
<meta name="description" content="{{home_page_description}}">
|
||||
</head>
|
||||
<body>
|
||||
<h1>My blogs</h1>
|
||||
<div class="info-bar" aria-hidden="true">
|
||||
<p>last blog on: <code>{{lastest_blog_time}}</code> | latest blog: <a href="{{latest_blog_url}}">{{latest_blog_title}}</a> | <a href="{{git_url}}">source code</a></p>
|
||||
<hr/>
|
||||
</div>
|
||||
<div>
|
||||
{{content}}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def log(message: str, header: str = "ERROR", code: int = EXIT_ERR) -> int:
|
||||
sys.stderr.write(f"{header}: {message}\n")
|
||||
return code
|
||||
|
||||
|
||||
def sanitise_title(title: str, titleset: dict) -> str:
|
||||
_title: str = ""
|
||||
|
||||
for char in title:
|
||||
_title += char if char not in string.whitespace + string.punctuation else "-"
|
||||
|
||||
_title = _title.lower()
|
||||
|
||||
return (
|
||||
_title
|
||||
if _title not in titleset and _title.strip()
|
||||
else sanitise_title(_title + random.choice(string.digits), titleset)
|
||||
)
|
||||
|
||||
|
||||
def format_time(timestamp: float) -> str:
|
||||
return datetime.fromtimestamp(timestamp).strftime(
|
||||
"%Y-%m-%d %H:%M:%S " + format_system_time("%Z")
|
||||
)
|
||||
|
||||
|
||||
def iinput(prompt: str, default_text: str = "") -> str:
|
||||
default_text = default_text.strip()
|
||||
|
||||
def hook():
|
||||
if not default_text:
|
||||
return
|
||||
|
||||
readline.insert_text(default_text)
|
||||
readline.redisplay()
|
||||
|
||||
readline.set_pre_input_hook(hook)
|
||||
user_inpt: str = input(f"({prompt}) ").strip()
|
||||
readline.set_pre_input_hook()
|
||||
|
||||
return user_inpt
|
||||
|
||||
|
||||
def new_config() -> None:
|
||||
log("Making new config...", "INFO")
|
||||
|
||||
with open(DEFAULT_CONFIG_FILE, "w") as cfg:
|
||||
json.dump(DEFAULT_CONFIG, cfg, indent=4)
|
||||
|
||||
|
||||
def pick_blog(config: dict) -> str:
|
||||
try:
|
||||
blog_id: str = (
|
||||
FzfPrompt()
|
||||
.prompt(
|
||||
map(
|
||||
lambda key: f"{key} | {b64decode(config['blogs'][key]['title']).decode()!r}",
|
||||
config["blogs"].keys(),
|
||||
),
|
||||
"--prompt='Pick blog'",
|
||||
)[0]
|
||||
.split()[0]
|
||||
)
|
||||
except ProcessExecutionError:
|
||||
log("Fzf process exited unexpectedly")
|
||||
return ""
|
||||
|
||||
if blog_id not in config["blogs"]:
|
||||
log(f"Blog {blog_id!r} does not exist")
|
||||
return ""
|
||||
|
||||
return blog_id
|
||||
|
||||
|
||||
def new_blog(config: dict) -> tuple[int, dict]:
|
||||
"""Make a new blog"""
|
||||
|
||||
if title := iinput("blog title"):
|
||||
us_title: str = title
|
||||
s_title: str = sanitise_title(us_title, config["blogs"])
|
||||
|
||||
blog = {
|
||||
"title": b64encode(us_title.encode()).decode(),
|
||||
"content": "",
|
||||
"version": BLOG_VERSION,
|
||||
"time": "Unknown",
|
||||
"keywords": "",
|
||||
}
|
||||
|
||||
file: str = f"/tmp/{s_title}.md"
|
||||
|
||||
open(file, "w").close()
|
||||
os.system(config["editor-command"] % file)
|
||||
|
||||
if not os.path.isfile(file):
|
||||
return log(f"{file!r} does not exist"), config
|
||||
|
||||
with open(file, "r") as md:
|
||||
blog["content"] = b64encode(md.read().encode()).decode()
|
||||
|
||||
if not blog["content"].strip(): # type: ignore
|
||||
return log("Blog cannot be empty"), config
|
||||
|
||||
blog["keywords"] = html_escape(
|
||||
iinput("keywords (seperated by spaces)")
|
||||
+ " "
|
||||
+ " ".join(config["default-keywords"])
|
||||
)
|
||||
|
||||
blog["time"] = datetime.now().timestamp()
|
||||
config["blogs"][s_title] = blog
|
||||
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
def build(config: dict) -> tuple[int, dict]:
|
||||
"""Build, minimise and generate site"""
|
||||
|
||||
if len(config["blogs"]) < 1:
|
||||
return log("Cannot build no blogs"), config
|
||||
|
||||
code_timestamp = code_timer()
|
||||
|
||||
if os.path.isdir(config["blog-dir"]):
|
||||
rmtree(config["blog-dir"])
|
||||
|
||||
os.makedirs(config["blog-dir"], exist_ok=True)
|
||||
|
||||
log("Building CSS...", "INFO")
|
||||
|
||||
def build_css() -> None:
|
||||
saved_stdout = sys.stdout
|
||||
sys.stdout = open(os.devnull, "w")
|
||||
|
||||
if os.path.isfile("content/styles.css"):
|
||||
process_single_css_file("content/styles.css")
|
||||
|
||||
sys.stdout.close()
|
||||
sys.stdout = saved_stdout
|
||||
|
||||
Thread(target=build_css, daemon=True).start()
|
||||
|
||||
log("Building blogs...", "INFO")
|
||||
|
||||
def thread(blog_name: str, blog_meta: dict):
|
||||
blog_dir: str = os.path.join(config["blog-dir"], blog_name)
|
||||
os.makedirs(blog_dir, exist_ok=True)
|
||||
|
||||
with open(os.path.join(blog_dir, "index.html"), "w") as blog_html:
|
||||
blog_time: str = format_time(blog_meta["time"])
|
||||
|
||||
blog_title: str = html_escape(b64decode(blog_meta["title"]).decode())
|
||||
|
||||
blog_base_html: str = markdown(
|
||||
BLOG_MARKDOWN_TEMPLATE
|
||||
% (
|
||||
blog_title,
|
||||
blog_time,
|
||||
config["base-homepage"],
|
||||
config["git-url"],
|
||||
markdown(b64decode(blog_meta["content"]).decode())
|
||||
.replace("<h1>", "<h2>")
|
||||
.replace("<h1/>", "<h2/>"),
|
||||
),
|
||||
extensions=config["py-markdown-extensions"],
|
||||
)
|
||||
|
||||
blog_html.write(
|
||||
html_minify(
|
||||
BLOG_HTML_TEMPLATE.format(
|
||||
title=config["page-title"],
|
||||
theme_type=config["colourscheme-type"],
|
||||
keywords=blog_meta["keywords"],
|
||||
blog_description=f"Blog on {blog_time} -- {blog_title}",
|
||||
blog=blog_base_html,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
log(f"Finished building blog {blog_name!r}", "BUILD")
|
||||
|
||||
_tmp_threads: list = []
|
||||
|
||||
for blog_name, blog_meta in config["blogs"].items():
|
||||
t: Thread = Thread(target=thread, args=(blog_name, blog_meta), daemon=True)
|
||||
_tmp_threads.append(t)
|
||||
|
||||
t.start()
|
||||
|
||||
for awaiting_thread in _tmp_threads:
|
||||
awaiting_thread.join()
|
||||
|
||||
log("Building blog index...", "INFO")
|
||||
|
||||
with open("index.html", "w") as index:
|
||||
latest_blog_id: str = tuple(config["blogs"].keys())[-1]
|
||||
lastest_blog: dict = config["blogs"][latest_blog_id]
|
||||
lastest_blog_time: str = format_time(lastest_blog["time"])
|
||||
|
||||
blog_list = "<ul>"
|
||||
|
||||
for blog_id, blog_meta in reversed(config["blogs"].items()):
|
||||
blog_list += f'<li><a href="{os.path.join(config["blog-dir"], blog_id)}">{html_escape(b64decode(blog_meta["title"]).decode())}</a></li>'
|
||||
|
||||
blog_list += "</ul>"
|
||||
|
||||
index.write(
|
||||
html_minify(
|
||||
HOME_PAGE_HTML_TEMPLATE.format(
|
||||
title=config["page-title"],
|
||||
theme_type=config["colourscheme-type"],
|
||||
keywords=config["home-keywords"],
|
||||
home_page_description=config["page-description"],
|
||||
lastest_blog_time=lastest_blog_time,
|
||||
latest_blog_url=os.path.join(config["blog-dir"], latest_blog_id),
|
||||
latest_blog_title=b64decode(
|
||||
html_escape(lastest_blog["title"])
|
||||
).decode(),
|
||||
git_url=config["git-url"],
|
||||
content=blog_list,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
log(
|
||||
f"Successfully built blog page in {code_timer() - code_timestamp} seconds",
|
||||
"TIME",
|
||||
)
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
def list_blogs(config: dict) -> tuple[int, dict]:
|
||||
"""List blogs"""
|
||||
|
||||
for blog_id, blog_meta in config["blogs"].items():
|
||||
print(
|
||||
f"""ID: {blog_id}
|
||||
Title: {b64decode(blog_meta['title']).decode()!r}
|
||||
Version: {blog_meta['version']}
|
||||
Time of creation: {format_time(blog_meta['time'])}
|
||||
Keywords: {blog_meta['keywords'].replace(' ', ', ')}
|
||||
"""
|
||||
)
|
||||
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
def remove_blog(config: dict) -> tuple[int, dict]:
|
||||
"""Remove a blog page"""
|
||||
|
||||
blog_id: str = pick_blog(config)
|
||||
|
||||
if not blog_id:
|
||||
return EXIT_ERR, config
|
||||
|
||||
del config["blogs"][blog_id]
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
def dummy() -> None:
|
||||
"""Print help/usage information"""
|
||||
|
||||
|
||||
def edit_title(blog: str, config: dict) -> int:
|
||||
new_title: str = iinput(
|
||||
"edit title", b64decode(config["blogs"][blog]["title"]).decode()
|
||||
)
|
||||
|
||||
if not new_title.strip():
|
||||
return log("New title cannot be empty")
|
||||
|
||||
config["blogs"][blog]["title"] = b64encode(new_title.encode()).decode()
|
||||
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def edit_keywords(blog: str, config: dict) -> int:
|
||||
new_keywords: str = iinput("edit keywords", config["blogs"][blog]["keywords"])
|
||||
|
||||
if not new_keywords.strip():
|
||||
return log("Keywords cannot be empty")
|
||||
|
||||
config["blogs"][blog]["keywords"] = new_keywords
|
||||
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def edit_content(blog: str, config: dict) -> int:
|
||||
file: str = f"/tmp/{blog}.md"
|
||||
|
||||
with open(file, "w") as blog_md:
|
||||
blog_md.write(b64decode(config["blogs"][blog]["content"]).decode())
|
||||
|
||||
os.system(config["editor-command"] % (file))
|
||||
|
||||
with open(file, "r") as blog_md_new:
|
||||
content: str = blog_md_new.read()
|
||||
|
||||
if not content.strip():
|
||||
blog_md_new.close()
|
||||
return log("Content of a blog cannot be empty")
|
||||
|
||||
config["blogs"][blog]["content"] = b64encode(content.encode()).decode()
|
||||
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
EDIT_HOOKS: dict = {
|
||||
"title": edit_title,
|
||||
"keywords": edit_keywords,
|
||||
"content": edit_content,
|
||||
}
|
||||
|
||||
|
||||
def edit(config: dict) -> tuple[int, dict]:
|
||||
"""Edit a blog"""
|
||||
|
||||
blog_id: str = pick_blog(config)
|
||||
|
||||
if not blog_id:
|
||||
return EXIT_ERR, config
|
||||
|
||||
try:
|
||||
hook: str = FzfPrompt().prompt(EDIT_HOOKS.keys(), "--prompt='What to edit'")[0]
|
||||
|
||||
if hook not in EDIT_HOOKS:
|
||||
return log(f"Hook {hook!r} does not exist"), config
|
||||
|
||||
EDIT_HOOKS[hook](blog_id, config)
|
||||
except ProcessExecutionError:
|
||||
return log("Fzf unexpectedly exited"), config
|
||||
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
def gen_new_config(config: dict) -> tuple[int, dict]:
|
||||
"""Make default config"""
|
||||
|
||||
if os.path.exists(DEFAULT_CONFIG_FILE):
|
||||
if iinput("Do you want to overwite config? (y/n)").lower()[0] != "y":
|
||||
return log("Not overwritting config", "INFO", EXIT_OK), config
|
||||
|
||||
new_config()
|
||||
|
||||
with open(DEFAULT_CONFIG_FILE, "r") as cfg:
|
||||
config = json.load(cfg)
|
||||
|
||||
return EXIT_OK, config
|
||||
|
||||
|
||||
SUBCOMMANDS: dict = {
|
||||
"help": dummy,
|
||||
"new": new_blog,
|
||||
"build": build,
|
||||
"list": list_blogs,
|
||||
"rm": remove_blog,
|
||||
"edit": edit,
|
||||
"new-config": gen_new_config,
|
||||
}
|
||||
|
||||
|
||||
def usage(code: int = EXIT_ERR, config: dict = None) -> int:
|
||||
sys.stderr.write(f"Usage: {sys.argv[0]} <subcommand>\n")
|
||||
|
||||
for subcommand, func in SUBCOMMANDS.items():
|
||||
sys.stderr.write(f" {subcommand:20s}{func.__doc__ or ''}\n")
|
||||
|
||||
return code
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Entry/main function"""
|
||||
|
||||
if not os.path.isfile(HISTORY_FILE):
|
||||
open(HISTORY_FILE, "w").close()
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
|
||||
fn_register(readline.write_history_file, HISTORY_FILE)
|
||||
fn_register(readline.read_history_file, HISTORY_FILE)
|
||||
|
||||
readline.read_history_file(HISTORY_FILE)
|
||||
readline.set_history_length(5000)
|
||||
|
||||
if not os.path.isfile(DEFAULT_CONFIG_FILE):
|
||||
new_config()
|
||||
return EXIT_ERR
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
return usage()
|
||||
elif sys.argv[1] not in SUBCOMMANDS:
|
||||
return log(f"{sys.argv[1]!r} is not a subcommand, try `{sys.argv[0]} help`")
|
||||
elif sys.argv[1] == "help":
|
||||
return usage(EXIT_OK)
|
||||
|
||||
with open(DEFAULT_CONFIG_FILE, "r") as lcfg:
|
||||
code, config = SUBCOMMANDS[sys.argv[1]](config=json.load(lcfg))
|
||||
|
||||
log("Sorting blogs by creation time...", "CLEANUP")
|
||||
config["blogs"] = dict(
|
||||
map(
|
||||
lambda k: (k, config["blogs"][k]),
|
||||
sorted(config["blogs"], key=lambda k: config["blogs"][k]["time"]),
|
||||
)
|
||||
)
|
||||
|
||||
with open(DEFAULT_CONFIG_FILE, "w") as dcfg:
|
||||
json.dump(config, dcfg, indent=4)
|
||||
|
||||
return code
|
||||
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert main.__annotations__.get("return") is int, "main() should return an integer"
|
||||
|
||||
filter_warnings("error", category=Warning)
|
||||
sys.exit(main())
|
11
scripts/git.sh
Executable file
11
scripts/git.sh
Executable file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
set -xe
|
||||
|
||||
main() {
|
||||
git add -A
|
||||
git commit -sam "update @ $(date)"
|
||||
git push -u origin "$(git rev-parse --abbrev-ref HEAD)"
|
||||
}
|
||||
|
||||
main "$@"
|
3
tox.ini
Normal file
3
tox.ini
Normal file
|
@ -0,0 +1,3 @@
|
|||
[flake8]
|
||||
max-line-length = 200
|
||||
|
Loading…
Add table
Reference in a new issue