forked from oxidane/tmuxomatic
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtmuxomatic
More file actions
executable file
·2302 lines (2154 loc) · 109 KB
/
Copy pathtmuxomatic
File metadata and controls
executable file
·2302 lines (2154 loc) · 109 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BSD 3-Clause License
Copyright 2013-2015, Oxidane
All rights reserved
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
##----------------------------------------------------------------------------------------------------------------------
##
## Name ....... tmuxomatic
## Synopsis ... Automated window layout and session management for tmux
## Author ..... Oxidane
## License .... BSD 3-Clause
## Source ..... https://github.com/oxidane/tmuxomatic
##
##---------------+------------------------------------------------------------------------------------------------------
## About |
##---------------+
##
## QUICKSTART: Examine the session file "session_example", and run it with "tmuxomatic session_example".
##
## The tmux interface for creating window splits is technically simple, but to use those splits to arrange layouts is a
## tedious and inefficient process. Other tmux session management tools offer no solutions when it comes to splitting
## windows, so they have the same usability problem of tmux, compounded by their needy configuration files.
##
## Ideally I wanted a more intuitive interface, completely reinvented to be as simple and as user-friendly as possible.
## You depict the window pane layout in a "windowgram", where each unique character identifies a pane. Then each pane
## is linked by its character to an optional directory, run commands, and focus state. The program would then translate
## this information to the necessary tmux commands for splitting, scaling, pathing, and sendkeys.
##
## So that's exactly what tmuxomatic does.
##
## For a quick introduction that demonstrates the core feature set of tmuxomatic, see the readme file.
##
##-------------------+--------------------------------------------------------------------------------------------------
## Revisions |
##-------------------+
##
DESCRIPTION = "Intelligent tmux session management" # Maybe add: "using windowgrams"
HOMEPAGE = "https://github.com/oxidane/tmuxomatic"
VERSION = "2.19" # x.y: x = Major feature, y = Minor feature or bug fix
##
## 2.19 TBD TBD
## Implemented #10: Adds windows to current session if run within tmux
## Optimized window creation by combining tmux commands into fewer executable calls
##
## 2.18 2015-07-03 New flex command: insert
## Fixed issues #11, #12: Better handling of the "window" directive
##
## 2.17 2014-12-24 New flex command: reset
## Fixed some bugs in the drag command
##
## 2.16 2014-12-18 New flex command: drag
## Added smudge core, required by upcoming flex commands
##
## 2.15 2014-12-12 Fixed PyPI readme and screenshot
## See PyPI information detailed in the notes section
##
## 2.14 2014-12-11 Applying screenshot scale fix to PyPI readme
## Added edge core, required by upcoming flex commands
##
## 2.13 2014-12-06 Fixed PyPI long description by setting the index links to absolute
## PyPI does not support relative links (see https://stackoverflow.com/q/16367770)
##
## 2.12 2014-12-05 Various PyPI fixes
## Fixed the screenshot link and index links
##
## 2.11 2014-12-03 Fixed PyPI distribution
## This version is otherwise identical to the previous release
##
## 2.10 2014-12-03 Better screenshot
## Added contributor agreement
## Added the readme to PyPI
##
## 2.9 2014-11-26 Added PyPI compatible readme file
## Flex scale single parameter mixed type support
## Fixed bugs from the windowgram module migration
##
## 2.8 2014-11-24 Added some unit tests, full testing for flex and main classes to be added in 2.x
## Windowgram group conversion between pattern and list representations
## Flex console supports user window resize
##
## 2.7 2014-11-21 Moved windowgram and flex to its own module, see windowgram.py for details
## Various source cleanup in windowgram and flex
##
## 2.6 2014-11-15 New flex command: swap
## Support for issue #9: Sessions may be renamed from the session file
## Flex command ambiguity resolver to eliminate the need for short aliases
##
## 2.5 2014-11-12 New flex command: rename
## Improved the split command
##
## 2.4 2014-09-14 New flex command: split
## Multiple flex commands on one line, like a unix shell
##
## 2.3 2014-09-10 New flex command: join
## Switched to scale core v1 for more accurate scale results
##
## 2.2 2014-09-08 New flex command: break
## Optional window specification with filename when using flex
## Fixed scale core to resolve accuracy problems in scale and break commands
## Moved windowgram functions into a Windowgram class
##
## 2.1 2014-09-01 New flex command: add
## Cleared revision history for 1.x, added link in case it's needed
## If specified session file does not exist when using flex, it is created
## Improved the window list, shares the table printer code with the help menu
##
## 2.0 2014-08-28 Began tmuxomatic --flex, commands will be added over the next few releases
## Fixed the readme to fit the recent github style changes
## Fixed issue #8: Uses window name for focus to support tmux base-index
## Moved scale feature into flex, added flex section to readme
## Source indentation now uses spaces, for github readability
## New versioning for tmuxomatic, version 1.1.0 re-released as 2.0
##
## ------- --------------------------------------------------------------------------------------------------------
##
## 1.x https://github.com/oxidane/tmuxomatic/blob/ac7290e2206d4470d85c4eb6fa91c88794a17e45/tmuxomatic#L75-157
##
##--------------------+-------------------------------------------------------------------------------------------------
## Expansions |
##--------------------+
##
## 2.x:
##
## Finish flex console
##
## License and release windowgram library
##
## Minimal color support for the windowgram
##
## 3.x:
##
## Refactor flex console to use Python's cmd module
##
## Add color to flex console with full color support for the windowgram
##
## Minor:
##
## Definitely add ncurses or urwid. The 8-bit background colors could be used to highlight panes. This would be
## quite awesome for usability, and makes demonstrations easier to follow. A toggle for edge mode could show
## background colors on neighboring panes to illustrate edges. Maybe this could be an objective for 3.x.
## When ncurses support is added, the flex shell should highlight panes for relevant flex modifier parameters as
## they're being typed. Normal pane display is white text on color background. Highlight is color text on white
## background. Use gray for secondary highlight, e.g. the optional scale panes parameter for the commands drag,
## insert, and clone.
##
## Video demonstration of tmuxomatic, including the "--scale" feature and how it's used for rapid development
## and modification of windowgrams ("12\n34" -> 4x -> add small windows). Keep it short, fast paced,
## demonstrating at least one small and one large example.
##
## Manual page. Include command line examples.
##
## Possibly embed the examples in the program, allowing the user to run, extract, or view the session files.
##
## Would be great to add a file format template that adds color to the tmuxomatic session file in text editors.
## If it could give an even unique color (e.g., evenly spaced over color wheel) to each pane in the windowgram,
## then I think it would make the custom format much more appealing. Detection abilities may be limited in some
## IDEs though, so an extension may be necessary. Anyway, a dimension of color will allow the windowgram to be
## more rapidly assessed at-a-glance.
##
## Support other multiplexers like screen, if they have similar capabilities (vertical splits, shell driven, etc).
## Screen currently does not have the ability to modify panes from the command line, this is required for support.
##
## If filename is not specified, show running tmuxomatic sessions, and allow reconnect without file being present.
##
## Port the readme to a format compatible with pypi. Add readme and sample sessions to the distribution.
##
## Command line auto-completion support for zsh, etc.
##
## Reversing function. This takes a split-centric configuration and produces a windowgram. Has size or accuracy
## parameter that defines the size of the windowgram. Utility is dubious, as it has not been requested, but it
## would be easy to code. Add conversions from popular managers.
##
## Runnable session files. Basically the session file invokes tmuxomatic with fixed and/or forwarded arguments.
## It copies itself via stdin or a /tmp file. For easy application to any session file, constrain code to only a
## few short lines at the top of the session file that are easily cut and pasted into another. A prototype of this
## concept was done in early development, though it had a slightly different design, so it's best rewritten.
##
## Pane view toggle in flex. With the command "pane <pane>", only the pane is shown with "." for other panes, and
## information about the pane is shown, width and height, along with lines to all the possible axial divisions, so
## a user could easily find the exact value they need to achieve a precise split, for example. These values are
## shown as positive and negative, characters and percentages, e.g., "+6 | +75% | -2 | -25%".
##
## Maybe unit testing for windowgram parser, and flex commands.
##
## Move unit testing online, for a faster flex startup.
##
## Major:
##
## Session Binding: A mode that keeps the session file and its running session synchronized. Some things won't be
## easy to do. Changing the name of a window is easy, but changing windowgram may not be (without unique
## identifiers in tmux). Use threading to keep them in sync. Error handling could be shown in a created error
## window, which would be destroyed on next session load if there was no error.
##
## Touch screen interface using flex commands. Select edges with tap, then drag them as a group, for example.
##
## Possible:
##
## Multiple commands in a single call to tmux for faster execution (requires tmux "stdin").
##
## Creating two differently-named tmuxomatic sessions at the same time may conflict. If all the tmux commands
## could be sent at once then this won't be a problem (requires tmux "stdin").
##
## The tmuxomatic pane numbers could be made equal to tmux pane numbers (0=0, a=10, A=36), but only if tmux will
## support pane renumbering, which is presently not supported (requires tmux "renumber-pane").
##
## If tmux ever supports some kind of aggregate window pane arrangements then the tmux edge case represented by the
## example "session_unsupported" could be fixed (requires tmux "add-pane").
##
##------------------+---------------------------------------------------------------------------------------------------
## Requests |
##------------------+
##
## These are some features I would like to see in tmux that would improve tmuxomatic. If anyone adds these features to
## tmux, notify me and I'll upgrade tmuxomatic accordingly.
##
## 1) tmux --stdin Run multiple line-delimited commands in one tmux call (with error reporting).
## Upgrades: Faster tmuxomatic run time, no concurrent session conflicts.
##
## 2) tmux renumber-pane old new Changes the pane number, once set it doesn't change, except from this command.
## Upgrades: The tmux pane numbers will reflect those in the session file.
##
## 3) tmux add-pane x y w h Explicit pane creation (exact placement and dimensions). This automatically
## pushes neighbors, subdivides, or re-appropriates, the affected unassigned panes.
## Upgrades: Fast, precise arbitrary windowgram algorithm; resolves the edge case.
##
## 4) tmux preserve-proportions If tmux preserves proportional pane sizes, when xterm is resized, the panes will
## be proportionally adjusted. This feature would save from having to restart
## tmuxomatic when the xterm size at session creation differed from what they
## intend to use. See relative pane sizing notes for more information.
##
##---------------+------------------------------------------------------------------------------------------------------
## Terms |
##---------------+
##
## windowgram A rectangle comprised of unique alphanumeric rectangles representing panes in a window.
##
## xterm Represents the user's terminal window, may be xterm, PuTTY, SecureCRT, iTerm, or similar.
##
## tmux The terminal multiplexer program, currently tmuxomatic only supports tmux.
##
## session A single tmux attachment, containing one or more windows.
##
## window One window within a session that contains one or more panes.
##
## pane Any subdivision of a window with its own shell.
##
##---------------+------------------------------------------------------------------------------------------------------
## Notes |
##---------------+
##
## This program addresses only the session layout (windows, panes). For tmux settings (status bar, key bindings), users
## should consult an online tutorial for ".tmux.conf".
##
## For best results, design windowgrams that have a similar width-to-height ratio as your xterm.
##
## The way tmuxomatic (and tmux) works is by recursively subdividing the window using vertical and horizontal splits.
## If you specify a windowgram where such a split is not possible, then it cannot be supported by tmux, or tmuxomatic.
## For more information about this limitation, including an example, see file "session_unsupported".
##
## Supports any pane arrangement that is also supported by tmux. Some windowgrams, like those in "session_unsupported",
## won't work because of tmux (see "add-pane").
##
## The pane numbers in the session file will not always correlate with tmux (see "renumber-pane").
##
## For a list of other tmux feature requests that would improve tmuxomatic support, see the "Expansions" section.
##
## This was largely written when I was still new to Python, so not everything is pythonic.
##
## Supporting PyPI has been problematic. Porting the readme from markdown to rst would solve many of the problems,
## since it's supported by both PyPI and Github. However I prefer it to be in markdown format, and PyPI will probably
## add support for it eventually. The PyPI rst-to-html conversion (or rst itself) has the following issues: there's no
## support for nested lists (main index), does not produce html/css for image fitting (screenshot size), no relative
## linking (main index), and no inline html (screenshot scaling). Releases 2.10 to 2.15 primarily dealt with these
## issues.
##
##--------------------+-------------------------------------------------------------------------------------------------
## Other Uses |
##--------------------+
##
## The windowgram parser and splitting code could be used for some other purposes:
##
## * HTML table generation
##
## * Layouts for other user interfaces
##
## * Level design for simple tiled games (requires allowing overlapped panes and performing depth ordering)
##
##----------------------------------------------------------------------------------------------------------------------
import sys, os, time, subprocess, argparse, signal, re, math, copy, inspect
import windowgram # Required for print(windowgram.__version__), eventually this will be the only import
from windowgram import * # Reorganize windowgram and its use so that only "import windowgram" is needed
try: import yaml ; INSTALLED_PYYAML = True
except ImportError as e: INSTALLED_PYYAML = False
##----------------------------------------------------------------------------------------------------------------------
##
## Globals
##
##----------------------------------------------------------------------------------------------------------------------
ARGS = None
# Flexible Settings (may be safely changed)
PROGRAM_THIS = "tmuxomatic" # Name of this executable, alternatively: sys.argv[0][sys.argv[0].rfind('/')+1:]
EXE_TMUX = "tmux" # Short variable name for short line lengths, also changes to an absolute path
MAXIMUM_WINDOWS = 16 # Maximum windows (not panes), easily raised by changing this value alone
VERBOSE_WAIT = 1.5 # Wait time prior to running commands, time is seconds, only in verbose mode
DEBUG_SCANLINE = False # Shows the clean break scanline in action if set to True and run with -vvv
# Fixed Settings (requires source update)
MINIMUM_TMUX = "1.8" # Minimum supported tmux version (1.8 is required for absolute sizing)
VERBOSE_MAX = 4 # 0 = quiet, 1 = summary, 2 = inputs, 3 = fitting, 4 = commands
# Aliases for flexible directions
ALIASES = {
'foc': "focus key keys cur cursor", # Use "use user" or reserve them for other use?
'dir': "directory path cd pwd cwd home",
'run': "exe exec execute",
}
##----------------------------------------------------------------------------------------------------------------------
##
## Public derivations ... These two functions come from credited sources believed to be in the public domain
##
##----------------------------------------------------------------------------------------------------------------------
def get_xterm_dimensions_wh(): # cols (x), rows (y)
"""
Returns the dimensions of the user's xterm
Based on: https://stackoverflow.com/a/566752
"""
rows = cols = None
#
# Linux
#
stty_exec = os.popen("stty size", "r").read()
if stty_exec:
stty_exec = stty_exec.split()
if len(stty_exec) >= 2:
rows = stty_exec[0]
cols = stty_exec[1]
if rows and cols:
return int(cols), int(rows) # cols, rows
#
# Solaris
#
rows = os.popen("tput lines", "r").read() # Issue #4: Use tput instead of stty on some systems
cols = os.popen("tput cols", "r").read()
if rows and cols:
return int(cols), int(rows) # cols, rows
#
# Unix
#
def ioctl_gwinsz(fd):
# Get xterm size via ioctl
try:
import fcntl, termios, struct
cr = struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234"))
except (IOError, RuntimeError, TypeError, NameError):
return
return cr
cr = ioctl_gwinsz(0) or ioctl_gwinsz(1) or ioctl_gwinsz(2)
if not cr:
try:
fd = os.open(os.ctermid(), os.O_RDONLY)
cr = ioctl_gwinsz(fd)
os.close(fd)
except (IOError, RuntimeError, TypeError, NameError):
pass
if not cr:
env = os.environ
cr = (env.get("LINES", 25), env.get("COLUMNS", 80))
if cr and len(cr) == 2 and int(cr[0]) > 0 and int(cr[1]) > 0:
return int(cr[1]), int(cr[0]) # cols, rows
#
# Unsupported ... Other platforms not needed since tmux doesn't run there
#
return 0, 0 # cols, rows
def which(program):
"""
Returns the absolute path of specified executable
Source: https://stackoverflow.com/a/377028
"""
def is_exe(fpath):
# Return true if file exists and is executable
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
fpath, _ = os.path.split(program)
if fpath:
if is_exe(program):
return program
else:
for path in os.environ["PATH"].split(os.pathsep):
path = path.strip('"')
exe_file = os.path.join(path, program)
if is_exe(exe_file):
return exe_file
return None
##----------------------------------------------------------------------------------------------------------------------
##
## Miscellaneous functions ... These are general use functions used throughout tmuxomatic
##
##----------------------------------------------------------------------------------------------------------------------
def synerr( errpkg, errmsg ):
"""
Syntax error: Display error and exit
"""
if 'quiet' in errpkg:
print("Error: " + errmsg)
elif errpkg['format'] == "shorthand":
# Shorthand has exact line numbers
print("Error on line " + str(errpkg['line']) + ": " + errmsg)
else:
# The exact line number in YAML is not easily known with pyyaml
print("Error on or after line " + str(errpkg['line']) + ": " + errmsg)
exit(0)
def tmux_run( command, nopipe=False, force=False, real=False ):
"""
Executes the specified shell command (i.e., tmux)
nopipe ... Do not return stdout or stderr
force .... Force the command to execute even if ARGS.noexecute is set
real ..... Command should be issued regardless, required for checking version, session exists, etc
"""
noexecute = ARGS.noexecute if ARGS and ARGS.noexecute else False
printonly = ARGS.printonly if ARGS and ARGS.printonly else False
verbose = ARGS.verbose if ARGS and ARGS.verbose else 0
if not noexecute or force:
if printonly and not real:
# Print only, do not run
print(str(command)) # Use "print(str(command), end=';')" to display all commands on one line
return
if verbose >= 4 and not real:
print("(4) " + str(command))
if nopipe:
os.system(command)
else:
proc = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True )
stdout, stderr = proc.communicate()
# Return stderr or stdout
if stderr: return str(stderr, "ascii")
return str(stdout, "ascii")
def tmux_version(): # -> name, version
"""
Queries tmux for the version
"""
result = tmux_run( EXE_TMUX + " -V", nopipe=False, force=True, real=True )
result = [ line.strip() for line in result.split("\n") if line.strip() ]
name = result[0].split(" ", 1)[0] # Name that was reported by tmux (should be "tmux")
version = result[0].split(" ", 1)[1] # Only the version is needed
return name, version
def signal_handler_break( signal_number, frame ):
"""
On break, displays interruption message and exits.
"""
_ = repr(signal_number) + repr(frame) # Satisfies pylint
print("User interrupted...")
exit(0)
def signal_handler_hup( signal_number, frame ):
"""
Use the KeyboardInterrupt exception to communicate user disconnection
"""
_ = repr(signal_number) + repr(frame) # Satisfies pylint
raise KeyboardInterrupt
def satisfies_minimum_version(minimum, version):
"""
Asserts compliance by tmux version. I've since seen a similar version check somewhere that may come with Python
and could probably replace this code, but this works fine for now.
Update:
Option 1: setuptools.pkg_resources.parse_version() ... The setuptools library is non-standard
Option 2: distutils.version.LooseVersion() ... Required for "1.9a" to be recognized
"""
qn = len(minimum.split("."))
pn = len(version.split("."))
if qn < pn: minimum += ".0" * (pn-qn) # Equalize the element counts
if pn < qn: version += ".0" * (qn-pn) # Equalize the element counts
ver_intlist = lambda ver_str: [int(re.sub(r'\D', r'', x)) for x in ver_str.split(".")] # Issues: #1, #2
for p, q in zip( ver_intlist(version), ver_intlist(minimum) ):
if int(p) == int(q): continue # Qualifies so far
if int(p) > int(q): break # Qualifies
return False
return True
def command_matches(command, primary):
"""
Matches the command (from file) with the primary (for branch)
Returns True if command is primary or a supported alias
"""
if command == primary: return True
if primary in ALIASES and command in ALIASES[primary].split(" "): return True
return False
##----------------------------------------------------------------------------------------------------------------------
##
## Session file objects
##
##----------------------------------------------------------------------------------------------------------------------
##
## Window declaration macros
## A window declaration without a specified name is not allowed, except during the file parsing
##
is_windowdeclaration = lambda line: re.search(r"^[ \t]*window", line)
windowdeclaration_name = lambda line: " ".join(re.split(r"[ \t]+", line)[1:]) if is_windowdeclaration(line) else ""
##
## Session declaration macros
##
is_sessiondeclaration = lambda line: re.search(r"^[ \t]*session", line)
sessiondeclaration_name = lambda line: " ".join(re.split(r"[ \t]+", line)[1:]) if is_sessiondeclaration(line) else ""
##
## Parsed session file classes
##
class BatchOfLines(object): # A batch of lines (delimited string) with the corresponding line numbers (int list)
def __init__(self):
self.lines = "" # Lines delimited by \n, expects this on the last line in each batch of lines
self.counts = [] # For each line in lines, an integer representing the corresponding line number
def __repr__(self): # Debugging
return "lines = \"" + self.lines.replace("\n", "\\n") + "\", counts = " + repr(self.counts)
def AppendBatch(self, lines, start, increment=True):
linecount = len(lines.split("\n")[:-1]) # Account for extra line
self.lines += lines
self.counts += [line for line in range(1, linecount+1)] if increment else ([start] * linecount)
def IsEmpty(self):
return True if not self.lines else False
class Window(object): # Common container of window data, divided into sections identified by the keys below
def __init__(self):
self.__dict__['data'] = {} # { 'title_comments': string_of_lines, 'title': string_of_lines, ... }
self.__dict__['line'] = {} # { 'title_comments': first_line_number, 'title': first_line_number, ... }
for key in self.ValidKeys(): self.ClearKey(key) # Clear all keys
def __getitem__(self, key): # Invalid keys always return ""
return self.__dict__['data'][key] if key in self.ValidKeys() else ""
def __setitem__(self, key, value): # Invalid keys quietly dropped
if key in self.ValidKeys(): self.__dict__['data'][key] = value
def __repr__(self): # Debugging
return "\n__repr__ = [\n" + \
", ".join(
[ "'" + key + "': [ data = \"" + self.__dict__['data'][key].replace("\n", "\\n") + \
"\", starting_line_number = " + str(self.__dict__['line'][key]) + " ]\n" \
for key in self.ValidKeys() if self[key] is not "" ] \
) + \
" ]\n"
def ClearKey(self, key):
if key in self.ValidKeys():
self.__dict__['data'][key] = ""
self.__dict__['line'][key] = 0
def ValidKeys(self): # Ordered by appearance
return "title_comments title windowgram_comments windowgram directions_comments directions".split(" ")
def Serialize(self): # Serialized by appearance
return "".join( [ self[key] for key in self.ValidKeys() ] )
def WorkingKeys(self):
return [ key for key in self.ValidKeys() if self[key] is not "" ]
def IsFooter(self):
summary = " ".join( self.WorkingKeys() )
return True if summary == "title_comments" or summary == "" else False
def FirstLine(self, key):
return True if key in self.ValidKeys() and self.__dict__['line'][key] == 0 else False
def SetLine(self, key, line):
if key in self.ValidKeys(): self.__dict__['line'][key] = line
def GetLine(self, key):
return self.__dict__['line'][key] if key in self.ValidKeys() else 0
def SetIfNotSet(self, key, line):
if self.FirstLine(key): self.SetLine(key, line)
def GetLines(self, key):
return self.__dict__['line'][key]
def SplitCleanByKey(self, key):
return [ line[:line.index('#')].strip() if '#' in line else line.strip() for line in self[key].split("\n") ]
class SessionFile(object):
def __init__(self, filename):
self.filename = filename
self.Clear()
self.modified = False # Explicit modification
def Clear(self):
self.format = None # "shorthand" or "yaml"
self.footer = "" # footer comments
self.windows = [] # [ window, window, ... ]
def Load_Shorthand_SharedCore(self, bol):
# Actually locals
self.state = 0
self.window = None
self.line = [ None, None ] # line without cr, line number
self.comments = [ "", None ] # lines with cr, first line number
# Switchboard
switchboard = [
"title_comments", # state == 0 <- loop to / file footer saved here in its own window
"title", # state == 1
"windowgram_comments", # state == 2
"windowgram", # state == 3
"directions_comments", # state == 4
"directions", # state == 5
"UNUSED_comments", # state == 6 <- loop from / always appends this to "title_comments"
]
# Iterate lines and append onto respective window keys
lines = bol.lines.split("\n")[:-1] # Account for extra line
lines_index = 0
while True:
def transfercomments(): # Transfer comments (if any) to the current window block
if self.comments[0] is not None:
self.window[ switchboard[self.state] ] += self.comments[0]
self.window.SetIfNotSet( switchboard[self.state], self.comments[1] )
self.comments[0] = self.comments[1] = None
def nextwindow(): # This is called in two cases: 1) window declaration found, 2) end of file reached
if self.window: self.windows.append( self.window )
self.window = Window() ; self.state = 0 ; transfercomments() ; self.state = 1
def addline(): # Adds current line to current block or comments
if switchboard[self.state].endswith("_comments"): # Add to comments
if self.comments[0] is None: self.comments[0] = self.line[0] + "\n"
else: self.comments[0] += self.line[0] + "\n"
self.comments[1] = self.line[1] if self.comments[1] is None else self.comments[1]
else: # Add to block
self.window[ switchboard[self.state] ] += self.line[0] + "\n"
self.window.SetIfNotSet( switchboard[self.state], self.line[1] )
self.line[0] = self.line[1] = None # Ready to load next line
# Load line with corresponding line number
if self.line[0] is None and lines_index < len(lines): # Line
self.line[0] = lines[lines_index] ; self.line[1] = bol.counts[lines_index]
lines_index += 1
if self.line[0] is None: # EOF
# Hold comments so the footer doesn't get lost to the non-existent state 6 block
hold = [ None, None ]
hold[0], hold[1] = self.comments[0], self.comments[1]
self.comments[0], self.comments[1] = None, None
nextwindow()
# Restore comments so they are assimilated as a proper footer
self.comments[0], self.comments[1] = hold[0], hold[1]
if self.comments[0] is not None:
self.state = 0
transfercomments()
nextwindow()
# Done parsing
break
# Line used for analysis is stripped of all comments and whitespace
lineused = self.line[0].strip()
if lineused.find("#") >= 0: lineused = lineused[:lineused.find("#")].strip()
# Append this line to section or comments
if is_windowdeclaration(lineused): nextwindow() ; addline() ; self.state = 2 # New window declaration
elif ( self.state == 2 or self.state == 4 ) and lineused: transfercomments() ; self.state += 1 ; addline()
elif ( self.state == 3 or self.state == 5 ) and not lineused: self.state += 1 ; addline()
elif self.state == 6 and lineused: addline() ; self.state = 5 ; transfercomments() # Back up and add to 5
else: addline() # Everything else adds the line / Until first window declaration is found add to comments
# Any comments at end of file should be extracted into the footer string
if len(self.windows) and self.windows[len(self.windows)-1].IsFooter():
window = self.windows.pop(len(self.windows)-1)
self.footer = window.Serialize()
def Load_Shorthand(self, rawfile):
self.Clear()
self.format = "shorthand"
bol = BatchOfLines()
bol.AppendBatch( rawfile, 1 )
self.Load_Shorthand_SharedCore( bol )
def Load_Yaml(self, rawfile):
self.Clear()
self.format = "yaml"
# Yaml -> Dict
try:
# Line numbers (per window) with pyyaml from: https://stackoverflow.com/a/13319530
loader = yaml.SafeLoader(rawfile)
def compose_node(parent, index):
line = loader.line # The line number where the previous token has ended (plus empty lines)
node = yaml.SafeLoader.compose_node(loader, parent, index)
node.__line__ = line + 1
return node
def construct_mapping(node, deep=False):
mapping = yaml.SafeLoader.construct_mapping(loader, node, deep=deep)
mapping['__line__'] = node.__line__
return mapping
loader.compose_node = compose_node
loader.construct_mapping = construct_mapping
# Load into dict, now with line numbers for location of window in YAML
filedict = loader.get_single_data() # filedict = yaml.safe_load( rawfile ) # Without line numbers
except:
filedict = {}
# Dict -> Shorthand
group_session = []
group_other = []
bol = BatchOfLines()
bol.AppendBatch( "\n", 0, False ) # Translated YAML -> Shorthand, no need for header
if type(filedict) is list:
for entry in filedict:
# Session renames
if type(entry) is dict and 'session' in entry:
linenumber = entry['__line__'] if '__line__' in entry else 0
rawfile_shorthand = "session " + str(entry['session']) + "\n\n"
group_session.append( [ rawfile_shorthand, linenumber, False ] )
# Name blocks... Windows are identified by 'name' key
elif type(entry) is dict and 'name' in entry:
# Must contain 'windowgram' and 'directions' as block literals
windowgram = entry['windowgram'] if 'windowgram' in entry else ""
directions = entry['directions'] if 'directions' in entry else ""
linenumber = entry['__line__'] if '__line__' in entry else 0
rawfile_shorthand = \
"window " + str(entry['name']) + "\n\n" + windowgram + "\n" + directions + "\n\n\n"
group_other.append( [ rawfile_shorthand, linenumber, False ] )
# Append data, if any; this will force session renames to the top of the shorthand file
if group_session or group_other:
# Session renaming is only valid at top of file
for rawfile_shorthand, linenumber, flag in group_session:
bol.AppendBatch( rawfile_shorthand, linenumber, False )
# Everything else follows
for rawfile_shorthand, linenumber, flag in group_other:
bol.AppendBatch( rawfile_shorthand, linenumber, False )
# Shorthand -> Core
self.Load_Shorthand_SharedCore( bol )
def Load(self):
# Load raw data
rawfile = ""
f = open(self.filename, "rU")
while True:
line = f.readline()
if not line: break # EOF
rawfile += line
# Detect file format
format_yaml = False
for line in rawfile.split("\n"):
if line.find("#") >= 0: line = line[:line.find("#")]
line = line.strip()
if line:
if line[0] == "-":
format_yaml = True
break
# Parse the file
if format_yaml:
if not INSTALLED_PYYAML:
print("You have specified a session file in YAML format, yet you do not have pyyaml installed.")
print("Install pyyaml first, usually with a command like: `sudo pip-python3 install pyyaml`")
exit(0)
self.Load_Yaml( rawfile )
else:
self.Load_Shorthand( rawfile )
def Save(self):
self.modified = False
if self.filename and self.format:
if self.format == "shorthand":
# Shorthand
f = open(self.filename, 'w')
for window in self.windows: f.write( window.Serialize() )
f.write( self.footer )
if self.format == "yaml":
# YAML
formatted = "##\n## YAML session file generated by tmuxomatic flex " + VERSION + "\n##\n\n---\n\n"
# Required for writing block literals, source: https://stackoverflow.com/a/6432605
def change_style(style, representer):
def new_representer(dumper, data):
scalar = representer(dumper, data)
scalar.style = style
return scalar
return new_representer
class literal_str(str): pass
represent_literal_str = change_style('|', yaml.representer.SafeRepresenter.represent_str)
yaml.add_representer(literal_str, represent_literal_str)
# Add the session name change
rename = self.RenameIfSpecified_Raw()
if rename is not None:
formatted += yaml.dump( [{'session': rename}], \
indent=2, default_flow_style=False, explicit_start=False )
formatted += "\n"
# Now add all windows to a dictionary for saving
for ix, window in enumerate(self.windows):
serial = 1+ix
# Extract name: "window panel 1\n" -> "panel 1"
name = windowdeclaration_name( self.Get_WindowDeclarationLine( serial ) )
# Append window definition
# TODO: Sort as "name", "windowgram", "directions". Maybe use: http://pyyaml.org/ticket/29
window_dict = {
'name': str(name),
'windowgram': literal_str(window['windowgram']),
'directions': literal_str(window['directions']),
}
# Dump to string, with linebreaks
formatted += yaml.dump( [window_dict], indent=2, default_flow_style=False, explicit_start=False )
formatted += "\n"
# Write file
f = open(self.filename, 'w')
f.write( formatted )
def Ascertain_Trailing_Padding(self, string):
count = 0
for ix in range( len(string)-1, -1, -1 ):
if string[ix] == "\n": count += 1
else: break
return count
def Duplicate_Trailing_Padding(self, string, minimum):
count = self.Ascertain_Trailing_Padding(string)
if count < minimum: count = minimum
return "\n" * count
def Replace_TitleComments(self, serial, comments):
if serial < 1 or serial > self.Count_Windows(): return
padding = self.Duplicate_Trailing_Padding(self.windows[serial-1]['title_comments'], 1)
self.windows[serial-1]['title_comments'] = comments + padding
self.modified = True
def Replace_Title(self, serial, name):
if serial < 1 or serial > self.Count_Windows(): return
padding = self.Duplicate_Trailing_Padding(self.windows[serial-1]['title'], 1)
self.windows[serial-1]['title'] = "window " + name + padding
self.modified = True
def Replace_Windowgram(self, serial, windowgram_string): # TODO: Replace by wg
if serial < 1 or serial > self.Count_Windows(): return
self.windows[serial-1]['windowgram'] = Windowgram( windowgram_string ).Export_String() # Clean via class
self.modified = True
def Modified(self): # See flag use for limitations
return self.modified
def Count_Windows(self):
return len(self.windows)
def Serial_Is_Valid(self, serial):
return serial >= 1 and serial <= len(self.windows)
def Get_WindowDeclarationLine(self, serial):
if serial < 1 or serial > self.Count_Windows(): return "???" # Out of range
return linestrip(self.windows[serial-1]['title'].split("\n")[0]) # Window declaration is on first line
def Get_Name(self, serial):
if serial < 1 or serial > self.Count_Windows(): return "???" # Out of range
return windowdeclaration_name( self.Get_WindowDeclarationLine( serial ) )
def Get_WindowgramDimensions_Int(self, serial):
windowgram_string = self.windows[serial-1]['windowgram']
return Windowgram(windowgram_string).Analyze_WidthHeight()
def Get_Windowgram(self, serial): # windowgram_string
if serial < 1 or serial > self.Count_Windows():
if warning is None: return None
return None, "Out of range"
windowgram_string = Windowgram_Convert.PurifyString(self.windows[serial-1]['windowgram'])
return windowgram_string
def Get_Wg(self, serial): # wg
windowgram_string = self.Get_Windowgram(serial)
return Windowgram(windowgram_string) if windowgram_string else None
def Add_Windowgram(self, comments, name, windowgram_string):
self.windows.append( Window() )
serial = len(self.windows)
# Transfer footer to title comments for new window
while len(self.footer) > 1 and not self.footer.endswith("\n\n"): self.footer += "\n"
if not self.footer: self.footer = "\n"
self.windows[serial-1]['title_comments'] = self.footer
self.footer = ""
# Build window
self.windows[serial-1]['title_comments'] += comments if comments[-1:] == "\n" else comments + "\n"
name = "window " + name # Make a declaration
self.windows[serial-1]['title'] = name if name[-1:] == "\n" else name + "\n"
self.windows[serial-1]['windowgram_comments'] = "\n"
self.windows[serial-1]['windowgram'] = \
windowgram_string if windowgram_string[-1:] == "\n" else windowgram_string + "\n"
# Modified
self.modified = True
return serial
def RenameIfSpecified_Raw(self): # new_name (raw) or None
# Parse every line and change the name if specified (session rename only valid in comments sections)
new_name = None
if self.windows:
batch = self.windows[0].SplitCleanByKey('title_comments')
for line in batch:
if is_sessiondeclaration(line):
new_name = sessiondeclaration_name(line)
return new_name
def RenameIfSpecified(self): # new_name (modified) or None
new_name = self.RenameIfSpecified_Raw()
return None if new_name is None else (PROGRAM_THIS + "_" + new_name)
##----------------------------------------------------------------------------------------------------------------------
##
## Flex ... Text console and related extensions for tmuxomatic
##
##----------------------------------------------------------------------------------------------------------------------
##
## Planned other:
##
## again repeat last command (flex recognized, not official)
## undo stack: undo command
## redo stack: redo command
## wipe stack: discard the windowgram modification history (cannot undo)
## clip stack: discard any commands that had been undone (cannot undo)
##
## Possible non-modifiers:
##
## links show list of directions
## link [data...] add line to directions
## unlink <line> remove from directions by line number
## mvlink <line_from> <line_to> move line in directions
## duplicate <newname> copy current selected window into a new window and select
## erase <window> remove a windowgram from the session (maybe "deletewindow")
## arguments <command> like help but only shows the arguments
## examples <command> like help but only shows the examples
##
## Stack Sketch:
##
## new base
## scale base < scale
## ... mirror base scale scale break scale < mirror
## ... undo base scale < scale > break scale mirror
## redo base scale scale < break > scale mirror
## ... undo base < scale > scale break scale mirror
## break base scale < break
## ... undo base > scale break
## clear base
##
## The current element on stack should include arguments, all others show only the command
##
## Any modification of the windowgram outside of flex will result in a "manual" entry in flex stack
##
## Stack Storage:
##
## @ FLEX HISTORY : Used by --flex shell, use flex command "clear" to remove, or manually remove these lines
## @ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
## @ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
## @ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
## @ aaaaaaaaaaaaaaaaaaaaaaaaaa
##
## Data has initial windowgram, current stack pointer, easily allows any step to be reproduced on demand
## Data also has version, length, data checksum, current windowgram checksum for detecting manual edits
## Data is stored between window header and the windowgram as compressed JSON + utf-8 encoded in base64
## Overwrites entire session file with updated history block for every windowgram modification
##
## Console will be simple text, possibly use ncurses or urwid if it's present (installation is optional like yaml)
## Aliases for commands: "u" = undo. Include control keys if possible: ^Z = undo, ^Y = redo, ^U = clear, ^D = exit
## Print warnings if common divisors could not be found (within a reasonable range, say up to 120 characters)
## Display / print: clear, windowgram, gap, warnings, stack, gap, menu, gap, prompt
##
##----------------------------------------------------------------------------------------------------------------------
unittestgen_name1 = "unittest" # Unit testing is activated when user creates a window starting with this name
unittestgen_name2 = "unittest_ignore" # Or this name
unittestgen_run = 0 # See notes in flex unit testing
unittestgen_fcl = [] # Flex Command List
unittestgen_wgp = "" # Windowgram Group Pattern
unittestgen_ign = 0 # Nonzero if "unittest_ignore" was used
##
## Table Printer (used by help and list)
##
def table(output, markers, marklines, title, contents):
def table_divider(marker, row):
output.append(marker + "+-" + "-+-".join( [ len(col) * "-" for col in row ] ) + "-+")
def table_line(marker, row):
output.append(marker + "| " + " | ".join( [ col for col in row ] ) + " |")
# Count columns
columns = 0
for row in contents:
if len(row) > columns: columns = len(row)
# Maximum width of each column, taking into account title and all lines
widths = [ len(col) for col in title ]
for line in contents: widths = [ l if l > n else n for l, n in zip( widths, [ len(n) for n in line ] ) ]
# Pad all lines
contents.insert( 0, title )
contents_unpadded = contents
contents = []
for line in contents_unpadded:
contents.append( [ l + ((((w - len(l)) if len(l) < w else 0)) * " ") for w, l in zip( widths, line ) ] )
if columns:
first = True