├── .gitignore
├── COPYING
├── MANIFEST.in
├── README.rst
├── doc
├── Makefile
├── conf.py
├── developer.rst
├── developer
│ ├── license.rst
│ ├── modules.rst
│ ├── modules
│ │ ├── io.rst
│ │ ├── io
│ │ │ └── fountain.rst
│ │ ├── main.rst
│ │ ├── menu_view.rst
│ │ ├── paragraph.rst
│ │ ├── screenplay.rst
│ │ └── sp_view.rst
│ └── todo.rst
├── index.rst
└── user.rst
├── img
└── screenshot.png
├── run.py
├── setup.cfg
├── setup.py
└── shane
├── __init__.py
├── __init__.pyc
├── io
├── __init__.py
└── fountain.py
├── main.py
├── menu_view.py
├── paragraph.py
├── screenplay.py
├── sp_view.py
└── view.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
3 | doc/_build/
4 |
--------------------------------------------------------------------------------
/COPYING:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU General Public License is a free, copyleft license for
11 | software and other kinds of works.
12 |
13 | The licenses for most software and other practical works are designed
14 | to take away your freedom to share and change the works. By contrast,
15 | the GNU General Public License is intended to guarantee your freedom to
16 | share and change all versions of a program--to make sure it remains free
17 | software for all its users. We, the Free Software Foundation, use the
18 | GNU General Public License for most of our software; it applies also to
19 | any other work released this way by its authors. You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | them if you wish), that you receive source code or can get it if you
26 | want it, that you can change the software or use pieces of it in new
27 | free programs, and that you know you can do these things.
28 |
29 | To protect your rights, we need to prevent others from denying you
30 | these rights or asking you to surrender the rights. Therefore, you have
31 | certain responsibilities if you distribute copies of the software, or if
32 | you modify it: responsibilities to respect the freedom of others.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must pass on to the recipients the same
36 | freedoms that you received. You must make sure that they, too, receive
37 | or can get the source code. And you must show them these terms so they
38 | know their rights.
39 |
40 | Developers that use the GNU GPL protect your rights with two steps:
41 | (1) assert copyright on the software, and (2) offer you this License
42 | giving you legal permission to copy, distribute and/or modify it.
43 |
44 | For the developers' and authors' protection, the GPL clearly explains
45 | that there is no warranty for this free software. For both users' and
46 | authors' sake, the GPL requires that modified versions be marked as
47 | changed, so that their problems will not be attributed erroneously to
48 | authors of previous versions.
49 |
50 | Some devices are designed to deny users access to install or run
51 | modified versions of the software inside them, although the manufacturer
52 | can do so. This is fundamentally incompatible with the aim of
53 | protecting users' freedom to change the software. The systematic
54 | pattern of such abuse occurs in the area of products for individuals to
55 | use, which is precisely where it is most unacceptable. Therefore, we
56 | have designed this version of the GPL to prohibit the practice for those
57 | products. If such problems arise substantially in other domains, we
58 | stand ready to extend this provision to those domains in future versions
59 | of the GPL, as needed to protect the freedom of users.
60 |
61 | Finally, every program is threatened constantly by software patents.
62 | States should not allow patents to restrict development and use of
63 | software on general-purpose computers, but in those that do, we wish to
64 | avoid the special danger that patents applied to a free program could
65 | make it effectively proprietary. To prevent this, the GPL assures that
66 | patents cannot be used to render the program non-free.
67 |
68 | The precise terms and conditions for copying, distribution and
69 | modification follow.
70 |
71 | TERMS AND CONDITIONS
72 |
73 | 0. Definitions.
74 |
75 | "This License" refers to version 3 of the GNU General Public License.
76 |
77 | "Copyright" also means copyright-like laws that apply to other kinds of
78 | works, such as semiconductor masks.
79 |
80 | "The Program" refers to any copyrightable work licensed under this
81 | License. Each licensee is addressed as "you". "Licensees" and
82 | "recipients" may be individuals or organizations.
83 |
84 | To "modify" a work means to copy from or adapt all or part of the work
85 | in a fashion requiring copyright permission, other than the making of an
86 | exact copy. The resulting work is called a "modified version" of the
87 | earlier work or a work "based on" the earlier work.
88 |
89 | A "covered work" means either the unmodified Program or a work based
90 | on the Program.
91 |
92 | To "propagate" a work means to do anything with it that, without
93 | permission, would make you directly or secondarily liable for
94 | infringement under applicable copyright law, except executing it on a
95 | computer or modifying a private copy. Propagation includes copying,
96 | distribution (with or without modification), making available to the
97 | public, and in some countries other activities as well.
98 |
99 | To "convey" a work means any kind of propagation that enables other
100 | parties to make or receive copies. Mere interaction with a user through
101 | a computer network, with no transfer of a copy, is not conveying.
102 |
103 | An interactive user interface displays "Appropriate Legal Notices"
104 | to the extent that it includes a convenient and prominently visible
105 | feature that (1) displays an appropriate copyright notice, and (2)
106 | tells the user that there is no warranty for the work (except to the
107 | extent that warranties are provided), that licensees may convey the
108 | work under this License, and how to view a copy of this License. If
109 | the interface presents a list of user commands or options, such as a
110 | menu, a prominent item in the list meets this criterion.
111 |
112 | 1. Source Code.
113 |
114 | The "source code" for a work means the preferred form of the work
115 | for making modifications to it. "Object code" means any non-source
116 | form of a work.
117 |
118 | A "Standard Interface" means an interface that either is an official
119 | standard defined by a recognized standards body, or, in the case of
120 | interfaces specified for a particular programming language, one that
121 | is widely used among developers working in that language.
122 |
123 | The "System Libraries" of an executable work include anything, other
124 | than the work as a whole, that (a) is included in the normal form of
125 | packaging a Major Component, but which is not part of that Major
126 | Component, and (b) serves only to enable use of the work with that
127 | Major Component, or to implement a Standard Interface for which an
128 | implementation is available to the public in source code form. A
129 | "Major Component", in this context, means a major essential component
130 | (kernel, window system, and so on) of the specific operating system
131 | (if any) on which the executable work runs, or a compiler used to
132 | produce the work, or an object code interpreter used to run it.
133 |
134 | The "Corresponding Source" for a work in object code form means all
135 | the source code needed to generate, install, and (for an executable
136 | work) run the object code and to modify the work, including scripts to
137 | control those activities. However, it does not include the work's
138 | System Libraries, or general-purpose tools or generally available free
139 | programs which are used unmodified in performing those activities but
140 | which are not part of the work. For example, Corresponding Source
141 | includes interface definition files associated with source files for
142 | the work, and the source code for shared libraries and dynamically
143 | linked subprograms that the work is specifically designed to require,
144 | such as by intimate data communication or control flow between those
145 | subprograms and other parts of the work.
146 |
147 | The Corresponding Source need not include anything that users
148 | can regenerate automatically from other parts of the Corresponding
149 | Source.
150 |
151 | The Corresponding Source for a work in source code form is that
152 | same work.
153 |
154 | 2. Basic Permissions.
155 |
156 | All rights granted under this License are granted for the term of
157 | copyright on the Program, and are irrevocable provided the stated
158 | conditions are met. This License explicitly affirms your unlimited
159 | permission to run the unmodified Program. The output from running a
160 | covered work is covered by this License only if the output, given its
161 | content, constitutes a covered work. This License acknowledges your
162 | rights of fair use or other equivalent, as provided by copyright law.
163 |
164 | You may make, run and propagate covered works that you do not
165 | convey, without conditions so long as your license otherwise remains
166 | in force. You may convey covered works to others for the sole purpose
167 | of having them make modifications exclusively for you, or provide you
168 | with facilities for running those works, provided that you comply with
169 | the terms of this License in conveying all material for which you do
170 | not control copyright. Those thus making or running the covered works
171 | for you must do so exclusively on your behalf, under your direction
172 | and control, on terms that prohibit them from making any copies of
173 | your copyrighted material outside their relationship with you.
174 |
175 | Conveying under any other circumstances is permitted solely under
176 | the conditions stated below. Sublicensing is not allowed; section 10
177 | makes it unnecessary.
178 |
179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
180 |
181 | No covered work shall be deemed part of an effective technological
182 | measure under any applicable law fulfilling obligations under article
183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
184 | similar laws prohibiting or restricting circumvention of such
185 | measures.
186 |
187 | When you convey a covered work, you waive any legal power to forbid
188 | circumvention of technological measures to the extent such circumvention
189 | is effected by exercising rights under this License with respect to
190 | the covered work, and you disclaim any intention to limit operation or
191 | modification of the work as a means of enforcing, against the work's
192 | users, your or third parties' legal rights to forbid circumvention of
193 | technological measures.
194 |
195 | 4. Conveying Verbatim Copies.
196 |
197 | You may convey verbatim copies of the Program's source code as you
198 | receive it, in any medium, provided that you conspicuously and
199 | appropriately publish on each copy an appropriate copyright notice;
200 | keep intact all notices stating that this License and any
201 | non-permissive terms added in accord with section 7 apply to the code;
202 | keep intact all notices of the absence of any warranty; and give all
203 | recipients a copy of this License along with the Program.
204 |
205 | You may charge any price or no price for each copy that you convey,
206 | and you may offer support or warranty protection for a fee.
207 |
208 | 5. Conveying Modified Source Versions.
209 |
210 | You may convey a work based on the Program, or the modifications to
211 | produce it from the Program, in the form of source code under the
212 | terms of section 4, provided that you also meet all of these conditions:
213 |
214 | a) The work must carry prominent notices stating that you modified
215 | it, and giving a relevant date.
216 |
217 | b) The work must carry prominent notices stating that it is
218 | released under this License and any conditions added under section
219 | 7. This requirement modifies the requirement in section 4 to
220 | "keep intact all notices".
221 |
222 | c) You must license the entire work, as a whole, under this
223 | License to anyone who comes into possession of a copy. This
224 | License will therefore apply, along with any applicable section 7
225 | additional terms, to the whole of the work, and all its parts,
226 | regardless of how they are packaged. This License gives no
227 | permission to license the work in any other way, but it does not
228 | invalidate such permission if you have separately received it.
229 |
230 | d) If the work has interactive user interfaces, each must display
231 | Appropriate Legal Notices; however, if the Program has interactive
232 | interfaces that do not display Appropriate Legal Notices, your
233 | work need not make them do so.
234 |
235 | A compilation of a covered work with other separate and independent
236 | works, which are not by their nature extensions of the covered work,
237 | and which are not combined with it such as to form a larger program,
238 | in or on a volume of a storage or distribution medium, is called an
239 | "aggregate" if the compilation and its resulting copyright are not
240 | used to limit the access or legal rights of the compilation's users
241 | beyond what the individual works permit. Inclusion of a covered work
242 | in an aggregate does not cause this License to apply to the other
243 | parts of the aggregate.
244 |
245 | 6. Conveying Non-Source Forms.
246 |
247 | You may convey a covered work in object code form under the terms
248 | of sections 4 and 5, provided that you also convey the
249 | machine-readable Corresponding Source under the terms of this License,
250 | in one of these ways:
251 |
252 | a) Convey the object code in, or embodied in, a physical product
253 | (including a physical distribution medium), accompanied by the
254 | Corresponding Source fixed on a durable physical medium
255 | customarily used for software interchange.
256 |
257 | b) Convey the object code in, or embodied in, a physical product
258 | (including a physical distribution medium), accompanied by a
259 | written offer, valid for at least three years and valid for as
260 | long as you offer spare parts or customer support for that product
261 | model, to give anyone who possesses the object code either (1) a
262 | copy of the Corresponding Source for all the software in the
263 | product that is covered by this License, on a durable physical
264 | medium customarily used for software interchange, for a price no
265 | more than your reasonable cost of physically performing this
266 | conveying of source, or (2) access to copy the
267 | Corresponding Source from a network server at no charge.
268 |
269 | c) Convey individual copies of the object code with a copy of the
270 | written offer to provide the Corresponding Source. This
271 | alternative is allowed only occasionally and noncommercially, and
272 | only if you received the object code with such an offer, in accord
273 | with subsection 6b.
274 |
275 | d) Convey the object code by offering access from a designated
276 | place (gratis or for a charge), and offer equivalent access to the
277 | Corresponding Source in the same way through the same place at no
278 | further charge. You need not require recipients to copy the
279 | Corresponding Source along with the object code. If the place to
280 | copy the object code is a network server, the Corresponding Source
281 | may be on a different server (operated by you or a third party)
282 | that supports equivalent copying facilities, provided you maintain
283 | clear directions next to the object code saying where to find the
284 | Corresponding Source. Regardless of what server hosts the
285 | Corresponding Source, you remain obligated to ensure that it is
286 | available for as long as needed to satisfy these requirements.
287 |
288 | e) Convey the object code using peer-to-peer transmission, provided
289 | you inform other peers where the object code and Corresponding
290 | Source of the work are being offered to the general public at no
291 | charge under subsection 6d.
292 |
293 | A separable portion of the object code, whose source code is excluded
294 | from the Corresponding Source as a System Library, need not be
295 | included in conveying the object code work.
296 |
297 | A "User Product" is either (1) a "consumer product", which means any
298 | tangible personal property which is normally used for personal, family,
299 | or household purposes, or (2) anything designed or sold for incorporation
300 | into a dwelling. In determining whether a product is a consumer product,
301 | doubtful cases shall be resolved in favor of coverage. For a particular
302 | product received by a particular user, "normally used" refers to a
303 | typical or common use of that class of product, regardless of the status
304 | of the particular user or of the way in which the particular user
305 | actually uses, or expects or is expected to use, the product. A product
306 | is a consumer product regardless of whether the product has substantial
307 | commercial, industrial or non-consumer uses, unless such uses represent
308 | the only significant mode of use of the product.
309 |
310 | "Installation Information" for a User Product means any methods,
311 | procedures, authorization keys, or other information required to install
312 | and execute modified versions of a covered work in that User Product from
313 | a modified version of its Corresponding Source. The information must
314 | suffice to ensure that the continued functioning of the modified object
315 | code is in no case prevented or interfered with solely because
316 | modification has been made.
317 |
318 | If you convey an object code work under this section in, or with, or
319 | specifically for use in, a User Product, and the conveying occurs as
320 | part of a transaction in which the right of possession and use of the
321 | User Product is transferred to the recipient in perpetuity or for a
322 | fixed term (regardless of how the transaction is characterized), the
323 | Corresponding Source conveyed under this section must be accompanied
324 | by the Installation Information. But this requirement does not apply
325 | if neither you nor any third party retains the ability to install
326 | modified object code on the User Product (for example, the work has
327 | been installed in ROM).
328 |
329 | The requirement to provide Installation Information does not include a
330 | requirement to continue to provide support service, warranty, or updates
331 | for a work that has been modified or installed by the recipient, or for
332 | the User Product in which it has been modified or installed. Access to a
333 | network may be denied when the modification itself materially and
334 | adversely affects the operation of the network or violates the rules and
335 | protocols for communication across the network.
336 |
337 | Corresponding Source conveyed, and Installation Information provided,
338 | in accord with this section must be in a format that is publicly
339 | documented (and with an implementation available to the public in
340 | source code form), and must require no special password or key for
341 | unpacking, reading or copying.
342 |
343 | 7. Additional Terms.
344 |
345 | "Additional permissions" are terms that supplement the terms of this
346 | License by making exceptions from one or more of its conditions.
347 | Additional permissions that are applicable to the entire Program shall
348 | be treated as though they were included in this License, to the extent
349 | that they are valid under applicable law. If additional permissions
350 | apply only to part of the Program, that part may be used separately
351 | under those permissions, but the entire Program remains governed by
352 | this License without regard to the additional permissions.
353 |
354 | When you convey a copy of a covered work, you may at your option
355 | remove any additional permissions from that copy, or from any part of
356 | it. (Additional permissions may be written to require their own
357 | removal in certain cases when you modify the work.) You may place
358 | additional permissions on material, added by you to a covered work,
359 | for which you have or can give appropriate copyright permission.
360 |
361 | Notwithstanding any other provision of this License, for material you
362 | add to a covered work, you may (if authorized by the copyright holders of
363 | that material) supplement the terms of this License with terms:
364 |
365 | a) Disclaiming warranty or limiting liability differently from the
366 | terms of sections 15 and 16 of this License; or
367 |
368 | b) Requiring preservation of specified reasonable legal notices or
369 | author attributions in that material or in the Appropriate Legal
370 | Notices displayed by works containing it; or
371 |
372 | c) Prohibiting misrepresentation of the origin of that material, or
373 | requiring that modified versions of such material be marked in
374 | reasonable ways as different from the original version; or
375 |
376 | d) Limiting the use for publicity purposes of names of licensors or
377 | authors of the material; or
378 |
379 | e) Declining to grant rights under trademark law for use of some
380 | trade names, trademarks, or service marks; or
381 |
382 | f) Requiring indemnification of licensors and authors of that
383 | material by anyone who conveys the material (or modified versions of
384 | it) with contractual assumptions of liability to the recipient, for
385 | any liability that these contractual assumptions directly impose on
386 | those licensors and authors.
387 |
388 | All other non-permissive additional terms are considered "further
389 | restrictions" within the meaning of section 10. If the Program as you
390 | received it, or any part of it, contains a notice stating that it is
391 | governed by this License along with a term that is a further
392 | restriction, you may remove that term. If a license document contains
393 | a further restriction but permits relicensing or conveying under this
394 | License, you may add to a covered work material governed by the terms
395 | of that license document, provided that the further restriction does
396 | not survive such relicensing or conveying.
397 |
398 | If you add terms to a covered work in accord with this section, you
399 | must place, in the relevant source files, a statement of the
400 | additional terms that apply to those files, or a notice indicating
401 | where to find the applicable terms.
402 |
403 | Additional terms, permissive or non-permissive, may be stated in the
404 | form of a separately written license, or stated as exceptions;
405 | the above requirements apply either way.
406 |
407 | 8. Termination.
408 |
409 | You may not propagate or modify a covered work except as expressly
410 | provided under this License. Any attempt otherwise to propagate or
411 | modify it is void, and will automatically terminate your rights under
412 | this License (including any patent licenses granted under the third
413 | paragraph of section 11).
414 |
415 | However, if you cease all violation of this License, then your
416 | license from a particular copyright holder is reinstated (a)
417 | provisionally, unless and until the copyright holder explicitly and
418 | finally terminates your license, and (b) permanently, if the copyright
419 | holder fails to notify you of the violation by some reasonable means
420 | prior to 60 days after the cessation.
421 |
422 | Moreover, your license from a particular copyright holder is
423 | reinstated permanently if the copyright holder notifies you of the
424 | violation by some reasonable means, this is the first time you have
425 | received notice of violation of this License (for any work) from that
426 | copyright holder, and you cure the violation prior to 30 days after
427 | your receipt of the notice.
428 |
429 | Termination of your rights under this section does not terminate the
430 | licenses of parties who have received copies or rights from you under
431 | this License. If your rights have been terminated and not permanently
432 | reinstated, you do not qualify to receive new licenses for the same
433 | material under section 10.
434 |
435 | 9. Acceptance Not Required for Having Copies.
436 |
437 | You are not required to accept this License in order to receive or
438 | run a copy of the Program. Ancillary propagation of a covered work
439 | occurring solely as a consequence of using peer-to-peer transmission
440 | to receive a copy likewise does not require acceptance. However,
441 | nothing other than this License grants you permission to propagate or
442 | modify any covered work. These actions infringe copyright if you do
443 | not accept this License. Therefore, by modifying or propagating a
444 | covered work, you indicate your acceptance of this License to do so.
445 |
446 | 10. Automatic Licensing of Downstream Recipients.
447 |
448 | Each time you convey a covered work, the recipient automatically
449 | receives a license from the original licensors, to run, modify and
450 | propagate that work, subject to this License. You are not responsible
451 | for enforcing compliance by third parties with this License.
452 |
453 | An "entity transaction" is a transaction transferring control of an
454 | organization, or substantially all assets of one, or subdividing an
455 | organization, or merging organizations. If propagation of a covered
456 | work results from an entity transaction, each party to that
457 | transaction who receives a copy of the work also receives whatever
458 | licenses to the work the party's predecessor in interest had or could
459 | give under the previous paragraph, plus a right to possession of the
460 | Corresponding Source of the work from the predecessor in interest, if
461 | the predecessor has it or can get it with reasonable efforts.
462 |
463 | You may not impose any further restrictions on the exercise of the
464 | rights granted or affirmed under this License. For example, you may
465 | not impose a license fee, royalty, or other charge for exercise of
466 | rights granted under this License, and you may not initiate litigation
467 | (including a cross-claim or counterclaim in a lawsuit) alleging that
468 | any patent claim is infringed by making, using, selling, offering for
469 | sale, or importing the Program or any portion of it.
470 |
471 | 11. Patents.
472 |
473 | A "contributor" is a copyright holder who authorizes use under this
474 | License of the Program or a work on which the Program is based. The
475 | work thus licensed is called the contributor's "contributor version".
476 |
477 | A contributor's "essential patent claims" are all patent claims
478 | owned or controlled by the contributor, whether already acquired or
479 | hereafter acquired, that would be infringed by some manner, permitted
480 | by this License, of making, using, or selling its contributor version,
481 | but do not include claims that would be infringed only as a
482 | consequence of further modification of the contributor version. For
483 | purposes of this definition, "control" includes the right to grant
484 | patent sublicenses in a manner consistent with the requirements of
485 | this License.
486 |
487 | Each contributor grants you a non-exclusive, worldwide, royalty-free
488 | patent license under the contributor's essential patent claims, to
489 | make, use, sell, offer for sale, import and otherwise run, modify and
490 | propagate the contents of its contributor version.
491 |
492 | In the following three paragraphs, a "patent license" is any express
493 | agreement or commitment, however denominated, not to enforce a patent
494 | (such as an express permission to practice a patent or covenant not to
495 | sue for patent infringement). To "grant" such a patent license to a
496 | party means to make such an agreement or commitment not to enforce a
497 | patent against the party.
498 |
499 | If you convey a covered work, knowingly relying on a patent license,
500 | and the Corresponding Source of the work is not available for anyone
501 | to copy, free of charge and under the terms of this License, through a
502 | publicly available network server or other readily accessible means,
503 | then you must either (1) cause the Corresponding Source to be so
504 | available, or (2) arrange to deprive yourself of the benefit of the
505 | patent license for this particular work, or (3) arrange, in a manner
506 | consistent with the requirements of this License, to extend the patent
507 | license to downstream recipients. "Knowingly relying" means you have
508 | actual knowledge that, but for the patent license, your conveying the
509 | covered work in a country, or your recipient's use of the covered work
510 | in a country, would infringe one or more identifiable patents in that
511 | country that you have reason to believe are valid.
512 |
513 | If, pursuant to or in connection with a single transaction or
514 | arrangement, you convey, or propagate by procuring conveyance of, a
515 | covered work, and grant a patent license to some of the parties
516 | receiving the covered work authorizing them to use, propagate, modify
517 | or convey a specific copy of the covered work, then the patent license
518 | you grant is automatically extended to all recipients of the covered
519 | work and works based on it.
520 |
521 | A patent license is "discriminatory" if it does not include within
522 | the scope of its coverage, prohibits the exercise of, or is
523 | conditioned on the non-exercise of one or more of the rights that are
524 | specifically granted under this License. You may not convey a covered
525 | work if you are a party to an arrangement with a third party that is
526 | in the business of distributing software, under which you make payment
527 | to the third party based on the extent of your activity of conveying
528 | the work, and under which the third party grants, to any of the
529 | parties who would receive the covered work from you, a discriminatory
530 | patent license (a) in connection with copies of the covered work
531 | conveyed by you (or copies made from those copies), or (b) primarily
532 | for and in connection with specific products or compilations that
533 | contain the covered work, unless you entered into that arrangement,
534 | or that patent license was granted, prior to 28 March 2007.
535 |
536 | Nothing in this License shall be construed as excluding or limiting
537 | any implied license or other defenses to infringement that may
538 | otherwise be available to you under applicable patent law.
539 |
540 | 12. No Surrender of Others' Freedom.
541 |
542 | If conditions are imposed on you (whether by court order, agreement or
543 | otherwise) that contradict the conditions of this License, they do not
544 | excuse you from the conditions of this License. If you cannot convey a
545 | covered work so as to satisfy simultaneously your obligations under this
546 | License and any other pertinent obligations, then as a consequence you may
547 | not convey it at all. For example, if you agree to terms that obligate you
548 | to collect a royalty for further conveying from those to whom you convey
549 | the Program, the only way you could satisfy both those terms and this
550 | License would be to refrain entirely from conveying the Program.
551 |
552 | 13. Use with the GNU Affero General Public License.
553 |
554 | Notwithstanding any other provision of this License, you have
555 | permission to link or combine any covered work with a work licensed
556 | under version 3 of the GNU Affero General Public License into a single
557 | combined work, and to convey the resulting work. The terms of this
558 | License will continue to apply to the part which is the covered work,
559 | but the special requirements of the GNU Affero General Public License,
560 | section 13, concerning interaction through a network will apply to the
561 | combination as such.
562 |
563 | 14. Revised Versions of this License.
564 |
565 | The Free Software Foundation may publish revised and/or new versions of
566 | the GNU General Public License from time to time. Such new versions will
567 | be similar in spirit to the present version, but may differ in detail to
568 | address new problems or concerns.
569 |
570 | Each version is given a distinguishing version number. If the
571 | Program specifies that a certain numbered version of the GNU General
572 | Public License "or any later version" applies to it, you have the
573 | option of following the terms and conditions either of that numbered
574 | version or of any later version published by the Free Software
575 | Foundation. If the Program does not specify a version number of the
576 | GNU General Public License, you may choose any version ever published
577 | by the Free Software Foundation.
578 |
579 | If the Program specifies that a proxy can decide which future
580 | versions of the GNU General Public License can be used, that proxy's
581 | public statement of acceptance of a version permanently authorizes you
582 | to choose that version for the Program.
583 |
584 | Later license versions may give you additional or different
585 | permissions. However, no additional obligations are imposed on any
586 | author or copyright holder as a result of your choosing to follow a
587 | later version.
588 |
589 | 15. Disclaimer of Warranty.
590 |
591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
599 |
600 | 16. Limitation of Liability.
601 |
602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
610 | SUCH DAMAGES.
611 |
612 | 17. Interpretation of Sections 15 and 16.
613 |
614 | If the disclaimer of warranty and limitation of liability provided
615 | above cannot be given local legal effect according to their terms,
616 | reviewing courts shall apply local law that most closely approximates
617 | an absolute waiver of all civil liability in connection with the
618 | Program, unless a warranty or assumption of liability accompanies a
619 | copy of the Program in return for a fee.
620 |
621 | END OF TERMS AND CONDITIONS
622 |
623 | How to Apply These Terms to Your New Programs
624 |
625 | If you develop a new program, and you want it to be of the greatest
626 | possible use to the public, the best way to achieve this is to make it
627 | free software which everyone can redistribute and change under these terms.
628 |
629 | To do so, attach the following notices to the program. It is safest
630 | to attach them to the start of each source file to most effectively
631 | state the exclusion of warranty; and each file should have at least
632 | the "copyright" line and a pointer to where the full notice is found.
633 |
634 |
635 | Copyright (C)
636 |
637 | This program is free software: you can redistribute it and/or modify
638 | it under the terms of the GNU General Public License as published by
639 | the Free Software Foundation, either version 3 of the License, or
640 | (at your option) any later version.
641 |
642 | This program is distributed in the hope that it will be useful,
643 | but WITHOUT ANY WARRANTY; without even the implied warranty of
644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
645 | GNU General Public License for more details.
646 |
647 | You should have received a copy of the GNU General Public License
648 | along with this program. If not, see .
649 |
650 | Also add information on how to contact you by electronic and paper mail.
651 |
652 | If the program does terminal interaction, make it output a short
653 | notice like this when it starts in an interactive mode:
654 |
655 | Copyright (C)
656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657 | This is free software, and you are welcome to redistribute it
658 | under certain conditions; type `show c' for details.
659 |
660 | The hypothetical commands `show w' and `show c' should show the appropriate
661 | parts of the General Public License. Of course, your program's commands
662 | might be different; for a GUI interface, you would use an "about box".
663 |
664 | You should also get your employer (if you work as a programmer) or school,
665 | if any, to sign a "copyright disclaimer" for the program, if necessary.
666 | For more information on this, and how to apply and follow the GNU GPL, see
667 | .
668 |
669 | The GNU General Public License does not permit incorporating your program
670 | into proprietary programs. If your program is a subroutine library, you
671 | may consider it more useful to permit linking proprietary applications with
672 | the library. If this is what you want to do, use the GNU Lesser General
673 | Public License instead of this License. But first, please read
674 | .
675 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include COPYING
2 | include README.rst
3 | include img/screenshot.png
4 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | =====
2 | Shane
3 | =====
4 |
5 | A poor man and/or hipster's TUI screenwriting software.
6 |
7 | Introduction
8 | ============
9 |
10 | Ever since the wide adoption of portable PCs screenwriters have been wondering
11 | how they can set themselves even more apart from everyone else than just doing
12 | their work in a local coffee shop on an expensive MacBook for everyone to see
13 | instead of in an office on a reasonably priced computer like normal people
14 | because nobody cares.
15 |
16 | Now maximum apartness can be achieved: Write your screenplay in a terminal just
17 | like in the olden days! Don't click around with your mouse. What's a mouse wheel
18 | anyway? Feel the comfort of a fully ASCII-rendered user interface!
19 |
20 | After all, screenplays are just text, why shouldn't screenwriting software be?
21 |
22 | .. image:: img/screenshot.png
23 |
24 | Requirements
25 | ============
26 |
27 | :OS: Linux or MacOS
28 | :Software: Python 3.3 or higher, ncurses
29 |
30 | Install
31 | =======
32 |
33 | *Shane* does *not* need to be installed. Still, if you like to then run
34 | ``python setup.py install`` on the command line from *Shane's* root directory.
35 |
36 | Run
37 | ===
38 |
39 | Either run ``python run.py`` from *Shane's* root directory or (if installed)
40 | simply run ``shane`` from wherever. To start *Shane* with opening a screenplay
41 | add the path as a parameter, e.g.
42 | ``python run.py /home/hotshot/screenplays/romcom.fountain`` or
43 | ``shane /home/hotshot/screenplays/romcom.fountain``.
44 |
45 | User Manual
46 | ===========
47 |
48 | Visit `the documentation. `_
49 |
50 | To Do
51 | =====
52 |
53 | While *Shane* is a fully usable screenwriting software it's very basic and far
54 | behind the competition (even competing freeware). Of course a lot of its
55 | simplicity is by design but some features are still nice to have and will
56 | probably come.
57 |
58 | Menu
59 | ----
60 |
61 | - Save As
62 | - Autocomplete path names
63 | - Set meta data (title page stuff)
64 |
65 | Screenplay
66 | ----------
67 |
68 | - Meta data
69 | - Optimize ``get_line_count()``
70 | - Store new names automatically in name database
71 | - Select
72 | - Set selection
73 | - Delete selection
74 | - Copy selection
75 | - Clipboard
76 | - Copy to internal clipboard
77 | - Paste from internal clipboard
78 | - Copy to system clipboard
79 | - Paste from system clipboard
80 | - Let edit methods error check
81 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help
18 | help:
19 | @echo "Please use \`make ' where is one of"
20 | @echo " html to make standalone HTML files"
21 | @echo " dirhtml to make HTML files named index.html in directories"
22 | @echo " singlehtml to make a single large HTML file"
23 | @echo " pickle to make pickle files"
24 | @echo " json to make JSON files"
25 | @echo " htmlhelp to make HTML files and a HTML help project"
26 | @echo " qthelp to make HTML files and a qthelp project"
27 | @echo " applehelp to make an Apple Help Book"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " epub3 to make an epub3"
31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
32 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
34 | @echo " text to make text files"
35 | @echo " man to make manual pages"
36 | @echo " texinfo to make Texinfo files"
37 | @echo " info to make Texinfo files and run them through makeinfo"
38 | @echo " gettext to make PO message catalogs"
39 | @echo " changes to make an overview of all changed/added/deprecated items"
40 | @echo " xml to make Docutils-native XML files"
41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
42 | @echo " linkcheck to check all external links for integrity"
43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
44 | @echo " coverage to run coverage check of the documentation (if enabled)"
45 | @echo " dummy to check syntax errors of document sources"
46 |
47 | .PHONY: clean
48 | clean:
49 | rm -rf $(BUILDDIR)/*
50 |
51 | .PHONY: html
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | .PHONY: dirhtml
58 | dirhtml:
59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
60 | @echo
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
62 |
63 | .PHONY: singlehtml
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | .PHONY: pickle
70 | pickle:
71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
72 | @echo
73 | @echo "Build finished; now you can process the pickle files."
74 |
75 | .PHONY: json
76 | json:
77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
78 | @echo
79 | @echo "Build finished; now you can process the JSON files."
80 |
81 | .PHONY: htmlhelp
82 | htmlhelp:
83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
84 | @echo
85 | @echo "Build finished; now you can run HTML Help Workshop with the" \
86 | ".hhp project file in $(BUILDDIR)/htmlhelp."
87 |
88 | .PHONY: qthelp
89 | qthelp:
90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
91 | @echo
92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Shane.qhcp"
95 | @echo "To view the help file:"
96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Shane.qhc"
97 |
98 | .PHONY: applehelp
99 | applehelp:
100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
101 | @echo
102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
103 | @echo "N.B. You won't be able to view it unless you put it in" \
104 | "~/Library/Documentation/Help or install it in your application" \
105 | "bundle."
106 |
107 | .PHONY: devhelp
108 | devhelp:
109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
110 | @echo
111 | @echo "Build finished."
112 | @echo "To view the help file:"
113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Shane"
114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Shane"
115 | @echo "# devhelp"
116 |
117 | .PHONY: epub
118 | epub:
119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
120 | @echo
121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
122 |
123 | .PHONY: epub3
124 | epub3:
125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
126 | @echo
127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
128 |
129 | .PHONY: latex
130 | latex:
131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
132 | @echo
133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
135 | "(use \`make latexpdf' here to do that automatically)."
136 |
137 | .PHONY: latexpdf
138 | latexpdf:
139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
140 | @echo "Running LaTeX files through pdflatex..."
141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
143 |
144 | .PHONY: latexpdfja
145 | latexpdfja:
146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
147 | @echo "Running LaTeX files through platex and dvipdfmx..."
148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
150 |
151 | .PHONY: text
152 | text:
153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
154 | @echo
155 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
156 |
157 | .PHONY: man
158 | man:
159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
160 | @echo
161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
162 |
163 | .PHONY: texinfo
164 | texinfo:
165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
166 | @echo
167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
168 | @echo "Run \`make' in that directory to run these through makeinfo" \
169 | "(use \`make info' here to do that automatically)."
170 |
171 | .PHONY: info
172 | info:
173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
174 | @echo "Running Texinfo files through makeinfo..."
175 | make -C $(BUILDDIR)/texinfo info
176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
177 |
178 | .PHONY: gettext
179 | gettext:
180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
181 | @echo
182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
183 |
184 | .PHONY: changes
185 | changes:
186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
187 | @echo
188 | @echo "The overview file is in $(BUILDDIR)/changes."
189 |
190 | .PHONY: linkcheck
191 | linkcheck:
192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
193 | @echo
194 | @echo "Link check complete; look for any errors in the above output " \
195 | "or in $(BUILDDIR)/linkcheck/output.txt."
196 |
197 | .PHONY: doctest
198 | doctest:
199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
200 | @echo "Testing of doctests in the sources finished, look at the " \
201 | "results in $(BUILDDIR)/doctest/output.txt."
202 |
203 | .PHONY: coverage
204 | coverage:
205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
206 | @echo "Testing of coverage in the sources finished, look at the " \
207 | "results in $(BUILDDIR)/coverage/python.txt."
208 |
209 | .PHONY: xml
210 | xml:
211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
212 | @echo
213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
214 |
215 | .PHONY: pseudoxml
216 | pseudoxml:
217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
218 | @echo
219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
220 |
221 | .PHONY: dummy
222 | dummy:
223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
224 | @echo
225 | @echo "Build finished. Dummy builder generates no files."
226 |
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Shane documentation build configuration file, created by
4 | # sphinx-quickstart on Tue Sep 13 11:19:47 2016.
5 | #
6 | # This file is execfile()d with the current directory set to its
7 | # containing dir.
8 | #
9 | # Note that not all possible configuration values are present in this
10 | # autogenerated file.
11 | #
12 | # All configuration values have a default; values that are commented out
13 | # serve to show the default.
14 |
15 | # If extensions (or modules to document with autodoc) are in another directory,
16 | # add these directories to sys.path here. If the directory is relative to the
17 | # documentation root, use os.path.abspath to make it absolute, like shown here.
18 | #
19 | import os
20 | import sys
21 | sys.path.insert(0, os.path.join(os.path.abspath('.'), ".."))
22 |
23 | # -- General configuration ------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #
27 | # needs_sphinx = '1.0'
28 |
29 | # Add any Sphinx extension module names here, as strings. They can be
30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
31 | # ones.
32 | extensions = [
33 | 'sphinx.ext.autodoc',
34 | ]
35 |
36 | # Add any paths that contain templates here, relative to this directory.
37 | templates_path = ['_templates']
38 |
39 | # The suffix(es) of source filenames.
40 | # You can specify multiple suffix as a list of string:
41 | #
42 | # source_suffix = ['.rst', '.md']
43 | source_suffix = '.rst'
44 |
45 | # The encoding of source files.
46 | #
47 | # source_encoding = 'utf-8-sig'
48 |
49 | # The master toctree document.
50 | master_doc = 'index'
51 |
52 | # General information about the project.
53 | project = u'Shane'
54 | copyright = u'2016, Tobias Heukäufer'
55 | author = u'Tobias Heukäufer'
56 |
57 | # The version info for the project you're documenting, acts as replacement for
58 | # |version| and |release|, also used in various other places throughout the
59 | # built documents.
60 | #
61 | # The short X.Y version.
62 | version = u'1.0'
63 | # The full version, including alpha/beta/rc tags.
64 | release = u'1.0'
65 |
66 | # The language for content autogenerated by Sphinx. Refer to documentation
67 | # for a list of supported languages.
68 | #
69 | # This is also used if you do content translation via gettext catalogs.
70 | # Usually you set "language" from the command line for these cases.
71 | language = None
72 |
73 | # There are two options for replacing |today|: either, you set today to some
74 | # non-false value, then it is used:
75 | #
76 | # today = ''
77 | #
78 | # Else, today_fmt is used as the format for a strftime call.
79 | #
80 | # today_fmt = '%B %d, %Y'
81 |
82 | # List of patterns, relative to source directory, that match files and
83 | # directories to ignore when looking for source files.
84 | # This patterns also effect to html_static_path and html_extra_path
85 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
86 |
87 | # The reST default role (used for this markup: `text`) to use for all
88 | # documents.
89 | #
90 | # default_role = None
91 |
92 | # If true, '()' will be appended to :func: etc. cross-reference text.
93 | #
94 | # add_function_parentheses = True
95 |
96 | # If true, the current module name will be prepended to all description
97 | # unit titles (such as .. function::).
98 | #
99 | # add_module_names = True
100 |
101 | # If true, sectionauthor and moduleauthor directives will be shown in the
102 | # output. They are ignored by default.
103 | #
104 | # show_authors = False
105 |
106 | # The name of the Pygments (syntax highlighting) style to use.
107 | pygments_style = 'sphinx'
108 |
109 | # A list of ignored prefixes for module index sorting.
110 | # modindex_common_prefix = []
111 |
112 | # If true, keep warnings as "system message" paragraphs in the built documents.
113 | # keep_warnings = False
114 |
115 | # If true, `todo` and `todoList` produce output, else they produce nothing.
116 | todo_include_todos = False
117 |
118 |
119 | # -- Options for HTML output ----------------------------------------------
120 |
121 | # The theme to use for HTML and HTML Help pages. See the documentation for
122 | # a list of builtin themes.
123 | #
124 | html_theme = 'classic'
125 | html_sidebars = {'**': ['globaltoc.html', 'relations.html', ], }
126 |
127 | # Theme options are theme-specific and customize the look and feel of a theme
128 | # further. For a list of options available for each theme, see the
129 | # documentation.
130 | #
131 | # html_theme_options = {}
132 |
133 | # Add any paths that contain custom themes here, relative to this directory.
134 | # html_theme_path = []
135 |
136 | # The name for this set of Sphinx documents.
137 | # " v documentation" by default.
138 | #
139 | # html_title = u'Shane v1.0'
140 |
141 | # A shorter title for the navigation bar. Default is the same as html_title.
142 | #
143 | # html_short_title = None
144 |
145 | # The name of an image file (relative to this directory) to place at the top
146 | # of the sidebar.
147 | #
148 | # html_logo = None
149 |
150 | # The name of an image file (relative to this directory) to use as a favicon of
151 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
152 | # pixels large.
153 | #
154 | # html_favicon = None
155 |
156 | # Add any paths that contain custom static files (such as style sheets) here,
157 | # relative to this directory. They are copied after the builtin static files,
158 | # so a file named "default.css" will overwrite the builtin "default.css".
159 | html_static_path = ['_static']
160 |
161 | # Add any extra paths that contain custom files (such as robots.txt or
162 | # .htaccess) here, relative to this directory. These files are copied
163 | # directly to the root of the documentation.
164 | #
165 | # html_extra_path = []
166 |
167 | # If not None, a 'Last updated on:' timestamp is inserted at every page
168 | # bottom, using the given strftime format.
169 | # The empty string is equivalent to '%b %d, %Y'.
170 | #
171 | # html_last_updated_fmt = None
172 |
173 | # If true, SmartyPants will be used to convert quotes and dashes to
174 | # typographically correct entities.
175 | #
176 | # html_use_smartypants = True
177 |
178 | # Custom sidebar templates, maps document names to template names.
179 | #
180 | # html_sidebars = {}
181 |
182 | # Additional templates that should be rendered to pages, maps page names to
183 | # template names.
184 | #
185 | # html_additional_pages = {}
186 |
187 | # If false, no module index is generated.
188 | #
189 | # html_domain_indices = True
190 |
191 | # If false, no index is generated.
192 | #
193 | # html_use_index = True
194 |
195 | # If true, the index is split into individual pages for each letter.
196 | #
197 | # html_split_index = False
198 |
199 | # If true, links to the reST sources are added to the pages.
200 | #
201 | html_show_sourcelink = False
202 |
203 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
204 | #
205 | # html_show_sphinx = True
206 |
207 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
208 | #
209 | # html_show_copyright = True
210 |
211 | # If true, an OpenSearch description file will be output, and all pages will
212 | # contain a tag referring to it. The value of this option must be the
213 | # base URL from which the finished HTML is served.
214 | #
215 | # html_use_opensearch = ''
216 |
217 | # This is the file name suffix for HTML files (e.g. ".xhtml").
218 | # html_file_suffix = None
219 |
220 | # Language to be used for generating the HTML full-text search index.
221 | # Sphinx supports the following languages:
222 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
223 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
224 | #
225 | # html_search_language = 'en'
226 |
227 | # A dictionary with options for the search language support, empty by default.
228 | # 'ja' uses this config value.
229 | # 'zh' user can custom change `jieba` dictionary path.
230 | #
231 | # html_search_options = {'type': 'default'}
232 |
233 | # The name of a javascript file (relative to the configuration directory) that
234 | # implements a search results scorer. If empty, the default will be used.
235 | #
236 | # html_search_scorer = 'scorer.js'
237 |
238 | # Output file base name for HTML help builder.
239 | htmlhelp_basename = 'Shanedoc'
240 |
241 | # -- Options for LaTeX output ---------------------------------------------
242 |
243 | latex_elements = {
244 | # The paper size ('letterpaper' or 'a4paper').
245 | #
246 | # 'papersize': 'letterpaper',
247 |
248 | # The font size ('10pt', '11pt' or '12pt').
249 | #
250 | # 'pointsize': '10pt',
251 |
252 | # Additional stuff for the LaTeX preamble.
253 | #
254 | # 'preamble': '',
255 |
256 | # Latex figure (float) alignment
257 | #
258 | # 'figure_align': 'htbp',
259 | }
260 |
261 | # Grouping the document tree into LaTeX files. List of tuples
262 | # (source start file, target name, title,
263 | # author, documentclass [howto, manual, or own class]).
264 | latex_documents = [
265 | (master_doc, 'Shane.tex', u'Shane Documentation',
266 | u'Tobias Heukäufer', 'manual'),
267 | ]
268 |
269 | # The name of an image file (relative to this directory) to place at the top of
270 | # the title page.
271 | #
272 | # latex_logo = None
273 |
274 | # For "manual" documents, if this is true, then toplevel headings are parts,
275 | # not chapters.
276 | #
277 | # latex_use_parts = False
278 |
279 | # If true, show page references after internal links.
280 | #
281 | # latex_show_pagerefs = False
282 |
283 | # If true, show URL addresses after external links.
284 | #
285 | # latex_show_urls = False
286 |
287 | # Documents to append as an appendix to all manuals.
288 | #
289 | # latex_appendices = []
290 |
291 | # It false, will not define \strong, \code, itleref, \crossref ... but only
292 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
293 | # packages.
294 | #
295 | # latex_keep_old_macro_names = True
296 |
297 | # If false, no module index is generated.
298 | #
299 | # latex_domain_indices = True
300 |
301 |
302 | # -- Options for manual page output ---------------------------------------
303 |
304 | # One entry per manual page. List of tuples
305 | # (source start file, name, description, authors, manual section).
306 | man_pages = [
307 | (master_doc, 'shane', u'Shane Documentation',
308 | [author], 1)
309 | ]
310 |
311 | # If true, show URL addresses after external links.
312 | #
313 | # man_show_urls = False
314 |
315 |
316 | # -- Options for Texinfo output -------------------------------------------
317 |
318 | # Grouping the document tree into Texinfo files. List of tuples
319 | # (source start file, target name, title, author,
320 | # dir menu entry, description, category)
321 | texinfo_documents = [
322 | (master_doc, 'Shane', u'Shane Documentation',
323 | author, 'Shane', 'One line description of project.',
324 | 'Miscellaneous'),
325 | ]
326 |
327 | # Documents to append as an appendix to all manuals.
328 | #
329 | # texinfo_appendices = []
330 |
331 | # If false, no module index is generated.
332 | #
333 | # texinfo_domain_indices = True
334 |
335 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
336 | #
337 | # texinfo_show_urls = 'footnote'
338 |
339 | # If true, do not generate a @detailmenu in the "Top" node's menu.
340 | #
341 | # texinfo_no_detailmenu = False
342 |
--------------------------------------------------------------------------------
/doc/developer.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Developer Guide
3 | ===============
4 |
5 | What follows is an admittedly weirdly structured and incomplete developer guide.
6 | Currently all explanations of design decisions, descriptions of inner workings
7 | of *Shane* and everything else are tied to the modules most suitable. This is
8 | hopefully reworked in the future.
9 |
10 | Contents
11 | ========
12 |
13 | .. toctree::
14 | :titlesonly:
15 |
16 | developer/modules
17 | developer/todo
18 | developer/license
19 |
--------------------------------------------------------------------------------
/doc/developer/license.rst:
--------------------------------------------------------------------------------
1 | =======
2 | License
3 | =======
4 |
5 | *Shane* is released under the `GNU General Public License v3.0
6 | `_
7 |
--------------------------------------------------------------------------------
/doc/developer/modules.rst:
--------------------------------------------------------------------------------
1 | =======
2 | Modules
3 | =======
4 |
5 | Here comes an explanation of *Shane's* modules.
6 |
7 | Contents
8 | ========
9 |
10 | .. toctree::
11 | :titlesonly:
12 |
13 | modules/main
14 | modules/sp_view
15 | modules/menu_view
16 | modules/screenplay
17 | modules/paragraph
18 | modules/io
19 |
--------------------------------------------------------------------------------
/doc/developer/modules/io.rst:
--------------------------------------------------------------------------------
1 | ==
2 | IO
3 | ==
4 |
5 | *Shane* currently only reads and writes `Fountain `_. When
6 | or if other file formats will be supported Fountain will most likely be the
7 | default format.
8 |
9 | Modules
10 | =======
11 |
12 | .. toctree::
13 | :titlesonly:
14 |
15 | io/fountain
16 |
--------------------------------------------------------------------------------
/doc/developer/modules/io/fountain.rst:
--------------------------------------------------------------------------------
1 | ========
2 | Fountain
3 | ========
4 |
5 | `Fountain `_ is a plain text markup language for
6 | screenwriting. It doesn't exist for a specific software which makes it perfect
7 | as the current default (and only) input and output format for *Shane*.
8 |
9 | Design Decisions
10 | ================
11 |
12 | Sadly Fountain's syntax seems to be either ambiguously defined or wrongly
13 | interpreted (e.g. `Trelby `_ doesn't precede the scene
14 | heading with an empty line if it's a screenplay's very first element). That's
15 | why it's been decided that the first line of a Fountain file is understood to be
16 | preceded by empty lines.
17 |
18 | Also, while Fountain defines a large list of screenplay elements *Shane's*
19 | Fountain reader (and writer) only understand a small subset of them due to not
20 | supporting a large list of screenplay elements itself. Most elements not known
21 | by *Shane* should be read as actions but the user cannot rely on it. *Shane*
22 | understands:
23 |
24 | - Scene Headings
25 | - Actions
26 | - Names
27 | - Parentheticals
28 | - Dialogs
29 |
30 | Source Code Docstrings
31 | ======================
32 |
33 | .. automodule:: shane.io.fountain
34 | :members:
35 |
--------------------------------------------------------------------------------
/doc/developer/modules/main.rst:
--------------------------------------------------------------------------------
1 | ====
2 | Main
3 | ====
4 |
5 | This is *Shane's* entry point.
6 |
7 | Life Cycle
8 | ==========
9 |
10 | *Shane* offers two views: A :doc:`sp_view` and a :doc:`menu_view`. (View is a
11 | term used very loosely here.) While they are shown at the same time only one can
12 | have the user's focus.
13 |
14 | *Shane* has a "master mainloop" running endlessly. The focused view runs its own
15 | mainloop (each pass taking input and then redrawing) meaning the other view
16 | "stands still." It only leaves its loop to let the master mainloop redraw the
17 | other view (e.g. for every undo the menu view needs the screenplay view to
18 | redraw itself to reflect the change), to let the master mainloop handle a
19 | screen resize or to give up focus.
20 |
21 | Source Code Docstrings
22 | ======================
23 |
24 | .. automodule:: shane.main
25 | :members:
26 |
--------------------------------------------------------------------------------
/doc/developer/modules/menu_view.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Menu View
3 | =========
4 |
5 | The menu view is basically a state machine.
6 |
7 | Source Code Docstrings
8 | ======================
9 |
10 | .. automodule:: shane.menu_view
11 | :members:
12 |
--------------------------------------------------------------------------------
/doc/developer/modules/paragraph.rst:
--------------------------------------------------------------------------------
1 | =========
2 | Paragraph
3 | =========
4 |
5 | Paragraphs are defined by their types. They contain text and with every edit
6 | they rebuild a list of characters per line used to cut up their text into lines
7 | (for display with word wrap).
8 |
9 | Design Decisions
10 | ================
11 |
12 | The only currently available paragraph types are ``SCENE``, ``ACTION``,
13 | ``NAME``, ``PARENTHETICALS``, ``DIALOG``. Transitions are notably missing but
14 | as they are frowned upon won't most likely ever get into *Shane* (there are
15 | probably useful edge cases for transitions but they are few and far between).
16 | *Shane* is supposed to be a simple alternative and will never be bloated up with
17 | screenplay elements that are almost definitely never used or actually needed.
18 |
19 | Coding Standard Specifics
20 | =========================
21 |
22 | ``Paragraph`` methods that make changes to the paragraph (i.e. are not getters)
23 | are to be prefixed with ``sp_`` and called only by the paragraph itself or the
24 | :doc:`screenplay`.
25 |
26 | Source Code Docstrings
27 | ======================
28 |
29 | .. automodule:: shane.paragraph
30 | :members:
31 |
--------------------------------------------------------------------------------
/doc/developer/modules/screenplay.rst:
--------------------------------------------------------------------------------
1 | ==========
2 | Screenplay
3 | ==========
4 |
5 | The screenplay is a list of paragraphs (see :doc:`paragraph`). It remembers the
6 | cursor's position and serves as an interface between :doc:`sp_view` and
7 | paragraphs (meaning a view cannot modify paragraphs directly but has to move
8 | the screenplay's cursor to the appropriate position and call input methods etc.
9 | of the screenplay.)
10 |
11 | Editing And the Undo/Redo System
12 | ================================
13 |
14 | All screenplay methods for editing have to make use of the ``_input``,
15 | ``_delete``, ``_new_paragraph``, ``_delete_paragraph`` and
16 | ``_change_paragraph_type`` methods *and* make error checks (e.g. don't delete
17 | more text than there is to delete) (error checks planned to happen in those
18 | methods instead of their callers in the future).
19 |
20 | Breaking down editing to these five methods ensures a simple undo/redo system.
21 | The undo/redo system makes use of a somewhat stack (not exactly a stack) of
22 | action bundles. An action bundle is a list of actions that happened in a short
23 | period of time.
24 |
25 | For example, entering a ``"\n"`` right into a paragraph deletes everything
26 | after the cursor in this paragraph (``_delete``), then creates a new paragraph
27 | with the deleted text (``_new_paragraph``). This pushes an action bundle of two
28 | actions onto the undo stack. If a user types in multiple characters in rapid
29 | succession she creates an action bundle of all those characters.
30 |
31 | Undoing an action now means undoing an action bundle by undoing every little
32 | action in the right order. Redoing an action means redoing an action bundle.
33 | This has the nice effect of 1) masking that complex actions are multiple simple
34 | ones and 2) bundling multiple inputs together to potentially be undone later
35 | (no user wants to undo character by character).
36 |
37 | .. Name Database
38 | =============
39 |
40 | .. The screenplay's name database sorts each name into one of three lists: One list
41 | for names starting from A to M, one for names starting from N to Z and one for
42 | anything else.
43 |
44 | Source Code Docstrings
45 | ======================
46 |
47 | .. automodule:: shane.screenplay
48 | :members:
49 |
--------------------------------------------------------------------------------
/doc/developer/modules/sp_view.rst:
--------------------------------------------------------------------------------
1 | ===============
2 | Screenplay View
3 | ===============
4 |
5 | The screenplay view takes input and translates it into screenplay commands
6 | (cursor movement, text editing, etc.) and draws the screenplay.
7 |
8 | Drawing
9 | =======
10 |
11 | Drawing the screenplay is very likely the most time consuming step in *Shane's*
12 | program flow. Here is how it works:
13 |
14 | #. Get cursor position (line and column) in screenplay
15 | #. Calculate scrollbar slider position (using screenplay's total line count)
16 | #. If required scroll view either up or down so that cursor's in view
17 | #. Get paragraph the first visible line's in and offset of that line from
18 | paragraph start
19 | #. Draw all paragraphs visible line by line starting with the first visible line
20 | #. Draw scrollbar
21 | #. Set cursor
22 |
23 | Design Decisions
24 | ================
25 |
26 | The scrollbar is not drawn as far down as it seemingly can be. It can't.
27 | ncurses throws an error when drawing a character at the lower right. A solution
28 | would be to make the screenplay view window one column wider but the aesthetics
29 | wouldn't make up for the "ugliness" in code and more importantly a whole new
30 | column to clear with every draw pass (without actually being drawn on anyway).
31 |
32 | Source Code Docstrings
33 | ======================
34 |
35 | .. automodule:: shane.sp_view
36 | :members:
37 |
--------------------------------------------------------------------------------
/doc/developer/todo.rst:
--------------------------------------------------------------------------------
1 | =====
2 | To Do
3 | =====
4 |
5 | While *Shane* is a fully usable screenwriting software it's very basic and far
6 | behind the competition (even competing freeware). Of course a lot of its
7 | simplicity is by design but some features are still nice to have.
8 |
9 | Menu
10 | ====
11 |
12 | - Save As
13 | - Autocomplete path names
14 | - Set meta data (title page stuff)
15 |
16 | Screenplay
17 | ==========
18 |
19 | - Meta data
20 | - Optimize ``get_line_count()``
21 | - Store new names automatically in name database
22 | - Select
23 | - Set selection
24 | - Delete selection
25 | - Copy selection
26 | - Clipboard
27 | - Copy to internal clipboard
28 | - Paste from internal clipboard
29 | - Copy to system clipboard
30 | - Paste from system clipboard
31 | - Let edit methods error check
32 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. Shane documentation master file, created by
2 | sphinx-quickstart on Tue Sep 13 11:19:47 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | =====
7 | Shane
8 | =====
9 |
10 | A poor man and/or hipster's TUI screenwriting software.
11 |
12 | Introduction
13 | ============
14 |
15 | Ever since the wide adoption of portable PCs screenwriters have been wondering
16 | how they can set themselves even more apart from everyone else than just doing
17 | their work in a local coffee shop on an expensive MacBook for everyone to see
18 | instead of in an office on a reasonably priced computer like normal people
19 | because nobody cares.
20 |
21 | Now maximum apartness can be achieved: Write your screenplay in a terminal just
22 | like in the olden days! Don't click around with your mouse. What's a mouse wheel
23 | anyway? Feel the comfort of a fully ASCII-rendered user interface!
24 |
25 | After all, screenplays are just text, why shouldn't screenwriting software be?
26 |
27 | Contents
28 | ========
29 |
30 | .. toctree::
31 | :titlesonly:
32 |
33 | user
34 | developer
35 |
36 |
--------------------------------------------------------------------------------
/doc/user.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | User Manual
3 | ===========
4 |
5 | Requirements
6 | ============
7 |
8 | :OS: Linux or MacOS
9 | :Software: Python 3.3 or higher, ncurses
10 |
11 | Installation
12 | ============
13 |
14 | *Shane* does *not* need to be installed. Still, if you like to then run
15 | ``python setup.py install`` on the command line from *Shane's* root directory.
16 |
17 | Run
18 | ===
19 |
20 | Either run ``python run.py`` from *Shane's* root directory or (if installed)
21 | simply run ``shane`` from wherever. To start *Shane* with opening a screenplay
22 | (for supported file formats, see `Supported File Formats`_) add the path as
23 | a parameter, e.g. ``python run.py /home/hotshot/screenplays/romcom.fountain``
24 | or ``shane /home/hotshot/screenplays/romcom.fountain``.
25 |
26 | Usage
27 | =====
28 |
29 | This is how to use *Shane:*
30 |
31 | Screenplay Window
32 | -----------------
33 |
34 | The screenplay window is where the magic happens! Type in or delete characters
35 | and navigate through the script!
36 |
37 | Press ``Esc`` to switch to the menu.
38 |
39 | Movement
40 | ########
41 |
42 | Use the arrow keys or ``Home`` or ``End`` to move the cursor.
43 |
44 | Press ``Page Up`` or ``Page Down`` to jump to the previous or next scene heading.
45 |
46 | New Paragraphs And Conversion
47 | #############################
48 |
49 | Press ``Enter`` to create a new paragraph. Which type this new paragraph's of
50 | depends on the current paragraph's type. ``Enter`` works slightly different for
51 | ``NAME`` paragraphs in that it won't cut up the paragraph.
52 |
53 | Press ``Tab`` to convert the current paragraph. Which type this converts to
54 | depends on the paragraph's current type.
55 |
56 | +--------------------+------------+--------------------+
57 | | Paragraph Type | ``Enter`` | ``Tab`` |
58 | +====================+============+====================+
59 | | ``SCENE`` | ``ACTION`` | ``ACTION`` |
60 | +--------------------+------------+--------------------+
61 | | ``ACTION`` | ``ACTION`` | ``NAME`` |
62 | +--------------------+------------+--------------------+
63 | | ``NAME`` | ``DIALOG`` | ``ACTION`` |
64 | +--------------------+------------+--------------------+
65 | | ``PARENTHETICALS`` | ``DIALOG`` | ``DIALOG`` |
66 | +--------------------+------------+--------------------+
67 | | ``DIALOG`` | ``NAME`` | ``PARENTHETICALS`` |
68 | +--------------------+------------+--------------------+
69 |
70 | Use ``Shift`` with the left or right arrow keys to cycle through types for the
71 | current paragraph backward or forward, respectively. The forward order being
72 | ``SCENE``, ``ACTION``, ``NAME``, ``PARENTHETICALS``, ``DIALOG``.
73 |
74 | Autocomplete
75 | ############
76 |
77 | Press ``_`` (underscore) in a ``NAME`` paragraph to autocomplete a name (and
78 | cycle through names). For autocomplete to work at least one character must have
79 | already been typed in. Also, a name database must have been built, see
80 | `Rebuild Name DB`_.
81 |
82 | Menu
83 | ----
84 |
85 | Use the arrow keys to navigate through the top level menu. Press ``Esc`` to
86 | switch the screenplay window. Press ``Enter`` to go to the highlighted sub menu
87 | or menu item.
88 |
89 | Save As
90 | #######
91 |
92 | Type in the path to save the screenplay to, then hit ``Enter`` to save. Press
93 | ``Esc`` to cancel and go back to the top level menu. For supported file formats,
94 | see `Supported File Formats`_.
95 |
96 | Undo
97 | ####
98 |
99 | Undo previous action.
100 |
101 | Redo
102 | ####
103 |
104 | Redo previously undone action.
105 |
106 | Rebuild Name DB
107 | ###############
108 |
109 | For autocomplete to work a name database has to be built (and rebuilt for every
110 | new or deleted character) (automatic building planned for future versions).
111 | This menu item will do just that.
112 |
113 | Quit
114 | ####
115 |
116 | Quit *Shane.*
117 |
118 | Supported File Formats
119 | ======================
120 |
121 | *Shane* reads
122 |
123 | - Fountain (\*.fountain)
124 |
125 | and writes
126 |
127 | - Fountain (\*.fountain)
128 |
129 | Frequently Asked Questions
130 | ==========================
131 |
132 | **Why no transitions?** *Shane's* developer doesn't like them. Is that a good
133 | reason to keep everyone else from using them? Probably: The very rare (and still
134 | avoidable) cases transitions *are* useful don't make up for cluttering up
135 | *Shane's* source code.
136 |
--------------------------------------------------------------------------------
/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tobchen/Shane/c7ba13ee9d5b4ab1204f6b6122c5da1929290d68/img/screenshot.png
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from shane import main
20 |
21 |
22 | if __name__ == "__main__":
23 | main.run()
24 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tobchen/Shane/c7ba13ee9d5b4ab1204f6b6122c5da1929290d68/setup.cfg
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from setuptools import setup
20 |
21 | with open("README.rst") as f:
22 | long_description = f.read()
23 |
24 | setup(
25 | name="Shane",
26 |
27 | version="1.0b1",
28 |
29 | license="GNU General Public License v3.0",
30 |
31 | description="A poor man and/or hipster\'s screenwriting software.",
32 | long_description=long_description,
33 | keywords="screenplay screenwriting movie film tv",
34 |
35 | url="https://github.com/Tobchen/Shane",
36 |
37 | author="Tobias Heukäufer",
38 | author_email="tobi@tobchen.de",
39 |
40 | # https://pypi.python.org/pypi?%3Aaction=list_classifiers
41 | classifiers=[
42 | "Development Status :: 4 - Beta",
43 |
44 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
45 |
46 | "Environment :: Console :: Curses",
47 |
48 | "Topic :: Text Editors",
49 | "Intended Audience :: End Users/Desktop",
50 |
51 | "Programming Language :: Python :: 3",
52 | "Programming Language :: Python :: 3.3",
53 | "Programming Language :: Python :: 3.4",
54 | "Programming Language :: Python :: 3.5",
55 | ],
56 |
57 | packages=["shane", "shane.io"],
58 |
59 | # install_requires=[],
60 |
61 | entry_points={
62 | "console_scripts": [
63 | "shane=shane.main:run",
64 | ],
65 | },
66 | )
67 |
--------------------------------------------------------------------------------
/shane/__init__.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 |
--------------------------------------------------------------------------------
/shane/__init__.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tobchen/Shane/c7ba13ee9d5b4ab1204f6b6122c5da1929290d68/shane/__init__.pyc
--------------------------------------------------------------------------------
/shane/io/__init__.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
--------------------------------------------------------------------------------
/shane/io/fountain.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import os
20 | import shutil
21 | import tempfile
22 | from enum import Enum
23 |
24 | from shane.paragraph import Paragraph, PType
25 |
26 |
27 | class _PrevState(Enum):
28 | """Enum for recording the previous line's type in fountain script."""
29 |
30 | EMPTY = 0
31 | SCENE_HINT = 1
32 | NAME_HINT = 2
33 | SCENE_AND_NAME_HINT = 3
34 | PARENT = 4
35 | DIALOG = 5
36 | ACTION = 6
37 |
38 |
39 | def _is_scene_conform(line: str):
40 | """Return if a line is scene conform (disregarding scene forcing)."""
41 | return line.lower().startswith(("int ", "ext ", "est ", "int./ext ",
42 | "int/ext ", "i/e ", "int.", "ext.", "est.",
43 | "int./ext.", "int/ext.", "i/e."))
44 |
45 |
46 | def _is_name_conform(line: str):
47 | """Return if a line is name conform (disregarding name forcing)."""
48 | has_at_least_one_alphabetical = False
49 | for char in line:
50 | if 65 <= ord(char) <= 90 or 97 <= ord(char) <= 122:
51 | has_at_least_one_alphabetical = True
52 | break
53 | return has_at_least_one_alphabetical and\
54 | line.split("(")[0].isupper() and\
55 | not line.startswith("!")
56 |
57 |
58 | def _hint_scene(line: str):
59 | """Return if a line is scene conform (including scene forcing)."""
60 | return line.lower().startswith(".") or _is_scene_conform(line)
61 |
62 |
63 | def _hint_name(line: str):
64 | """Return if a line is name conform (including name forcing)."""
65 | return line.startswith("@") or _is_name_conform(line)
66 |
67 |
68 | def _hint_parenthetical(line: str):
69 | """Return if a line is parentheticals conform."""
70 | return len(line) >= 2 and line[0] == "(" and line[-1] == ")"
71 |
72 |
73 | def _append_scene(plist, line):
74 | """Append a scene to a paragraph list."""
75 | if line.startswith("."):
76 | line = line[1:]
77 | plist.append(Paragraph(PType.SCENE, line))
78 |
79 |
80 | def _append_action(plist, line):
81 | """Append an action to a paragraph list."""
82 | if line.startswith("!"):
83 | line = line[1:]
84 | plist.append(Paragraph(PType.ACTION, line))
85 |
86 |
87 | def _append_name(plist, line):
88 | """Append a name to a paragraph list."""
89 | if line.startswith("@"):
90 | line = line[1:]
91 | plist.append(Paragraph(PType.NAME, line))
92 |
93 |
94 | def _append_parent(plist, line):
95 | """Append parentheticals to a paragraph list."""
96 | plist.append(Paragraph(PType.PARENTHETICALS, line[1:-1]))
97 |
98 |
99 | def read(path: str):
100 | """Read fountain script from path."""
101 | result = []
102 |
103 | with open(path, "r") as file:
104 | prev_state = _PrevState.EMPTY
105 | prev_line = ""
106 |
107 | for line in file:
108 | line = line.strip()
109 |
110 | if prev_state == _PrevState.EMPTY:
111 | if line:
112 | scene_hint = _hint_scene(line)
113 | name_hint = _hint_name(line)
114 | if scene_hint and name_hint:
115 | prev_state = _PrevState.SCENE_AND_NAME_HINT
116 | elif scene_hint:
117 | prev_state = _PrevState.SCENE_HINT
118 | elif name_hint:
119 | prev_state = _PrevState.NAME_HINT
120 | else:
121 | _append_action(result, line)
122 | prev_state = _PrevState.ACTION
123 | elif prev_state == _PrevState.SCENE_HINT:
124 | if not line:
125 | _append_scene(result, prev_line)
126 | prev_state = _PrevState.EMPTY
127 | else:
128 | _append_action(result, prev_line)
129 | _append_action(result, line)
130 | prev_state = _PrevState.ACTION
131 | elif prev_state == _PrevState.NAME_HINT:
132 | if line:
133 | _append_name(result, prev_line)
134 | if _hint_parenthetical(line):
135 | _append_parent(result, line)
136 | prev_state = _PrevState.PARENT
137 | else:
138 | result.append(Paragraph(PType.DIALOG, line))
139 | prev_state = _PrevState.DIALOG
140 | else:
141 | _append_action(result, prev_line)
142 | prev_state = _PrevState.EMPTY
143 | elif prev_state == _PrevState.SCENE_AND_NAME_HINT:
144 | if line:
145 | result.append(Paragraph(PType.NAME, prev_line))
146 | if _hint_parenthetical(line):
147 | _append_parent(result, line)
148 | prev_state = _PrevState.PARENT
149 | else:
150 | result.append(Paragraph(PType.DIALOG, line))
151 | prev_state = _PrevState.DIALOG
152 | else:
153 | _append_scene(result, prev_line)
154 | prev_state = _PrevState.EMPTY
155 | elif prev_state == _PrevState.PARENT:
156 | if line:
157 | result.append(Paragraph(PType.DIALOG, line))
158 | prev_state = _PrevState.DIALOG
159 | else:
160 | prev_state = _PrevState.EMPTY
161 | elif prev_state == _PrevState.DIALOG:
162 | if line:
163 | if _hint_parenthetical(line):
164 | _append_parent(result, line)
165 | prev_state = _PrevState.PARENT
166 | else:
167 | paragraph = result[-1]
168 | paragraph.sp_input(len(paragraph.get_text()) - 1,
169 | line)
170 | prev_state = _PrevState.DIALOG
171 | else:
172 | prev_state = _PrevState.EMPTY
173 | elif prev_state == _PrevState.ACTION:
174 | if line:
175 | _append_action(result, line)
176 | prev_state = _PrevState.ACTION
177 | else:
178 | prev_state = _PrevState.EMPTY
179 |
180 | prev_line = line
181 |
182 | if prev_state == _PrevState.SCENE_HINT:
183 | _append_scene(result, prev_line)
184 | elif prev_state == _PrevState.NAME_HINT:
185 | _append_name(result, prev_line)
186 | elif prev_state == _PrevState.SCENE_AND_NAME_HINT:
187 | if '.' in prev_line:
188 | result.append(Paragraph(PType.SCENE, prev_line))
189 | else:
190 | result.append(Paragraph(PType.NAME, prev_line))
191 |
192 | return result
193 |
194 |
195 | def write(path, paragraphs):
196 | """Save fountain script to path."""
197 | file, tmp_path = tempfile.mkstemp()
198 |
199 | prev_empty = False
200 | for par in paragraphs:
201 | line = par.get_text()[:-1]
202 | ptype = par.get_type()
203 |
204 | do_prev_empty = False
205 | do_next_empty = False
206 |
207 | if ptype == PType.SCENE:
208 | if not _is_scene_conform(line):
209 | line = "." + line
210 | do_prev_empty = do_next_empty = True
211 | elif ptype == PType.ACTION:
212 | if _is_scene_conform(line) or _is_name_conform(line) or\
213 | _hint_parenthetical(line):
214 | line = "!" + line
215 | do_prev_empty = do_next_empty = True
216 | elif ptype == PType.NAME:
217 | if not _is_name_conform(line):
218 | line = "@" + line
219 | do_prev_empty = True
220 | elif ptype == PType.PARENTHETICALS:
221 | line = "(" + line + ")"
222 |
223 | if not prev_empty and do_prev_empty:
224 | os.write(file, "\n".encode())
225 | os.write(file, (line + "\n").encode())
226 | if do_next_empty:
227 | os.write(file, "\n".encode())
228 |
229 | prev_empty = do_next_empty
230 |
231 | os.close(file)
232 |
233 | shutil.copy(tmp_path, path)
234 | os.remove(tmp_path)
235 |
--------------------------------------------------------------------------------
/shane/main.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import curses
20 | import os
21 | import sys
22 | from enum import Enum
23 | import locale
24 |
25 | from shane.screenplay import Screenplay
26 | from shane.sp_view import ScreenplayView, ScreenplayViewEvent
27 |
28 | from shane.menu_view import MenuView, MenuViewEvent
29 |
30 |
31 | class ActiveWindow(Enum):
32 | """Enum for recording the currently active view."""
33 |
34 | SCREENPLAY = 0
35 | MENU = 1
36 | NONE = 2
37 |
38 |
39 | def main(stdscr):
40 | """Run *Shane.*"""
41 | path = sys.argv[1] if len(sys.argv) > 1 else None
42 | screenplay = Screenplay(path)
43 |
44 | size = stdscr.getmaxyx()
45 |
46 | sp_view_width = ScreenplayView.get_required_window_width()
47 | if size[1] < sp_view_width or size[0] < 2:
48 | raise RuntimeError("Terminal's not large enough! Must be at least " +
49 | str(sp_view_width) + " x 2!")
50 |
51 | screenplay_window = curses.newwin(size[0] - 1, sp_view_width, 1,
52 | int((size[1] - sp_view_width) / 2))
53 | screenplay_view = ScreenplayView(screenplay)
54 | screenplay_view.set_window(screenplay_window)
55 |
56 | menu_window = curses.newwin(1, size[1], 0, 0)
57 | menu_view = MenuView(screenplay)
58 | menu_view.set_window(menu_window)
59 |
60 | active_window = actual_active_window = ActiveWindow.SCREENPLAY
61 |
62 | while True:
63 | resize = False
64 |
65 | if active_window == ActiveWindow.SCREENPLAY:
66 | event = screenplay_view.run()
67 | if event == ScreenplayViewEvent.ESCAPE:
68 | active_window = actual_active_window = ActiveWindow.MENU
69 | elif event == ScreenplayViewEvent.RESIZE:
70 | resize = True
71 | elif active_window == ActiveWindow.MENU:
72 | event = menu_view.run()
73 | if event == MenuViewEvent.ESCAPE:
74 | active_window = actual_active_window = ActiveWindow.SCREENPLAY
75 | elif event == MenuViewEvent.QUIT:
76 | break
77 | elif event == MenuViewEvent.RESIZE:
78 | resize = True
79 | elif event == MenuViewEvent.REDRAW_SCREEN_VIEW:
80 | screenplay_view.redraw()
81 | elif active_window == ActiveWindow.NONE:
82 | char = stdscr.getch()
83 | if char == 27:
84 | break
85 | elif char == curses.KEY_RESIZE:
86 | resize = True
87 |
88 | if resize:
89 | size = stdscr.getmaxyx()
90 |
91 | if "screenplay_window" in locals():
92 | screenplay_view.remove_window()
93 | del screenplay_window
94 | if "menu_window" in locals():
95 | menu_view.remove_window()
96 | del menu_window
97 |
98 | if size[1] < sp_view_width or size[0] < 2:
99 | stdscr.clear()
100 | stdscr.addstr(0, 0, "Terminal to small!")
101 | stdscr.refresh()
102 |
103 | active_window = ActiveWindow.NONE
104 | else:
105 | stdscr.clear()
106 | stdscr.refresh()
107 |
108 | screenplay_window = curses.newwin(size[0] - 1, sp_view_width, 1,
109 | int((size[1] -
110 | sp_view_width) / 2))
111 | screenplay_view.set_window(screenplay_window)
112 |
113 | menu_window = curses.newwin(1, size[1], 0, 0)
114 | menu_view.set_window(menu_window)
115 |
116 | active_window = actual_active_window
117 |
118 |
119 | def run():
120 | """Run *Shane* by wrapping its main, preventing terminal corruption."""
121 | locale.setlocale(locale.LC_ALL, "")
122 |
123 | try:
124 | # A LOT of thanks to this guy for the delay thing:
125 | # http://en.chys.info/2009/09/esdelay-ncurses/
126 | if "ESCDELAY" not in os.environ:
127 | os.environ["ESCDELAY"] = "25"
128 | curses.wrapper(main)
129 | except RuntimeError as error:
130 | print(error)
131 | except KeyboardInterrupt: # Not my fault people chose to quit this way!
132 | pass
133 |
--------------------------------------------------------------------------------
/shane/menu_view.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import curses
20 | from enum import Enum
21 |
22 | from shane.screenplay import Screenplay
23 | from shane.view import View
24 |
25 |
26 | class MenuViewEvent(Enum):
27 | """Enum for results from menu mainloop."""
28 |
29 | ESCAPE = 0
30 | RESIZE = 1
31 | QUIT = 3
32 | REDRAW_SCREEN_VIEW = 4
33 |
34 |
35 | class Menu(Enum):
36 | """Enum for current menu state."""
37 |
38 | TOP = 0
39 | SAVE_AS = 1
40 | QUIT = 2
41 | REBUILD_NAME_DB = 3
42 | UNDO = 4
43 | REDO = 5
44 | SAVE_ERROR = 6
45 |
46 |
47 | class MenuView(View):
48 | """View for screenplay menu."""
49 |
50 | buttons = [(Menu.SAVE_AS, "Save As"), (Menu.UNDO, "Undo"),
51 | (Menu.REDO, "Redo"), (Menu.REBUILD_NAME_DB, "Rebuild Name DB"),
52 | (Menu.QUIT, "Quit")]
53 |
54 | def __init__(self, screenplay: Screenplay):
55 | """Initialize menu view."""
56 | super().__init__(screenplay)
57 |
58 | self._current_button = 0
59 | self._current_menu = Menu.TOP
60 |
61 | self._save_path = ""
62 |
63 | def run(self):
64 | """Run menu view's mainloop."""
65 | super().run()
66 |
67 | while True:
68 | self._draw()
69 |
70 | char = self._window.getch()
71 |
72 | escape = False
73 | if char == 27: # ESC
74 | self._window.nodelay(True)
75 | char = self._window.getch()
76 | self._window.nodelay(False)
77 | if char == -1:
78 | escape = True
79 | elif char == curses.KEY_RESIZE:
80 | return MenuViewEvent.RESIZE
81 |
82 | if self._current_menu == Menu.TOP:
83 | if char == curses.KEY_LEFT:
84 | self._current_button -= 1
85 | if self._current_button < 0:
86 | self._current_button = len(MenuView.buttons) - 1
87 | elif char == curses.KEY_RIGHT:
88 | self._current_button += 1
89 | if self._current_button >= len(MenuView.buttons):
90 | self._current_button = 0
91 | elif char == 10:
92 | if MenuView.buttons[self._current_button][0]\
93 | == Menu.SAVE_AS:
94 | self._save_path = self._screenplay.get_path()
95 | if not self._save_path:
96 | self._save_path = ""
97 | self._current_menu = Menu.SAVE_AS
98 | elif MenuView.buttons[self._current_button][0] ==\
99 | Menu.UNDO:
100 | self._screenplay.do_undo()
101 | return MenuViewEvent.REDRAW_SCREEN_VIEW
102 | elif MenuView.buttons[self._current_button][0] == \
103 | Menu.REDO:
104 | self._screenplay.do_redo()
105 | return MenuViewEvent.REDRAW_SCREEN_VIEW
106 | elif MenuView.buttons[self._current_button][0] ==\
107 | Menu.REBUILD_NAME_DB:
108 | self._draw_processing()
109 | self._screenplay.do_rebuild_autocomplete_db()
110 | elif MenuView.buttons[self._current_button][0] == Menu.QUIT:
111 | return MenuViewEvent.QUIT
112 | elif escape:
113 | return MenuViewEvent.ESCAPE
114 |
115 | elif self._current_menu == Menu.SAVE_AS:
116 | if 32 <= char <= 126:
117 | self._save_path += chr(char)
118 | elif char == 8 or char == curses.KEY_BACKSPACE:
119 | if len(self._save_path) > 0:
120 | self._save_path = self._save_path[:-1]
121 | elif char == 10:
122 | self._draw_processing()
123 | try:
124 | self._screenplay.do_save(self._save_path)
125 | self._current_menu = Menu.TOP
126 | except OSError:
127 | self._current_menu = Menu.SAVE_ERROR
128 | elif escape:
129 | self._current_menu = Menu.TOP
130 |
131 | elif self._current_menu == Menu.SAVE_ERROR:
132 | if char == 10 or escape:
133 | self._current_menu = Menu.SAVE_AS
134 |
135 | def _draw(self):
136 | """Draw menu."""
137 | self._window.clear()
138 |
139 | if self._current_menu == Menu.TOP:
140 | menu = ""
141 | pos = 0
142 | for i in range(len(MenuView.buttons)):
143 | menu += "[" + MenuView.buttons[i][1] + "] "
144 | if i < self._current_button:
145 | pos += len(MenuView.buttons[i][1]) + 3
146 | self._window.addstr(0, 0, menu)
147 | self._window.move(0, pos)
148 | elif self._current_menu == Menu.SAVE_AS:
149 | if self._save_path:
150 | self._window.addstr(0, 0, self._save_path[-self._width + 1:])
151 | elif self._current_menu == Menu.SAVE_ERROR:
152 | self._window.addstr(0, 0, "Error saving!")
153 |
154 | self._window.refresh()
155 |
156 | def _draw_processing(self):
157 | """Draw processing notice."""
158 | self._window.refresh()
159 | self._window.addstr(0, 0, "Processing...")
160 | self._window.clear()
161 |
--------------------------------------------------------------------------------
/shane/paragraph.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from enum import Enum
20 |
21 |
22 | class PType(Enum):
23 | """Enum for paragraph types."""
24 |
25 | SCENE = 0
26 | ACTION = 1
27 | NAME = 2
28 | PARENTHETICALS = 3
29 | DIALOG = 4
30 |
31 |
32 | class PPrefs(object):
33 | """Enum for paragraph preferences."""
34 |
35 | SCENE_INDENT = 0
36 | SCENE_WIDTH = 60
37 | SCENE_PREC_EMPTY = 2
38 | SCENE_ENTER = PType.ACTION
39 | SCENE_TAB = PType.ACTION
40 |
41 | ACTION_INDENT = 0
42 | ACTION_WIDTH = 60
43 | ACTION_PREC_EMPTY = 1
44 | ACTION_ENTER = PType.ACTION
45 | ACTION_TAB = PType.NAME
46 |
47 | NAME_INDENT = 25
48 | NAME_WIDTH = 35
49 | NAME_PREC_EMPTY = 1
50 | NAME_ENTER = PType.DIALOG
51 | NAME_TAB = PType.ACTION
52 |
53 | PARENT_INDENT = 20
54 | PARENT_WIDTH = 15
55 | PARENT_PREC_EMPTY = 0
56 | PARENT_ENTER = PType.DIALOG
57 | PARENT_TAB = PType.DIALOG
58 |
59 | DIALOG_INDENT = 15
60 | DIALOG_WIDTH = 35
61 | DIALOG_PREC_EMPTY = 0
62 | DIALOG_ENTER = PType.NAME
63 | DIALOG_TAB = PType.PARENTHETICALS
64 |
65 | @staticmethod
66 | def get_indent(ptype: PType) -> int:
67 | """Return indentation of paragraph type."""
68 | if ptype == PType.SCENE:
69 | return PPrefs.SCENE_INDENT
70 | elif ptype == PType.ACTION:
71 | return PPrefs.ACTION_INDENT
72 | elif ptype == PType.NAME:
73 | return PPrefs.NAME_INDENT
74 | elif ptype == PType.PARENTHETICALS:
75 | return PPrefs.PARENT_INDENT
76 | else:
77 | return PPrefs.DIALOG_INDENT
78 |
79 | @staticmethod
80 | def get_width(ptype: PType) -> int:
81 | """Return width of paragraph type."""
82 | if ptype == PType.SCENE:
83 | return PPrefs.SCENE_WIDTH
84 | elif ptype == PType.ACTION:
85 | return PPrefs.ACTION_WIDTH
86 | elif ptype == PType.NAME:
87 | return PPrefs.NAME_WIDTH
88 | elif ptype == PType.PARENTHETICALS:
89 | return PPrefs.PARENT_WIDTH
90 | else:
91 | return PPrefs.DIALOG_WIDTH
92 |
93 | @staticmethod
94 | def get_prec_empty(ptype: PType) -> int:
95 | """Return preceding empty lines of paragraph type."""
96 | if ptype == PType.SCENE:
97 | return PPrefs.SCENE_PREC_EMPTY
98 | elif ptype == PType.ACTION:
99 | return PPrefs.ACTION_PREC_EMPTY
100 | elif ptype == PType.NAME:
101 | return PPrefs.NAME_PREC_EMPTY
102 | elif ptype == PType.PARENTHETICALS:
103 | return PPrefs.PARENT_PREC_EMPTY
104 | else:
105 | return PPrefs.DIALOG_PREC_EMPTY
106 |
107 | @staticmethod
108 | def get_enter(ptype: PType) -> PType:
109 | """Return paragraph type to come after typing enter in a paragraph."""
110 | if ptype == PType.SCENE:
111 | return PPrefs.SCENE_ENTER
112 | elif ptype == PType.ACTION:
113 | return PPrefs.ACTION_ENTER
114 | elif ptype == PType.NAME:
115 | return PPrefs.NAME_ENTER
116 | elif ptype == PType.PARENTHETICALS:
117 | return PPrefs.PARENT_ENTER
118 | else:
119 | return PPrefs.DIALOG_ENTER
120 |
121 | @staticmethod
122 | def get_tab(ptype: PType) -> PType:
123 | """Return paragraph type to convert to from paragraph type."""
124 | if ptype == PType.SCENE:
125 | return PPrefs.SCENE_TAB
126 | elif ptype == PType.ACTION:
127 | return PPrefs.ACTION_TAB
128 | elif ptype == PType.NAME:
129 | return PPrefs.NAME_TAB
130 | elif ptype == PType.PARENTHETICALS:
131 | return PPrefs.PARENT_TAB
132 | else:
133 | return PPrefs.DIALOG_TAB
134 |
135 |
136 | class Paragraph(object):
137 | """A paragraph in a screenplay."""
138 |
139 | def __init__(self, ptype: PType, text=None):
140 | """Initialize paragraph with type and optionally text."""
141 | if not text:
142 | text = "\0"
143 | elif len(text) == 0 or text[-1] != "\0":
144 | text += "\0"
145 |
146 | self._ptype = ptype
147 | self._text = text
148 | self._lines = [1]
149 | self._line_count = 1 + PPrefs.get_prec_empty(self._ptype)
150 |
151 | self._reformat()
152 |
153 | def _reformat(self):
154 | """Reformat paragraph calculating line wraps."""
155 | width = PPrefs.get_width(self._ptype)
156 |
157 | self._lines.clear()
158 | self._lines.append(0)
159 |
160 | current_word_len = 0
161 |
162 | for char in self._text:
163 | current_word_len += 1
164 |
165 | # TODO work with hyphen
166 | if char == ' ' or char == '\0':
167 | self._lines[-1] += current_word_len
168 | current_word_len = 0
169 | elif current_word_len == width:
170 | self._lines[-1] += current_word_len
171 | self._lines.append(0)
172 | current_word_len = 0
173 | elif self._lines[-1] + current_word_len > width:
174 | self._lines.append(0)
175 |
176 | self._line_count = len(self._lines) + PPrefs.get_prec_empty(self._ptype)
177 |
178 | def sp_input(self, pos: int, text: str):
179 | """Input text into paragraph."""
180 | self._text = self._text[:pos] + text + self._text[pos:]
181 | self._reformat()
182 |
183 | def sp_delete(self, pos: int, length: int) -> str:
184 | """Delete text from paragraph and return deleted text."""
185 | deleted = self._text[pos:pos + length]
186 | self._text = self._text[:pos] + self._text[pos + length:]
187 | self._reformat()
188 | return deleted
189 |
190 | def sp_set_type(self, ptype: PType):
191 | """Set paragraph's type."""
192 | self._ptype = ptype
193 | self._reformat()
194 |
195 | def get_text(self) -> str:
196 | """Return paragraph's text (ending with null character)."""
197 | return self._text
198 |
199 | def get_line_count(self) -> int:
200 | """Return number of lines."""
201 | return self._line_count
202 |
203 | def get_lines(self) -> list():
204 | """Return text in line wrapped form."""
205 | result = []
206 |
207 | for i in range(PPrefs.get_prec_empty(self._ptype)):
208 | result.append("")
209 |
210 | line_start = 0
211 | for line_len in self._lines:
212 | result.append(self._text[line_start: line_start + line_len])
213 | line_start += line_len
214 |
215 | return result
216 |
217 | def get_line_column_at_pos(self, pos: int) -> (int, int):
218 | """Return line and column for position."""
219 | line = 0
220 | for i in range(0, len(self._lines)):
221 | if pos - self._lines[i] >= 0:
222 | pos -= self._lines[i]
223 | line += 1
224 | else:
225 | break
226 |
227 | return line + PPrefs.get_prec_empty(self._ptype), pos
228 |
229 | def get_pos_at_line_column(self, line: int, column: int) -> int:
230 | """Return position for column in line."""
231 | line_count = len(self._lines)
232 | line -= PPrefs.get_prec_empty(self._ptype)
233 | if line < 0:
234 | return -1
235 | elif line >= line_count:
236 | return -2
237 |
238 | if column < 0:
239 | column = 0
240 | if column >= self._lines[line]:
241 | column = self._lines[line] - 1
242 |
243 | for i in range(line):
244 | column += self._lines[i]
245 |
246 | return column
247 |
248 | def get_type(self):
249 | """Return type."""
250 | return self._ptype
251 |
252 | def __str__(self) -> PType:
253 | """Return as string."""
254 | result = ""
255 | indent = " " * PPrefs.get_indent(self._ptype)
256 | for line in self.get_lines():
257 | result += indent + line + "\n"
258 | return result
259 |
--------------------------------------------------------------------------------
/shane/screenplay.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import time
20 |
21 | from shane.io import fountain
22 | from shane.paragraph import Paragraph, PType, PPrefs
23 |
24 |
25 | class NameDB(object):
26 | """Name database for a screenplay to store and retrieve names."""
27 |
28 | def __init__(self):
29 | """Initialize the name database."""
30 | self._a_m = []
31 | self._n_z = []
32 | self._else = []
33 |
34 | def clear(self):
35 | """Clear the database's contents."""
36 | self._a_m.clear()
37 | self._n_z.clear()
38 | self._else.clear()
39 |
40 | def add(self, name: str):
41 | """Add a name to the database."""
42 | if not name:
43 | return
44 |
45 | first_char = ord(name[0].lower())
46 | list_to_add_to = self._else
47 | if 97 <= first_char <= 109:
48 | list_to_add_to = self._a_m
49 | elif 110 <= first_char <= 122:
50 | list_to_add_to = self._n_z
51 |
52 | for i in range(len(list_to_add_to)):
53 | if name == list_to_add_to[i]:
54 | break
55 | elif name < list_to_add_to[i]:
56 | list_to_add_to.insert(i, name)
57 | break
58 | else:
59 | list_to_add_to.append(name)
60 |
61 | def get_next(self, name: str, starting_with: str) -> str:
62 | """Get a name from the database based on a name's beginning."""
63 | if not starting_with:
64 | return name
65 |
66 | first_char = ord(starting_with[0].lower())
67 | list_to_go_from = self._else
68 | if 97 <= first_char <= 109:
69 | list_to_go_from = self._a_m
70 | elif 110 <= first_char <= 122:
71 | list_to_go_from = self._n_z
72 |
73 | proposals = [n for n in list_to_go_from if n.startswith(starting_with)]
74 | try:
75 | index = proposals.index(name)
76 | if index < len(proposals) - 1:
77 | return proposals[index + 1]
78 | else:
79 | return proposals[0]
80 | except ValueError:
81 | if len(proposals) > 0:
82 | return proposals[0]
83 | else:
84 | return name
85 |
86 |
87 | class ActionBundle(object):
88 | """A bundle of actions remembering when last action was inserted."""
89 |
90 | # 200ms
91 | TIME_DISTANCE = 0.2
92 |
93 | def __init__(self, action):
94 | """Initialize the bundle with a new action."""
95 | self.actions = [action]
96 | self.last_action = time.time()
97 |
98 | def is_relatively_new(self):
99 | """Return if not much time has passed since last action addition."""
100 | return time.time() - self.last_action < ActionBundle.TIME_DISTANCE
101 |
102 | def add_action(self, action):
103 | """Add new action."""
104 | self.actions.insert(0, action)
105 | self.last_action = time.time()
106 |
107 |
108 | class InputAction(object):
109 | """Action for text input."""
110 | def __init__(self, pindex: int, position: int, text: str):
111 | """Initialize action."""
112 | self.pindex = pindex
113 | self.position = position
114 | self.text = text
115 |
116 |
117 | class DeleteAction(object):
118 | """Action for text deletion."""
119 | def __init__(self, pindex: int, position: int, text: str):
120 | """Initialize action."""
121 | self.pindex = pindex
122 | self.position = position
123 | self.text = text
124 |
125 |
126 | class NewParagraphAction(object):
127 | """Action for paragraph creation."""
128 | def __init__(self, pindex: int, ptype: PType, text: str):
129 | """Initialize action."""
130 | self.pindex = pindex
131 | self.ptype = ptype
132 | self.text = text
133 |
134 |
135 | class DeleteParagraphAction(object):
136 | """Action for paragraph deletion."""
137 | def __init__(self, pindex: int, ptype: PType, text: str):
138 | """Initialize action."""
139 | self.pindex = pindex
140 | self.ptype = ptype
141 | self.text = text
142 |
143 |
144 | class ChangePTypeAction(object):
145 | """Action for paragraph type change."""
146 | def __init__(self, pindex: int, prev_type: PType, new_type: PType):
147 | """Initialize action."""
148 | self.pindex = pindex
149 | self.prev_type = prev_type
150 | self.new_type = new_type
151 |
152 |
153 | class Screenplay(object):
154 | """A screenplay."""
155 |
156 | # Why 20? Dunno.
157 | MAX_ACTION_SIZE = 20
158 |
159 | def __init__(self, path: str=None):
160 | """Initialize the screenplay."""
161 | self._paragraphs = []
162 |
163 | self._path = path
164 | if self._path:
165 | try:
166 | self._paragraphs.extend(fountain.read(path))
167 | except OSError:
168 | pass
169 | if not path or len(self._paragraphs) == 0:
170 | self._paragraphs.append(Paragraph(PType.SCENE))
171 |
172 | self._cursor_par = 0
173 | self._cursor_pos = 0
174 |
175 | self._name_db = NameDB()
176 | self.do_rebuild_autocomplete_db()
177 |
178 | # First action is newest action
179 | self._previous_actions = []
180 | self._current_action = 0
181 |
182 | # HERE COME THE ONLY METHODS ALLOWED TO USE PARAGRAPH'S SP_-METHODS OR
183 | # CHANGE SELF._PARAGRAPHS
184 |
185 | def _input(self, pindex: int, position: int, text: str, undo: bool=True):
186 | """Input text into a paragraph."""
187 | self._paragraphs[pindex].sp_input(position, text)
188 | if undo:
189 | self._add_action(InputAction(pindex, position, text))
190 |
191 | def _delete(self, pindex: int, position: int, length: int, undo: bool=True) -> str:
192 | """Delete text from a pragraph and return deleted text."""
193 | deleted = self._paragraphs[pindex].sp_delete(position, length)
194 | if undo:
195 | self._add_action(DeleteAction(pindex, position, deleted))
196 | return deleted
197 |
198 | def _new_paragraph(self, pindex: int, ptype: PType, text: str=None, undo: bool=True):
199 | """Add a new paragraph at index."""
200 | self._paragraphs.insert(pindex, Paragraph(ptype, text))
201 | if undo:
202 | self._add_action(NewParagraphAction(pindex, ptype, text))
203 |
204 | def _delete_paragraph(self, pindex: int, undo: bool=True):
205 | """Delete paragraph at index."""
206 | text = self._paragraphs[pindex].get_text()[:-1]
207 | ptype = self._paragraphs[pindex].get_type()
208 | self._paragraphs.pop(pindex)
209 | if undo:
210 | self._add_action(DeleteParagraphAction(pindex, ptype, text))
211 |
212 | def _change_paragraph_type(self, pindex: int, ptype: PType, undo: bool=True):
213 | """Change given paragraph's type at index."""
214 | prev_type = self._paragraphs[pindex].get_type()
215 | self._paragraphs[pindex].sp_set_type(ptype)
216 | if undo:
217 | self._add_action(ChangePTypeAction(pindex, prev_type, ptype))
218 |
219 | # HERE COME METHODS ONLY TO BE USED BY METHODS FROM PREVIOUS BLOCK
220 |
221 | def _add_action(self, action):
222 | """Add action to previous actions list."""
223 | if len(self._previous_actions) > 0 and self._current_action > 0:
224 | self._previous_actions = \
225 | self._previous_actions[self._current_action:]
226 | self._current_action = 0
227 |
228 | if len(self._previous_actions) > 0 and\
229 | self._previous_actions[0].is_relatively_new():
230 | self._previous_actions[0].add_action(action)
231 | else:
232 | self._previous_actions.insert(0, ActionBundle(action))
233 | if len(self._previous_actions) > Screenplay.MAX_ACTION_SIZE:
234 | self._previous_actions = \
235 | self._previous_actions[:Screenplay.MAX_ACTION_SIZE]
236 |
237 | # OKAY, THANKS FOR YOUR UNDERSTANDING, YOU CAN GO BACK TO YOUR STUFF
238 |
239 | def do_input(self, text: str):
240 | """Input text at cursor position."""
241 | text = text.split("\n")
242 | self._input(self._cursor_par, self._cursor_pos, text[0])
243 | self._cursor_pos += len(text[0])
244 |
245 | # Oh noes, there were newlines in input text!
246 | if len(text) > 1:
247 | cur_par = self._paragraphs[self._cursor_par]
248 |
249 | removed = self._delete(self._cursor_par, self._cursor_pos,
250 | len(cur_par.get_text()) -
251 | self._cursor_pos - 1)
252 | for line in text[1:]:
253 | prev_par = self._cursor_par
254 | self._cursor_par += 1
255 | self._new_paragraph(self._cursor_par,
256 | PPrefs.get_enter(
257 | self._paragraphs[prev_par].get_type()),
258 | line)
259 | self._cursor_pos = len(text[-1])
260 | self._input(self._cursor_par, self._cursor_pos, removed)
261 |
262 | def do_delete_backward(self):
263 | """Delete one character backwards from cursor position."""
264 | if not self._cursor_par == self._cursor_pos == 0:
265 | self.do_move_cursor_left()
266 | self.do_delete_forward()
267 |
268 | def do_delete_forward(self):
269 | """Delete one character forwards from cursor position."""
270 | cur_par = self._paragraphs[self._cursor_par]
271 | if self._cursor_pos < len(cur_par.get_text()) - 1:
272 | self._delete(self._cursor_par, self._cursor_pos, 1)
273 | elif self._cursor_par < len(self._paragraphs) - 1:
274 | self._input(self._cursor_par, self._cursor_pos,
275 | self._paragraphs[self._cursor_par + 1].get_text()[:-1])
276 | self._delete_paragraph(self._cursor_par + 1)
277 |
278 | def do_autocomplete_name(self):
279 | """Autocomplete name at cursor position."""
280 | cur_par = self._paragraphs[self._cursor_par]
281 | if cur_par.get_type() != PType.NAME:
282 | return
283 | cur_name = cur_par.get_text()[:-1]
284 | proposal = self._name_db.get_next(cur_name,
285 | cur_name[:self._cursor_pos])
286 | self._delete(self._cursor_par, 0, len(cur_name))
287 | self._input(self._cursor_par, 0, proposal)
288 |
289 | def do_convert_tab_style(self):
290 | """Convert paragraph at cursor position according to tab convention."""
291 | self._change_paragraph_type(self._cursor_par, PPrefs.get_tab(
292 | self._paragraphs[self._cursor_par].get_type()))
293 |
294 | def do_convert_to_prev_ptype(self):
295 | """Convert paragraph at cursor position to previous type in order."""
296 | ptypes = list(PType.__iter__())
297 | index = ptypes.index(self._paragraphs[self._cursor_par].get_type()) - 1
298 | self._change_paragraph_type(self._cursor_par, ptypes[index])
299 |
300 | def do_convert_to_next_ptype(self):
301 | """Convert paragraph at cursor position to next type in order."""
302 | ptypes = list(PType.__iter__())
303 | index = ptypes.index(self._paragraphs[self._cursor_par].get_type()) + 1
304 | self._change_paragraph_type(self._cursor_par, ptypes[index % len(ptypes)])
305 |
306 | def do_move_cursor_left(self):
307 | """Move cursor one character backward."""
308 | if self._cursor_pos > 0:
309 | self._cursor_pos -= 1
310 | elif self._cursor_par > 0:
311 | self._cursor_par -= 1
312 | self._cursor_pos = \
313 | len(self._paragraphs[self._cursor_par].get_text()) - 1
314 |
315 | def do_move_cursor_right(self):
316 | """Move cursor one character forward."""
317 | if self._cursor_pos \
318 | < len(self._paragraphs[self._cursor_par].get_text()) - 1:
319 | self._cursor_pos += 1
320 | elif self._cursor_par < len(self._paragraphs) - 1:
321 | self._cursor_pos = 0
322 | self._cursor_par += 1
323 |
324 | def do_move_cursor_up(self):
325 | """Move cursor one line upward."""
326 | cur_par = self._paragraphs[self._cursor_par]
327 | line, col = cur_par.get_line_column_at_pos(self._cursor_pos)
328 | line -= 1
329 | cur_pos_new = cur_par.get_pos_at_line_column(line, col)
330 | if cur_pos_new >= 0:
331 | self._cursor_pos = cur_pos_new
332 | elif self._cursor_par > 0:
333 | self._cursor_par -= 1
334 | prev_par = self._paragraphs[self._cursor_par]
335 | line = prev_par.get_line_count() - 1
336 | col += PPrefs.get_indent(cur_par.get_type()) \
337 | - PPrefs.get_indent(prev_par.get_type())
338 | self._cursor_pos = prev_par.get_pos_at_line_column(line, col)
339 |
340 | def do_move_cursor_down(self):
341 | """Move cursor one line downward."""
342 | cur_par = self._paragraphs[self._cursor_par]
343 | line, col = cur_par.get_line_column_at_pos(self._cursor_pos)
344 | line += 1
345 | cur_pos_new = cur_par.get_pos_at_line_column(line, col)
346 | if cur_pos_new >= 0:
347 | self._cursor_pos = cur_pos_new
348 | elif self._cursor_par < len(self._paragraphs) - 1:
349 | self._cursor_par += 1
350 | next_par = self._paragraphs[self._cursor_par]
351 | next_par_type = next_par.get_type()
352 | line = PPrefs.get_prec_empty(next_par_type)
353 | col += PPrefs.get_indent(cur_par.get_type()) \
354 | - PPrefs.get_indent(next_par_type)
355 | self._cursor_pos = next_par.get_pos_at_line_column(line, col)
356 |
357 | def do_move_cursor_line_start(self):
358 | """Move cursor to line start (as in Home)."""
359 | column = self._paragraphs[self._cursor_par].get_line_column_at_pos(
360 | self._cursor_pos)[1]
361 | self._cursor_pos -= column
362 |
363 | def do_move_cursor_line_end(self):
364 | """Move cursor to line and (as in End)."""
365 | cur_par = self._paragraphs[self._cursor_par]
366 | line, column = cur_par.get_line_column_at_pos(self._cursor_pos)
367 | line_length = len(cur_par.get_lines()[line])
368 | self._cursor_pos += line_length - column - 1
369 |
370 | def do_move_cursor_paragraph_end(self):
371 | """Move cursor to paragraph end."""
372 | self._cursor_pos = \
373 | len(self._paragraphs[self._cursor_par].get_text()) - 1
374 |
375 | def do_move_cursor_prev_scene(self):
376 | """Move cursor to previous scene heading."""
377 | for i in range(self._cursor_par - 1, -1, -1):
378 | if self._paragraphs[i].get_type() == PType.SCENE:
379 | self._cursor_pos = 0
380 | self._cursor_par = i
381 | break
382 | else:
383 | for i in range(len(self._paragraphs) - 1, self._cursor_par, -1):
384 | if self._paragraphs[i].get_type() == PType.SCENE:
385 | self._cursor_pos = 0
386 | self._cursor_par = i
387 | break
388 |
389 | def do_move_cursor_next_scene(self):
390 | """Move cursor to next scene heading."""
391 | for i in range(self._cursor_par + 1, len(self._paragraphs)):
392 | if self._paragraphs[i].get_type() == PType.SCENE:
393 | self._cursor_pos = 0
394 | self._cursor_par = i
395 | break
396 | else:
397 | for i in range(0, self._cursor_par):
398 | if self._paragraphs[i].get_type() == PType.SCENE:
399 | self._cursor_pos = 0
400 | self._cursor_par = i
401 | break
402 |
403 | def do_rebuild_autocomplete_db(self):
404 | """Rebuild name database."""
405 | self._name_db.clear()
406 | for paragraph in self._paragraphs:
407 | if paragraph.get_type() == PType.NAME:
408 | self._name_db.add(paragraph.get_text()[:-1])
409 |
410 | def do_save(self, path: str):
411 | """Save screenplay to path."""
412 | fountain.write(path, self._paragraphs)
413 | self._path = path
414 |
415 | def do_undo(self):
416 | """Undo newest action."""
417 | if self._current_action < len(self._previous_actions):
418 | for action in self._previous_actions[self._current_action].actions:
419 | if type(action) is InputAction:
420 | self._delete(action.pindex, action.position, len(action.text), False)
421 | self._cursor_par = action.pindex
422 | self._cursor_pos = action.position
423 | elif type(action) is DeleteAction:
424 | self._input(action.pindex, action.position, action.text, False)
425 | self._cursor_par = action.pindex
426 | self._cursor_pos = action.position + len(action.text)
427 | elif type(action) is NewParagraphAction:
428 | self._delete_paragraph(action.pindex, False)
429 | if self._cursor_par == action.pindex:
430 | self._cursor_par -= 1
431 | if self._cursor_par < 0:
432 | self._cursor_par = 0
433 | self._cursor_pos = 0
434 | else:
435 | self.do_move_cursor_paragraph_end()
436 | elif type(action) is DeleteParagraphAction:
437 | self._new_paragraph(action.pindex, action.ptype, action.text, False)
438 | self._cursor_par = action.pindex
439 | self._cursor_pos = 0
440 | elif type(action) is ChangePTypeAction:
441 | self._change_paragraph_type(action.pindex, action.prev_type, False)
442 | self._current_action += 1
443 |
444 | def do_redo(self):
445 | """Redo recently undone action."""
446 | if self._current_action > 0:
447 | self._current_action -= 1
448 | for action in reversed(self._previous_actions[self._current_action].actions):
449 | if type(action) is InputAction:
450 | self._input(action.pindex, action.position, action.text, False)
451 | self._cursor_par = action.pindex
452 | self._cursor_pos = action.position + len(action.text)
453 | elif type(action) is DeleteAction:
454 | self._delete(action.pindex, action.position, len(action.text),
455 | False)
456 | self._cursor_par = action.pindex
457 | self._cursor_pos = action.position
458 | elif type(action) is NewParagraphAction:
459 | self._new_paragraph(action.pindex, action.ptype, action.text,
460 | False)
461 | self._cursor_par = action.pindex
462 | self._cursor_pos = 0
463 | elif type(action) is DeleteParagraphAction:
464 | self._delete_paragraph(action.pindex, False)
465 | if self._cursor_par == action.pindex:
466 | self._cursor_par -= 1
467 | if self._cursor_par < 0:
468 | self._cursor_par = 0
469 | self._cursor_pos = 0
470 | else:
471 | self.do_move_cursor_paragraph_end()
472 | elif type(action) is ChangePTypeAction:
473 | self._change_paragraph_type(action.pindex, action.new_type, False)
474 |
475 | def get_pindex_at_line(self, line: int) -> (int, int):
476 | """Return paragraph's index for line and offset into said paragraph."""
477 | for index in range(len(self._paragraphs)):
478 | length = self._paragraphs[index].get_line_count()
479 | if line < length:
480 | return index, line
481 | line -= length
482 | raise ValueError("Not enough lines in screenplay!")
483 |
484 | def get_paragraph_at_index(self, index: int) -> Paragraph:
485 | """Return paragraph at index."""
486 | return self._paragraphs[index]
487 |
488 | def get_paragraph_count(self) -> int:
489 | """Return number of paragraphs."""
490 | return len(self._paragraphs)
491 |
492 | def get_cursor_info(self) -> (int, int):
493 | """Return cursor's paragraph index and position in paragraph."""
494 | line, col = self._paragraphs[self._cursor_par]\
495 | .get_line_column_at_pos(self._cursor_pos)
496 | for i in range(0, self._cursor_par):
497 | line += self._paragraphs[i].get_line_count()
498 | return line, col
499 |
500 | def get_cursor_paragraph(self) -> Paragraph:
501 | """Return cursor's paragraph."""
502 | return self._paragraphs[self._cursor_par]
503 |
504 | def get_path(self) -> str:
505 | """Get path for screenplay."""
506 | return self._path
507 |
508 | def get_line_count(self) -> int:
509 | """Get number of lines."""
510 | # TODO Optimize (e.g. calculate line count once for every change)
511 | # Some hints:
512 | # - Paragraph conversion: Subtract par line count, convert, add new
513 | # - Input into same paragraph (no \n): Same as paragraph conversion
514 | count = 0
515 | for paragraph in self._paragraphs:
516 | count += paragraph.get_line_count()
517 | return count
518 |
519 | def __str__(self) -> str:
520 | """Return screenplay as string."""
521 | result = ""
522 | for paragraph in self._paragraphs:
523 | result += str(paragraph)
524 | return result
525 |
--------------------------------------------------------------------------------
/shane/sp_view.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | import curses
20 | from enum import Enum
21 |
22 | from shane.screenplay import Screenplay, PPrefs, PType
23 | from shane.view import View
24 |
25 |
26 | class ScreenplayViewEvent(Enum):
27 | """Enum for results from editing mainloop."""
28 | ESCAPE = 0
29 | RESIZE = 1
30 |
31 |
32 | class ScreenplayView(View):
33 | """View for screenplay editing."""
34 |
35 | def __init__(self, screenplay: Screenplay):
36 | """Initialize screenplay view."""
37 | super().__init__(screenplay)
38 |
39 | self._top_line = 0
40 |
41 | def run(self):
42 | """Run screenplay view's mainloop."""
43 | super().run()
44 |
45 | while True:
46 | self._draw()
47 |
48 | char = self._window.get_wch()
49 | if type(char) is str:
50 | asc = ord(char)
51 |
52 | if asc == 8: # Backspace
53 | self._screenplay.do_delete_backward()
54 | self._dirty = True
55 | elif asc == 9: # Tab
56 | self._screenplay.do_convert_tab_style()
57 | self._dirty = True
58 | elif asc == 10: # Enter
59 | if self._screenplay.get_cursor_paragraph().get_type() \
60 | == PType.NAME:
61 | self._screenplay.do_move_cursor_paragraph_end()
62 | self._screenplay.do_input("\n")
63 | self._dirty = True
64 | elif asc == 27: # ESC
65 | self._window.nodelay(True)
66 | asc = self._window.getch()
67 | self._window.nodelay(False)
68 | if asc == -1:
69 | return ScreenplayViewEvent.ESCAPE
70 | elif asc == 95: # Underscore
71 | if self._screenplay.get_cursor_paragraph().get_type() \
72 | == PType.NAME:
73 | self._screenplay.do_autocomplete_name()
74 | else:
75 | self._screenplay.do_input("_")
76 | self._dirty = True
77 | elif asc == 127: # Delete
78 | self._screenplay.do_delete_forward()
79 | self._dirty = True
80 | elif 32 <= asc: # Letter (except underscore)
81 | self._screenplay.do_input(char)
82 | self._dirty = True
83 | elif char == curses.KEY_BACKSPACE:
84 | self._screenplay.do_delete_backward()
85 | self._dirty = True
86 | elif char == curses.KEY_DC:
87 | self._screenplay.do_delete_forward()
88 | self._dirty = True
89 | elif char == curses.KEY_SLEFT: # Shift + left
90 | self._screenplay.do_convert_to_prev_ptype()
91 | self._dirty = True
92 | elif char == curses.KEY_SRIGHT: # Shift + right
93 | self._screenplay.do_convert_to_next_ptype()
94 | self._dirty = True
95 | elif char == curses.KEY_LEFT:
96 | self._screenplay.do_move_cursor_left()
97 | elif char == curses.KEY_RIGHT:
98 | self._screenplay.do_move_cursor_right()
99 | elif char == curses.KEY_UP:
100 | self._screenplay.do_move_cursor_up()
101 | elif char == curses.KEY_DOWN:
102 | self._screenplay.do_move_cursor_down()
103 | elif char == curses.KEY_HOME:
104 | self._screenplay.do_move_cursor_line_start()
105 | elif char == curses.KEY_END:
106 | self._screenplay.do_move_cursor_line_end()
107 | elif char == curses.KEY_PPAGE:
108 | self._screenplay.do_move_cursor_prev_scene()
109 | self._top_line = self._screenplay.get_cursor_info()[0]
110 | self._dirty = True
111 | elif char == curses.KEY_NPAGE:
112 | self._screenplay.do_move_cursor_next_scene()
113 | self._top_line = self._screenplay.get_cursor_info()[0]
114 | self._dirty = True
115 | elif char == curses.KEY_RESIZE:
116 | return ScreenplayViewEvent.RESIZE
117 |
118 | def _draw(self):
119 | """Draw screenplay."""
120 | cursor_line, cursor_column = self._screenplay.get_cursor_info()
121 |
122 | scrollbar_slider = int(cursor_line / self._screenplay.get_line_count() *
123 | (self._height - 1))
124 | if scrollbar_slider >= self._height - 1:
125 | scrollbar_slider = self._height - 2
126 |
127 | cursor_line -= self._top_line
128 | if cursor_line >= self._height:
129 | self._top_line += cursor_line - self._height + 1
130 | cursor_line = self._height - 1
131 | self._dirty = True
132 | elif cursor_line < 0:
133 | self._top_line += cursor_line
134 | cursor_line = 0
135 | self._dirty = True
136 |
137 | if self._dirty:
138 | par_index, line_off = \
139 | self._screenplay.get_pindex_at_line(self._top_line)
140 |
141 | self._window.clear()
142 |
143 | current_line = 0
144 | while current_line < self._height\
145 | and par_index < self._screenplay.get_paragraph_count():
146 | paragraph = self._screenplay.get_paragraph_at_index(par_index)
147 | lines = paragraph.get_lines()
148 | ptype = paragraph.get_type()
149 | indent = PPrefs.get_indent(ptype)
150 | width = PPrefs.get_width(ptype)
151 |
152 | for i in range(line_off, len(lines)):
153 | if current_line < self._height:
154 | if ptype == PType.SCENE:
155 | self._window.addnstr(current_line, indent,
156 | lines[i].upper(), width,
157 | curses.A_BOLD)
158 | elif ptype == PType.PARENTHETICALS:
159 | if i == 0:
160 | self._window.addstr(current_line, indent - 1,
161 | "(")
162 | if i == len(lines) - 1:
163 | self._window.addstr(current_line,
164 | indent + len(lines[i]) - 1,
165 | ")")
166 | self._window.addnstr(current_line, indent, lines[i],
167 | width)
168 | else:
169 | self._window.addnstr(current_line, indent, lines[i],
170 | width)
171 | else:
172 | break
173 | current_line += 1
174 |
175 | line_off = 0
176 | par_index += 1
177 |
178 | for y in range(0, self._height - 1):
179 | self._window.addstr(y, self._width - 1, "|")
180 | self._window.addstr(scrollbar_slider, self._width - 1, "O")
181 |
182 | cursor_paragraph = self._screenplay.get_cursor_paragraph()
183 | cursor_ptype = cursor_paragraph.get_type()
184 | cursor_indent = PPrefs.get_indent(cursor_ptype)
185 | self._window.move(cursor_line, min(cursor_indent + cursor_column,
186 | cursor_indent +
187 | PPrefs.get_width(cursor_ptype)))
188 |
189 | self._window.refresh()
190 |
191 | self._dirty = False
192 |
193 | def redraw(self):
194 | """Completely redraw screen view."""
195 | self._dirty = True
196 | if self._window:
197 | self._draw()
198 |
199 | @staticmethod
200 | def get_required_window_width():
201 | """Return minimum width needed for view."""
202 | screenplay_width = 0
203 | for ptype in PType:
204 | width = PPrefs.get_indent(ptype) + PPrefs.get_width(ptype)
205 | if screenplay_width < width:
206 | screenplay_width = width
207 | return screenplay_width + 2
208 |
--------------------------------------------------------------------------------
/shane/view.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 |
3 | # Shane - a poor man and/or hipster's TUI screenwriting software
4 | # Copyright (C) 2016 Tobias Heukäufer
5 | #
6 | # This program is free software: you can redistribute it and/or modify
7 | # it under the terms of the GNU General Public License as published by
8 | # the Free Software Foundation, either version 3 of the License, or
9 | # (at your option) any later version.
10 | #
11 | # This program is distributed in the hope that it will be useful,
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 | # GNU General Public License for more details.
15 | #
16 | # You should have received a copy of the GNU General Public License
17 | # along with this program. If not, see .
18 |
19 | from shane.screenplay import Screenplay
20 |
21 |
22 | class View(object):
23 | """A screenplay view to display and take input relevant for a screenplay."""
24 |
25 | def __init__(self, screenplay: Screenplay):
26 | """Initialize view for screenplay."""
27 | self._screenplay = screenplay
28 | self._window = None
29 | self._width = 0
30 | self._height = 0
31 | self._dirty = False
32 |
33 | def run(self):
34 | """Run view (most likely run a main loop)."""
35 | self._dirty = True
36 |
37 | def _draw(self):
38 | """Draw view contents."""
39 | raise NotImplementedError("_draw() not implemented!")
40 |
41 | def set_window(self, window):
42 | """Set ncurses window to draw on."""
43 | self._window = window
44 | self._window.keypad(True)
45 | self._height, self._width = self._window.getmaxyx()
46 | self._dirty = True
47 | self._draw()
48 |
49 | def remove_window(self):
50 | """Remove ncurses window."""
51 | self._window = None
52 |
--------------------------------------------------------------------------------