Compare commits

...

2 Commits

Author SHA1 Message Date
magical 804438f045 add bundle.py to create single-file bbj client
python is able to import modules from a zip file. if the zip file
contains __main__.py it will even run it as a script! this lets us bundle
bbj's frontend together with its sole external dependency (urwid) to
create a single executable file that'll run anywhere with python
installed, no virtualenv needed.

the only downside is that python can't import shared objects (.so) from a
zip file, so urwid can't use its C-accelerated str_util module and has
to fall back to the python version. which is slower, probably.
2022-08-10 07:09:06 +00:00
magical ab1550c09c redo mark function 2022-08-10 03:44:15 +00:00
2 changed files with 70 additions and 17 deletions

32
bundle.py 100644
View File

@ -0,0 +1,32 @@
import zipfile
import glob
import os
# TODO: should we include .pyc files?
# TODO: add urwid source into the repo somewhere
files = {
'__main__.py': 'clients/urwid/main.py',
'network.py': 'clients/network_client.py',
'urwid': 'env/lib/python3.8/site-packages/urwid/*.py',
}
with open('bbj_demo', 'wb') as f:
f.write(b"#!/usr/bin/env python3\n")
with zipfile.ZipFile(f, 'w', compression=zipfile.ZIP_DEFLATED) as z:
z.comment = b'BBJ'
for name, source in files.items():
if '*' in source:
dirname = name
for path in sorted(glob.glob(source)):
name = dirname + '/' + os.path.basename(path)
z.write(path, name)
else:
z.write(source, name)
try:
mask = os.umask(0)
os.umask(mask)
except OSError:
mask = 0
os.chmod(z.filename, 0o777&~mask)

View File

@ -793,7 +793,7 @@ class App(object):
""" """
if self.mode == "thread": if self.mode == "thread":
# mark the current position in this thread before going back to the index # mark the current position in this thread before going back to the index
mark() self.mark()
self.body.attr_map = {None: "default"} self.body.attr_map = {None: "default"}
self.mode = "index" self.mode = "index"
@ -859,7 +859,7 @@ class App(object):
self.walker += self.make_message_body(message) self.walker += self.make_message_body(message)
self.set_default_header() self.set_default_header()
self.set_default_footer() self.set_default_footer()
self.goto_post(mark(thread_id)) self.goto_mark(thread_id)
def toggle_client_pin(self): def toggle_client_pin(self):
@ -983,10 +983,10 @@ class App(object):
self.last_index_pos = self.get_focus_post(True).thread["thread_id"] self.last_index_pos = self.get_focus_post(True).thread["thread_id"]
self.index() self.index()
else: else:
mark() self.mark()
thread = self.thread["thread_id"] thread = self.thread["thread_id"]
self.thread_load(None, thread) self.thread_load(None, thread)
self.goto_post(mark(thread)) self.goto_mark(thread)
self.temp_footer_message("Refreshed content!", 1) self.temp_footer_message("Refreshed content!", 1)
@ -1021,10 +1021,31 @@ class App(object):
width=30, height=6) width=30, height=6)
else: else:
mark() self.mark()
self.index() self.index()
def mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = self.get_focus_post()
mark(thread_id, pos, default=0)
return pos
def goto_mark(self, thread_id=None):
if self.mode != "thread":
return
if thread_id is None:
thread_id = self.thread['thread_id']
pos = mark(thread_id, default=0)
self.goto_post(pos)
def get_focus_post(self, return_widget=False): def get_focus_post(self, return_widget=False):
pos = self.box.get_focus_path()[0] pos = self.box.get_focus_path()[0]
if self.mode == "thread": if self.mode == "thread":
@ -2501,11 +2522,16 @@ def bbjrc(mode, **params):
return values return values
def mark(directive=True): def mark(key, value=None, default=None):
""" """
Set and retrieve positional marks for threads. Sets a value in the markfile and returns the old value (or default).
This uses a seperate file from the preferences This uses a seperate file from the preferences
to keep it free from clutter. to keep it free from clutter.
The key must be a string, and the value must be
json-encodable. If value isn't provided (or is None)
then this doesn't set anything and it is only a
read operation.
""" """
try: try:
with open(markpath, "r") as _in: with open(markpath, "r") as _in:
@ -2513,19 +2539,14 @@ def mark(directive=True):
except FileNotFoundError: except FileNotFoundError:
values = {} values = {}
if directive == True and app.mode == "thread": old = values.get(key, default)
pos = app.get_focus_post()
values[app.thread["thread_id"]] = pos if value is not None and value != old:
values[key] = value
with open(markpath, "w") as _out: with open(markpath, "w") as _out:
json.dump(values, _out) json.dump(values, _out)
return pos
elif isinstance(directive, str):
try:
return values[directive]
except KeyError:
return 0
return old
def load_client_pins(): def load_client_pins():
""" """