├── README.md ├── deps ├── pdf.js ├── pdf.js.map ├── pdf.worker.js └── pdf.worker.js.map ├── display ├── index.html ├── requirements.txt └── server.py /README.md: -------------------------------------------------------------------------------- 1 | ## CS_Store 2 | 3 | An infinite canvas for your filesystem. 4 | 5 | ### Some design decisions 6 | 1. All canvas state is stored in `.CS_Store` files. To send a canvas to 7 | someone, simply zip the directory and email it over. 8 | 2. Human-readable format for `.CS_Store` which can be manually edited with any 9 | text editor. 10 | 3. Size-as-context: When an element is added to the canvas, it's a fixed size 11 | in pixel width. To add a small comment, zoom-in to the context it concerns. 12 | 4. Audio as region: Sound files are played when visible in the canvas. Drop a 13 | file in to function as a soundtrack for a region of space. 14 | 5. Export to an .html with image files in a zip. 15 | 16 | ### Installation 17 | 1. Make sure you have the packages in `requirements.txt` installed 18 | 2. Add `display` to your path or move it to a bin in your path. 19 | 3. Run `display`! 20 | 21 | ### To Do: 22 | 1. Watch for changes to the `.CS_Store` for live updates from text editor 23 | 2. Store text as `.txt` files, rather than in `.CS_Store` directly 24 | 3. Export button to generate `.zip` 25 | 4. Fix jittering on zoom-in on Safari 26 | -------------------------------------------------------------------------------- /display: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python3 ~/dev/CS_Store/server.py "$PWD" & 4 | PID=$! 5 | trap "kill $PID" EXIT 6 | echo 'CTRL + C to kill' 7 | sleep 0.1 8 | 9 | if [[ "$OSTYPE" == "linux-gnu"* ]]; then 10 | chromium http://localhost:1234/static/index.html 11 | else 12 | open http://localhost:1234/static/index.html 13 | fi 14 | 15 | while :; do sleep 1; done 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | 641 | 642 | 756 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mimetypes 3 | import json 4 | 5 | import tornado.ioloop 6 | import tornado.web 7 | import tornado.websocket 8 | 9 | FILE_PATH = os.getcwd() 10 | SRC_PATH = os.path.dirname(__file__) 11 | 12 | VALID_TYPES = [ 13 | "image/png", 14 | "image/jpeg", 15 | "video/mp4", 16 | "application/pdf", 17 | "audio/mpeg" 18 | ] 19 | 20 | def pwd(): 21 | files = [{ 22 | "type": "dir", 23 | "path": "parent", 24 | "absolute": os.path.split(FILE_PATH)[0], 25 | }] 26 | 27 | listing = os.listdir(FILE_PATH) 28 | for f in listing: 29 | t = mimetypes.guess_type(f)[0] 30 | if t in VALID_TYPES: 31 | files.append({ 32 | "type": t, 33 | "path": f"/files/{f}", 34 | }) 35 | elif os.path.isdir(f) and f[0] != ".": 36 | files.append({ 37 | "type": "dir", 38 | "path": f, 39 | "absolute": os.path.join(FILE_PATH, f) 40 | }) 41 | 42 | layout = {} 43 | if ".CS_Store" in listing: 44 | with open(f"{FILE_PATH}/.CS_Store", "r") as f: 45 | layout = json.loads(f.read()) 46 | 47 | return { 48 | "path": FILE_PATH, 49 | "files": files, 50 | "layout": layout, 51 | } 52 | 53 | 54 | class WSHandler(tornado.websocket.WebSocketHandler): 55 | def on_message(self, message): 56 | global FILE_PATH 57 | 58 | message = json.loads(message) 59 | if message["type"] == "initialize": 60 | self.write_message(json.dumps(pwd())) 61 | 62 | elif message["type"] == "layout": 63 | with open(f"{FILE_PATH}/.CS_Store", "w") as f: 64 | f.write(json.dumps(message["layout"], indent=4)) 65 | 66 | elif message["type"] == "cd": 67 | FILE_PATH = message["path"] 68 | os.chdir(FILE_PATH) 69 | server.redirect() 70 | self.write_message(json.dumps(pwd())) 71 | 72 | 73 | class Server: 74 | def __init__(self): 75 | pass 76 | 77 | def start(self): 78 | self.app = tornado.web.Application([ 79 | (r'/ws', WSHandler), 80 | (r'/static/(.*)', tornado.web.StaticFileHandler, { "path": SRC_PATH }), 81 | (r'/files/(.*)', tornado.web.StaticFileHandler, { "path": FILE_PATH }), 82 | ]) 83 | self.server = self.app.listen(1234) 84 | tornado.ioloop.IOLoop.current().start() 85 | 86 | def redirect(self): 87 | global FILE_PATH, SRC_PATH 88 | 89 | self.app.default_router.rules = [] 90 | self.app.add_handlers(r".*", [ 91 | (r'/ws', WSHandler), 92 | (r'/static/(.*)', tornado.web.StaticFileHandler, { "path": SRC_PATH }), 93 | (r'/files/(.*)', tornado.web.StaticFileHandler, { "path": FILE_PATH }), 94 | ]) 95 | 96 | server = Server() 97 | 98 | if __name__ == "__main__": 99 | print("Files:", FILE_PATH) 100 | print("Code:", SRC_PATH) 101 | server.start() 102 | --------------------------------------------------------------------------------