├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── darkdraw.jsonl ├── darkdraw ├── __init__.py ├── ansi.html ├── ansihtml.py ├── box.py ├── charbrowser.py ├── drawing.py ├── loader_scr.py ├── save.py ├── stamps.py └── upgrade.py ├── dwimmer_darkdraw.gif ├── dwimmer_faerie-fire.gif ├── plugins └── typing_mode.py ├── pxplus_ibm_vga9-webfont.woff ├── pxplus_ibm_vga9-webfont.woff2 ├── requirements.txt ├── samples ├── arrows.ddw ├── bouncyball.ddw ├── boxes.ddw ├── bw16colors.ddw ├── colors.ddw └── policecar.ddw └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.6 2 | 3 | ## Major features 4 | 5 | - add PgUp/PgDown to scroll down Drawings which are bigger than screen 6 | - add select-top (`zs`) to select the topmost item under cursor 7 | 8 | ## Improvements 9 | 10 | - Ensure drawings and backing sheets have different names 11 | 12 | ## Bugfixes 13 | 14 | - new sheets now unnamed if /usr/share/dict/words not available 15 | - visidata: 474d38 ENTER pushes copy of source sheet with cursor rows 16 | - visidata: 9c6d36 fix duplicate columns in backing sheet 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-alpine 2 | 3 | RUN pip install requests python-dateutil wcwidth 4 | 5 | RUN apk add git 6 | RUN pip install git+https://github.com/devottys/darkdraw.git 7 | RUN sh -c "echo >>~/.visidatarc import darkdraw" 8 | RUN git clone https://github.com/devottys/studio 9 | 10 | ENV TERM="xterm-256color" 11 | ENTRYPOINT ["vd", "studio/darkdraw-tutorial.ddw"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # PolyForm Noncommercial License 1.0.0 2 | 3 | 4 | 5 | ## Acceptance 6 | 7 | In order to get any license under these terms, you must agree 8 | to them as both strict obligations and conditions to all 9 | your licenses. 10 | 11 | ## Copyright License 12 | 13 | The licensor grants you a copyright license for the 14 | software to do everything you might do with the software 15 | that would otherwise infringe the licensor's copyright 16 | in it for any permitted purpose. However, you may 17 | only distribute the software according to [Distribution 18 | License](#distribution-license) and make changes or new works 19 | based on the software according to [Changes and New Works 20 | License](#changes-and-new-works-license). 21 | 22 | ## Distribution License 23 | 24 | The licensor grants you an additional copyright license 25 | to distribute copies of the software. Your license 26 | to distribute covers distributing the software with 27 | changes and new works permitted by [Changes and New Works 28 | License](#changes-and-new-works-license). 29 | 30 | ## Notices 31 | 32 | You must ensure that anyone who gets a copy of any part of 33 | the software from you also gets a copy of these terms or the 34 | URL for them above, as well as copies of any plain-text lines 35 | beginning with `Required Notice:` that the licensor provided 36 | with the software. For example: 37 | 38 | > Required Notice: Copyright Saul Pwanson (http://saul.pw) 39 | 40 | ## Changes and New Works License 41 | 42 | The licensor grants you an additional copyright license to 43 | make changes and new works based on the software for any 44 | permitted purpose. 45 | 46 | ## Patent License 47 | 48 | The licensor grants you a patent license for the software that 49 | covers patent claims the licensor can license, or becomes able 50 | to license, that you would infringe by using the software. 51 | 52 | ## Noncommercial Purposes 53 | 54 | Any noncommercial purpose is a permitted purpose. 55 | 56 | ## Personal Uses 57 | 58 | Personal use for research, experiment, and testing for 59 | the benefit of public knowledge, personal study, private 60 | entertainment, hobby projects, amateur pursuits, or religious 61 | observance, without any anticipated commercial application, 62 | is use for a permitted purpose. 63 | 64 | ## Noncommercial Organizations 65 | 66 | Use by any charitable organization, educational institution, 67 | public research organization, public safety or health 68 | organization, environmental protection organization, 69 | or government institution is use for a permitted purpose 70 | regardless of the source of funding or obligations resulting 71 | from the funding. 72 | 73 | ## Fair Use 74 | 75 | You may have "fair use" rights for the software under the 76 | law. These terms do not limit them. 77 | 78 | ## No Other Rights 79 | 80 | These terms do not allow you to sublicense or transfer any of 81 | your licenses to anyone else, or prevent the licensor from 82 | granting licenses to anyone else. These terms do not imply 83 | any other licenses. 84 | 85 | ## Patent Defense 86 | 87 | If you make any written claim that the software infringes or 88 | contributes to infringement of any patent, your patent license 89 | for the software granted under these terms ends immediately. If 90 | your company makes such a claim, your patent license ends 91 | immediately for work on behalf of your company. 92 | 93 | ## Violations 94 | 95 | The first time you are notified in writing that you have 96 | violated any of these terms, or done anything with the software 97 | not covered by your licenses, your licenses can nonetheless 98 | continue if you come into full compliance with these terms, 99 | and take practical steps to correct past violations, within 100 | 32 days of receiving notice. Otherwise, all your licenses 101 | end immediately. 102 | 103 | ## No Liability 104 | 105 | ***As far as the law allows, the software comes as is, without 106 | any warranty or condition, and the licensor will not be liable 107 | to you for any damages arising out of these terms or the use 108 | or nature of the software, under any kind of legal claim.*** 109 | 110 | ## Definitions 111 | 112 | The **licensor** is the individual or entity offering these 113 | terms, and the **software** is the software the licensor makes 114 | available under these terms. 115 | 116 | **You** refers to the individual or entity agreeing to these 117 | terms. 118 | 119 | **Your company** is any legal entity, sole proprietorship, 120 | or other kind of organization that you work for, plus all 121 | organizations that have control over, are under the control of, 122 | or are under common control with that organization. **Control** 123 | means ownership of substantially all the assets of an entity, 124 | or the power to direct its management and policies by vote, 125 | contract, or otherwise. Control can be direct or indirect. 126 | 127 | **Your licenses** are all the licenses granted to you for the 128 | software under these terms. 129 | 130 | **Use** means anything you do with the software requiring one 131 | of your licenses. 132 | 133 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include darkdraw/ansi.html 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DarkDraw II 2 | 3 | Art and animation for the terminal, in the terminal. 4 | 5 | ![darkdraw](dwimmer_darkdraw.gif) 6 | 7 | - unicode and 256-color support 8 | - discover useful glyphs with the unicode browser 9 | - frame-by-frame animation 10 | - manipulate the data behind the drawing with [VisiData](https://visidata.org) 11 | - define your own character and colour palettes (they're just regular drawings) 12 | 13 | [A brief history of the project](https://twitter.com/saulfp/status/1377747742290665475). 14 | 15 | All art and animations in this README were created by [dwimmer](https://www.instagram.com/dwimmer.tm/) using DarkDraw. 16 | 17 | ## Platform requirements 18 | 19 | - Linux, OS/X, or Windows (with WSL) 20 | - [Python 3.6+](https://www.python.org/downloads/) 21 | - [VisiData >= v2.3](https://github.com/saulpw/visidata), [requests](https://docs.python-requests.org/en/master/) and [wcwidth](https://github.com/jquast/wcwidth) 22 | - will be installed automatically through the installer 23 | 24 | ## Install 25 | 26 | DarkDraw is a plugin for [VisiData](https://github.com/saulpw/visidata). There are two ways to install it. 27 | 28 | ### Install VisiData first 29 | 30 | 0. Ensure Python3 and pip3 are installed on your system. 31 | 32 | ``` 33 | python3 --version 34 | pip3 --version 35 | ``` 36 | 37 | 1. Install the latest VisiData release from GitHub: 38 | 39 | ``` 40 | pip3 install visidata 41 | ``` 42 | 43 | 2. Then once installed launch VisiData: 44 | 45 | ``` 46 | vd 47 | ``` 48 | 49 | 3. Install the latest darkdraw release from GitHub: 50 | 51 | ``` 52 | pip3 install git+https://github.com/devottys/darkdraw.git@master 53 | ``` 54 | 55 | ### Use existing Dockerfile (default opens with a tutorial) 56 | 57 | 0. Clone the darkdraw directory 58 | 59 | ``` 60 | git clone https://github.com/devottys/darkdraw.git 61 | cd darkdraw 62 | ``` 63 | 64 | 1. Build the Docker image 65 | 66 | ``` 67 | docker build -t darkdraw . 68 | ``` 69 | 70 | 2. Then whenever you want to run darkdraw with its tutorial 71 | 72 | ``` 73 | docker run --rm -it darkdraw 74 | ``` 75 | 76 | ## Usage 77 | 78 | Darkdraw is a VisiData plugin, to use the darkdraw loader with a new project, have VisiData open a new filetype with the suffix `*.ddw`. 79 | 80 | ``` 81 | vd foo.ddw 82 | ``` 83 | 84 | To start, you can play with some of the samples: 85 | 86 | ``` 87 | vd https://bluebird.sh/ddw/colors.ddw 88 | vd https://bluebird.sh/ddw/bouncyball.ddw 89 | vd https://bluebird.sh/ddw/arrows.ddw 90 | vd https://bluebird.sh/ddw/bw16colors.ddw 91 | ``` 92 | 93 | or with the tutorial: 94 | 95 | ``` 96 | vd https://bluebird.sh/ddw/darkdraw-tutorial.ddw 97 | ``` 98 | 99 | If pulling from the url does not work, try downloading the file, and then: 100 | 101 | ``` 102 | vd colors.ddw 103 | vd darkdraw-tutorial.ddw 104 | ``` 105 | 106 | ![faerie fire](dwimmer_faerie-fire.gif) 107 | 108 | ## Commands 109 | 110 | - `Ctrl+S` (save-sheet): save drawing to .ddw (either from drawing or backing sheet) 111 | - `Shift+A` (new-drawing): open blank untitled drawing 112 | 113 | ## Backing sheet 114 | 115 | The drawing is rendered from a straightforward list of items with x/y/text/color and other attributes. 116 | This list can be used directly and is called the 'backing sheet'. 117 | 118 | - `` ` `` (open-source): push backing sheet (if on drawing) or drawing (if on backing sheet) 119 | 120 | - `Enter` (dive-cursor): push backing subsheet with only rows under the cursor 121 | - Note: edits to rows on any backing subsheets will be applied directly, but deleting rows or adding new rows will have no effect. Any adding, deleting, or reordering must be done from the top sheet. 122 | - `g Enter` (dive-selected): push backing sheet with all selected rows. 123 | 124 | ### Movement 125 | 126 | - `zh` `zj` `zk` `zl`: move the cursor to the next object in that direction. Useful for sparse areas. 127 | - `g` Arrow (or `gh/gj/gk/gl`): move all the way to the left/bottom/top/right of the drawing 128 | - `PgUp` `PgDown` move the viewport up/down the Drawing; for Drawings bigger than the visible screen 129 | 130 | ### Cursor 131 | 132 | - `Ctrl+Arrow` / `z Arrow`: expand or contract the cursor box by one cell 133 | - `gz Left` / `gz Up`: contract cursor to minimum width/height 134 | 135 | 136 | ### Selection 137 | 138 | - `s` `t` `u`: select/toggle/unselect all items at cursor 139 | - `gs` `gt` `gu`: select/toggle/unselect all items in drawing 140 | - `zs` (select-top-cursor): select only top item at cursor 141 | 142 | - `,` (select-equal-char): select all items with the same text as the top item at the cursor 143 | 144 | - `{` / `}` (go-prev-selected/go-next-selected): go to prev/next selected item on drawing (same as VisiData sheets) 145 | 146 | #### Tags 147 | 148 | - `+` `z+` `g+` (tag): add given tag to cursor/topcursor/selected items 149 | - `|` (select-tag): select all items with the given tag 150 | - `\` (unselect-tag): unselect all items with the given tag 151 | 152 | ### Extra hotkeys 153 | 154 | - `v` (): cycles through 3 "visibility modes": 155 | - 0: don't show extra hotkeys 156 | - 1: show tag hotkeys 157 | - 2: show clipboard hotkeys 158 | 159 | In visibility mode 1, tags are shown on the right side. 160 | Press the two-digit number (e.g. `01`) before the tag to hide/unhide objects in that tag. 161 | Use `g01` to select and `z01` to unselect objects with that tag. Press `00` to unhide all tags. 162 | 163 | These extra hotkeys will function regardless of whether they are currently shown. 164 | 165 | ### Character positioning 166 | 167 | - `Shift+H/J/K/L` slide the selected items one cell in the given direction 168 | - `g Home` / `g End` send selected rows to the 'top' or 'bottom' of the drawing, by changing their position in the underlying DrawingSheet 169 | - `i` (insert-row): insert a new line at the cursor 170 | - `zi` (insert-col): insert a new column at the cursor 171 | 172 | ### Editing 173 | 174 | - `a` (add-input): add text at the cursor (typing it directly) 175 | - `e` (edit-char): change text of the top cursor item to input 176 | - `ge` (edit-selected): change text all selected characters to input 177 | - `d` (delete-cursor): delete all items under the cursor 178 | - `gd` (delete-selected): delete all selected items 179 | 180 | ### Clipboard (copy/paste) 181 | 182 | - `y` (yank-char): yank items at cursor to clipboard 183 | - `gy` (yank-selected): yank all selected items to clipboard 184 | - `x` (cut-char): delete all chars at cursor and move to clipboard (shortcut for `yd`) 185 | - `zx` (cut-char-top): delete top character at cursor and move to clipboard 186 | 187 | - `;` (cycle-paste-mode): cycle between the three paste modes: 188 | - **all**: the character and its color are pasted as a new item 189 | - **char**: the character is pasted as a new item, with the default color (whatever color the paste item had is ignored) 190 | - **color**: existing characters are recolored with the paste color 191 | 192 | - `p` (paste-chars): paste items on clipbard at cursor according to paste mode (above) 193 | 194 | - `zp` (paste-special): 195 | - if paste mode is color, paste color only over top of cursor. 196 | - if group is on clipboard, paste **reference** at cursor. 197 | - otherwise no effect 198 | 199 | - Individual objects that were copied to the clipboard (with `gy`, for instance) are 200 | available to be pasted onto the drawing with the number keys. In visibility mode 2, the objects on the clipboard are shown next to a number. Press that number key to paste that object at the cursor. 201 | 202 | 203 | ### Glyph Discovery 204 | 205 | - `Shift+M` to open a unicode browser 206 | - use standard VisiData commands (`/` to search in a column, `|` to select, `"` to pull selected into their own sheet, etc) 207 | - `y` (or `gy`) to copy one (or more) characters to the clipboard 208 | - which can then be `p`asted directly into a Drawing 209 | 210 | ### Color 211 | 212 | - `c` (set-default-color): set default color to color of top item at cursor 213 | 214 | - `zc` (set-color-input): set default color to input color 215 | 216 | - `<` `>` (cycle-cursor): cycle numeric colors for items under cursor to prev/next color 217 | - `z<` `z>` (cycle-topcursor): cycle numeric colors for top (displayed) items under cursor to prev/next color 218 | - `g<` `g>` (cycle-selected): cycle numeric colors for selected items to prev/next color 219 | 220 | - or, edit the color field directly on the backing sheet 221 | - all the standard bulk VisiData editing commands are available on the backing sheet 222 | 223 | #### Color values 224 | 225 | Color values are strings like `' on '`. Any of these may be omitted; order does not matter. `fg` and `bg`/`on` indicate whether the color is the foreground or background. 226 | 227 | - color names can be standard terminal colors (`red` `green` `blue` `yellow` `cyan` `magenta` `white` `black`) or a number from 0-255 for [xterm 256 colors](https://jonasjacek.github.io/colors/). 228 | - The terminal attributes `underline` and/or `bold` can be anywhere in the string. 229 | - Omitting the fg or bg color will fall back to the default color (white for fg, black for bg) for display. 230 | 231 | - Examples: 232 | - `magenta` 233 | - `green on black` 234 | - `bg cyan fg white` 235 | - `bold 36 on 243` 236 | - `on yellow underline fg 57` 237 | 238 | ### Groups 239 | 240 | Groups are handled as a single entity, both on the drawing and on the backing sheet. 241 | 242 | - `)` `z)` `g)` group cursor/topcursor/selected 243 | - `(` `z(` `g(` degroup cursor/topcursor/selected 244 | 245 | ### Animation 246 | 247 | If an object or group has its 'frame' attribute set, it will only be drawn in frames with that id. 248 | 249 | - `Shift+F` (open-frames): open list of the Frames in this Drawing 250 | - `[` (prev-frame) and `]` (next-frame): go to previous or next frame 251 | - `g[` (first-frame) and `g]` (last-frame): go to first or last frame 252 | - `z[` (new-frame-before) and `z]` (new-frame-after): create a new frame right before or right after the current frame 253 | 254 | - `r` (reset-time): play all frames in animation 255 | 256 | ### VisiData commands (not specific to DarkDraw) 257 | 258 | - `o`: open a new file (open a Drawing if extension is .ddw) 259 | 260 | #### panes and windows 261 | 262 | - `Shift+Z`: split window into 50% top and 50% bottom pane 263 | - `Tab`: swap active pane 264 | - `g Ctrl+^`: cycle through sheets in this pane 265 | 266 | ## Options 267 | 268 | - `autosave_interval_s`: number of seconds between autosaves (default 0 is disabled) 269 | - `autosave_path`: folder for autosave files 270 | - `disp_guide_xy`: string of x y to draw guides onscreen (default `80 25`) 271 | 272 | ## Notes for VisiData users 273 | 274 | - on DrawingSheet, `[` and `]` are unbound (normally sort): accidentally sorting a DrawingSheet can be disastrous, since characters are drawn in order (so later characters are 'on top') 275 | -------------------------------------------------------------------------------- /darkdraw.jsonl: -------------------------------------------------------------------------------- 1 | {"name": "darkdraw", "description": "text art and animation editor", "maintainer": "@devottys", "latest_release": "", "url": "", "latest_ver": "", "visidata_ver": "2.3", "pydeps": "wcwidth", "vdplugindeps": "", "sha256": ""} 2 | -------------------------------------------------------------------------------- /darkdraw/__init__.py: -------------------------------------------------------------------------------- 1 | from .box import * 2 | from .charbrowser import * 3 | from .drawing import * 4 | from .upgrade import * 5 | from .stamps import * 6 | 7 | from .ansihtml import * # save to .ansihtml 8 | from .save import * 9 | 10 | from .loader_scr import * # deprecated 2020 format, remove anytime 11 | 12 | vd.addGlobals(dict(CharBox=CharBox, 13 | Drawing=Drawing, 14 | DrawingSheet=DrawingSheet, 15 | FramesSheet=FramesSheet)) 16 | -------------------------------------------------------------------------------- /darkdraw/ansi.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | $body$ 25 | 26 | 27 | -------------------------------------------------------------------------------- /darkdraw/ansihtml.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from pkg_resources import resource_filename 3 | from visidata import AttrDict, VisiData, colors, vd, dispwidth 4 | import curses 5 | 6 | from .drawing import Drawing, DrawingSheet 7 | 8 | vd.option('darkdraw_html_tmpl', resource_filename(__name__, 'ansi.html'), '') 9 | 10 | 11 | def split_colorstr(colorstr): 12 | 'Return (fgstr, bgstr, attrlist) parsed from colorstr.' 13 | fgbgattrs = ['', '', []] # fgstr, bgstr, attrlist 14 | if not colorstr: 15 | return fgbgattrs 16 | colorstr = str(colorstr) 17 | 18 | i = 0 # fg by default 19 | for x in colorstr.split(): 20 | if x == 'fg': 21 | i = 0 22 | continue 23 | elif x in ['on', 'bg']: 24 | i = 1 25 | continue 26 | 27 | if hasattr(curses, 'A_' + x.upper()): 28 | fgbgattrs[2].append(x) 29 | else: 30 | if not fgbgattrs[i]: # keep first known color 31 | fgbgattrs[i] = x 32 | 33 | return fgbgattrs 34 | 35 | 36 | 37 | def termcolor_to_rgb(n): 38 | if not n: 39 | return (255,255,255) 40 | colordict = dict( 41 | black=(0,0,0), 42 | blue=(114,159,207), 43 | green=(78,154,6), 44 | red=(204,0,0), 45 | cyan=(6,152,154), 46 | magenta=(255,0,255), 47 | brown=(196,160, 0), 48 | white=(211,215,207), 49 | gray=(85,87,83), 50 | lightblue=(50,175,255), 51 | lightgreen=(138,226,52), 52 | lightaqua=(52,226,226), 53 | lightred=(239,41,41), 54 | lightpurple=(173,127,168), 55 | lightyellow=(252,233,79), 56 | brightwhite=(255,255,255), 57 | ) 58 | if n in colordict: 59 | return colordict.get(n) 60 | n = int(n) 61 | if 0 <= n < 16: 62 | return list(colordict.values())[n] 63 | if 16 <= n < 232: 64 | n -= 16 65 | r,g,b = n//36,(n%36)//6,n%6 66 | ints = [0x00, 0x66, 0x88,0xbb,0xdd,0xff] 67 | return ints[r],ints[g],ints[b] 68 | else: 69 | n=list(range(8,255,10))[n-232] 70 | return n,n,n 71 | 72 | 73 | def termcolor_to_css_color(n): 74 | if not n.isdigit(): 75 | return n 76 | r,g,b = termcolor_to_rgb(n) 77 | return '#%02x%02x%02x' % (r,g,b) 78 | 79 | def htmlattrstr(r, attrnames, **kwargs): 80 | d = AttrDict(kwargs) 81 | for a in attrnames: 82 | if a in r: 83 | d[a] = r[a] 84 | return ' '.join('%s="%s"' % (k,v) for k, v in d.items() if v) 85 | 86 | 87 | def colorstr_to_style(color): 88 | fg, bg, attrs = split_colorstr(color) 89 | 90 | style = '' 91 | classes = [] 92 | if 'underline' in attrs: 93 | # style += f'text-decoration: underline; ' 94 | classes.append('underline') 95 | if 'bold' in attrs: 96 | # style += f'font-weight: bold; ' 97 | classes.append('bold') 98 | if 'reverse' in attrs: 99 | bg, fg = fg, bg 100 | if bg: 101 | bg = termcolor_to_css_color(bg) 102 | style += f'background-color: {bg}; ' 103 | if fg: 104 | fg = termcolor_to_css_color(fg) 105 | style += f'color: {fg}; ' 106 | ret = dict(style=style) 107 | if classes: 108 | ret['class'] = ' '.join(classes) 109 | return ret 110 | 111 | def iterline(dwg, y): 112 | leftover = 0 113 | for x in range(dwg.minX, dwg.maxX+1): 114 | # if leftover: 115 | # leftover -= 1 116 | # continue 117 | 118 | rows = dwg._displayedRows.get((x,y), None) 119 | if not rows: 120 | yield x, ' ', AttrDict() 121 | else: 122 | for i in range(len(rows)): 123 | r = rows[-i-1] 124 | if dispwidth(r.text) > x-r.x: 125 | break 126 | if len(r.text) > x-r.x: 127 | ch = r.text[x-r.x] 128 | yield x, ch, r 129 | leftover = dispwidth(ch) - 1 130 | else: 131 | yield x, ' ', AttrDict() 132 | 133 | 134 | def matches(a, b, attrs): 135 | return all(a.get(attr) == b.get(attr) for attr in attrs) 136 | 137 | 138 | @VisiData.api 139 | def save_ansihtml(vd, p, *sheets): 140 | for vs in sheets: 141 | if isinstance(vs, DrawingSheet): 142 | dwg = Drawing('', source=vs) 143 | elif isinstance(vs, Drawing): 144 | dwg = vs 145 | else: 146 | vd.fail(f'{vs.name} not a drawing') 147 | 148 | dwg._scr = mock.MagicMock(__bool__=mock.Mock(return_value=True), 149 | getmaxyx=mock.Mock(return_value=(9999, 9999))) 150 | dwg.reload() 151 | dwg.draw(dwg._scr) 152 | body = '''
'''
153 | 
154 |         for y in range(dwg.minY, dwg.maxY+1):
155 |             line = ''
156 |             text = ''
157 |             lastrow = AttrDict()
158 |             for x, ch, r in iterline(dwg, y):
159 |                 divch = f'
{ch}
' 160 | if matches(r, lastrow, 'color id class href title'.split()): 161 | text += divch 162 | continue 163 | 164 | if text: 165 | kwargs = colorstr_to_style(lastrow.color) 166 | 167 | spanattrstr = htmlattrstr(lastrow, 'id class'.split(), **kwargs) 168 | span = f'{text}' 169 | if lastrow.href: 170 | linkattrstr = htmlattrstr(lastrow, 'href title'.split()) 171 | span = f'{span}' 172 | 173 | line += span 174 | 175 | text = divch 176 | lastrow = r 177 | 178 | body += f'
{line}
\n' 179 | body += '
\n' 180 | 181 | try: 182 | tmpl = open(vs.options.darkdraw_html_tmpl).read() 183 | out = tmpl.replace('$body$', body) 184 | except FileNotFoundError: 185 | out = body 186 | 187 | with p.open_text(mode='w') as fp: 188 | fp.write(out) 189 | -------------------------------------------------------------------------------- /darkdraw/box.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import curses 3 | 4 | from visidata import vd, colors, CharBox 5 | from wcwidth import wcswidth 6 | 7 | def wc_rjust(text, length, padding=' '): 8 | return padding * max(0, (length - wcswidth(text))) + text 9 | 10 | def wc_center(text, length, padding=' '): 11 | x = max(0, (length - wcswidth(text))) 12 | return padding*(x//2) + text + padding*((x+1)//2) 13 | 14 | def wc_ljust(text, length, padding=' '): 15 | return text + padding * max(0, (length - wcswidth(text))) 16 | 17 | 18 | class DrawableBox(CharBox): 19 | def reverse(self): 20 | if not self.scr: return 21 | for y in range(self.y1, self.y2): 22 | for x in range(self.x1, self.x2): 23 | try: 24 | self.scr.chgat(y, x, 1, screen_contents.get((x,y), (0,0))[1] | curses.A_REVERSE) 25 | except curses.error: 26 | vd.fail(y, x) 27 | 28 | def erase(self): 29 | # screen_contents.clear() 30 | for y in range(self.y1, self.y2): 31 | self.scr.addstr(y, 0, ' '*self.w, 0) 32 | 33 | def draw(self, yr, xr, s, attr=0): 34 | 'Draw *s* into box. *yr* an *xr* in relative coordinates; may be ranges.' 35 | if isinstance(attr, str): 36 | attr = colors.get(attr) 37 | 38 | if not isinstance(yr, range): yr = range(yr, yr+1) 39 | if not isinstance(xr, range): xr = range(xr, xr+1) 40 | ymax, xmax = self.scr.getmaxyx() 41 | if self.y1+max(yr) > ymax: fail('need taller screen (at least %s)' % max(yr)) 42 | if self.x1+max(xr) > xmax: fail('need wider screen (at least %s)' % max(xr)) 43 | for y in yr: 44 | for x in xr: 45 | screen_contents[(self.x1+x, self.y1+y)] = (s, attr) 46 | self.scr.addstr(self.y1+y, self.x1+x, s, attr) 47 | 48 | def box(self, x1=0, y1=0, dx=0, color=''): 49 | if self.w <= 0 or self.h <= 0: return 50 | attr = colors.get(color) 51 | x2, y2=x1+self.w+1, y1+self.h+1 52 | self.draw(0, range(x1, x2), '━', attr) 53 | self.draw(y2, range(x1, x2), '━', attr) 54 | self.draw(range(y1, y2), x1, '┃', attr) 55 | self.draw(range(y1, y2), x2, '┃', attr) 56 | self.draw(y1, x1, '┏', attr) 57 | self.draw(y1, x2, '┓', attr) 58 | self.draw(y2, x1, '┗', attr) 59 | self.draw(y2, x2, '┛', attr) 60 | if dx: 61 | self.draw(y1, range(x1+dx, x2, dx), '┯', attr) 62 | self.draw(y1, range(x1+dx, x2, dx), '┯', attr) 63 | self.draw(range(y1+1, y2), range(x1+dx, x2, dx), '│', attr) 64 | self.draw(y2, range(x1+dx, x2, dx), '┷', attr) 65 | 66 | def rjust(self, s, x=0, y=0, w=0, color=' '): 67 | w = w or self.w 68 | return self.ljust(s, x=self.x1+x+w-wcswidth(s)-1, y=y, color=color) 69 | 70 | def center(self, s, x=0, y=0, w=0, padding=' '): 71 | x += max(0, ((w or self.w) - wcswidth(s))) 72 | return self.ljust(s, x=self.x1+x//2, y=y, w=w-x) 73 | 74 | def ljust(self, s, x=0, y=0, w=0, color=' '): 75 | if self.w <= 0 or self.h <= 0: return 76 | if y > self.h: fail(f'{y}/{self.h}') 77 | 78 | scrh, scrw = self.scr.getmaxyx() 79 | attr = colors.get(color) 80 | pre = '' 81 | xi = x 82 | for c in s: 83 | cw = wcswidth(c) 84 | if xi+cw >= self.w: 85 | break 86 | if cw == 0: 87 | pre += c 88 | elif cw < 0: # not printable 89 | pass 90 | else: 91 | self.draw(y, xi, pre+c, attr) 92 | pre = '' 93 | xi += cw 94 | 95 | # add blanks to fill width 96 | for i in range(xi-x, w+1): 97 | self.draw(y, xi+i, ' ', attr) 98 | 99 | return w or xi-x 100 | 101 | def blit(self, tile, *, y1=0, x1=0, y2=None, x2=None, xoff=0, yoff=0): 102 | y2 = y2 or self.h-1 103 | x2 = x2 or self.w-1 104 | y = y1 105 | lines = list(itertools.zip_longest(tile.lines, tile.pcolors)) 106 | while y-y1+yoff < 0 and y < y2: 107 | self.draw(y, x1, ' '*(x2-x1), 0) 108 | y += 1 109 | 110 | while y < y2: 111 | if y-y1+yoff >= len(lines): 112 | self.draw(y, x1, ' '*(x2-x1), 0) 113 | y += 1 114 | continue 115 | 116 | # line, linemask = lines[(y-y1+yoff)%len(lines)] 117 | line, linemask = lines[y-y1+yoff] 118 | pre = '' 119 | x = x1 120 | i = 0 121 | while x-x1+xoff < 0 and x < x2: 122 | self.draw(y, x1, ' '*(x2-x1), 0) 123 | x += 1 124 | 125 | while x < x2: 126 | if x-x1+xoff >= len(line): 127 | self.draw(y, x, ' '*(x2-x), 0) 128 | break 129 | # c = line[(xoff+i)%len(line)] 130 | c = line[x-x1+xoff] 131 | cmask = linemask[x-x1+xoff] if linemask else 0 132 | w = wcswidth(c) 133 | if w == 0: 134 | pre = c 135 | elif w < 0: # not printable 136 | pass 137 | else: 138 | attr = colors.get(tile.palette[cmask]) if cmask else 0 139 | try: 140 | self.draw(y, x, pre+c, attr) 141 | except curses.error: 142 | raise Exception(f'y={y} x={x}') 143 | x += w 144 | pre = '' 145 | i += 1 146 | 147 | y += 1 148 | 149 | while y < y2: 150 | try: 151 | self.draw(y, x1, ' '*(x2-x1), 0) 152 | y += 1 153 | except curses.error: 154 | raise Exception(y, y2) 155 | -------------------------------------------------------------------------------- /darkdraw/charbrowser.py: -------------------------------------------------------------------------------- 1 | import unicodedata 2 | 3 | from visidata import * 4 | 5 | enumvalues = { 6 | 'category': { 7 | 'Lu': 'Letter, uppercase', 8 | 'Ll': 'Letter, lowercase', 9 | 'Lt': 'Letter, titlecase', 10 | 'Lm': 'Letter, modifier', 11 | 'Lo': 'Letter, other', 12 | 'Mn': 'Mark, nonspacing', 13 | 'Mc': 'Mark, spacing combining', 14 | 'Me': 'Mark, enclosing', 15 | 'Nd': 'Number, decimal digit', 16 | 'Nl': 'Number, letter', 17 | 'No': 'Number, other', 18 | 'Pc': 'Punctuation, connector', 19 | 'Pd': 'Punctuation, dash', 20 | 'Ps': 'Punctuation, open', 21 | 'Pe': 'Punctuation, close', 22 | 'Pi': 'Punctuation, initial quote', 23 | 'Pf': 'Punctuation, final quote', 24 | 'Po': 'Punctuation, other', 25 | 'Sm': 'Symbol, math', 26 | 'Sc': 'Symbol, currency', 27 | 'Sk': 'Symbol, modifier', 28 | 'So': 'Symbol, other', 29 | 'Zs': 'Separator, space', 30 | 'Zl': 'Separator, line', 31 | 'Zp': 'Separator, paragraph', 32 | 'Cc': 'Other, control', 33 | 'Cf': 'Other, format', 34 | 'Cs': 'Other, surrogate', 35 | 'Co': 'Other, private use', 36 | 'Cn': 'Other, not assigned', 37 | }, 38 | 39 | 'bidirectional': { 40 | 'L': 'Left_To_Right', # any strong left-to-right character 41 | 'R': 'Right_To_Left', # any strong right-to-left (non-Arabic-type) character 42 | 'AL': 'Arabic_Letter', # any strong right-to-left (Arabic-type) character 43 | #Weak Types 44 | 'EN': 'European_Number', # any ASCII digit or Eastern Arabic-Indic digit 45 | 'ES': 'European_Separator', # plus and minus signs 46 | 'ET': 'European_Terminator', # a terminator in a numeric format context, includes currency signs 47 | 'AN': 'Arabic_Number', # any Arabic-Indic digit 48 | 'CS': 'Common_Separator', # commas, colons, and slashes 49 | 'NSM': 'Nonspacing_Mark', # any nonspacing mark 50 | 'BN': 'Boundary_Neutral', # most format characters, control codes, or noncharacters 51 | #Neutral Types 52 | 'B': 'Paragraph_Separator', # various newline characters 53 | 'S': 'Segment_Separator', # various segment-related control codes 54 | 'WS': 'White_Space', # spaces 55 | 'ON': 'Other_Neutral', # most other symbols and punctuation marks 56 | #Explicit Formatting Types 57 | 'LRE': 'Left_To_Right_Embedding', # U+202A: the LR embedding control 58 | 'LRO': 'Left_To_Right_Override', # U+202D: the LR override control 59 | 'RLE': 'Right_To_Left_Embedding', # U+202B: the RL embedding control 60 | 'RLO': 'Right_To_Left_Override', # U+202E: the RL override control 61 | 'PDF': 'Pop_Directional_Format', # U+202C: terminates an embedding or override control 62 | 'LRI': 'Left_To_Right_Isolate', # U+2066: the LR isolate control 63 | 'RLI': 'Right_To_Left_Isolate', # U+2067: the RL isolate control 64 | 'FSI': 'First_Strong_Isolate', # U+2068: the first strong isolate control 65 | 'PDI': 'Pop_Directional_Isolate', # U+2069: terminates an isolate control 66 | }, 67 | 'east_asian_width': { 68 | 'A': 'Ambiguous', 69 | 'F': 'Fullwidth', 70 | 'H': 'Halfwidth', 71 | 'N': 'Neutral', 72 | 'Na': 'Narrow', 73 | 'W': 'Wide', 74 | }, 75 | 'mirrored': { 0: 'No', 1: 'Yes' }, 76 | } 77 | 78 | 79 | class UnicodeDataColumn(Column): 80 | def __init__(self, name, *args, **kwargs): 81 | super().__init__(name, expr=name, *args, **kwargs) 82 | 83 | def calcValue(self, row): 84 | r = getattr(unicodedata, self.expr)(row.text) 85 | if self.expr in enumvalues: 86 | return enumvalues[self.expr][r] 87 | return r 88 | 89 | 90 | class UnicodeBrowser(Sheet): 91 | rowtype='chars' # rowdef: AttrDict(.text=ch) 92 | precious=False 93 | columns = [ 94 | UnicodeDataColumn('name', width=40), 95 | Column('text', getter=lambda c,r: unicodedata.normalize('NFC', r.text)), 96 | Column('num', fmtstr='%04X', type=int, getter=lambda c,r: ord(r.text)), 97 | UnicodeDataColumn('category'), 98 | # UnicodeDataColumn('decimal'), 99 | # UnicodeDataColumn('digit'), 100 | UnicodeDataColumn('numeric'), 101 | UnicodeDataColumn('bidirectional'), 102 | UnicodeDataColumn('east_asian_width'), 103 | UnicodeDataColumn('combining'), 104 | UnicodeDataColumn('mirrored'), 105 | # UnicodeDataColumn('decomposition'), 106 | # UnicodeDataColumn('normalize'), 107 | # UnicodeDataColumn('is_normalized', width=0), 108 | ] 109 | 110 | 111 | @VisiData.lazy_property 112 | def unibrowser(vd): 113 | return UnicodeBrowser('unicode_chars', rows=[AttrDict(text=chr(i)) for i in range(32, 0x10000) if unicodedata.category(chr(i))[0] not in 'CM']) 114 | -------------------------------------------------------------------------------- /darkdraw/drawing.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from collections import defaultdict 3 | import itertools 4 | import functools 5 | from random import choice 6 | import time 7 | import unicodedata 8 | from copy import copy, deepcopy 9 | from visidata import * 10 | from visidata import dispwidth, CharBox, boundingBox 11 | from visidata.bezier import bezier 12 | 13 | 14 | vd.allPrefixes += list('01') 15 | vd.option('pen_down', False, 'is pen down') 16 | vd.option('disp_guide_xy', '', 'x y position of onscreen guides') 17 | vd.option('autosave_interval_s', 0, 'seconds between autosave') 18 | vd.option('autosave_path', 'autosave', 'path to put autosave files') 19 | vd.option('ddw_add_baseframe', False, 'add text to baseframe instead of current frame') 20 | 21 | vd.charPalWidth = charPalWidth = 16 22 | vd.charPalHeight = charPalHeight = 16 23 | 24 | @VisiData.api 25 | def open_ddw(vd, p): 26 | vd.timeouts_before_idle = 1000 27 | return DrawingSheet(p.name, source=p).drawing 28 | 29 | vd.new_ddw = vd.open_ddw 30 | 31 | vd.save_ddw = vd.save_jsonl 32 | 33 | @VisiData.lazy_property 34 | def words(vd): 35 | return [x.strip() for x in open('/usr/share/dict/words').readlines() if 3 <= len(x) < 8 and x.islower() and x.strip().isalpha()] 36 | 37 | @VisiData.api 38 | def random_word(vd): 39 | try: 40 | return choice(vd.words) 41 | except FileNotFoundError: 42 | pass 43 | except Exception as e: 44 | vd.exceptionCaught(e) 45 | 46 | return 'unnamed' 47 | 48 | 49 | def any_match(G1, G2): 50 | if G1 and G2: 51 | for g in G1: 52 | if g in G2: return True 53 | 54 | class FramesSheet(Sheet): 55 | rowtype='frames' # rowdef: { .type, .id, .duration_ms, .x, .y } 56 | columns = [ 57 | ItemColumn('type', width=0), 58 | ItemColumn('id'), 59 | ItemColumn('duration_ms', type=int), 60 | ItemColumn('x', type=int), 61 | ItemColumn('y', type=int), 62 | ] 63 | 64 | class DrawingSheet(JsonSheet): 65 | rowtype='elements' # rowdef: { .type, .x, .y, .text, .color, .group, .tags=[], .frame, .id, .rows=[] } 66 | columns=[ 67 | ItemColumn('id'), 68 | ItemColumn('type'), 69 | ItemColumn('x', type=int), 70 | ItemColumn('y', type=int), 71 | 72 | ItemColumn('text'), # for text objects (type == '') 73 | ItemColumn('color'), # for text 74 | 75 | # for all objects 76 | ItemColumn('tags'), # for all objs 77 | ItemColumn('group'), # " 78 | ItemColumn('frame'), # " 79 | 80 | ItemColumn('rows'), # for groups 81 | ItemColumn('duration_ms', type=int), # for frames 82 | 83 | ItemColumn('ref'), 84 | ] 85 | colorizers = [ 86 | CellColorizer(3, None, lambda s,c,r,v: r and c and c.name == 'text' and r.color) 87 | ] 88 | def newRow(self): 89 | return AttrDict(x=None, y=None, text='', color='', tags=[], group='') 90 | 91 | @functools.cached_property 92 | def drawing(self): 93 | return Drawing(self.name+".ddw", source=self) 94 | 95 | def addRow(self, row, **kwargs): 96 | row = super().addRow(row, **kwargs) 97 | vd.addUndo(self.rows.remove, row) 98 | return row 99 | 100 | def iterdeep(self, rows, x=0, y=0, parents=None): 101 | for r in rows: 102 | try: 103 | newparents = (parents or []) + [r] 104 | if r.type == 'frame': continue 105 | if r.ref: 106 | assert r.type == 'ref' 107 | g = self.groups[r.ref] 108 | yield from self.iterdeep(g.rows, x+r.x, y+r.y, newparents) 109 | else: 110 | yield r, x+r.x, y+r.y, newparents 111 | yield from self.iterdeep(r.rows or [], x+r.x, x+r.y, newparents) 112 | except Exception as e: 113 | vd.exceptionCaught(e) 114 | 115 | def untag_rows(self, rows, s): 116 | col = self.column('tags') 117 | for row in Progress(rows): 118 | v = col.getValue(row) 119 | assert isinstance(v, (list, tuple)), type(r).__name__ 120 | v = [x for x in v if x != s] 121 | col.setValue(row, v) 122 | 123 | def tag_rows(self, rows, tagstr): 124 | tags = tagstr.split() 125 | for r in rows: 126 | if not r.tags: r.tags = [] 127 | for tag in tags: 128 | if tag not in r.tags: 129 | r.tags.append(tag) 130 | 131 | @property 132 | def groups(self): 133 | return {r.id:r for r in self.rows if r.type == 'group'} 134 | 135 | def create_group(self, gname): 136 | nr = self.newRow() 137 | nr.id = gname 138 | nr.type = 'group' 139 | vd.status('created group "%s"' % gname) 140 | return self.addRow(nr) 141 | 142 | @drawcache_property 143 | def frames(self): 144 | return [r for r in self.rows if r.type == 'frame'] 145 | 146 | @property 147 | def nFrames(self): 148 | return len(self.frames) 149 | 150 | def new_between_frame(self, fidx1, fidx2): 151 | f1 = f2 = None 152 | if not self.frames: 153 | name = '0' 154 | else: 155 | if 0 <= fidx1 < len(self.frames): 156 | f1 = self.frames[fidx1] 157 | if 0 <= fidx2 < len(self.frames): 158 | f2 = self.frames[fidx2] 159 | if f1 and f2: 160 | name = str(f1.id)+'-'+str(f2.id) 161 | elif f1: 162 | name = str(int(f1.id)+1) 163 | elif f2: 164 | name = str(int(f2.id)-1) 165 | 166 | newf = self.newRow() 167 | newf.type = 'frame' 168 | newf.id = name 169 | newf.duration_ms = 100 170 | if f1: 171 | # insert frame just after the first frame in the actual rowset 172 | for i, r in enumerate(self.rows): 173 | if r is f1: 174 | vd.clearCaches() 175 | self.addRow(newf, index=i+1) 176 | break 177 | 178 | # copy all rows on frame1 179 | thisframerows = list(copy(r) for r in self.rows if f1.id in r.frame.split()) 180 | for r in thisframerows: 181 | r.frame = newf.id 182 | self.addRow(r) 183 | return newf 184 | else: 185 | vd.clearCaches() 186 | return self.addRow(newf, index=0) 187 | vd.error('no existing frame ' + str(f1)) 188 | 189 | def group_selected(self, gname): 190 | nr = self.create_group(gname) 191 | 192 | nr.rows = deepcopy(self.selectedRows) 193 | x1, y1, x2, y2 = boundingBox(nr.rows) 194 | nr.x, nr.y, nr.w, nr.h = x1, y1, x2-x1, y2-y1 195 | for r in nr.rows: 196 | r.x = (r.x or 0) - x1 197 | r.y = (r.y or 0) - y1 198 | 199 | def _undoGroupSelected(sheet, group): 200 | sheet.rows.pop(sheet.rows.index(group)) 201 | 202 | self.deleteSelected() 203 | self.select([nr]) 204 | 205 | vd.addUndo(_undoGroupSelected, self, nr) 206 | vd.status('group "%s" (%d objects)' % (gname, self.nSelectedRows)) 207 | 208 | def regroup(self, rows): 209 | regrouped = [] 210 | groups = set() # that items were grouped into 211 | new_rows = deepcopy(rows) 212 | for r in new_rows: 213 | if r.group: 214 | regrouped.append(r) 215 | if r.group not in self.groups: 216 | g = self.create_group(r.group) 217 | g.x = r.x 218 | g.y = r.y 219 | self.addRow(g) 220 | else: 221 | g = self.groups[r.group] 222 | 223 | r.x -= g.x 224 | r.y -= g.y 225 | g.rows.append(r) 226 | vd.addUndo(g.rows.pop, g.rows.index(r)) 227 | groups.add(r.group) 228 | 229 | self.deleteBy(lambda r,rows=regrouped: r in rows) 230 | 231 | self.select(list(g for name, g in self.groups.items() if name in groups)) 232 | 233 | vd.status('regrouped %d %s' % (len(regrouped), self.rowtype)) 234 | 235 | def degroup(self, rows): 236 | degrouped = [] 237 | groups = set() 238 | for row in rows: 239 | if row.type == 'ref': 240 | vd.warning("can't degroup reference (to '%s')" % row.ref) 241 | continue 242 | for r, x, y, parents in self.iterdeep([row]): 243 | r.x = x 244 | r.y = y 245 | r.group = '.'.join((p.id or '') for p in parents[:-1]) 246 | 247 | if r.type == 'group': 248 | groups.add(r.id) 249 | 250 | if r is not parents[0]: 251 | self.addRow(r) 252 | degrouped.append(r) 253 | 254 | for g in groups: 255 | oldrows = copy(self.groups[g].rows) 256 | self.groups[g].rows.clear() 257 | vd.addUndo(self.regroup, oldrows) 258 | 259 | vd.status('ungrouped %d %s' % (len(degrouped), self.rowtype)) 260 | return degrouped 261 | 262 | def gatherTag(self, gname): 263 | return list(r for r in self.rows if gname in r.get('tags', '')) 264 | 265 | def slide_top(self, rows, index=0): 266 | 'Move selected rows to top of sheet (bottom of drawing)' 267 | for r in rows: 268 | self.rows.pop(self.rows.index(r)) 269 | self.rows.insert(index, r) 270 | 271 | def sort(self): 272 | vd.fail('sort disabled on drawing sheet') 273 | 274 | def save_txt(self, p, *sheets): 275 | dwg = self.drawing 276 | dwg.draw(None) 277 | with p.open_text(mode='w') as fp: 278 | for vs in sheets: 279 | line = '' 280 | for y in range(dwg.maxY+1): 281 | for x in range(dwg.maxX+1): 282 | r = dwg._displayedRows.get((x,y), None) 283 | if r: line += r[-1].text[x-r[-1].x] 284 | else: line += ' ' 285 | line = line.rstrip(' ') + '\n' 286 | 287 | if line.strip(): 288 | fp.write(line) 289 | line = '' 290 | # if only newlines, let it ride 291 | 292 | 293 | class Drawing(TextCanvas): 294 | rowtype = 'elements' # rowdef: AttrDict (same as DrawingSheet) 295 | def iterbox(self, box, n=None, frames=None): 296 | 'If *frames* is None, return top *n* elements from each cell within the given box (current frame falling back to base frame). Otherwise return all elements from each cell within the given box (so base frame + current frame). Otherwise return all elements that would be displayed in displayed in either If frames is None, uses actually displayed elements; otherwise, ' 297 | ret = list() 298 | if frames is None: 299 | for nx in range(box.x1, box.x2-1): 300 | for ny in range(box.y1, box.y2-1): 301 | for r in self._displayedRows[(nx,ny)][-(n or 0):]: 302 | if r not in ret: 303 | ret.append(r) 304 | else: 305 | for r in self.rows: 306 | if self.inFrame(r, frames): 307 | if box.contains(CharBox(None, r.x, r.y, r.w or dispwidth(r.text or ''), r.h or 1)): 308 | ret.append(r) 309 | 310 | return ret 311 | 312 | def __getattr__(self, k): 313 | if k == 'source' or self.source is self: 314 | return super().__getattr__(k) 315 | return getattr(self.source, k) 316 | 317 | @property 318 | def currentFrame(self): 319 | if self.frames and 0 <= self.cursorFrameIndex < self.nFrames: 320 | return self.frames[self.cursorFrameIndex] 321 | return AttrDict() 322 | 323 | def elements(self, frames=None): 324 | 'Return elements in *frames*. If *frames* is None, then base image only. Otherwise, *frames* must be a list of frame rows (like from .currentFrame or .frames).' 325 | return [r for r in self.rows if self.inFrame(r, frames)] 326 | 327 | def inFrame(self, r, frames): 328 | 'Return True if *r* is an element that would be displayed (even if hidden or buried) in the given set of *frames*.' 329 | if r.type: return False # frame or other non-element type 330 | if not r.frame: return True 331 | if not frames: return False 332 | return any(f.id in r.frame.split() for f in frames) 333 | 334 | def moveToRow(self, rowstr): 335 | self.refresh() 336 | a, b = map(int, rowstr.split()) 337 | self.cursorBox.y1, self.cursorBox.y2 = a, b 338 | return True 339 | 340 | def moveToCol(self, colstr): 341 | self.refresh() 342 | a, b = map(int, colstr.split()) 343 | self.cursorBox.x1, self.cursorBox.x2 = a, b 344 | return True 345 | 346 | def itercursor(self, n=None, frames=None): 347 | return self.iterbox(self.cursorBox, n=n, frames=frames) 348 | 349 | def refresh(self): 350 | 'Clear and redraw the existing screen.' 351 | if not self._scr: 352 | self._scr = mock.MagicMock(__bool__=mock.Mock(return_value=False)) 353 | self._scr.clear() 354 | self.draw(self._scr) 355 | 356 | def autosave(self): 357 | try: 358 | now = time.time() 359 | autosave_interval_s = self.options.autosave_interval_s 360 | if autosave_interval_s and now-self.last_autosave > autosave_interval_s: 361 | p = Path(options.autosave_path) 362 | if not p.exists(): 363 | os.makedirs(p) 364 | vd.saveSheets(p/time.strftime(self.name+'-%Y%m%dT%H%M%S.ddw', time.localtime(now)), self) 365 | self.last_autosave = now 366 | except Exception as e: 367 | vd.exceptionCaught(e) 368 | 369 | def draw(self, scr): 370 | now = time.time() 371 | self.autosave() 372 | vd.getHelpPane('darkdraw', module='darkdraw').draw(scr, y=-1, x=-1) 373 | 374 | thisframe = self.currentFrame 375 | if self.autoplay_frames: 376 | vd.timeouts_before_idle = -1 377 | ft, f = self.autoplay_frames[0] 378 | thisframe = f 379 | if not ft: 380 | self.autoplay_frames[0][0] = now 381 | elif now-ft > f.duration_ms/1000: # frame expired 382 | # vd.status('next frame after %0.3fs' % (now-ft)) 383 | self.autoplay_frames.pop(0) 384 | if self.autoplay_frames: 385 | self.autoplay_frames[0][0] = now 386 | thisframe = self.autoplay_frames[0][1] 387 | vd.curses_timeout = thisframe.duration_ms 388 | else: 389 | vd.status('ending autoplay') 390 | vd.timeouts_before_idle = 10 391 | vd.curses_timeout = 100 392 | 393 | self._displayedRows = defaultdict(list) # (x, y) -> list of rows; actual screen layout (topmost last in list) 394 | self._tags = defaultdict(list) # "tag" -> list of rows with that tag 395 | 396 | selectedGroups = set() # any group with a selected element 397 | 398 | self.yoffset = max(self.yoffset, 0) 399 | self.xoffset = max(self.xoffset, 0) 400 | self.yoffset = min(self.yoffset, self.maxY+1) 401 | self.xoffset = min(self.xoffset, self.maxX+1) 402 | 403 | def draw_guides(xmax, ymax): 404 | if ymax < self.windowHeight-1: 405 | for x in range(xmax): 406 | if x < self.windowWidth-1: 407 | scr.addstr(ymax, x, '-') 408 | 409 | if xmax < self.windowWidth-1: 410 | for y in range(ymax): 411 | if y < self.windowHeight-1: 412 | scr.addstr(y, xmax, '|') 413 | 414 | #draw_guides(self.maxX+1, self.maxY+1) 415 | guidexy = self.options.disp_guide_xy 416 | if guidexy: 417 | try: 418 | guidex,guidey = map(int, guidexy.split()) 419 | draw_guides(guidex, guidey) 420 | except Exception as e: 421 | vd.exceptionCaught(e) 422 | 423 | # draw blank cursor as backdrop but on top of guides 424 | for i in range(self.cursorBox.h): 425 | for j in range(self.cursorBox.w): 426 | y = self.cursorBox.y1+i-self.yoffset 427 | x = self.cursorBox.x1+j-self.xoffset 428 | clipdraw(scr, y, x, ' ', colors.color_current_row) 429 | 430 | for r, x, y, parents in self.iterdeep(self.source.rows): 431 | sy = y - self.yoffset 432 | sx = x - self.xoffset 433 | toprow = parents[0] 434 | for g in (r.tags or []): 435 | self._tags[g].append(r) 436 | 437 | if not r.text: continue 438 | if any_match(r.tags, self.disabled_tags): continue 439 | if toprow.frame or r.frame: 440 | if not self.inFrame(r, [thisframe]): continue 441 | 442 | c = r.color or '' 443 | if self.cursorBox.contains(CharBox(scr, x, y, r.w or dispwidth(r.text), r.h or 1)): 444 | c = self.options.color_current_row + ' ' + str(c) 445 | if self.source.isSelected(toprow): 446 | c = self.options.color_selected_row + ' ' + str(c) 447 | if r.tags: selectedGroups |= set(r.tags) 448 | a = colors[c] 449 | 450 | if (0 <= sy < self.windowHeight-1 and 0 <= sx < self.windowWidth): # inside screen 451 | w = clipdraw(scr, sy, sx, r.text, a) 452 | 453 | for i in range(0, dispwidth(r.text)): 454 | cellrows = self._displayedRows[(x+i, y)] 455 | if toprow not in cellrows: 456 | cellrows.append(toprow) 457 | 458 | defcolor = self.options.color_default 459 | defattr = colors[defcolor] 460 | if self.options.visibility == 1: # draw tags 461 | clipdraw(scr, 0, self.windowWidth-20, ' 00: (reset) ', defattr) 462 | for i, tag in enumerate(self._tags.keys()): 463 | c = defcolor 464 | if tag in self.disabled_tags: 465 | c = self.options.color_graph_hidden 466 | if self.cursorRow and tag in self.cursorRow.get('group', ''): 467 | c = self.options.color_current_row + ' ' + c 468 | if tag in selectedGroups: 469 | c = self.options.color_selected_row + ' ' + c 470 | clipdraw(scr, i+1, self.windowWidth-20, ' %02d: %7s ' % (i+1, tag), colors[c]) 471 | 472 | elif self.options.visibility == 2: # draw clipboard item shortcuts 473 | if not vd.memory.cliprows: 474 | return 475 | for i, r in enumerate(vd.memory.cliprows[:10]): 476 | x = self.windowWidth-20 477 | x += clipdraw(scr, i+1, x, ' %d: ' % (i+1), defattr) 478 | x += clipdraw(scr, i+1, x, r.text + ' ', colors[r.color]) 479 | 480 | 481 | # draw lstatus2 (paste status with default color) 482 | y = self.windowHeight-2 483 | x = 3 484 | x += clipdraw(scr, y, x, 'paste ' + self.paste_mode + ' ', defattr) 485 | 486 | x += clipdraw(scr, y, x, ' %s %s ' % (len(vd.memory.cliprows or []), self.rowtype), defattr) 487 | 488 | x += clipdraw(scr, y, x, ' default color: ', defattr) 489 | x += clipdraw(scr, y, x, '##', colors[vd.default_color]) 490 | x += clipdraw(scr, y, x, ' %s' % vd.default_color, defattr) 491 | 492 | # draw rstatus2 (cursor status) 493 | if hasattr(self, 'cursorRows') and self.cursorRows: 494 | c = self.cursorRows[0].color 495 | x = self.windowWidth-30-len(c) 496 | x += clipdraw(scr, y, x, '%s ' % c, defattr) 497 | x += clipdraw(scr, y, x, '##', colors[c]) 498 | if self.cursorChar: 499 | x += clipdraw(scr, y, x, ' '+self.cursorChar[0], colors[c], w=3) 500 | x += clipdraw(scr, y, x, ' U+%04X' % ord(self.cursorChar[0]), defattr) 501 | 502 | x = self.windowWidth-16 503 | x += clipdraw(scr, y, x, ' %s' % self.cursorBox, defattr) 504 | 505 | def reload(self): 506 | self.source.ensureLoaded() 507 | vd.sync() 508 | self.minX, self.minY, self.maxX, self.maxY = boundingBox(self.source.rows) 509 | if self._scr: 510 | self.draw(self._scr) 511 | 512 | def add_text(self, text, x, y, color=''): 513 | r = self.newRow() 514 | r.x, r.y, r.text, r.color = x, y, text, color 515 | if not self.options.ddw_add_baseframe: 516 | r.frame = self.currentFrame.id 517 | 518 | self.source.addRow(r) 519 | self.modified = True 520 | return r 521 | 522 | def place_text(self, text, box, dx=0, dy=0, go_forward=True): 523 | 'Return (width, height) of drawn text.' 524 | self.add_text(text, box.x1, box.y1) 525 | 526 | if go_forward: 527 | self.go_forward(dispwidth(text)+dx, 1+dy) 528 | if self.cursorBox.x1 > self.windowWidth: 529 | self.cursorBox.x1 = 1 530 | self.cursorBox.y1 += 1 531 | if self.cursorBox.y1 > self.windowHeight-1: 532 | self.cursorBox.x1 += 1 533 | self.cursorBox.y1 = 1 534 | 535 | def edit_text(self, text, row): 536 | if row is None: 537 | self.place_text(text, self.cursorBox, dx=1) 538 | return 539 | oldtext = row.text 540 | row.text = text 541 | vd.addUndo(setattr, row, 'text', oldtext) 542 | 543 | 544 | def get_text(self, x=None, y=None): 545 | 'Return text of topmost visible element at (x,y) (or cursor if not given).' 546 | if x is None: x = self.cursorBox.x1 547 | if y is None: y = self.cursorBox.y1 548 | r = self._displayedRows.get((x,y), None) 549 | if not r: return '' 550 | return r[-1]['text'][x-r[-1].x] 551 | 552 | def remove_at(self, box): 553 | rows = list(self.iterbox(box)) 554 | self.source.deleteBy(lambda r,rows=rows: r in rows) 555 | return rows 556 | 557 | @property 558 | def cursorRows(self): 559 | return list(self.iterbox(self.cursorBox)) 560 | 561 | @property 562 | def topCursorRows(self): 563 | return list(self.iterbox(self.cursorBox, n=1)) 564 | 565 | @property 566 | def cursorRow(self): 567 | cr = self.cursorRows 568 | if cr: return cr[-1] 569 | 570 | @property 571 | def cursorChar(self): 572 | cr = self.cursorRow 573 | if cr: return cr.get('text', '') 574 | return '' 575 | 576 | @property 577 | def cursorDesc(self): 578 | cr = self.cursorRow 579 | if cr and cr.text: 580 | return 'U+%04X' % ord(cr.text[0]) 581 | if cr and cr.type == 'group': 582 | n = len(list(self.iterdeep(cr.rows))) 583 | return '%s (%s objects)' % (cr.id, n) 584 | return '???' 585 | 586 | @property 587 | def frameDesc(sheet): 588 | if not sheet.frames: 589 | return '' 590 | return f'Frame {sheet.currentFrame.id} {sheet.cursorFrameIndex}/{sheet.nFrames-1}' 591 | 592 | @property 593 | def cursorCharName(self): 594 | ch = self.cursorChar 595 | if not ch: return '' 596 | return unicodedata.name(ch[0]) 597 | 598 | def go_left(self): 599 | if self.options.pen_down: 600 | self.pendir = 'l' 601 | self.place_text(ch, self.cursorBox, **vd.memory.cliprows[0]) 602 | else: 603 | self.cursorBox.x1 -= 1 604 | 605 | def go_right(self): 606 | if self.options.pen_down: 607 | self.pendir = 'r' 608 | self.place_text(ch, self.cursorBox, **vd.memory.cliprows[0]) 609 | else: 610 | self.cursorBox.x1 += 1 611 | 612 | def go_down(self): 613 | if self.options.pen_down: 614 | self.pendir = 'd' 615 | self.place_text(ch, self.cursorBox, **vd.memory.cliprows[0]) 616 | else: 617 | self.cursorBox.y1 += 1 618 | 619 | def go_up(self): 620 | if self.options.pen_down: 621 | self.pendir = 'u' 622 | self.place_text(ch, self.cursorBox, **vd.memory.cliprows[0]) 623 | else: 624 | self.cursorBox.y1 -= 1 625 | 626 | def go_pagedown(self, n): 627 | if n < 0: 628 | self.cursorBox.y1 = 0 629 | else: 630 | self.cursorBox.y1 = self.windowHeight-2 631 | 632 | def go_leftmost(self): 633 | self.cursorBox.x1 = 0 634 | 635 | def go_rightmost(self): 636 | self.cursorBox.x1 = self.maxX 637 | 638 | def go_top(self): 639 | self.cursorBox.y1 = 0 640 | 641 | def go_bottom(self): 642 | self.cursorBox.y1 = self.maxY 643 | 644 | def go_forward(self, x, y): 645 | if self.pendir == 'd': self.cursorBox.y1 += y 646 | elif self.pendir == 'u': self.cursorBox.y1 -= y 647 | elif self.pendir == 'r': self.cursorBox.x1 += x 648 | elif self.pendir == 'l': self.cursorBox.x1 -= x 649 | 650 | def go_obj(self, xdir=0, ydir=0): 651 | x=self.cursorBox.x1 652 | y=self.cursorBox.y1 653 | currows = self._displayedRows.get((x, y), []) 654 | xmin = min(x for x, y in self._displayedRows.keys()) 655 | ymin = min(y for x, y in self._displayedRows.keys()) 656 | xmax = max(x for x, y in self._displayedRows.keys()) 657 | ymax = max(y for x, y in self._displayedRows.keys()) 658 | 659 | while xmin <= x <= xmax and ymin <= y <= ymax: 660 | for r in self._displayedRows.get((x, y), [])[::-1]: 661 | if r and r not in currows: 662 | self.cursorBox.x1 = x 663 | self.cursorBox.y1 = y 664 | return 665 | x += xdir 666 | y += ydir 667 | 668 | def checkCursor(self): 669 | super().checkCursor() 670 | self.cursorFrameIndex = max(min(self.cursorFrameIndex, len(self.frames)-1), 0) 671 | 672 | def join_rows(dwg, rows): 673 | vd.addUndo(setattr, rows[0], 'text', rows[0].text) 674 | rows[0].text = ''.join(r.text for r in rows) 675 | dwg.source.deleteBy(lambda r,rows=rows[1:]: r in rows) 676 | 677 | def cycle_paste_mode(self): 678 | modes = ['all', 'char', 'color'] 679 | self.paste_mode = modes[(modes.index(self.paste_mode)+1)%len(modes)] 680 | 681 | def paste_chars(self, srcrows, box, n=None): 682 | srcrows or vd.fail('no rows to paste') 683 | 684 | newrows = [] 685 | npasted = 0 686 | x1, y1, x2, y2 = boundingBox(srcrows) 687 | for oldr in srcrows: 688 | if oldr.x is None: 689 | newx = box.x1 690 | newy = box.y1 691 | if len(srcrows) > 1: 692 | self.go_forward(dispwidth(oldr.text)+1, 1) 693 | else: 694 | newx = (oldr.x or 0)+box.x1-x1 695 | newy = (oldr.y or 0)+box.y1-y1 696 | 697 | if self.paste_mode in 'all char': 698 | r = self.newRow() 699 | r.update(deepcopy(oldr)) 700 | if not self.options.ddw_add_baseframe: 701 | r.frame = self.currentFrame.id 702 | r.text = oldr.text 703 | r.x, r.y = newx, newy 704 | if self.paste_mode == 'char': 705 | r.color = vd.default_color 706 | newrows.append(r) 707 | self.source.addRow(r) 708 | npasted += 1 709 | elif self.paste_mode == 'color': 710 | if oldr.color and newx < box.x2 and newy < box.y2-1: 711 | for existing in self._displayedRows[(newx, newy)][-(n or 0):]: 712 | npasted += 1 713 | existing.color = oldr.color 714 | 715 | if npasted == 0: 716 | vd.warning(f'paste mode {self.paste_mode} had nothing to paste') 717 | 718 | def paste_special(self): 719 | if self.paste_mode == 'color': # top only 720 | return self.paste_chars(vd.memory.cliprows, self.cursorBox, n=1) 721 | 722 | for r in vd.memory.cliprows: 723 | if r.type == 'group': 724 | newr = self.newRow() 725 | newr.type = 'ref' 726 | newr.x, newr.y = self.cursorBox.x1, self.cursorBox.y1 727 | newr.ref = r.id 728 | self.addRow(newr) 729 | elif r.type: 730 | vd.status('ignoring %s type row' % r.type) 731 | 732 | def select_tag(self, tag): 733 | self.select(list(r for r in self.source.rows if tag in (r.tags or ''))) 734 | 735 | def unselect_tag(self, tag): 736 | self.unselect(list(r for r in self.rows if tag in (r.tags or ''))) 737 | 738 | def align_selected(self, attrname): 739 | rows = self.someSelectedRows 740 | for r in rows: 741 | r.x = rows[0].x 742 | 743 | 744 | Drawing.init('mode', str) 745 | Drawing.init('linepoints', list) 746 | Drawing.init('cursorBox', lambda: CharBox(None, 0,0,1,1)) 747 | Drawing.init('_displayedRows', dict) # (x,y) -> list of rows 748 | Drawing.init('pendir', lambda: 'r') 749 | Drawing.init('disabled_tags', set) # set of groupnames which should not be drawn or interacted with 750 | DrawingSheet.init('minX', int) 751 | DrawingSheet.init('minY', int) 752 | DrawingSheet.init('maxX', int) 753 | DrawingSheet.init('maxY', int) 754 | 755 | Drawing.addCommand(None, 'go-left', 'go_left()', 'go left one char') 756 | Drawing.addCommand(None, 'go-down', 'go_down()', 'go down one char') 757 | Drawing.addCommand(None, 'go-up', 'go_up()', 'go up one char') 758 | Drawing.addCommand(None, 'go-right', 'go_right()', 'go right one char in the palette') 759 | Drawing.addCommand(None, 'go-pagedown', 'go_pagedown(+1);', 'scroll one page forward in the palette') 760 | Drawing.addCommand(None, 'go-pageup', 'go_pagedown(-1)', 'scroll one page backward in the palette') 761 | 762 | Drawing.addCommand(None, 'go-leftmost', 'go_leftmost()', 'go all the way to the left of the palette') 763 | Drawing.addCommand(None, 'go-top', 'go_top()', 'go all the way to the top of the palette') 764 | Drawing.addCommand(None, 'go-bottom', 'go_bottom()', 'go all the way to the bottom ') 765 | Drawing.addCommand(None, 'go-rightmost', 'go_rightmost()', 'go all the way to the right') 766 | 767 | Drawing.addCommand('', 'pen-left', 'sheet.pendir="l"', '') 768 | Drawing.addCommand('', 'pen-down', 'sheet.pendir="d"', '') 769 | Drawing.addCommand('', 'pen-up', 'sheet.pendir="u"', '') 770 | Drawing.addCommand('', 'pen-right', 'sheet.pendir="r"', '') 771 | 772 | Drawing.addCommand('', 'align-x-selected', 'align_selected("x")') 773 | 774 | Drawing.addCommand('F', 'open-frames', 'vd.push(FramesSheet(sheet, "frames", source=sheet, rows=sheet.frames, cursorRowIndex=sheet.cursorFrameIndex))') 775 | Drawing.addCommand('[', 'prev-frame', 'sheet.cursorFrameIndex -= 1 if sheet.cursorFrameIndex > 0 else fail("first frame")') 776 | Drawing.addCommand(']', 'next-frame', 'sheet.cursorFrameIndex += 1 if sheet.cursorFrameIndex < sheet.nFrames-1 else fail("last frame")') 777 | Drawing.addCommand('g[', 'first-frame', 'sheet.cursorFrameIndex = 0') 778 | Drawing.addCommand('g]', 'last-frame', 'sheet.cursorFrameIndex = sheet.nFrames-1') 779 | Drawing.addCommand('z[', 'new-frame-before', 'sheet.new_between_frame(sheet.cursorFrameIndex-1, sheet.cursorFrameIndex)') 780 | Drawing.addCommand('z]', 'new-frame-after', 'sheet.new_between_frame(sheet.cursorFrameIndex, sheet.cursorFrameIndex+1); sheet.cursorFrameIndex += 1') 781 | 782 | Drawing.addCommand('gKEY_HOME', 'slide-top-selected', 'source.slide_top(source.someSelectedRows, -1)', 'move selected items to top layer of drawing') 783 | Drawing.addCommand('gKEY_END', 'slide-bottom-selected', 'source.slide_top(source.someSelectedRows, 0)', 'move selected items to bottom layer of drawing') 784 | Drawing.addCommand('d', 'delete-cursor', 'remove_at(cursorBox)', 'delete first item under cursor') 785 | Drawing.addCommand('gd', 'delete-selected', 'source.deleteSelected()', 'delete selected rows on source sheet') 786 | 787 | @Drawing.api 788 | def input_canvas(sheet, box, row=None): 789 | kwargs = {} 790 | if row: 791 | x, y = row.x, row.y 792 | kwargs['value'] = row.text 793 | kwargs['i'] = box.x1-row.x 794 | else: 795 | x, y = box.x1-sheet.xoffset, box.y1-sheet.yoffset 796 | 797 | return vd.editText(y, x, sheet.windowWidth-x, fillchar='', clear=False, **kwargs) 798 | 799 | Drawing.addCommand('a', 'add-input', 'place_text(input_canvas(cursorBox, None), cursorBox)', 'place text string at cursor') 800 | Drawing.addCommand('e', 'edit-text', 'r=cursorRow; edit_text(input_canvas(cursorBox, r), r)') 801 | Drawing.addCommand('ge', 'edit-selected', 'v=input("text: ", value=get_text())\nfor r in source.selectedRows: r.text=v') 802 | Drawing.addCommand('y', 'yank-char', 'sheet.copyRows(cursorRows)') 803 | Drawing.addCommand('gy', 'yank-selected', 'sheet.copyRows(sheet.selectedRows)') 804 | Drawing.addCommand('x', 'cut-char', 'sheet.copyRows(remove_at(cursorBox))') 805 | Drawing.addCommand('zx', 'cut-char-top', 'r=list(itercursor())[-1]; sheet.copyRows([r]); source.deleteBy(lambda r,row=r: r is row)') 806 | Drawing.addCommand('p', 'paste-chars', 'sheet.paste_chars(vd.memory.cliprows, cursorBox)') 807 | Drawing.addCommand('zp', 'paste-special', 'sheet.paste_special()') 808 | 809 | Drawing.addCommand('zh', 'go-left-obj', 'go_obj(-1, 0)') 810 | Drawing.addCommand('zj', 'go-down-obj', 'go_obj(0, +1)') 811 | Drawing.addCommand('zk', 'go-up-obj', 'go_obj(0, -1)') 812 | Drawing.addCommand('zl', 'go-right-obj', 'go_obj(+1, 0)') 813 | 814 | Drawing.addCommand('g)', 'group-selected', 'sheet.group_selected(input("group name: ", value=random_word()))') 815 | Drawing.addCommand('g(', 'degroup-selected-temp', 'degrouped = sheet.degroup(source.someSelectedRows); source.clearSelected(); source.select(degrouped)') 816 | Drawing.addCommand('gz(', 'degroup-selected-perm', 'sheet.degroup_all()') 817 | Drawing.addCommand('gz)', 'regroup-selected', 'sheet.regroup(source.someSelectedRows)') 818 | DrawingSheet.addCommand('g)', 'group-selected', 'sheet.group_selected(input("group name: ", value=random_word()))') 819 | DrawingSheet.addCommand('g(', 'degroup-selected-perm', 'sheet.degroup_all()') 820 | DrawingSheet.addCommand('gz(', 'degroup-selected-temp', 'degroup = sheet.degroup(someSelectedRows); clearSelected(); select(degrouped)') 821 | DrawingSheet.addCommand('gz)', 'regroup-selected', 'sheet.regroup(someSelectedRows)') 822 | 823 | Drawing.addCommand('zs', 'select-top', 'select_top(cursorBox)') 824 | Drawing.addCommand('gzs', 'select-all-this-frame', 'sheet.select(list(source.gatherBy(lambda r,f=currentFrame: r.frame == f.id)))') 825 | Drawing.addCommand('gzu', 'unselect-all-this-frame', 'sheet.unselect(list(source.gatherBy(lambda r,f=currentFrame: r.frame == f.id)))') 826 | Drawing.addCommand(',', 'select-equal-char', 'sheet.select(list(source.gatherBy(lambda r,ch=cursorChar: r.text==ch)))') 827 | Drawing.addCommand('|', 'select-tag', 'sheet.select_tag(input("select tag: ", type="group"))') 828 | Drawing.addCommand('\\', 'unselect-tag', 'sheet.unselect_tag(input("unselect tag: ", type="group"))') 829 | 830 | Drawing.addCommand('gs', 'select-all', 'source.select(itercursor(frames=source.frames))') 831 | Drawing.addCommand('gt', 'toggle-all', 'source.toggle(itercursor(frames=source.frames))') 832 | 833 | Drawing.addCommand('z00', 'enable-all-groups', 'disabled_tags.clear()') 834 | for i in range(1, 10): 835 | Drawing.addCommand('%02d'%i, 'toggle-enabled-group-%s'%i, 'g=list(_tags.keys())[%s]; disabled_tags.remove(g) if g in disabled_tags else disabled_tags.add(g)' %(i-1)) 836 | Drawing.addCommand('g%02d'%i, 'select-group-%s'%i, 'g=list(_tags.keys())[%s]; source.select(source.gatherTag(g))' %(i-1)) 837 | Drawing.addCommand('z%02d'%i, 'unselect-group-%s'%i, 'g=list(_tags.keys())[%s]; source.unselect(source.gatherTag(g))' %(i-1)) 838 | 839 | Drawing.addCommand('A', 'new-drawing', 'vd.push(vd.new_ddw(Path(vd.random_word()+".ddw")))', 'open blank drawing') 840 | Drawing.addCommand('M', 'open-unicode', 'vd.push(vd.unibrowser)', 'open unicode character table') 841 | Drawing.addCommand('`', 'push-source', 'vd.push(sheet.source)', 'push backing sheet for this drawing') 842 | DrawingSheet.addCommand('`', 'open-drawing', 'vd.push(sheet.drawing)', 'push drawing for this backing sheet') 843 | 844 | Drawing.addCommand('^G', 'show-char', 'status(f"{sheet.cursorBox} <{cursorDesc}> {sheet.cursorCharName}")') 845 | DrawingSheet.addCommand(ENTER, 'dive-group', 'cursorRow.rows or fail("no elements in group"); vd.push(DrawingSheet(source=sheet, rows=cursorRow.rows))') 846 | DrawingSheet.addCommand('g'+ENTER, 'dive-selected', 'ret=sum(((r.rows or []) for r in selectedRows), []) or fail("no groups"); vd.push(DrawingSheet(source=sheet, rows=ret))') 847 | Drawing.addCommand('&', 'join-selected', 'join_rows(source.selectedRows)', 'join selected objects into one text object') 848 | 849 | 850 | @Drawing.api 851 | def flip_horiz(sheet, box): 852 | for r in sheet.iterbox(box): 853 | oldx = copy(r.x) 854 | r.x = box.x2+box.x1-r.x-2 855 | vd.addUndo(setattr, r, 'x', oldx) 856 | 857 | 858 | @Drawing.api 859 | def flip_vert(sheet, box): 860 | for r in sheet.iterbox(box): 861 | oldy = r.y 862 | r.y = box.y2+box.y1-r.y-2 863 | vd.addUndo(setattr, r, 'y', oldy) 864 | 865 | @Drawing.api 866 | def cycle_color(sheet, rows, n=1): 867 | for r in rows: 868 | clist = [] 869 | for c in r.color.split(): 870 | try: 871 | c = str((int(c)+n) % 256) 872 | except Exception: 873 | pass 874 | clist.append(c) 875 | r.color = ''.join(clist) 876 | 877 | 878 | @Drawing.api 879 | def set_color(self, color): 880 | for r in self.cursorRows: 881 | oldcolor = copy(r.color) 882 | r.color = color 883 | vd.addUndo(setattr, r, 'color', oldcolor) 884 | 885 | @Drawing.api 886 | def select_top(sheet, box): 887 | r = [] 888 | for x in range(box.x1, box.x2-1): 889 | for y in range(box.y1, box.y2-1): 890 | vd.status(x,y) 891 | rows = sheet._displayedRows[(x,y)] 892 | if rows: 893 | r.append(rows[-1]) 894 | sheet.select(r) 895 | 896 | 897 | 898 | Drawing.addCommand('', 'flip-cursor-horiz', 'flip_horiz(sheet.cursorBox)', 'Flip elements under cursor horizontally') 899 | Drawing.addCommand('', 'flip-cursor-vert', 'flip_vert(sheet.cursorBox)', 'Flip elements under cursor vertically') 900 | Drawing.addCommand('zc', 'set-color-input', 'set_color(input("color: ", value=sheet.cursorRows[0].color))') 901 | Drawing.addCommand('<', 'cycle-cursor-prev', 'cycle_color(cursorRows, -1)') 902 | Drawing.addCommand('>', 'cycle-cursor-next', 'cycle_color(cursorRows, 1)') 903 | Drawing.addCommand('g<', 'color-selected-prev', 'cycle_color(selectedRows, -1)') 904 | Drawing.addCommand('g>', 'color-selected-next', 'cycle_color(selectedRows, 1)') 905 | Drawing.addCommand('z<', 'cycle-topcursor-prev', 'cycle_color(topCursorRows, -1)') 906 | Drawing.addCommand('z>', 'cycle-topcursor-next', 'cycle_color(topCursorRows, 1)') 907 | 908 | Drawing.addCommand('g+', 'tag-selected', 'sheet.tag_rows(sheet.someSelectedRows, vd.input("tag selected as: ", type="tag"))') 909 | Drawing.addCommand('+', 'tag-cursor', 'sheet.tag_rows(sheet.cursorRows, vd.input("tag cursor as: ", type="tag"))') 910 | Drawing.addCommand('z+', 'tag-topcursor', 'sheet.tag_rows(sheet.topCursorRows, vd.input("tag top of cursor as: ", type="tag"))') 911 | 912 | Drawing.addCommand('-', 'untag-cursor', 'sheet.untag_rows(sheet.cursorRows, vd.input("untag cursor as: ", type="tag"))') 913 | Drawing.addCommand('g-', 'untag-selected', 'sheet.untag_rows(sheet.someSelectedRows, vd.input("untag selected as: ", type="tag"))') 914 | Drawing.addCommand('z-', 'untag-topcursor', 'sheet.untag_rows(sheet.topCursorRows, vd.input("untag top of cursor as: ", type="tag"))') 915 | 916 | Drawing.addCommand('{', 'go-prev-selected', 'source.moveToNextRow(lambda row,source=source: source.isSelected(row), reverse=True) or fail("no previous selected row"); sheet.cursorBox.x1=source.cursorRow.x; sheet.cursorBox.y1=source.cursorRow.y', 'go to previous selected row'), 917 | Drawing.addCommand('}', 'go-next-selected', 'source.moveToNextRow(lambda row,source=source: source.isSelected(row)) or fail("no next selected row"); sheet.cursorBox.x1=source.cursorRow.x; sheet.cursorBox.y1=source.cursorRow.y', 'go to next selected row'), 918 | Drawing.addCommand('z^Y', 'pyobj-cursor', 'vd.push(PyobjSheet("cursor_top", source=cursorRow))') 919 | Drawing.addCommand('^Y', 'pyobj-cursor', 'vd.push(PyobjSheet("cursor", source=cursorRows))') 920 | 921 | Drawing.addCommand('^S', 'save-sheet', 'vd.saveSheets(inputPath("save to: ", value=source.getDefaultSaveName()), sheet.source)', 'save current drawing') 922 | Drawing.addCommand('i', 'insert-row', 'for r in source.someSelectedRows: r.y += (r.y >= cursorBox.y1)', '') 923 | Drawing.addCommand('zi', 'insert-col', 'for r in source.someSelectedRows: r.x += (r.x >= cursorBox.x1)', '') 924 | 925 | Drawing.addCommand('zm', 'place-mark', 'sheet.mark=(cursorBox.x1, cursorBox.y1)') 926 | Drawing.addCommand('m', 'swap-mark', '(cursorBox.x1, cursorBox.y1), sheet.mark=sheet.mark, (cursorBox.x1, cursorBox.y1)') 927 | Drawing.addCommand('v', 'visibility', 'options.visibility = (options.visibility+1)%3') 928 | Drawing.addCommand('r', 'reset-time', 'sheet.autoplay_frames.extend([[0, f] for f in sheet.frames])') 929 | Drawing.addCommand('c', 'set-default-color', 'vd.default_color=list(itercursor())[-1].color') 930 | 931 | Drawing.addCommand(';', 'cycle-paste-mode', 'sheet.cycle_paste_mode()') 932 | Drawing.addCommand('^G', 'toggle-help', 'vd.show_help = not vd.show_help') 933 | Drawing.addCommand('PgDn', 'page-down', 'n = windowHeight//2; sheet.cursorBox.y1 += n; sheet.yoffset += n; sheet.refresh()') 934 | Drawing.addCommand('PgUp', 'page-up', 'n = windowHeight//2; sheet.cursorBox.y1 -= n; sheet.yoffset -= n; sheet.refresh()') 935 | 936 | for i in range(1,10): 937 | Drawing.addCommand('%s'%str(i)[-1], 'paste-char-%d'%i, 'sheet.paste_chars([vd.memory.cliprows[%d]], cursorBox)'%(i-1)) 938 | 939 | Drawing.bindkey('zKEY_RIGHT', 'resize-cursor-wider') 940 | Drawing.bindkey('zKEY_LEFT', 'resize-cursor-thinner') 941 | Drawing.bindkey('zKEY_UP', 'resize-cursor-shorter') 942 | Drawing.bindkey('zKEY_DOWN', 'resize-cursor-taller') 943 | 944 | Drawing.bindkey('C', 'open-colors') 945 | Drawing.unbindkey('^R') 946 | 947 | Drawing.init('mark', lambda: (0,0)) 948 | Drawing.init('paste_mode', lambda: 'all') 949 | Drawing.init('cursorFrameIndex', lambda: 0) 950 | Drawing.init('autoplay_frames', list) 951 | Drawing.init('last_autosave', int) 952 | 953 | # (xoffset, yoffset) is absolute coordinate of upper left of viewport (0, 0) 954 | Drawing.init('yoffset', int) 955 | Drawing.init('xoffset', int) 956 | 957 | vd.default_color = '' 958 | Drawing.class_options.disp_rstatus_fmt='{sheet.frameDesc} | {sheet.source.nRows} {sheet.rowtype} {sheet.options.disp_selected_note}{sheet.source.nSelectedRows}' 959 | Drawing.class_options.quitguard='modified' 960 | Drawing.class_options.null_value='' 961 | DrawingSheet.class_options.null_value='' 962 | 963 | Drawing.tutorial_url='https://raw.githubusercontent.com/devottys/studio/master/darkdraw-tutorial.ddw' 964 | BaseSheet.addCommand(None, 'open-tutorial-darkdraw', 'vd.push(openSource(Drawing.tutorial_url))', 'Download and open DarkDraw tutorial as a DarkDraw sheet') 965 | 966 | @Drawing.api 967 | def set_linedraw_mode(sheet): 968 | if sheet.mode != 'linedraw': 969 | sheet.mode = 'linedraw' 970 | sheet.linepoints = [] 971 | else: 972 | sheet.mode = '' 973 | sheet.linepoints = [] 974 | 975 | 976 | @Drawing.api 977 | def next_point(sheet, x2, y2): 978 | if sheet.linepoints: 979 | objs = vd.memory.cliprows 980 | if not objs: 981 | r = sheet.newRow() 982 | r.text = '.' 983 | objs = [r] 984 | if len(sheet.linepoints) == 1 or sheet.linepoints[-1] == (x2, y2): 985 | sheet.draw_line(objs, *sheet.linepoints[0], x2, y2) 986 | else: 987 | xy1, xy3 = sheet.linepoints 988 | objit = itertools.cycle(objs) 989 | for x, y in bezier(*xy1, x2, y2, *xy3): 990 | sheet.paste_chars([next(objit)], CharBox(None, int(x), int(y), 1, 1)) 991 | 992 | sheet.linepoints = [sheet.linepoints[-1]] 993 | 994 | 995 | @Drawing.api 996 | def click(sheet, x, y): 997 | if sheet.mode == 'linedraw': 998 | sheet.linepoints.append((x,y)) 999 | 1000 | sheet.cursorBox = CharBox(None, x, y, 1, 1) 1001 | 1002 | @Drawing.api 1003 | def release(sheet, x, y): 1004 | if sheet.mode == 'linedraw': 1005 | sheet.next_point(x, y) 1006 | else: 1007 | sheet.cursorBox.x2=x+2 1008 | sheet.cursorBox.y2=y+2 1009 | sheet.cursorBox.normalize() 1010 | 1011 | Drawing.addCommand('.', 'next-point', 'next_point(cursorBox.x1, cursorBox.y1)', '') 1012 | Drawing.addCommand('w', 'line-drawing-mode', 'set_linedraw_mode()', '') 1013 | Drawing.addCommand('BUTTON1_PRESSED', 'click-cursor', 'click(mouseX, mouseY)', 'start cursor box with left mouse button press') 1014 | Drawing.addCommand('BUTTON1_RELEASED', 'end-cursor', 'release(mouseX, mouseY)', 'end cursor box with left mouse button release') 1015 | 1016 | @Drawing.api 1017 | def draw_line(self, objlist, x0, y0, x1, y1): 1018 | dx = abs(x1-x0) 1019 | sx = 1 if x0 < x1 else -1 1020 | dy = -abs(y1-y0) 1021 | sy = 1 if y0 < y1 else -1 1022 | error = dx + dy 1023 | 1024 | objit = itertools.cycle(objlist) 1025 | 1026 | while True: 1027 | row = next(objit) 1028 | self.paste_chars([row], CharBox(None, x0, y0, 1, 1)) 1029 | 1030 | if x0 == x1 and y0 == y1: 1031 | break 1032 | e2 = 2 * error 1033 | if e2 >= dy: 1034 | if x0 == x1: break 1035 | error += dy 1036 | x0 += sx 1037 | if e2 <= dx: 1038 | if y0 == y1: break 1039 | error += dx 1040 | y0 += sy 1041 | 1042 | 1043 | @Drawing.api 1044 | def qcurve(self, vertexes, objrows): 1045 | x1, y1 = vertexes[0] 1046 | x2, y2 = vertexes[1] 1047 | x3, y3 = vertexes[2] 1048 | 1049 | 1050 | 1051 | @Drawing.command('', 'box-cursor', 'draw a box to fill the inner edge of the cursor') 1052 | def box_cursor(sheet): 1053 | pass 1054 | 1055 | vd.addMenuItem('File', 'New drawing', 'new-drawing') 1056 | vd.addMenuItem('View', 'Unicode browser', 'open-unicode') 1057 | vd.addMenuItem('View', 'Drawing table', 'open-drawing') 1058 | vd.addMenuItem('Help', 'DarkDraw tutorial', 'open-tutorial-darkdraw') 1059 | vd.addMenuItem('Edit', 'Add text', 'add-input') 1060 | 1061 | vd.addMenu(Menu('DarkDraw', 1062 | Menu('New drawing', 'new-drawing'), 1063 | Menu('View', 1064 | Menu('Colors sheet', 'open-colors'), 1065 | Menu('Unicode characters', 'open-unicode'), 1066 | Menu('Backing table', 'open-backing'), 1067 | Menu('Frames sheet', 'open-frames'), 1068 | ), 1069 | Menu('Cycle paste mode', 'cycle-paste-mode'), 1070 | Menu('Flip cursor', 1071 | Menu('horizontally', 'flip-cursor-horiz'), 1072 | Menu('vertically', 'flip-cursor-vert'), 1073 | ), 1074 | Menu('Animation', 1075 | Menu('New frame', 1076 | Menu('before', 'new-frame-before'), 1077 | Menu('after', 'new-frame-after'), 1078 | ), 1079 | Menu('Go to frame', 1080 | Menu('first', 'first-frame'), 1081 | Menu('last', 'last-frame'), 1082 | Menu('prev', 'prev-frame'), 1083 | Menu('next', 'next-frame'), 1084 | ), 1085 | Menu('Start', 'reset-time'), 1086 | ), 1087 | Menu('Color', 1088 | Menu('Set default from cursor', 'set-default-cursor'), 1089 | Menu('Set to input', 'set-color-input'), 1090 | Menu('Cycle', 1091 | Menu('cursor', 1092 | Menu('down', 'cycle-cursor-prev'), 1093 | Menu('up', 'cycle-cursor-next'), 1094 | ), 1095 | Menu('selected', 1096 | Menu('down', 'color-selected-next'), 1097 | Menu('up', 'color-selected-prev'), 1098 | ), 1099 | Menu('top of cursor', 1100 | Menu('down', 'cycle-topcursor-next'), 1101 | Menu('up', 'cycle-topcursor-prev'), 1102 | ), 1103 | ), 1104 | Menu('Tag', 1105 | Menu('selected', 'tag-selected'), 1106 | Menu('under cursor', 'tag-cursor'), 1107 | Menu('top of cursor', 'tag-topcursor'), 1108 | ), 1109 | Menu('Insert', 1110 | Menu('Line', 'insert-row'), 1111 | Menu('Character', 'insert-col'), 1112 | ), 1113 | ), 1114 | )) 1115 | -------------------------------------------------------------------------------- /darkdraw/loader_scr.py: -------------------------------------------------------------------------------- 1 | from itertools import zip_longest 2 | 3 | from visidata import VisiData, AttrDict, dispwidth 4 | from .drawing import Drawing, DrawingSheet 5 | 6 | @VisiData.api 7 | def open_scr(vd, p): 8 | palette = {} # colorcode -> colorstr 9 | pcolors = [] 10 | lines = [] 11 | with p.open_text() as fp: 12 | for line in fp.readlines(): 13 | line = line[:-1] 14 | if not line: continue 15 | if line.startswith('#C '): #C S fg on bg underline reverse 16 | palette[line[3]] = line[4:].strip() 17 | elif line.startswith('#M '): #M mask of color id (S above) corresponding to line 18 | pcolors.append(line[3:]) 19 | else: 20 | lines.append(list(line)) 21 | 22 | rows=[] 23 | for y, (line, mask) in enumerate(zip_longest(lines, pcolors)): 24 | if mask is None: mask = [] 25 | x = 0 26 | sheet = DrawingSheet(p.name, 'table', source=p) 27 | for ch, mch in zip_longest(line, mask): 28 | if ch == ' ': 29 | x += 1 30 | else: 31 | newr = sheet.newRow() 32 | newr.x, newr.y, newr.text, newr.color = x, y, ch, palette[mch] if mch else '' 33 | rows.append(newr) 34 | x += dispwidth(ch) 35 | 36 | sheet.rows=rows 37 | return Drawing(p.name, source=sheet) 38 | 39 | -------------------------------------------------------------------------------- /darkdraw/save.py: -------------------------------------------------------------------------------- 1 | from visidata import VisiData, colors, vd 2 | from .drawing import Drawing, DrawingSheet 3 | from .ansihtml import termcolor_to_rgb 4 | from unittest import mock 5 | 6 | from PIL import Image, ImageDraw, ImageFont 7 | 8 | vd.option('darkdraw_font', '/usr/share/fonts/truetype/unifont/unifont.ttf', 'path of TTF font file for save_png') 9 | vd.option('darkdraw_font_size', 16, 'font size for save_png') 10 | 11 | 12 | @Drawing.api 13 | def createPillowImage(dwg): 14 | im = Image.new("RGB", (640, 480), color=(0,0,0)) 15 | 16 | draw = ImageDraw.Draw(im) 17 | font = ImageFont.truetype(dwg.options.darkdraw_font, dwg.options.darkdraw_font_size) 18 | 19 | vd.clearCaches() 20 | dwg._scr = mock.MagicMock(__bool__=mock.Mock(return_value=True), 21 | getmaxyx=mock.Mock(return_value=(9999, 9999))) 22 | dwg.draw(dwg._scr) 23 | 24 | displayed = set() 25 | for y in range(dwg.minY, dwg.maxY+1): 26 | for x in range(dwg.minX, dwg.maxX+1): 27 | rows = dwg._displayedRows.get((x,y), None) 28 | if not rows: continue 29 | r = rows[-1] 30 | k = str(r) 31 | if k in displayed: continue 32 | if not r.text: continue 33 | if x-r.x >= len(r.text): continue 34 | i = x-r.x 35 | s = r.text[i:] 36 | fg, bg, attrs = colors.split_colorstr(r.color) 37 | c = termcolor_to_rgb(fg) 38 | xy = ((r.x+i)*8, r.y*16) 39 | if bg: 40 | draw.rectangle((xy, (xy[0]+16, xy[1]+8)), fill=termcolor_to_rgb(bg)) 41 | draw.text(xy, s, font=font, fill=c) 42 | if 'underline' in attrs: 43 | draw.line((xy, (xy[0]+16, xy[1])), fill=c) 44 | draw.line(((xy[0], xy[1]+8), (xy[0]+16, xy[1]+8)), fill=c) 45 | displayed.add(k) 46 | 47 | vd.status(' '.join(displayed)) 48 | return im 49 | 50 | 51 | @VisiData.api 52 | def save_png(vd, p, *sheets): 53 | frames = [] 54 | for vs in sheets: 55 | dwg = vs.drawing 56 | im = dwg.createPillowImage() 57 | 58 | if vs.frames: 59 | for i in range(vs.nFrames): 60 | dwg.cursorFrameIndex = i 61 | frames.append(dwg.createPillowImage()) 62 | else: 63 | frames.append(dwg.createPillowImage()) 64 | 65 | frames[0].save(str(p), append_images=frames[1:], save_all=True, duration=100, loop=0) 66 | 67 | 68 | @VisiData.api 69 | def save_gif(vd, p, *sheets): 70 | frames = [] 71 | for vs in sheets: 72 | dwg = vs.drawing 73 | if vs.frames: 74 | for i in range(vs.nFrames): 75 | dwg.cursorFrameIndex = i 76 | frames.append(dwg.createPillowImage()) 77 | else: 78 | frames.append(dwg.createPillowImage()) 79 | 80 | frames[0].save(str(p), append_images=frames[1:], optimize=False, save_all=True, duration=100, loop=0) 81 | -------------------------------------------------------------------------------- /darkdraw/stamps.py: -------------------------------------------------------------------------------- 1 | from visidata import CharBox, vd 2 | import itertools 3 | from darkdraw import Drawing 4 | 5 | @Drawing.api 6 | def stamp_circle(sheet, box): 7 | import math 8 | # attributes that i have: 9 | # box.x1, box.x2, box.y1, box.y2, box.h, box.w 10 | 11 | # x = cen_x + (r * cosine(theta)) 12 | # y = cen_y + (r * sine(theta)) 13 | xr = (box.w-1)/2 14 | yr = (box.h-1)/2 15 | x = (2*box.x1 + box.w)/2 16 | y = (2*box.y1 + box.h)/2 17 | 18 | coords = set() 19 | for theta in range(0, 361): 20 | # i need radians 21 | theta = math.radians(theta) 22 | coords.add((int(x+(xr*math.cos(theta))), int(y+(yr*math.sin(theta))))) 23 | 24 | itchars = itertools.cycle([(r.text, r.color) for r in vd.memory.cliprows or []] or [('*', '')]) 25 | for coord in coords: 26 | ch, color = next(itchars) 27 | sheet.place_text(ch, CharBox(x1=coord[0], y1=coord[1]), go_forward=False) 28 | 29 | 30 | Drawing.addCommand('', 'stamp-circle', 'sheet.stamp_circle(cursorBox); # sheet.go_forward(cursorBox.w, 0)') 31 | 32 | -------------------------------------------------------------------------------- /darkdraw/upgrade.py: -------------------------------------------------------------------------------- 1 | from darkdraw import Drawing, VisiData, vd 2 | 3 | upgradeables = [] 4 | upgradeables += ''' 5 | ┌ ╒ ┍ ╔ ┏ 6 | ┌ ╓ ┎ ╔ ┏ 7 | └ ╘ ┕ ╚ ┗ 8 | └ ╙ ┖ ╚ ┗ 9 | ┐ ╕ ┑ ╗ ┓ 10 | ┐ ╖ ┒ ╗ ┓ 11 | ┘ ╛ ┙ ╝ ┛ 12 | ┘ ╜ ┚ ╝ ┛ 13 | ├ ╞ ┝ ╠ ┣ 14 | ├ ╟ ┠ ╠ ┣ 15 | ┤ ╡ ┥ ╣ ┫ 16 | ┤ ╢ ┨ ╣ ┫ 17 | ┬ ╤ ┯ ╦ ┳ 18 | ┬ ╥ ┰ ╦ ┳ 19 | ┴ ╧ ┷ ╩ ┻ 20 | ┴ ┸ ╨ ╩ ┻ 21 | ┼ ╪ ┿ ╬ ╋ 22 | ┼ ╫ ╂ ╬ ╋ 23 | ─ ═ ━ 24 | │ ║ ┃ 25 | 26 | ┞┟ ┠ 27 | ┡┢ ┣ 28 | ┦┧ ┨ 29 | ┩┪ ┫ 30 | ┭┮ ┯ 31 | ┵┶ ┷ 32 | ┱┲ ┳ 33 | ┹┺ ┻ 34 | ╁╀ ╂ 35 | ┽┾ ┿ 36 | ╄╃╅╆╇╈╉╊ ╋ 37 | 38 | - = 39 | . : 40 | ! | 41 | '''.splitlines() 42 | 43 | upgradepath = {} 44 | downgradepath = {} 45 | for u in upgradeables[::-1]: 46 | v = u.split() 47 | for i, x in enumerate(v): 48 | for ch in x: 49 | if i+1 < len(v): 50 | upgradepath[ch] = v[i+1][0] 51 | if i-1 >= 0: 52 | downgradepath[ch] = v[i-1][0] 53 | 54 | 55 | @VisiData.api 56 | def downgrade(vd, s): 57 | return ''.join(downgradepath.get(ch, ch) for ch in s) 58 | 59 | @VisiData.api 60 | def upgrade(vd, s): 61 | return ''.join(upgradepath.get(ch, ch) for ch in s) 62 | 63 | 64 | Drawing.addCommand('-', 'downgrade-cursor', 'for r in itercursor(): edit_text(downgrade(r.text), r)') 65 | Drawing.addCommand('=', 'upgrade-cursor', 'for r in itercursor(): edit_text(upgrade(r.text), r)') 66 | -------------------------------------------------------------------------------- /dwimmer_darkdraw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devottys/darkdraw/f99162e455c7dd38dd8fb4a3f8c8af0094429e20/dwimmer_darkdraw.gif -------------------------------------------------------------------------------- /dwimmer_faerie-fire.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devottys/darkdraw/f99162e455c7dd38dd8fb4a3f8c8af0094429e20/dwimmer_faerie-fire.gif -------------------------------------------------------------------------------- /plugins/typing_mode.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import random 3 | import json 4 | 5 | from visidata import vd, VisiData, dispwidth 6 | 7 | from darkdraw import Drawing 8 | 9 | vd.option('keymap', 'keymap.jsonl', 'JSONL keymap filename to load for DarkDraw typing mode') 10 | 11 | Drawing.init('typing_mode_map', dict) 12 | 13 | @VisiData.api 14 | def typing_mode(vd, scr): 15 | ddw = vd.activeSheet 16 | if not ddw.typing_mode_map: 17 | try: 18 | ddw.keymap_layers = [] 19 | ddw.load_keymap(ddw.options.keymap) 20 | except Exception as e: 21 | vd.exceptionCaught(e) 22 | 23 | oldmode = ddw.paste_mode 24 | try: 25 | ddw.paste_mode = '' 26 | ddw.run_typing_mode(scr) 27 | finally: 28 | ddw.paste_mode = oldmode 29 | 30 | 31 | @Drawing.api 32 | def load_keymap(ddw, fn): 33 | keymap_layers = dict(random=1) 34 | with open(fn) as fp: 35 | ddw.typing_mode_map = collections.defaultdict(dict) 36 | for line in fp: 37 | d = json.loads(line) 38 | keych = d.pop('keypress') 39 | for keymap_layer, v in d.items(): 40 | ddw.typing_mode_map[keych][keymap_layer] = v 41 | keymap_layers[keymap_layer] = 2 42 | ddw.keymap_layers = list(keymap_layers.keys()) 43 | 44 | 45 | def rotate(L:list, n:int): 46 | return L[n:] + L[:n] 47 | 48 | @Drawing.api 49 | def run_typing_mode(ddw, scr): 50 | cur_edits = {} 51 | last_dispwidth = 0 52 | 53 | while True: 54 | ddw.paste_mode = ddw.keymap_layers[0] + ' layer' 55 | vd.drawSheet(scr, ddw) 56 | x, y = ddw.cursorBox.x1, ddw.cursorBox.y1 57 | 58 | if scr: scr.move(y, x) 59 | ch = vd.getkeystroke(scr) 60 | if ch == '': continue 61 | elif ch == '^Q': return 62 | elif ch == '^[': return 63 | elif ch == '^C': return 64 | elif ch == '^J': y += 1; x = 0 65 | elif ch == 'KEY_UP': y -= 1 66 | elif ch == 'KEY_DOWN': y += 1 67 | elif ch == 'KEY_LEFT': x -= 1 68 | elif ch == 'KEY_RIGHT': x += 1 69 | elif ch == 'KEY_BACKSPACE': 70 | x -= last_dispwidth 71 | if (x,y) in cur_edits: 72 | ddw.rows.remove(cur_edits[(x,y)]) 73 | del cur_edits[(x,y)] 74 | 75 | elif ch == '^P': 76 | ddw.keymap_layers = rotate(ddw.keymap_layers, -1) 77 | elif ch == '^N': 78 | ddw.keymap_layers = rotate(ddw.keymap_layers, +1) 79 | 80 | elif len(ch) == 1: 81 | poss = ddw.typing_mode_map.get(ch, {'straight': ch}) 82 | layer = ddw.keymap_layers[0] 83 | if layer == 'random': 84 | s = random.choice(list(poss.values())) 85 | else: 86 | s = poss.get(layer, ch) 87 | if (x,y) in cur_edits: 88 | ddw.rows.remove(cur_edits[(x,y)]) 89 | cur_edits[(x,y)] = ddw.add_text(s, x, y, vd.default_color) 90 | last_dispwidth = dispwidth(s) 91 | x += last_dispwidth 92 | else: 93 | vd.status(f'unknown keypress {ch}') 94 | 95 | ddw.cursorBox.x1 = max(0, x) 96 | ddw.cursorBox.y1 = max(0, y) 97 | 98 | 99 | Drawing.addCommand('Shift+N', 'typing-mode', 'vd.typing_mode(_scr)', 'enter raw typing mode') 100 | Drawing.addCommand('zShift+N', 'load-keymap', 'load_keymap(inputFilename("keymap to load: ", value=options.keymap))', 'load different keymap for typing mode') 101 | -------------------------------------------------------------------------------- /pxplus_ibm_vga9-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devottys/darkdraw/f99162e455c7dd38dd8fb4a3f8c8af0094429e20/pxplus_ibm_vga9-webfont.woff -------------------------------------------------------------------------------- /pxplus_ibm_vga9-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devottys/darkdraw/f99162e455c7dd38dd8fb4a3f8c8af0094429e20/pxplus_ibm_vga9-webfont.woff2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | visidata>=3.0.2 2 | wcwidth 3 | Pillow 4 | requests 5 | -------------------------------------------------------------------------------- /samples/arrows.ddw: -------------------------------------------------------------------------------- 1 | {"x": 1, "y": 0, "text": "\u2303", "color": "", "group": "up arrow"} 2 | {"x": 1, "y": 2, "text": "\u2304", "color": ""} 3 | {"x": 3, "y": 6, "text": "\u279c", "color": ""} 4 | {"x": 3, "y": 7, "text": "\u21f0", "color": ""} 5 | {"x": 3, "y": 8, "text": "\u21ea", "color": "", "group": "up arrow"} 6 | {"x": 3, "y": 10, "text": "\u21ba", "color": ""} 7 | {"x": 4, "y": 1, "text": "\u02c2", "color": ""} 8 | {"x": 4, "y": 4, "text": "\u02f1", "color": ""} 9 | {"x": 5, "y": 0, "text": "\u02c4", "color": "", "group": "up arrow"} 10 | {"x": 5, "y": 2, "text": "\u02c5", "color": ""} 11 | {"x": 5, "y": 3, "text": "\u02f0", "color": "", "group": "up arrow"} 12 | {"x": 5, "y": 5, "text": "\u02ef", "color": ""} 13 | {"x": 5, "y": 6, "text": "\u279e", "color": ""} 14 | {"x": 5, "y": 7, "text": "\u27a9", "color": ""} 15 | {"x": 5, "y": 8, "text": "\u21eb", "color": "", "group": "up arrow"} 16 | {"x": 5, "y": 10, "text": "\u21bb", "color": ""} 17 | {"x": 6, "y": 1, "text": "\u02c3", "color": ""} 18 | {"x": 6, "y": 4, "text": "\u02f2", "color": ""} 19 | {"x": 7, "y": 6, "text": "\u27a0", "color": ""} 20 | {"x": 7, "y": 7, "text": "\u27ab", "color": ""} 21 | {"x": 7, "y": 8, "text": "\u21ec", "color": "", "group": "up arrow"} 22 | {"x": 7, "y": 10, "text": "\u27f2", "color": ""} 23 | {"x": 9, "y": 0, "text": "\u2196", "color": ""} 24 | {"x": 9, "y": 1, "text": "\u2190", "color": ""} 25 | {"x": 9, "y": 2, "text": "\u2199", "color": ""} 26 | {"x": 9, "y": 3, "text": "\u2b09", "color": ""} 27 | {"x": 9, "y": 4, "text": "\u2b05", "color": ""} 28 | {"x": 9, "y": 5, "text": "\u2b0b", "color": ""} 29 | {"x": 9, "y": 6, "text": "\u2794", "color": ""} 30 | {"x": 9, "y": 7, "text": "\u27ac", "color": ""} 31 | {"x": 9, "y": 8, "text": "\u21ed", "color": "", "group": "up arrow"} 32 | {"x": 9, "y": 10, "text": "\u27f3", "color": ""} 33 | {"x": 10, "y": 0, "text": "\u2191", "color": "", "group": "up arrow"} 34 | {"x": 10, "y": 2, "text": "\u2193", "color": ""} 35 | {"x": 10, "y": 3, "text": "\u2b06", "color": "", "group": "up arrow"} 36 | {"x": 10, "y": 5, "text": "\u2b07", "color": ""} 37 | {"x": 11, "y": 0, "text": "\u2197", "color": ""} 38 | {"x": 11, "y": 1, "text": "\u2192", "color": ""} 39 | {"x": 11, "y": 2, "text": "\u2198", "color": ""} 40 | {"x": 11, "y": 3, "text": "\u2b08", "color": ""} 41 | {"x": 11, "y": 4, "text": "\u27a1", "color": ""} 42 | {"x": 11, "y": 5, "text": "\u2b0a", "color": ""} 43 | {"x": 11, "y": 7, "text": "\u27aa", "color": ""} 44 | {"x": 11, "y": 8, "text": "\u21ee", "color": "", "group": "up arrow"} 45 | {"x": 11, "y": 10, "text": "\u2940", "color": ""} 46 | {"x": 13, "y": 0, "text": "\u2195", "color": "", "group": "up arrow"} 47 | {"x": 13, "y": 1, "text": "\u2194", "color": ""} 48 | {"x": 13, "y": 3, "text": "\u2b0d", "color": ""} 49 | {"x": 13, "y": 4, "text": "\u2b0c", "color": ""} 50 | {"x": 13, "y": 6, "text": "\u27a8", "color": ""} 51 | {"x": 13, "y": 7, "text": "\u27af", "color": ""} 52 | {"x": 13, "y": 8, "text": "\u21ef", "color": "", "group": "up arrow"} 53 | {"x": 13, "y": 10, "text": "\u2941", "color": ""} 54 | {"x": 15, "y": 6, "text": "\u27ad", "color": ""} 55 | {"x": 15, "y": 7, "text": "\u27b1", "color": ""} 56 | {"x": 16, "y": 0, "text": "\u21d6", "color": ""} 57 | {"x": 16, "y": 1, "text": "\u21d0", "color": ""} 58 | {"x": 16, "y": 2, "text": "\u21d9", "color": ""} 59 | {"x": 16, "y": 3, "text": "\u2b01", "color": "", "group": "white"} 60 | {"x": 16, "y": 4, "text": "\u21e6", "color": "", "group": "white"} 61 | {"x": 16, "y": 5, "text": "\u2b03", "color": "", "group": "white"} 62 | {"x": 17, "y": 0, "text": "\u21d1", "color": "", "group": "up arrow"} 63 | {"x": 17, "y": 2, "text": "\u21d3", "color": ""} 64 | {"x": 17, "y": 3, "text": "\u21e7", "color": "", "group": "white"} 65 | {"x": 17, "y": 5, "text": "\u21e9", "color": "", "group": "white"} 66 | {"x": 17, "y": 6, "text": "\u27ae", "color": ""} 67 | {"x": 17, "y": 7, "text": "\u279b", "color": ""} 68 | {"x": 18, "y": 0, "text": "\u21d7", "color": ""} 69 | {"x": 18, "y": 1, "text": "\u21d2", "color": ""} 70 | {"x": 18, "y": 2, "text": "\u21d8", "color": ""} 71 | {"x": 18, "y": 3, "text": "\u2b00", "color": "", "group": "white"} 72 | {"x": 18, "y": 4, "text": "\u21e8", "color": "", "group": "white"} 73 | {"x": 18, "y": 5, "text": "\u2b02", "color": "", "group": "white"} 74 | {"x": 18, "y": 8, "text": "\u21f1", "color": ""} 75 | {"x": 18, "y": 10, "text": "\u21f2", "color": ""} 76 | {"x": 19, "y": 6, "text": "\u27b2", "color": ""} 77 | {"x": 19, "y": 7, "text": "\u21f4", "color": ""} 78 | {"x": 19, "y": 8, "text": "\u279a", "color": ""} 79 | {"x": 19, "y": 9, "text": "\u2799", "color": ""} 80 | {"x": 19, "y": 10, "text": "\u2798", "color": ""} 81 | {"x": 20, "y": 0, "text": "\u21d5", "color": ""} 82 | {"x": 20, "y": 2, "text": "\u21d4", "color": ""} 83 | {"x": 20, "y": 3, "text": "\u21f3", "color": "", "group": "white"} 84 | {"x": 20, "y": 5, "text": "\u2b04", "color": ""} 85 | {"x": 20, "y": 8, "text": "\u27b6", "color": ""} 86 | {"x": 20, "y": 10, "text": "\u27b4", "color": ""} 87 | {"x": 21, "y": 7, "text": "\u279d", "color": ""} 88 | {"x": 21, "y": 8, "text": "\u27b9", "color": ""} 89 | {"x": 21, "y": 10, "text": "\u27b7", "color": ""} 90 | {"x": 22, "y": 1, "text": "\u21da", "color": ""} 91 | {"x": 23, "y": 0, "text": "\u290a", "color": "", "group": "up arrow"} 92 | {"x": 23, "y": 2, "text": "\u290b", "color": ""} 93 | {"x": 24, "y": 1, "text": "\u21db", "color": ""} 94 | {"x": 24, "y": 4, "text": "\u21e0", "color": ""} 95 | {"x": 24, "y": 9, "text": "\u27b3", "color": ""} 96 | {"x": 25, "y": 3, "text": "\u21e1", "color": "", "group": "up arrow"} 97 | {"x": 25, "y": 5, "text": "\u21e3", "color": ""} 98 | {"x": 25, "y": 7, "text": "\u279f", "color": ""} 99 | {"x": 25, "y": 8, "text": "\u27bc", "color": ""} 100 | {"x": 25, "y": 9, "text": "\u27b5", "color": ""} 101 | {"x": 25, "y": 10, "text": "\u27bb", "color": ""} 102 | {"x": 26, "y": 4, "text": "\u21e2", "color": ""} 103 | {"x": 26, "y": 8, "text": "\u27bd", "color": ""} 104 | {"x": 26, "y": 9, "text": "\u27b8", "color": ""} 105 | {"x": 26, "y": 10, "text": "\u27ba", "color": ""} 106 | {"x": 27, "y": 0, "text": "\u27f0", "color": "", "group": "up arrow"} 107 | {"x": 27, "y": 2, "text": "\u27f1", "color": "", "group": "up arrow"} 108 | {"x": 27, "y": 7, "text": "\u27a7", "color": ""} 109 | {"x": 28, "y": 4, "text": "\u21f6", "color": ""} 110 | {"x": 29, "y": 3, "text": "\u21fc", "color": ""} 111 | {"x": 29, "y": 6, "text": "\u21af", "color": ""} 112 | {"x": 30, "y": 4, "text": "\u21fa", "color": ""} 113 | {"x": 31, "y": 3, "text": "\u21de", "color": "", "group": "up arrow"} 114 | {"x": 31, "y": 5, "text": "\u21df", "color": ""} 115 | {"x": 31, "y": 6, "text": "\u2301", "color": ""} 116 | {"x": 31, "y": 7, "text": "\u27a2", "color": ""} 117 | {"x": 32, "y": 1, "text": "\u21a4", "color": ""} 118 | {"x": 32, "y": 4, "text": "\u21fb", "color": ""} 119 | {"x": 33, "y": 0, "text": "\u21a5", "color": "", "group": "up arrow"} 120 | {"x": 33, "y": 2, "text": "\u21a7", "color": ""} 121 | {"x": 33, "y": 7, "text": "\u27a3", "color": ""} 122 | {"x": 34, "y": 1, "text": "\u21a6", "color": ""} 123 | {"x": 35, "y": 7, "text": "\u27a4", "color": ""} 124 | {"x": 36, "y": 4, "text": "\u21b0", "color": "", "group": "up arrow"} 125 | {"x": 36, "y": 5, "text": "\u21b2", "color": ""} 126 | {"x": 37, "y": 1, "text": "\u219e", "color": ""} 127 | {"x": 37, "y": 4, "text": "\u21b1", "color": "", "group": "up arrow"} 128 | {"x": 37, "y": 5, "text": "\u21b3", "color": ""} 129 | {"x": 37, "y": 7, "text": "\u27be", "color": ""} 130 | {"x": 38, "y": 0, "text": "\u219f", "color": "", "group": "up arrow"} 131 | {"x": 38, "y": 2, "text": "\u21a1", "color": ""} 132 | {"x": 38, "y": 4, "text": "\u21b4", "color": ""} 133 | {"x": 38, "y": 5, "text": "\u21b5", "color": ""} 134 | {"x": 39, "y": 1, "text": "\u21a0", "color": ""} 135 | {"x": 39, "y": 7, "text": "\u27a6", "color": "", "group": "up arrow"} 136 | {"x": 39, "y": 8, "text": "\u27a5", "color": ""} 137 | {"x": 41, "y": 0, "text": "\u21b9", "color": ""} 138 | {"x": 41, "y": 2, "text": "\u21a8", "color": ""} 139 | {"x": 41, "y": 7, "text": "\ua71b", "color": "", "group": "up arrow"} 140 | {"x": 41, "y": 8, "text": "\u27a5", "color": ""} 141 | {"x": 41, "y": 8, "text": "\ua71c", "color": ""} 142 | {"x": 42, "y": 1, "text": "\u21e4", "color": ""} 143 | {"x": 42, "y": 10, "text": "\u238b", "color": ""} 144 | {"x": 43, "y": 0, "text": "\u2912", "color": "", "group": "up arrow"} 145 | {"x": 43, "y": 2, "text": "\u2913", "color": ""} 146 | {"x": 44, "y": 1, "text": "\u21e5", "color": ""} 147 | {"x": 45, "y": 3, "text": "\u21c4", "color": ""} 148 | {"x": 45, "y": 5, "text": "\u21c6", "color": ""} 149 | {"x": 45, "y": 10, "text": "\u237c", "color": ""} 150 | {"x": 46, "y": 4, "text": "\u21c7", "color": ""} 151 | {"x": 46, "y": 7, "text": "\u2347", "color": ""} 152 | {"x": 47, "y": 3, "text": "\u21c8", "color": "", "group": "up arrow"} 153 | {"x": 47, "y": 5, "text": "\u21ca", "color": ""} 154 | {"x": 47, "y": 6, "text": "\u2350", "color": "", "group": "up arrow"} 155 | {"x": 47, "y": 8, "text": "\u2357", "color": ""} 156 | {"x": 48, "y": 4, "text": "\u21c9", "color": ""} 157 | {"x": 48, "y": 7, "text": "\u2348", "color": ""} 158 | {"x": 48, "y": 10, "text": "\u2a17", "color": ""} 159 | {"x": 49, "y": 3, "text": "\u21c5", "color": "", "group": "up arrow"} 160 | {"x": 49, "y": 5, "text": "\u21f5", "color": "", "group": "up arrow"} 161 | {"x": 51, "y": 10, "text": "\u2324", "color": "", "group": "up arrow"} 162 | {"x": 55, "y": 4, "text": "\u2b11", "color": ""} 163 | {"x": 55, "y": 5, "text": "\u2b10", "color": ""} 164 | {"x": 56, "y": 7, "text": "\u219a", "color": ""} 165 | {"x": 56, "y": 9, "text": "\u21cd", "color": ""} 166 | {"x": 56, "y": 10, "text": "\u21f7", "color": ""} 167 | {"x": 57, "y": 4, "text": "\u2b0f", "color": "", "group": "up arrow"} 168 | {"x": 57, "y": 5, "text": "\u2b0e", "color": ""} 169 | {"x": 57, "y": 7, "text": "\u21ae", "color": ""} 170 | {"x": 57, "y": 9, "text": "\u21ce", "color": ""} 171 | {"x": 57, "y": 10, "text": "\u21f9", "color": ""} 172 | {"x": 58, "y": 7, "text": "\u219b", "color": ""} 173 | {"x": 58, "y": 10, "text": "\u21f8", "color": ""} 174 | {"x": 61, "y": 0, "text": "\u21a2", "color": ""} 175 | {"x": 61, "y": 1, "text": "\u219c", "color": ""} 176 | {"x": 61, "y": 2, "text": "\u21dc", "color": ""} 177 | {"x": 61, "y": 3, "text": "\u21a9", "color": ""} 178 | {"x": 61, "y": 4, "text": "\u21ab", "color": ""} 179 | {"x": 61, "y": 5, "text": "\u21b6", "color": ""} 180 | {"x": 61, "y": 6, "text": "\u2906", "color": ""} 181 | {"x": 61, "y": 7, "text": "\u21fd", "color": ""} 182 | {"x": 61, "y": 8, "text": "\u27fb", "color": ""} 183 | {"x": 61, "y": 9, "text": "\u27f8", "color": ""} 184 | {"x": 61, "y": 10, "text": "\u27f5", "color": ""} 185 | {"x": 62, "y": 1, "text": "\u21ad", "color": ""} 186 | {"x": 62, "y": 7, "text": "\u21ff", "color": ""} 187 | {"x": 62, "y": 9, "text": "\u27fa", "color": ""} 188 | {"x": 62, "y": 10, "text": "\u27f7", "color": ""} 189 | {"x": 63, "y": 0, "text": "\u21a3", "color": ""} 190 | {"x": 63, "y": 1, "text": "\u219d", "color": ""} 191 | {"x": 63, "y": 2, "text": "\u21dd", "color": ""} 192 | {"x": 63, "y": 3, "text": "\u21aa", "color": ""} 193 | {"x": 63, "y": 4, "text": "\u21ac", "color": ""} 194 | {"x": 63, "y": 5, "text": "\u21b7", "color": ""} 195 | {"x": 63, "y": 6, "text": "\u2907", "color": ""} 196 | {"x": 63, "y": 7, "text": "\u21fe", "color": ""} 197 | {"x": 63, "y": 8, "text": "\u27fc", "color": ""} 198 | {"x": 63, "y": 9, "text": "\u27f9", "color": ""} 199 | {"x": 63, "y": 10, "text": "\u27f6", "color": ""} 200 | -------------------------------------------------------------------------------- /samples/bouncyball.ddw: -------------------------------------------------------------------------------- 1 | {"x": 3, "y": 15, "text": "| |", "color": "underline", "group": "", "tags": []} 2 | {"id": "0", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 200} 3 | {"id": "1", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 190} 4 | {"id": "2", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 180} 5 | {"id": "3", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 170} 6 | {"id": "4", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 160} 7 | {"id": "5", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 150} 8 | {"id": "6", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 140} 9 | {"id": "7", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 130} 10 | {"id": "8", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 120} 11 | {"id": "9", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 110} 12 | {"id": "10", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 100} 13 | {"id": "11", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 90} 14 | {"id": "12", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 80} 15 | {"id": "13", "type": "frame", "x": 0, "y": 0, "tags": ["down"], "duration_ms": 70} 16 | {"id": "14", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 80} 17 | {"id": "15", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 90} 18 | {"id": "16", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 100} 19 | {"id": "17", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 110} 20 | {"id": "18", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 120} 21 | {"id": "19", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 130} 22 | {"id": "20", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 140} 23 | {"id": "21", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 150} 24 | {"id": "22", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 160} 25 | {"id": "23", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 170} 26 | {"id": "24", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 180} 27 | {"id": "25", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 190} 28 | {"id": "26", "type": "frame", "x": 0, "y": 0, "tags": ["up"], "duration_ms": 200} 29 | {"x": 8, "y": 3, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "0"} 30 | {"x": 8, "y": 4, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "1"} 31 | {"x": 8, "y": 5, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "2"} 32 | {"x": 8, "y": 6, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "3"} 33 | {"x": 8, "y": 7, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "4"} 34 | {"x": 8, "y": 8, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "5"} 35 | {"x": 8, "y": 9, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "6"} 36 | {"x": 8, "y": 10, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "7"} 37 | {"x": 8, "y": 11, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "8"} 38 | {"x": 8, "y": 12, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "9"} 39 | {"x": 8, "y": 13, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "10"} 40 | {"x": 8, "y": 14, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "11"} 41 | {"x": 8, "y": 15, "text": "\u25cf", "color": "", "group": "", "tags": ["downball"], "frame": "12"} 42 | {"x": 8, "y": 15, "text": "_", "color": "", "group": "", "tags": ["downball"], "frame": "13"} 43 | {"x": 8, "y": 3, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "26"} 44 | {"x": 8, "y": 4, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "25"} 45 | {"x": 8, "y": 5, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "24"} 46 | {"x": 8, "y": 6, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "23"} 47 | {"x": 8, "y": 7, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "22"} 48 | {"x": 8, "y": 8, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "21"} 49 | {"x": 8, "y": 9, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "20"} 50 | {"x": 8, "y": 10, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "19"} 51 | {"x": 8, "y": 11, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "18"} 52 | {"x": 8, "y": 12, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "17"} 53 | {"x": 8, "y": 13, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "16"} 54 | {"x": 8, "y": 14, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "15"} 55 | {"x": 8, "y": 15, "text": "\u25cf", "color": "", "group": "", "tags": [], "frame": "14"} 56 | -------------------------------------------------------------------------------- /samples/boxes.ddw: -------------------------------------------------------------------------------- 1 | {"x": 10, "y": 1, "text": "\u2500", "color": ""} 2 | {"x": 10, "y": 3, "text": "\u2502", "color": ""} 3 | {"x": 4, "y": 1, "text": "\u2504", "color": ""} 4 | {"x": 6, "y": 1, "text": "\u2508", "color": ""} 5 | {"x": 4, "y": 3, "text": "\u250a", "color": ""} 6 | {"x": 12, "y": 1, "text": "\u250c", "color": ""} 7 | {"x": 16, "y": 1, "text": "\u2510", "color": ""} 8 | {"x": 12, "y": 5, "text": "\u2514", "color": ""} 9 | {"x": 16, "y": 5, "text": "\u2518", "color": ""} 10 | {"x": 12, "y": 3, "text": "\u251c", "color": ""} 11 | {"x": 16, "y": 3, "text": "\u2524", "color": ""} 12 | {"x": 14, "y": 1, "text": "\u252c", "color": ""} 13 | {"x": 14, "y": 5, "text": "\u2534", "color": ""} 14 | {"x": 14, "y": 3, "text": "\u253c", "color": ""} 15 | {"x": 2, "y": 1, "text": "\u254c", "color": ""} 16 | {"x": 2, "y": 3, "text": "\u254e", "color": ""} 17 | {"x": 15, "y": 11, "text": "\u256d", "color": "", "group": " circle arc"} 18 | {"x": 17, "y": 11, "text": "\u256e", "color": "", "group": " circle arc"} 19 | {"x": 17, "y": 13, "text": "\u256f", "color": "", "group": " circle arc"} 20 | {"x": 15, "y": 13, "text": "\u2570", "color": "", "group": " circle arc"} 21 | {"x": 6, "y": 5, "text": "\u2571", "color": ""} 22 | {"x": 2, "y": 5, "text": "\u2572", "color": ""} 23 | {"x": 5, "y": 6, "text": "\u2573", "color": ""} 24 | {"x": 10, "y": 2, "text": "\u2574", "color": ""} 25 | {"x": 8, "y": 4, "text": "\u2575", "color": ""} 26 | {"x": 10, "y": 4, "text": "\u2576", "color": ""} 27 | {"x": 8, "y": 2, "text": "\u2577", "color": ""} 28 | {"x": 6, "y": 3, "text": "\u2506", "color": ""} 29 | {"x": 19, "y": 11, "text": "\u256d", "color": "", "group": " circle arc"} 30 | {"x": 20, "y": 11, "text": "\u256e", "color": "", "group": " circle arc"} 31 | {"x": 19, "y": 12, "text": "\u2570", "color": "", "group": " circle arc"} 32 | {"x": 20, "y": 12, "text": "\u256f", "color": "", "group": " circle arc"} 33 | {"x": 0, "y": 6, "text": "\u250d", "color": ""} 34 | {"x": 2, "y": 6, "text": "\u250e", "color": ""} 35 | {"x": 5, "y": 6, "text": "\u2511", "color": ""} 36 | {"x": 7, "y": 6, "text": "\u2512", "color": ""} 37 | {"x": 0, "y": 8, "text": "\u2515", "color": ""} 38 | {"x": 2, "y": 8, "text": "\u2516", "color": ""} 39 | {"x": 5, "y": 8, "text": "\u2519", "color": ""} 40 | {"x": 7, "y": 8, "text": "\u251a", "color": ""} 41 | {"x": 1, "y": 9, "text": "\u251d", "color": "", "group": "lsidemid"} 42 | {"x": 3, "y": 9, "text": "\u251e", "color": "", "group": "lsidemid"} 43 | {"x": 5, "y": 9, "text": "\u251f", "color": "", "group": "lsidemid"} 44 | {"x": 7, "y": 9, "text": "\u2520", "color": "", "group": "lsidemid"} 45 | {"x": 9, "y": 9, "text": "\u2521", "color": "", "group": "lsidemid"} 46 | {"x": 11, "y": 9, "text": "\u2522", "color": "", "group": "lsidemid"} 47 | {"x": 17, "y": 9, "text": "\u2525", "color": "", "group": " rsidemid"} 48 | {"x": 19, "y": 9, "text": "\u2526", "color": "", "group": " rsidemid"} 49 | {"x": 21, "y": 9, "text": "\u2527", "color": "", "group": " rsidemid"} 50 | {"x": 23, "y": 9, "text": "\u2528", "color": "", "group": " rsidemid"} 51 | {"x": 25, "y": 9, "text": "\u2529", "color": "", "group": " rsidemid"} 52 | {"x": 27, "y": 9, "text": "\u252a", "color": "", "group": " rsidemid"} 53 | {"x": 1, "y": 11, "text": "\u252d", "color": "", "group": " topmid"} 54 | {"x": 3, "y": 11, "text": "\u252e", "color": "", "group": " topmid"} 55 | {"x": 5, "y": 11, "text": "\u252f", "color": "", "group": " topmid"} 56 | {"x": 7, "y": 11, "text": "\u2530", "color": "", "group": " topmid"} 57 | {"x": 9, "y": 11, "text": "\u2531", "color": "", "group": " topmid"} 58 | {"x": 11, "y": 11, "text": "\u2532", "color": "", "group": " topmid"} 59 | {"x": 1, "y": 14, "text": "\u2535", "color": "", "group": " botmid"} 60 | {"x": 3, "y": 14, "text": "\u2536", "color": "", "group": " botmid"} 61 | {"x": 5, "y": 14, "text": "\u2537", "color": "", "group": " botmid"} 62 | {"x": 7, "y": 14, "text": "\u2538", "color": "", "group": " botmid"} 63 | {"x": 9, "y": 14, "text": "\u2539", "color": "", "group": " botmid"} 64 | {"x": 11, "y": 14, "text": "\u253a", "color": "", "group": " botmid"} 65 | {"x": 25, "y": 7, "text": "\u253d", "color": "", "group": " midmid"} 66 | {"x": 27, "y": 7, "text": "\u253e", "color": "", "group": " midmid"} 67 | {"x": 29, "y": 7, "text": "\u253f", "color": "", "group": " midmid"} 68 | {"x": 31, "y": 7, "text": "\u2540", "color": "", "group": " midmid"} 69 | {"x": 33, "y": 7, "text": "\u2541", "color": "", "group": " midmid"} 70 | {"x": 35, "y": 7, "text": "\u2542", "color": "", "group": " midmid"} 71 | {"x": 9, "y": 7, "text": "\u2543", "color": "", "group": " midmid"} 72 | {"x": 11, "y": 7, "text": "\u2544", "color": "", "group": " midmid"} 73 | {"x": 13, "y": 7, "text": "\u2545", "color": "", "group": " midmid"} 74 | {"x": 15, "y": 7, "text": "\u2546", "color": "", "group": " midmid"} 75 | {"x": 17, "y": 7, "text": "\u2547", "color": "", "group": " midmid"} 76 | {"x": 19, "y": 7, "text": "\u2548", "color": "", "group": " midmid"} 77 | {"x": 21, "y": 7, "text": "\u2549", "color": "", "group": " midmid"} 78 | {"x": 23, "y": 7, "text": "\u254a", "color": "", "group": " midmid"} 79 | {"x": 5, "y": 12, "text": "\u257c", "color": ""} 80 | {"x": 7, "y": 12, "text": "\u257d", "color": "", "group": " side"} 81 | {"x": 9, "y": 12, "text": "\u257e", "color": ""} 82 | {"x": 11, "y": 12, "text": "\u257f", "color": "", "group": " side"} 83 | {"x": 29, "y": 1, "text": "\u2501", "color": "", "group": " heavy"} 84 | {"x": 29, "y": 3, "text": "\u2503", "color": "", "group": " heavy"} 85 | {"x": 22, "y": 1, "text": "\u2505", "color": "", "group": " heavy"} 86 | {"x": 20, "y": 3, "text": "\u2507", "color": "", "group": " heavy"} 87 | {"x": 24, "y": 1, "text": "\u2509", "color": "", "group": " heavy"} 88 | {"x": 24, "y": 3, "text": "\u250b", "color": "", "group": " heavy"} 89 | {"x": 31, "y": 1, "text": "\u250f", "color": "", "group": " heavy"} 90 | {"x": 35, "y": 1, "text": "\u2513", "color": "", "group": " heavy"} 91 | {"x": 31, "y": 5, "text": "\u2517", "color": "", "group": " heavy"} 92 | {"x": 35, "y": 5, "text": "\u251b", "color": "", "group": " heavy"} 93 | {"x": 31, "y": 3, "text": "\u2523", "color": "", "group": " heavy"} 94 | {"x": 35, "y": 3, "text": "\u252b", "color": "", "group": " heavy"} 95 | {"x": 33, "y": 1, "text": "\u2533", "color": "", "group": " heavy"} 96 | {"x": 33, "y": 5, "text": "\u253b", "color": "", "group": " heavy"} 97 | {"x": 33, "y": 3, "text": "\u254b", "color": "", "group": " heavy"} 98 | {"x": 20, "y": 1, "text": "\u254d", "color": "", "group": " heavy"} 99 | {"x": 22, "y": 3, "text": "\u254f", "color": "", "group": " heavy"} 100 | {"x": 29, "y": 2, "text": "\u2578", "color": "", "group": " heavy"} 101 | {"x": 27, "y": 4, "text": "\u2579", "color": "", "group": " heavy"} 102 | {"x": 29, "y": 4, "text": "\u257a", "color": "", "group": " heavy"} 103 | {"x": 27, "y": 2, "text": "\u257b", "color": "", "group": " heavy"} 104 | -------------------------------------------------------------------------------- /samples/bw16colors.ddw: -------------------------------------------------------------------------------- 1 | {"x": 2, "y": 1, "text": "\u2588\u2588", "color": "1", "tags": [], "group": ""} 2 | {"x": 4, "y": 1, "text": "\u2588\u2588", "color": "2", "tags": [], "group": ""} 3 | {"x": 6, "y": 1, "text": "\u2588\u2588", "color": "3", "tags": [], "group": ""} 4 | {"x": 8, "y": 1, "text": "\u2588\u2588", "color": "4", "tags": [], "group": ""} 5 | {"x": 10, "y": 1, "text": "\u2588\u2588", "color": "5", "tags": [], "group": ""} 6 | {"x": 12, "y": 1, "text": "\u2588\u2588", "color": "6", "tags": [], "group": ""} 7 | {"x": 2, "y": 2, "text": "\u2588\u2588", "color": "9", "tags": [], "group": ""} 8 | {"x": 4, "y": 2, "text": "\u2588\u2588", "color": "10", "tags": [], "group": ""} 9 | {"x": 6, "y": 2, "text": "\u2588\u2588", "color": "11", "tags": [], "group": ""} 10 | {"x": 8, "y": 2, "text": "\u2588\u2588", "color": "12", "tags": [], "group": ""} 11 | {"x": 10, "y": 2, "text": "\u2588\u2588", "color": "13", "tags": [], "group": ""} 12 | {"x": 12, "y": 2, "text": "\u2588\u2588", "color": "14", "tags": [], "group": ""} 13 | {"x": 2, "y": 4, "text": "\u2588\u2588", "color": "232", "tags": [], "group": ""} 14 | {"x": 4, "y": 4, "text": "\u2588\u2588", "color": "233", "tags": [], "group": ""} 15 | {"x": 6, "y": 4, "text": "\u2588\u2588", "color": "234", "tags": [], "group": ""} 16 | {"x": 8, "y": 4, "text": "\u2588\u2588", "color": "235", "tags": [], "group": ""} 17 | {"x": 10, "y": 4, "text": "\u2588\u2588", "color": "236", "tags": [], "group": ""} 18 | {"x": 12, "y": 4, "text": "\u2588\u2588", "color": "237", "tags": [], "group": ""} 19 | {"x": 14, "y": 4, "text": "\u2588\u2588", "color": "238", "tags": [], "group": ""} 20 | {"x": 16, "y": 4, "text": "\u2588\u2588", "color": "239", "tags": [], "group": ""} 21 | {"x": 18, "y": 4, "text": "\u2588\u2588", "color": "240", "tags": [], "group": ""} 22 | {"x": 20, "y": 4, "text": "\u2588\u2588", "color": "241", "tags": [], "group": ""} 23 | {"x": 22, "y": 4, "text": "\u2588\u2588", "color": "242", "tags": [], "group": ""} 24 | {"x": 24, "y": 4, "text": "\u2588\u2588", "color": "243", "tags": [], "group": ""} 25 | {"x": 26, "y": 4, "text": "\u2588\u2588", "color": "244", "tags": [], "group": ""} 26 | {"x": 28, "y": 4, "text": "\u2588\u2588", "color": "245", "tags": [], "group": ""} 27 | {"x": 30, "y": 4, "text": "\u2588\u2588", "color": "246", "tags": [], "group": ""} 28 | {"x": 32, "y": 4, "text": "\u2588\u2588", "color": "247", "tags": [], "group": ""} 29 | {"x": 34, "y": 4, "text": "\u2588\u2588", "color": "248", "tags": [], "group": ""} 30 | {"x": 36, "y": 4, "text": "\u2588\u2588", "color": "249", "tags": [], "group": ""} 31 | {"x": 38, "y": 4, "text": "\u2588\u2588", "color": "250", "tags": [], "group": ""} 32 | {"x": 40, "y": 4, "text": "\u2588\u2588", "color": "251", "tags": [], "group": ""} 33 | {"x": 42, "y": 4, "text": "\u2588\u2588", "color": "252", "tags": [], "group": ""} 34 | {"x": 44, "y": 4, "text": "\u2588\u2588", "color": "253", "tags": [], "group": ""} 35 | {"x": 46, "y": 4, "text": "\u2588\u2588", "color": "254", "tags": [], "group": ""} 36 | {"x": 48, "y": 4, "text": "\u2588\u2588", "color": "255", "tags": [], "group": ""} 37 | -------------------------------------------------------------------------------- /samples/policecar.ddw: -------------------------------------------------------------------------------- 1 | {"id": "0", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 2 | {"id": "1", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 3 | {"id": "2", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 4 | {"id": "3", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 5 | {"id": "4", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 6 | {"id": "5", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 7 | {"id": "6", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 8 | {"id": "7", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 9 | {"id": "8", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 10 | {"id": "9", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 11 | {"id": "10", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 12 | {"id": "11", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 13 | {"id": "12", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 14 | {"id": "13", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 15 | {"id": "14", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 16 | {"id": "15", "type": "frame", "x": 0, "y": 0, "tags": [], "duration_ms": 50} 17 | {"id": "car1", "type": "group", "x": 0, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "none", "rows": [{"x": 6, "y": 0, "text": ",", "color": "245", "group": "car1", "tags": []}, {"x": 5, "y": 0, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 6, "y": 0, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 7, "y": 0, "text": "\u25da", "color": "", "group": "car1", "tags": []}, {"x": 8, "y": 0, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 9, "y": 0, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 10, "y": 0, "text": ",", "color": "245", "group": "car1", "tags": []}, {"x": 0, "y": 1, "text": "\u256d", "color": "", "group": "car1", "tags": []}, {"x": 1, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 2, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 3, "y": 1, "text": "\u2534", "color": "", "group": "car1", "tags": []}, {"x": 4, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 5, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 6, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 7, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 8, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 9, "y": 1, "text": "\u22fc", "color": "bold 250", "group": "car1", "tags": []}, {"x": 10, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 11, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 12, "y": 1, "text": "\u2534", "color": "", "group": "car1", "tags": []}, {"x": 13, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 14, "y": 1, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 15, "y": 1, "text": "\u256e", "color": "", "group": "car1", "tags": []}, {"x": 0, "y": 2, "text": "\u2567", "color": "", "group": "car1", "tags": []}, {"x": 1, "y": 2, "text": "\u2550", "color": "", "group": "car1", "tags": []}, {"x": 2, "y": 2, "text": "(", "color": "", "group": "car1", "tags": []}, {"x": 3, "y": 2, "text": "\u25ce", "color": "", "group": "car1", "tags": []}, {"x": 4, "y": 2, "text": ")", "color": "", "group": "car1", "tags": []}, {"x": 5, "y": 2, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 6, "y": 2, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 7, "y": 2, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 8, "y": 2, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 9, "y": 2, "text": "\u2500", "color": "", "group": "car1", "tags": []}, {"x": 10, "y": 2, "text": "(", "color": "", "group": "car1", "tags": []}, {"x": 11, "y": 2, "text": "\u25ce", "color": "", "group": "car1", "tags": []}, {"x": 12, "y": 2, "text": ")", "color": "", "group": "car1", "tags": []}, {"x": 13, "y": 2, "text": "\u2550", "color": "", "group": "car1", "tags": []}, {"x": 14, "y": 2, "text": "\u2501", "color": "", "group": "car1", "tags": []}, {"x": 15, "y": 2, "text": "\u251b", "color": "", "group": "car1", "tags": []}], "w": 15, "h": 2} 18 | {"id": "car2", "type": "group", "x": 0, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "none", "rows": [{"x": 0, "y": 0, "text": "", "color": "", "tags": [], "group": "", "type": "ref", "ref": "car1", "frame": ""}, {"x": 7, "y": 0, "text": "\u25da", "color": "bold red", "group": "car1", "tags": [], "frame": ""}], "w": 7, "h": 0} 19 | {"id": "car3", "type": "group", "x": 0, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "none", "rows": [{"x": 0, "y": 0, "text": "", "color": "", "tags": [], "group": "", "type": "ref", "ref": "car1", "frame": ""}, {"x": 7, "y": 0, "text": "\u25da", "color": "bold blue", "group": "car1", "tags": [], "frame": ""}], "w": 7, "h": 0} 20 | {"type": "ref", "x": 1, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "0", "ref": "car1"} 21 | {"type": "ref", "x": 2, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "1", "ref": "car2"} 22 | {"type": "ref", "x": 3, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "2", "ref": "car3"} 23 | {"type": "ref", "x": 4, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "3", "ref": "car1"} 24 | {"type": "ref", "x": 5, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "4", "ref": "car2"} 25 | {"type": "ref", "x": 6, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "5", "ref": "car3"} 26 | {"type": "ref", "x": 7, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "6", "ref": "car1"} 27 | {"type": "ref", "x": 8, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "7", "ref": "car2"} 28 | {"type": "ref", "x": 9, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "8", "ref": "car3"} 29 | {"type": "ref", "x": 10, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "9", "ref": "car1"} 30 | {"type": "ref", "x": 11, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "10", "ref": "car2"} 31 | {"type": "ref", "x": 12, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "11", "ref": "car3"} 32 | {"type": "ref", "x": 13, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "12", "ref": "car1"} 33 | {"type": "ref", "x": 14, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "13", "ref": "car2"} 34 | {"type": "ref", "x": 15, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "14", "ref": "car3"} 35 | {"type": "ref", "x": 16, "y": 0, "text": "", "color": "", "tags": [], "group": "", "frame": "15", "ref": "car2"} 36 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup 4 | from pathlib import Path 5 | 6 | def requirements(): 7 | return Path('requirements.txt').read_text().splitlines() 8 | 9 | __version__='0.3.1' 10 | 11 | setup(name='darkdraw', 12 | version=__version__, 13 | description='art and animation for the terminal, in the terminal', 14 | author='devottys', 15 | python_requires='>=3.7', 16 | url='bluebird.sh', 17 | py_modules=['darkdraw'], 18 | install_requires=requirements(), 19 | packages=['darkdraw'], 20 | include_package_data=True, 21 | entry_points={'visidata.plugins': 'darkdraw=darkdraw'}, 22 | package_data={'darkdraw': ['darkdraw/ansi.html']}, 23 | ) 24 | --------------------------------------------------------------------------------