├── .github └── workflows │ └── c-cpp.yml ├── .gitignore ├── HISTORY ├── INSTALL ├── ISSUES ├── LICENSE ├── Makefile,v ├── Makefile.am ├── README.rst ├── TODO ├── action.c ├── autogen.sh ├── bindings.c ├── build-aux └── package-version ├── configure.ac ├── cset.c ├── handler.c ├── smtx-main.c ├── smtx.c ├── smtx.h ├── smtx.ti ├── smtx.txt ├── test-coverage ├── test-describe.c ├── test-main.c ├── test-shell ├── test-unit.c ├── test-unit.h ├── vtparser.c └── vtparser.h /.github/workflows/c-cpp.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: bootstrap 17 | run: autoreconf -ivf 18 | - name: configure 19 | run: ./configure 20 | - name: make 21 | run: make 22 | - name: make dist 23 | run: make dist 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Makefile 2 | Makefile.in 3 | aclocal.m4 4 | ar-lib 5 | autom4te.cache/ 6 | build/ 7 | compile 8 | config.guess 9 | config.h.in 10 | config.sub 11 | configure 12 | depcomp 13 | install-sh 14 | ltmain.sh 15 | missing 16 | m4/ 17 | smtx.1 18 | test-driver 19 | -------------------------------------------------------------------------------- /HISTORY: -------------------------------------------------------------------------------- 1 | 2 | 3 | smtx is derived from mtm (git@github.com:deadpixi/mtm.git). See the git 4 | log for details. 5 | 6 | What is the motivation for smtx? 7 | 8 | Originally, I was annoyed at the diffculty of pane management in tmux. I would 9 | typically only use one pane per window, with occasionally (rarely) 2 panes. It 10 | is just too difficult to manipulate panes. I experimented with dvtm, and got as 11 | far as https://github.com/wrp/dvtm/tree/mvtm but was not happy with where that 12 | was going. While doing that development, I realized that what I wanted was 13 | modes and the ability to scroll horizontally in a window. I wanted to be able 14 | to use the multiplexer on my phone, so I needed the pty to be wider than the 15 | physical screen. While trying to decide on a name, I discovered mtm and shifted 16 | over to using it because the code base is simpler than dvtm. I played around with 17 | different behaviors of keystrokes in various modes. Pretty sure now that what I 18 | want is the ability to easily swap out keybindings. Right now, playing with the 19 | idea of having one keybinding in which '<>' will scroll the pty horizontally 20 | while all other keys pass through. Perhaps also have a mode in which 'hjkl' is 21 | used to navigate. But the key is that it must be very simple to change key 22 | bindings to make the terminal usable on a device with limited keyboard (like 23 | termux on Android). So the ultimate answer to the existential question is 24 | that smtx provides a window into ptys that are larger than the phsical 25 | device on which it is run. Also, the ability to easily swap out preset 26 | layouts or generate layouts dynamically. For example, 27 | to access the preset layout with 5 screens, you could do: CMD-5v 28 | (or printf '\033[60;1:.5 .25:1 .5:1 .75:1 1:1\007') to generate 29 | a layout that looks like: 30 | 31 | +--------------------------------------+---------------------------------------+ 32 | | | | 33 | | | | 34 | | +---------------------------------------+ 35 | | | | 36 | | | | 37 | | +---------------------------------------+ 38 | | | | 39 | | | | 40 | | +---------------------------------------+ 41 | | | | 42 | | | | 43 | +--------------------------------------+---------------------------------------+ 44 | 45 | By generating layouts on the fly with an osc string, it is trivial to generate 46 | scripts to manipulate the windows. 47 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | 2 | To build from the git repo, you will need to install the autotools (autoconf, 3 | libtool, and automake), asciidoctor, rcs, and probably other packages that I am 4 | forgetting about at the moment. It will be simpler to download a source 5 | tarball and build from that. As I write this document there are no 6 | official source tarballs since no official release has yet been made, so 7 | acaqiring a source tarball will be problematic. Perhaps a release will be 8 | made in the near future. 9 | 10 | This package is intended to be usable in the standard way. If you have 11 | met all the dependencies, just cd into the top level directory and run 'make'. 12 | This is a slight divergence from the usual autoconfiscated package, but should 13 | work. The git repo includes a Makefile,v which 'make' should recognize. The 14 | initial invocation of make will checkout a Makefile (using rcs) from the 15 | Makefile,v which will be used to generate a build directory and run the typical 16 | command chain. If you don't want to abuse the autotools this way, just ignore 17 | the Makefile,v and run: 18 | 19 | autoreconf -ivf 20 | ./configure --prefix=/p/a/t/h 21 | make 22 | 23 | 24 | The test suite is currently fragile. At the moment, it mostly works in 25 | a debian docker image on macos, but breaks completely elsewhere. In the 26 | debian docker image, typically 1 or 2 of the 51 tests will fail. IOW, 27 | don't expect 'make check' to be reliable. 28 | -------------------------------------------------------------------------------- /ISSUES: -------------------------------------------------------------------------------- 1 | 2 | BUGS: 3 | Test suite is broken. Tests sporadically pass in a debian docker image 4 | on macos, but I get consistent failures in other platforms. 5 | 6 | Syntax highlighting doesn't work in vim. 7 | 8 | This is dog slow on macos. top regularly reports 100% cpu. Should 9 | stop using select and switch to kqueue. Probably not worth the 10 | effort to use libevent or libev. 11 | 12 | Need to handle memory allocation errors more thoroughly. 13 | 14 | Make tput rep work. eg, tput rep w 5 should write 5 'w' to term, 15 | but the parameters do not seem to be getting sent properly. We get 16 | argc == 1 and argv[0] == 5 - 1, but the w is chomped. Note that this 17 | is the only terminfo entry that uses %c, and I suspect there is a bug 18 | in vtparser 19 | 20 | Figure out why I am getting an extra line at the bottom of screen in 21 | termux. Note this happens outside of smtx, so it is not our bug to 22 | fix, but may be a terminfo issue. It needs to be understood. 23 | 24 | As of 61c41fe0f1dd73829315c959b69cf44df2af102f, the test suite hangs 25 | on macos. Note that all the tests in esctest.py also timeout. 26 | 27 | test_ich consistently fails on macos. This seems to 28 | be an issue with terminfo. On macos: 29 | $ infocmp $TERM | grep '\' 30 | dl=\E[%p1%dM, dl1=\E[M, ech=\E[%p1%dX, ed=\E[J, el=\E[K, 31 | $ tput dl 5 | xxd 32 | 00000000: 1b5b 4d .[M 33 | but on debian: 34 | $ infocmp $TERM | grep '\' 35 | dl=\E[%p1%dM, dl1=\E[M, ech=\E[%p1%dX, ed=\E[J, el=\E[K, 36 | $ tput dl 5 | xxd 37 | 00000000: 1b5b 354d .[5M 38 | tput is not passing the parameter, so test ich on macos only deletes 39 | one line. 40 | 41 | Title bars are wonky with zero height canvas. 42 | 43 | csr does not work well with multple canvasses. Probably the 44 | correct thing to do is to remove csr functionality. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /Makefile,v: -------------------------------------------------------------------------------- 1 | head 1.1; 2 | access; 3 | symbols; 4 | locks; strict; 5 | comment @# @; 6 | 7 | 8 | 1.1 9 | date 2020.10.19.16.14.18; author williamp; state Exp; 10 | branches; 11 | next ; 12 | 13 | 14 | desc 15 | @A stub Makefile to bootstrap the project 16 | @ 17 | 18 | 19 | 1.1 20 | log 21 | @Initial revision 22 | @ 23 | text 24 | @# A stub Makefile used to bootstrap. 25 | 26 | srcdir := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) 27 | BUILDDIR := $(srcdir)build/$(shell uname -s)/$(shell uname -m) 28 | 29 | .PHONY: all check clean dist distclean install loc release version 30 | all: $(BUILDDIR) $(BUILDDIR)/Makefile 31 | cd $(BUILDDIR) && make $(MAKEFLAGS) all 32 | 33 | release: $(BUILDDIR) 34 | -cd $(BUILDDIR) && make $(MAKEFLAGS) distclean 35 | CFLAGS='-O2 -DNDEBUG' make $(MAKEFLAGS) all 36 | cd $(BUILDDIR) && make $(MAKEFLAGS) version && make $(MAKEFLAGS) dist 37 | 38 | configure: 39 | autoreconf -ivf 40 | 41 | $(BUILDDIR)/Makefile: configure 42 | cd $(BUILDDIR) && $(srcdir)configure CFLAGS="$${CFLAGS---coverage -g -O0}" 43 | 44 | $(BUILDDIR): 45 | mkdir -p $(BUILDDIR) 46 | 47 | loc: 48 | @@cd $(BUILDDIR) && \ 49 | for file in action handler smtx-main vtparser smtx; do \ 50 | printf assert:; grep 'assert(' $(srcdir)$${file}.c | wc -l; \ 51 | gcov $${file}.c $$(test -f .libs/$${file}.gcda \ 52 | && printf -- '-o .libs'); \ 53 | done | tr "':" ' '| awk ' \ 54 | /^assert/ { assert = $$2 } \ 55 | /^File/ {file=$$2; gsub(".*/", "", file)} \ 56 | /Lines executed/{c = $$5 - assert; \ 57 | printf "%32s:\t%s\t(%s)\n", file, c, $$3; t+=c} \ 58 | END{printf "%32s:\t%s\n", "Total", t}' 59 | 60 | install check clean dist distclean version: $(BUILDDIR) $(BUILDDIR)/Makefile 61 | cd $(BUILDDIR) && make $(MAKEFLAGS) $@@ 62 | @ 63 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | 2 | bin_PROGRAMS = smtx 3 | man1_MANS = smtx.1 4 | ACLOCAL_AMFLAGS = -I m4 5 | 6 | LDADD = libsmtx.la 7 | noinst_LTLIBRARIES = libsmtx.la 8 | libsmtx_la_SOURCES = vtparser.c smtx-main.c cset.c handler.c action.c test-describe.c \ 9 | bindings.c 10 | check_PROGRAMS = test-main 11 | AM_TESTS_ENVIRONMENT = LC_ALL=en_US.UTF-8; export LC_ALL; 12 | TESTS = test-shell test-main test-coverage 13 | test_main_SOURCES = test-main.c test-unit.c 14 | test_main_DEPENDENCIES = smtx 15 | 16 | CLEANFILES = *.gcda *.gcno *.gcov version *.1 17 | EXTRA_DIST = build-aux/package-version version test-shell test-coverage smtx.ti \ 18 | smtx.1 smtx.txt 19 | noinst_HEADERS = vtparser.h smtx.h test-unit.h 20 | 21 | version: $(DIST_SOURCES) $(DIST_COMMON) 22 | { cd $(top_srcdir) && ./autogen.sh && build-aux/package-version >&3; } 3> version 23 | 24 | install-data-hook: 25 | mkdir -p $(DESTDIR)$(sysconfdir)/terminfo 26 | tic -o $(DESTDIR)$(sysconfdir)/terminfo -s -x $(top_srcdir)/smtx.ti 27 | 28 | uninstall-hook: 29 | rm -rf $(DESTDIR)$(sysconfdir)/terminfo/s/smtx 30 | rm -rf $(DESTDIR)$(sysconfdir)/terminfo/s/smtx-256color 31 | 32 | smtx.1: smtx.txt 33 | @asciidoctor -a ver=${PACKAGE_VERSION} -b manpage $< 2> /dev/null \ 34 | || echo 'This page blank since asciidoctor was not available' > $@ 35 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | smtx -- Simple Modal Terminal Multiplexer 5 | 6 | Designed for simplicity and ease of use in a limited environment (eg, 7 | on a small physical device), smtx provides provides ptys that are wider 8 | than the physical device and the ability to have multiple views in 9 | different locations of the pty. 10 | 11 | https://terminalizer.com/view/28083a105080 12 | https://terminalizer.com/view/e38467005081 13 | 14 | Quickstart 15 | ========== 16 | 17 | When first started, `smtx` creates a single window with a pty running 18 | the program specified in SHELL. The window that is created will fill 19 | the physical screen, and the underlying pty that it is viewing will have 20 | a width of at least 80. The `CMD` keysequence (default is `CTRL+g`) 21 | will put smtx in `command` mode in which key sequences are interpreted 22 | to manipulate the windows. Press `RETURN` to exit command mode. 23 | Press `CMD` to exit command mode and send `CMD` to the underlying pty. 24 | In command mode, create new windows using `c` or `C`. Use `hjkl` to 25 | navigate between windows. Use `<` and `>` to horizontally scroll the 26 | underlying pty. Most commands accept a count, so you could create 5 new 27 | windows with `5c` and move down N times with `Nj`. (In the follwoing, `N` 28 | will represent an arbitrary integer). Choose one of the preset layouts 29 | with `Nv`. To attach a different pty to the current window, use `Na`. 30 | To swap the pty in the current window with the pty in a different 31 | window, use `NS`. To move the focus to the window with pty N, use `Ng`. 32 | To transpose the orientation of the current window, use `T`. To 33 | close the currently focused window (the underlying pty is not 34 | affected), use `x`. To destroy all of the ptys and exit, use `0x`. 35 | 36 | Features 37 | ======== 38 | 39 | smtx is modal, so you can enter multiple commands from command mode without 40 | typing the `CMD` keysequence multiple times. From command mode, you can 41 | create 5 new windows with `ccccc`, or `5c`. There are several preset layouts, 42 | so you can get a layout of 5 windows with `5v`. 43 | You can recursively convert an axial split to a sagittal split with `T` 44 | (transpose), and you can discard the currently focused window and all 45 | its children with `x`. To attach the current window to a different pty, 46 | use `a`. To swap the pty of the current window with a pty in a different 47 | window, use `s`. Change the width of the pty in the currectly focused window 48 | with `W`. 49 | 50 | You can also generate a window layout with an osc sequence. 51 | To generate a layout with seven windows, you could do:: 52 | 53 | printf '\033]60;.5:.5 .25:.66 .5:.66 .5:.83 .5:1 1:.25 1:1\007' 54 | 55 | where each coordinate pair represents the lower right hand corner of the window 56 | as a fraction of the full screen (note that order matters). 57 | 58 | Windows 59 | ======= 60 | 61 | New windows are created in `command` mode with `create`, which is by 62 | default bound to the keystrokes `c` and `C`. To navigate among the 63 | windows use `j`, `k`, `l`, and `h`. The navigation is not directly related to 64 | the physical layout on the screen, but navigates the tree structure of the 65 | extant windows. Each window belongs to one "canvas", 66 | which is simply a node in the tree used to keep track of windows. 67 | The root canvas fills the entire physical screen and contains the window 68 | in the upper left. Each canvas can have 0, 1, or 2 children. If the root 69 | window is split on the axial plane (ie, split top to bottom), the root 70 | canvas is said to be of "type 0" and has one child. If the root window 71 | is split on the sagittal plane (ie, split left to right), the root 72 | canvas is of "type 1". That is, a layout with 3 windows in which the 73 | left half of the screen is full height and the right half of the screen 74 | is split into 2 half-height windows indicates a root canvas of type 1. 75 | In that layout, the root canvas has one child (the right half of the screen), 76 | and that child (which is a type 0 canvas) has one child. The window navigation 77 | follows this tree structure naturally. 78 | `h` and `l` move the cursor to the left and right child of the 79 | currently focused canvas. `k` moves to the parent canvas, and `j` moves to the 80 | primary child; left for type 0, and right for type 1. 81 | 82 | 83 | Usage 84 | ===== 85 | 86 | Usage is simple:: 87 | 88 | smtx [-c ctrl-key] [-s history-size] [-t terminal-type] [-v] [-w width] 89 | 90 | The `-t` flag tells smtx what terminal type to advertise itself as. 91 | (This just controls what the `TERM` environment variable is set to.) 92 | 93 | The `-c` flag lets you specify a keyboard character to use as the "command 94 | prefix" for smtx when modified with *control* (see below). By default, 95 | this is `g`. 96 | 97 | The `-s` flag controls the amount of scrollback saved for each terminal. 98 | 99 | The `-w` flag sets the minimum width for newly created ptys (default is 80). 100 | 101 | Ths `-v` flag causes smtx to print its version and exit. 102 | 103 | Once inside smtx, things pretty much work like any other terminal. However, 104 | smtx lets you split up the terminal into multiple virtual terminals. 105 | 106 | At any given moment, exactly one virtual terminal is *focused*. It is 107 | to this terminal that keyboad input is sent. The focused terminal is 108 | indicated by the location of the cursor. 109 | 110 | The following commands are recognized in smtx when in command mode: 111 | 112 | h/j/k/l/Up/Down/Left/Right Arrow 113 | Move the focus to the virtual terminal in the window of the canvas 114 | that is the child/parent of 115 | the currently focused terminal. 116 | 117 | c / C 118 | Split the focused virtual terminal in half horizontally/vertically, 119 | creating a new virtual terminal to the right/below. The old virtual 120 | terminal retains the focus. 121 | 122 | b/f 123 | Scroll the screen back/forward half a screenful, or recenter the 124 | screen on the actual terminal. 125 | 126 | -/| 127 | Change the size of the current window to be the specifiec percentage 128 | of the enclosing canvas. eg, '25|' will make the current window use 129 | 25% of the horizontal space of the canvas. 130 | 131 | = Recursively rebalance windows. By itself, rebalance in both 132 | directions. 1= and 2= will rebalance only horizontally or 133 | vertically, respectively. 134 | 135 | W 136 | Set the width of the focused pty. eg, to set the width of the currently 137 | focused pty to 120, enter command mode and type `120W` 138 | 139 | [0-9] Set a command count. 140 | 141 | (Note that these keybindings can be changed at compile time, and that the 142 | above list is incomplete and subject to change.) 143 | 144 | Compatibility 145 | ============= 146 | 147 | The `smtx` Terminal Types 148 | ------------------------ 149 | smtx comes with a terminfo description file called smtx.ti. This file 150 | describes all of the features supported by smtx. 151 | 152 | If you want to install this terminal type, use the `tic` compiler that 153 | comes with ncurses:: 154 | 155 | tic -s -x smtx.ti 156 | 157 | 158 | Using these terminfo entries allows programs to use the full power of smtx's 159 | terminal emulation, but it is entirely optional. If the terminfo file is 160 | not installed, smtx will use reasonable defaults. 161 | 162 | Copyright and License 163 | ===================== 164 | 165 | Copyright 2016-2019 Rob King 166 | 167 | Copyright 2020-2023 William Pursell 168 | 169 | This program is free software: you can redistribute it and/or modify 170 | it under the terms of the GNU General Public License as published by 171 | the Free Software Foundation, either version 3 of the License, or 172 | (at your option) any later version. 173 | 174 | This program is distributed in the hope that it will be useful, 175 | but WITHOUT ANY WARRANTY; without even the implied warranty of 176 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 177 | GNU General Public License for more details. 178 | 179 | You should have received a copy of the GNU General Public License 180 | along with this program. If not, see . 181 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 2 | TODO: 3 | Make the preset keybindings modifiable at startup. 4 | (eg, implement bind_key as osc) 5 | 6 | Copy-mode, with stack of registers and ability to edit. 7 | Or, maybe just have a binding (y) that dumps the current content 8 | of the scrollback region to a file like ~/.smtx-pid-timestamp 9 | Perhaps use (e) to edit the file (eg, spawn $EDITOR), then (p) 10 | to paste it. 11 | 12 | Replace select() with kqueue()/epoll() 13 | 14 | Enable a mode to read input and interpret key sequences. ie, 15 | a filter to convert sequences like "abcdxx^H^Hef" to "abcdef" 16 | See: https://stackoverflow.com/questions/67415977/is-there-a-shell-utility-to-cleanup-interactive-tty-sessions 17 | 18 | 19 | 20 | MAYBE TODO: 21 | 22 | Make it easy to swap bindings. eg, so that hjkl could be used for 23 | scrolling in non-full screen mode. Maybe have labelled bindings, 24 | so perhaps 'a or 'b would select binding a or b. Would be simpler 25 | to use numbers, so 1B or 2B could select bindings 1 or 2. It seems 26 | hjkl would be better for scrolling than <> and fb 27 | 28 | Configure bindings from a startup file or ? 29 | 30 | Speed things up for hidden windows. If a pty is not visible 31 | on the display, we should (?) read a big chunk of data and store 32 | it but not actually display anything until the pty is added to 33 | a canvas. This may be a bad idea since we would lose escape 34 | sequences, but overall I like the idea. 35 | 36 | Multi-key bindings (?) 37 | 38 | Add character in title bar to indicate mode. Need to make 39 | title bar more functional in general, so this thought should 40 | probably be consumed by that. Probably want to be able to send 41 | arbitrary format strings in through an escape sequence. Note 42 | that we currently can set the "title" of the window via an osc 43 | sequence, but that title is only a portion of the title bar. 44 | Probably want the ability to control the format of the title bar. 45 | Also, the title bar/row should probably be a subwindow rather 46 | than just consuming one line of the main window. All the '-1' 47 | in the code gets confusing. 48 | 49 | Make it possible for windows to overlap. (Almost certainly don't do this.) 50 | 51 | Implement method to save the current layout into a register. 52 | 53 | Make an action on that moves to the next canvas. 54 | 55 | In full-screen mode, make hjkl scroll the pty. 56 | 57 | -1v should make the focused canvas the root and keep 58 | the layout below it instead of making it single screen. Perhaps 59 | 0v should attempt to layout all windows at one line each. 60 | 61 | speed up the grep() in the tests. We read one byte at a time because 62 | the row validation and layout validation is crazy fragile. If we make 63 | it more robust, we can probably read larger data buffers. 64 | 65 | Allocate a chunk of struct pty on startup, or just use a static array 66 | of size 512 or 1024 and stop callocing individual elements. That should 67 | cleanup the implementation of new_pty() a bit. 68 | -------------------------------------------------------------------------------- /action.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include "smtx.h" 19 | 20 | /* Index of a pty is based on its fd */ 21 | #define IDX(t) ((t)->fd - 2) 22 | 23 | /* Attach pty p to the canvas n. */ 24 | static void 25 | attach_pty(struct canvas *n, struct pty *p) 26 | { 27 | n->p->count -= 1; 28 | n->p = p; 29 | p->count += 1; 30 | S.reshape = 1; /* Need to adjust row count of pty */ 31 | } 32 | 33 | /* 34 | * Attach the pty with id matching the user selected index 35 | * to the currently focused canvas. 36 | */ 37 | void 38 | attach(void) 39 | { 40 | for (struct pty *t = S.p; t != NULL; t = t->next) { 41 | if (IDX(t) == S.count) { 42 | attach_pty(S.f, t); 43 | return; 44 | } 45 | } 46 | check(0, errno = 0, "No pty exists with id %d", S.count); 47 | } 48 | 49 | /* Count the canvassas below the focus point and reset split points */ 50 | static const char *default_balance_arg[] = { "-|", "-", "|", "-|" }; 51 | void 52 | balance(const char *arg) 53 | { 54 | int c = MIN(S.count + 1, 3); 55 | const char *a = (*arg == '=') ? default_balance_arg[c] : arg; 56 | for ( ; *a != '\0'; a += 1) { 57 | int d = *a == '|'; 58 | int count = 0; 59 | for (struct canvas *n = S.f; n != NULL; n = n->c[d]) { 60 | count += 1; 61 | } 62 | for (struct canvas *n = S.f; n != NULL; n = n->c[d]) { 63 | *(d ? &n->split.x : &n->split.y) = 1.0 / count--; 64 | n->p->ws.ws_row = 0; /* rows is set during resize */ 65 | } 66 | } 67 | S.reshape = 1; 68 | } 69 | 70 | /* 71 | * Create S.count new canvasses in the currently focused canvas. 72 | * If the argument is '|', add new canvasses in the right side 73 | * of the current canvas. Otherwise, create new canvasses in 74 | * the bottom. Rebalance so all new canvasses have equal portion 75 | * of the current canvas. 76 | */ 77 | void 78 | create(const char *arg) 79 | { 80 | int dir = *arg == '|'; 81 | struct canvas *n = S.f; 82 | while (n->c[dir]) { 83 | n = n->c[dir]; 84 | } 85 | for (int i = S.count < 1 ? 1 : S.count; i > 0; i -= 1) { 86 | struct canvas *v = n->c[dir] = newcanvas(0, n); 87 | if (v && v->p) { 88 | v->typ = n->typ = n->c[!dir] ? n->typ : dir; 89 | assert(v->parent == n); 90 | n = v; 91 | } else if (v && !v->p) { 92 | freecanvas(v); 93 | } 94 | } 95 | balance(arg); 96 | reshape(S.root, 0, 0, LINES, COLS); 97 | } 98 | 99 | /* 100 | * Increment S.count when the user enters a digit 101 | */ 102 | void 103 | digit(const char *arg) 104 | { 105 | S.count = 10 * (S.count == -1 ? 0 : S.count) + *arg - '0'; 106 | } 107 | 108 | static struct canvas * find_canvas(struct canvas *c, int id); 109 | 110 | /* 111 | * Move the focus to the canvas with id matching S.count 112 | */ 113 | void 114 | focus(void) 115 | { 116 | struct canvas *t = find_canvas(S.root, S.count); 117 | S.f = t ? t : S.f; 118 | } 119 | 120 | /* 121 | * Navigate the canvas tree 122 | */ 123 | void 124 | mov(const char *arg) 125 | { 126 | int count = S.count < 1 ? 1 : S.count; 127 | struct canvas *n = S.f; 128 | for (struct canvas *t = S.f; t && count--; n = t ? t : n) { 129 | switch (*arg) { 130 | case 'k': 131 | t = t->parent; 132 | break; 133 | case 'j': 134 | t = t->c[t->typ]; 135 | break; 136 | case 'h': 137 | t = t->c[0]; 138 | break; 139 | case 'l': 140 | t = t->c[1]; 141 | break; 142 | } 143 | } 144 | S.f = n ? n : S.root; 145 | } 146 | 147 | /* 148 | * Recursive function used to find a canvas with id in the tree. 149 | */ 150 | static struct canvas * 151 | find_canvas(struct canvas *c, int id) 152 | { 153 | struct canvas *r = NULL; 154 | if (c && id > 0) { 155 | if (IDX(c->p) == id) { 156 | r = c; 157 | } else if ((r = find_canvas(c->c[0], id)) == NULL) { 158 | r = find_canvas(c->c[1], id); 159 | } 160 | } 161 | return id < 1 ? S.f : r; 162 | } 163 | 164 | static void 165 | pty_size(struct pty *p) 166 | { 167 | check(p->fd == -1 || ! ioctl(p->fd, TIOCGWINSZ, &p->ws), errno = 0, 168 | "ioctl error getting size of pty %d", IDX(p)); 169 | } 170 | 171 | void 172 | new_tabstop(void) 173 | { 174 | pty_size(S.f->p); /* Update S.f->p->ws */ 175 | set_tabs(S.f->p, S.f->p->tabstop = S.count > -1 ? S.count : 8); 176 | } 177 | 178 | void 179 | new_shell(void) 180 | { 181 | attach_pty(S.f, new_pty(S.history, MAX(S.width, S.f->extent.x), true)); 182 | } 183 | 184 | void 185 | next(void) 186 | { 187 | S.f->p = S.f->p->next ? S.f->p->next : S.p; 188 | } 189 | 190 | void 191 | prune(void) 192 | { 193 | struct canvas *t = find_canvas(S.root, S.count); 194 | if (S.count == 0) { 195 | S.root = NULL; /* Trigger an exit from main loop */ 196 | } else if (t && t->parent) { 197 | struct canvas *p = t->parent; 198 | int child_index = t == p->c[1]; 199 | *(child_index ? &p->split.x : &p->split.y) = 1.0; 200 | p->c[child_index] = NULL; 201 | change_count(t, -1, 1); 202 | } 203 | S.reshape = 1; 204 | } 205 | 206 | static void grow_screens(struct pty *p, int siz); 207 | 208 | void 209 | reshape_root(void) 210 | { 211 | if (LINES > S.history) { 212 | S.history = LINES; 213 | } 214 | for (struct pty *p = S.p; p; p = p->next) { 215 | grow_screens(p, LINES); 216 | } 217 | resize_pad(&S.werr, 1, COLS); 218 | resize_pad(&S.wbkg, LINES, COLS); 219 | reshape(S.root, 0, 0, LINES, COLS); 220 | } 221 | 222 | void 223 | resize(const char *arg) 224 | { 225 | struct canvas *n = S.f, *child = S.f->c[!strchr("-", *arg)]; 226 | double *s = strchr("-", *arg) ? &n->split.y : &n->split.x; 227 | int count = S.count < 0 ? 50 : S.count > 100 ? 100 : S.count; 228 | *s = child ? count / 100.0 : 1.0; 229 | S.reshape = 1; 230 | } 231 | 232 | void 233 | scrollh(const char *arg) 234 | { 235 | struct canvas *n = S.f; 236 | if (n && n->p && n->p->s && n->p->s->w) { 237 | int c = S.count; 238 | int count = c < 0 ? n->extent.x : !c ? n->p->ws.ws_col : c; 239 | int x = n->offset.x + (*arg == '<' ? -count : count); 240 | n->offset.x = MAX(0, MIN(x, n->p->ws.ws_col - n->extent.x)); 241 | n->manualscroll = !!c; 242 | } 243 | } 244 | 245 | void 246 | scrolln(const char *arg) 247 | { 248 | struct canvas *n = S.f; 249 | if (n && n->p && n->p->s && n->p->s->w) { 250 | int count = S.count == -1 ? n->extent.y - 1 : S.count; 251 | int top = n->p->s->maxy - n->extent.y + 1; 252 | n->offset.y += *arg == '-' ? -count : count; 253 | n->offset.y = MIN(MAX(0, n->offset.y), top); 254 | } 255 | } 256 | 257 | void 258 | send(const char *arg) 259 | { 260 | rewrite(S.f->p->fd, arg + 1, arg[0]); 261 | scrollbottom(S.f); 262 | } 263 | 264 | void 265 | send_cr(void) 266 | { 267 | rewrite(S.f->p->fd, "\r\n", S.f->p->lnm ? 2 : 1); 268 | scrollbottom(S.f); 269 | } 270 | 271 | static void 272 | grow_screens(struct pty *p, int siz) 273 | { 274 | struct screen *s, *w[] = { &p->scr[0], &p->scr[1], NULL }; 275 | for (struct screen **sp = w; *sp && (s = *sp)->rows < siz; sp++) { 276 | WINDOW *new = NULL; 277 | if (resize_pad(&new, siz, p->ws.ws_col)) { 278 | copywin(s->w, new, 0, 0, siz - s->rows, 0, 279 | siz - 1, p->ws.ws_col - 1, 1); 280 | delwin(s->w); 281 | s->w = new; 282 | wmove(s->w, s->c.y += siz - s->rows, s->c.x); 283 | /* TODO?: mov maxy to struct pty */ 284 | s->maxy += siz - s->rows; 285 | p->tos = MAX(0, s->maxy - p->ws.ws_row + 1); 286 | s->scroll.top += siz - s->rows; 287 | s->scroll.bot += siz - s->rows; 288 | s->rows = siz; 289 | } 290 | } 291 | } 292 | 293 | void 294 | help(void) 295 | { 296 | for (int i = 0; i < LINES * COLS; i++) { 297 | putchar(' '); 298 | } 299 | putchar('\r'); 300 | putchar('\n'); 301 | printf("Command key is ^%c\r\n", S.rawkey); 302 | puts("Avaliable commands (in command mode):\r"); 303 | puts("[N]C create N new windows (left/right)\r"); 304 | puts("[N]c create N new windows (up/down)\r"); 305 | puts("[N]g move focus to window N\r"); 306 | puts("[N]v use preset window layout N\r"); 307 | puts("[N]W set width of underlying tty to N\r"); 308 | puts("[N]x Close window N. If N == 0, exit smtx\r"); 309 | puts("[N]= rebalance all windows below current\r"); 310 | puts("[N]< scroll left N characters\r"); 311 | puts("[N]> scroll right N characters\r"); 312 | puts("[N]- set height of current window to N% of canvas\r"); 313 | puts("[N]| set width of current window to N% of canvas\r"); 314 | fflush(stdout); 315 | } 316 | 317 | void 318 | set_history(void) 319 | { 320 | struct canvas *n = S.f; 321 | struct pty *p = n->p; 322 | S.history = MAX(LINES, S.count); 323 | grow_screens(p, S.history); 324 | S.reshape = 1; 325 | } 326 | 327 | void 328 | set_layout(void) 329 | { 330 | const char *defaults[] = { 331 | [0] = "1,1", 332 | [1] = "1,1", 333 | [2] = ".5,1 1,1", 334 | [3] = "1,.5 .5,1 1,1", 335 | [4] = ".5,.5 .5,1 1,.5 1,1", 336 | [5] = "1,.5 .25,1 .5,1 .75,1 1,1", 337 | [6] = ".5,1 .75,.5 .75,1 1,.33 1,.66 1,1", 338 | [7] = ".4,1 .8,1 1,.2 1,.4 1,.6 1,.8 1,1", 339 | [8] = ".5,.25 .5,.5 .5,.75 .5,1 1,.25 1,.5 1,.75 1,1", 340 | [9] = ( 341 | ".34,.33 .34,.67 .34,1 .68,.33 .68,.67 .68,1 " 342 | "1,.33 1,.67 1,1" 343 | ), 344 | }; 345 | size_t count = S.count < 0 ? 1 : S.count; 346 | if (count < sizeof defaults / sizeof *defaults) { 347 | build_layout(defaults[count]); 348 | } else { 349 | check(0, errno = 0, "Invalid layout: %d", count); 350 | } 351 | } 352 | 353 | /* Set width of the underlying tty */ 354 | void 355 | set_width(const char *arg) 356 | { 357 | struct canvas *n = S.f; 358 | struct pty *p = n->p; 359 | int w = *arg ? strtol(arg, NULL, 10) : S.count; 360 | if (w == -1) { 361 | w = n->extent.x; 362 | } 363 | if (p->fd > 0 && (pty_size(p), w != p->ws.ws_col)) { 364 | p->ws.ws_col = w; 365 | resize_pad(&p->scr[0].w, p->scr[0].rows, w); 366 | resize_pad(&p->scr[1].w, p->scr[1].rows, w); 367 | if (p->s->c.x > w - 1) { 368 | wmove(p->s->w, p->s->c.y, p->s->c.x = w - 1); 369 | } 370 | set_tabs(p, p->tabstop); 371 | reshape_window(p); 372 | } 373 | } 374 | 375 | void 376 | swap(void) 377 | { 378 | struct canvas *n = S.f; 379 | struct canvas *t; 380 | if (S.count == -1) { 381 | t = n->c[n->typ]; 382 | t = t ? t : n->c[!n->typ]; 383 | t = t ? t : n->parent; 384 | } else { 385 | t = find_canvas(S.root, S.count); 386 | } 387 | if (t) { 388 | struct pty *tmp = n->p; 389 | n->p = t->p; 390 | t->p = tmp; 391 | S.reshape = 1; 392 | } else { 393 | check(0, errno = 0, "Cannot find target canvas"); 394 | } 395 | } 396 | 397 | void 398 | transition(const char *arg) 399 | { 400 | switch (*arg++) { 401 | case '*': 402 | rewrite(S.f->p->fd, (char *)&S.ctlkey, 1); 403 | break; 404 | case '\n': 405 | send_cr(); 406 | } 407 | S.errmsg[0] = 0; /* Clear any existing error message */ 408 | switch (*arg) { 409 | case 'e': 410 | S.binding = k1; /* enter */ 411 | break; 412 | case 'i': 413 | S.binding = k2; /* insert */ 414 | break; 415 | case 'c': 416 | S.binding = ctl; /* control */ 417 | break; 418 | } 419 | scrollbottom(S.f); 420 | wrefresh(curscr); 421 | } 422 | 423 | static void 424 | transpose_r(struct canvas *c) 425 | { 426 | if (c) { 427 | c->typ = !c->typ; 428 | struct canvas *t = c->c[0]; 429 | double s = c->split.y; 430 | c->split.y = c->split.x; 431 | c->split.x = s; 432 | transpose_r(c->c[0] = c->c[1]); 433 | transpose_r(c->c[1] = t); 434 | } 435 | } 436 | 437 | void 438 | transpose(void) 439 | { 440 | if (S.f && !S.f->c[0] && !S.f->c[1]) { 441 | transpose_r(S.f->parent); 442 | } else { 443 | transpose_r(S.f); 444 | } 445 | S.reshape = 1; 446 | } 447 | 448 | void 449 | vbeep(void) 450 | { 451 | (void)beep(); 452 | } 453 | -------------------------------------------------------------------------------- /autogen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | autoreconf -ivf 3 | -------------------------------------------------------------------------------- /bindings.c: -------------------------------------------------------------------------------- 1 | 2 | #include "smtx.h" 3 | #pragma GCC diagnostic ignored "-Woverride-init" 4 | 5 | struct handler ctl[128] = { 6 | [0x00 ... 0x7f] = { .act = { .v = vbeep }, NULL }, 7 | 8 | [L'0' ] = { { .a = digit}, "0" }, 9 | [L'1' ] = { { .a = digit}, "1" }, 10 | [L'2' ] = { { .a = digit}, "2" }, 11 | [L'3' ] = { { .a = digit}, "3" }, 12 | [L'4' ] = { { .a = digit}, "4" }, 13 | [L'5' ] = { { .a = digit}, "5" }, 14 | [L'6' ] = { { .a = digit}, "6" }, 15 | [L'7' ] = { { .a = digit}, "7" }, 16 | [L'8' ] = { { .a = digit}, "8" }, 17 | [L'9' ] = { { .a = digit}, "9" }, 18 | [L'-' ] = { { .a = resize}, "-" }, 19 | [L'<' ] = { { .a = scrollh}, "<" }, 20 | [L'=' ] = { { .a = balance}, "=" }, 21 | [L'>' ] = { { .a = scrollh}, ">" }, 22 | [L'?' ] = { { .v = help}, NULL }, 23 | [L'|' ] = { { .a = resize}, "|" }, 24 | [L'C' ] = { { .a = create}, "|" }, 25 | [L'N' ] = { { .v = new_shell}, NULL }, 26 | #ifndef NDEBUG 27 | [L'Q' ] = { { .a = show_status}, "x" }, 28 | #endif 29 | [L'S' ] = { { .v = swap}, NULL }, 30 | [L'T' ] = { { .v = transpose}, NULL }, 31 | [L'W' ] = { { .a = set_width}, "" }, 32 | [L'Z' ] = { { .v = set_history}, NULL }, 33 | [L'\n'] = { { .a = transition}, " enter" }, 34 | [L'\r'] = { { .a = transition}, " enter" }, 35 | [L'a' ] = { { .v = attach}, NULL }, 36 | [L'b' ] = { { .a = scrolln}, "-" }, 37 | [L'c' ] = { { .a = create}, "-" }, 38 | [L'f' ] = { { .a = scrolln}, "+" }, 39 | [L'g' ] = { { .v = focus}, NULL }, 40 | [L'h' ] = { { .a = mov}, "h" }, 41 | [L'i' ] = { { .a = transition}, " insert" }, 42 | [L'j' ] = { { .a = mov}, "j" }, 43 | [L'k' ] = { { .a = mov}, "k" }, 44 | [L'l' ] = { { .a = mov}, "l" }, 45 | [L'n' ] = { { .v = next}, NULL }, 46 | [L't' ] = { { .v = new_tabstop}, NULL }, 47 | [L'v' ] = { { .v = set_layout}, NULL }, 48 | [L'x' ] = { { .v = prune}, NULL }, 49 | }; 50 | 51 | struct handler code_keys[KEY_MAX - KEY_MIN + 1] = { 52 | [KEY_RESIZE - KEY_MIN] = { { .v = reshape_root }, NULL }, 53 | [KEY_F(1) - KEY_MIN] = { { .a = send }, "\x01\033" }, 54 | [KEY_F(2) - KEY_MIN] = { { .a = send }, "\x03\033OQ" }, 55 | [KEY_F(3) - KEY_MIN] = { { .a = send }, "\x03\033OR" }, 56 | [KEY_F(4) - KEY_MIN] = { { .a = send }, "\x03\033OS" }, 57 | [KEY_F(5) - KEY_MIN] = { { .a = send }, "\x05\033[15~" }, 58 | [KEY_F(6) - KEY_MIN] = { { .a = send }, "\x05\033[17~" }, 59 | [KEY_F(7) - KEY_MIN] = { { .a = send }, "\x05\033[18~" }, 60 | [KEY_F(8) - KEY_MIN] = { { .a = send }, "\x05\033[19~" }, 61 | [KEY_F(9) - KEY_MIN] = { { .a = send }, "\x05\033[20~" }, 62 | [KEY_F(10) - KEY_MIN] = { { .a = send }, "\x05\033[21~" }, 63 | [KEY_F(11) - KEY_MIN] = { { .a = send }, "\x05\033[23~" }, 64 | [KEY_F(12) - KEY_MIN] = { { .a = send }, "\x05\033[24~" }, 65 | [KEY_HOME - KEY_MIN] = { { .a = send }, "\x04\033[1~" }, 66 | [KEY_END - KEY_MIN] = { { .a = send }, "\x04\033[4~" }, 67 | [KEY_PPAGE - KEY_MIN] = { { .a = send }, "\x04\033[5~" }, 68 | [KEY_NPAGE - KEY_MIN] = { { .a = send }, "\x04\033[6~" }, 69 | [KEY_BACKSPACE - KEY_MIN] = { { .a = send }, "\x01\177" }, 70 | [KEY_DC - KEY_MIN] = { { .a = send }, "\x04\033[3~" }, 71 | [KEY_IC - KEY_MIN] = { { .a = send }, "\x04\033[2~" }, 72 | [KEY_BTAB - KEY_MIN] = { { .a = send }, "\x04\033[Z" }, 73 | [KEY_ENTER - KEY_MIN] = { { .v = send_cr }, NULL }, 74 | [KEY_UP - KEY_MIN] = { { .a = sendarrow }, "A" }, 75 | [KEY_DOWN - KEY_MIN] = { { .a = sendarrow }, "B" }, 76 | [KEY_RIGHT - KEY_MIN] = { { .a = sendarrow }, "C" }, 77 | [KEY_LEFT - KEY_MIN] = { { .a = sendarrow }, "D" }, 78 | }; 79 | 80 | struct handler k1[128] = { 81 | [0x00] = { .act = { .a = send }, "\x01\x00" }, 82 | [0x01] = { .act = { .a = send }, "\x01\x01" }, 83 | [0x02] = { .act = { .a = send }, "\x01\x02" }, 84 | [0x03] = { .act = { .a = send }, "\x01\x03" }, 85 | [0x04] = { .act = { .a = send }, "\x01\x04" }, 86 | [0x05] = { .act = { .a = send }, "\x01\x05" }, 87 | [0x06] = { .act = { .a = send }, "\x01\x06" }, 88 | [0x07] = { .act = { .a = send }, "\x01\x07" }, 89 | [0x08] = { .act = { .a = send }, "\x01\x08" }, 90 | [0x09] = { .act = { .a = send }, "\x01\x09" }, 91 | [0x0a] = { .act = { .a = send }, "\x01\x0a" }, 92 | [0x0b] = { .act = { .a = send }, "\x01\x0b" }, 93 | [0x0c] = { .act = { .a = send }, "\x01\x0c" }, 94 | [0x0d] = { .act = { .v = send_cr }, NULL }, 95 | [0x0e] = { .act = { .a = send }, "\x01\x0e" }, 96 | [0x0f] = { .act = { .a = send }, "\x01\x0f" }, 97 | 98 | [0x10] = { .act = { .a = send }, "\x01\x10" }, 99 | [0x11] = { .act = { .a = send }, "\x01\x11" }, 100 | [0x12] = { .act = { .a = send }, "\x01\x12" }, 101 | [0x13] = { .act = { .a = send }, "\x01\x13" }, 102 | [0x14] = { .act = { .a = send }, "\x01\x14" }, 103 | [0x15] = { .act = { .a = send }, "\x01\x15" }, 104 | [0x16] = { .act = { .a = send }, "\x01\x16" }, 105 | [0x17] = { .act = { .a = send }, "\x01\x17" }, 106 | [0x18] = { .act = { .a = send }, "\x01\x18" }, 107 | [0x19] = { .act = { .a = send }, "\x01\x19" }, 108 | [0x1a] = { .act = { .a = send }, "\x01\x1a" }, 109 | [0x1b] = { .act = { .a = send }, "\x01\x1b" }, 110 | [0x1c] = { .act = { .a = send }, "\x01\x1c" }, 111 | [0x1d] = { .act = { .a = send }, "\x01\x1d" }, 112 | [0x1e] = { .act = { .a = send }, "\x01\x1e" }, 113 | [0x1f] = { .act = { .a = send }, "\x01\x1f" }, 114 | 115 | [0x20] = { .act = { .a = send }, "\x01\x20" }, 116 | [0x21] = { .act = { .a = send }, "\x01\x21" }, 117 | [0x22] = { .act = { .a = send }, "\x01\x22" }, 118 | [0x23] = { .act = { .a = send }, "\x01\x23" }, 119 | [0x24] = { .act = { .a = send }, "\x01\x24" }, 120 | [0x25] = { .act = { .a = send }, "\x01\x25" }, 121 | [0x26] = { .act = { .a = send }, "\x01\x26" }, 122 | [0x27] = { .act = { .a = send }, "\x01\x27" }, 123 | [0x28] = { .act = { .a = send }, "\x01\x28" }, 124 | [0x29] = { .act = { .a = send }, "\x01\x29" }, 125 | [0x2a] = { .act = { .a = send }, "\x01\x2a" }, 126 | [0x2b] = { .act = { .a = send }, "\x01\x2b" }, 127 | [0x2c] = { .act = { .a = send }, "\x01\x2c" }, 128 | [0x2d] = { .act = { .a = send }, "\x01\x2d" }, 129 | [0x2e] = { .act = { .a = send }, "\x01\x2e" }, 130 | [0x2f] = { .act = { .a = send }, "\x01\x2f" }, 131 | 132 | [0x30] = { .act = { .a = send }, "\x01\x30" }, 133 | [0x31] = { .act = { .a = send }, "\x01\x31" }, 134 | [0x32] = { .act = { .a = send }, "\x01\x32" }, 135 | [0x33] = { .act = { .a = send }, "\x01\x33" }, 136 | [0x34] = { .act = { .a = send }, "\x01\x34" }, 137 | [0x35] = { .act = { .a = send }, "\x01\x35" }, 138 | [0x36] = { .act = { .a = send }, "\x01\x36" }, 139 | [0x37] = { .act = { .a = send }, "\x01\x37" }, 140 | [0x38] = { .act = { .a = send }, "\x01\x38" }, 141 | [0x39] = { .act = { .a = send }, "\x01\x39" }, 142 | [0x3a] = { .act = { .a = send }, "\x01\x3a" }, 143 | [0x3b] = { .act = { .a = send }, "\x01\x3b" }, 144 | [0x3c] = { .act = { .a = send }, "\x01\x3c" }, 145 | [0x3d] = { .act = { .a = send }, "\x01\x3d" }, 146 | [0x3e] = { .act = { .a = send }, "\x01\x3e" }, 147 | [0x3f] = { .act = { .a = send }, "\x01\x3f" }, 148 | 149 | [0x40] = { .act = { .a = send }, "\x01\x40" }, 150 | [0x41] = { .act = { .a = send }, "\x01\x41" }, 151 | [0x42] = { .act = { .a = send }, "\x01\x42" }, 152 | [0x43] = { .act = { .a = send }, "\x01\x43" }, 153 | [0x44] = { .act = { .a = send }, "\x01\x44" }, 154 | [0x45] = { .act = { .a = send }, "\x01\x45" }, 155 | [0x46] = { .act = { .a = send }, "\x01\x46" }, 156 | [0x47] = { .act = { .a = send }, "\x01\x47" }, 157 | [0x48] = { .act = { .a = send }, "\x01\x48" }, 158 | [0x49] = { .act = { .a = send }, "\x01\x49" }, 159 | [0x4a] = { .act = { .a = send }, "\x01\x4a" }, 160 | [0x4b] = { .act = { .a = send }, "\x01\x4b" }, 161 | [0x4c] = { .act = { .a = send }, "\x01\x4c" }, 162 | [0x4d] = { .act = { .a = send }, "\x01\x4d" }, 163 | [0x4e] = { .act = { .a = send }, "\x01\x4e" }, 164 | [0x4f] = { .act = { .a = send }, "\x01\x4f" }, 165 | 166 | [0x50] = { .act = { .a = send }, "\x01\x50" }, 167 | [0x51] = { .act = { .a = send }, "\x01\x51" }, 168 | [0x52] = { .act = { .a = send }, "\x01\x52" }, 169 | [0x53] = { .act = { .a = send }, "\x01\x53" }, 170 | [0x54] = { .act = { .a = send }, "\x01\x54" }, 171 | [0x55] = { .act = { .a = send }, "\x01\x55" }, 172 | [0x56] = { .act = { .a = send }, "\x01\x56" }, 173 | [0x57] = { .act = { .a = send }, "\x01\x57" }, 174 | [0x58] = { .act = { .a = send }, "\x01\x58" }, 175 | [0x59] = { .act = { .a = send }, "\x01\x59" }, 176 | [0x5a] = { .act = { .a = send }, "\x01\x5a" }, 177 | [0x5b] = { .act = { .a = send }, "\x01\x5b" }, 178 | [0x5c] = { .act = { .a = send }, "\x01\x5c" }, 179 | [0x5d] = { .act = { .a = send }, "\x01\x5d" }, 180 | [0x5e] = { .act = { .a = send }, "\x01\x5e" }, 181 | [0x5f] = { .act = { .a = send }, "\x01\x5f" }, 182 | 183 | [0x60] = { .act = { .a = send }, "\x01\x60" }, 184 | [0x61] = { .act = { .a = send }, "\x01\x61" }, 185 | [0x62] = { .act = { .a = send }, "\x01\x62" }, 186 | [0x63] = { .act = { .a = send }, "\x01\x63" }, 187 | [0x64] = { .act = { .a = send }, "\x01\x64" }, 188 | [0x65] = { .act = { .a = send }, "\x01\x65" }, 189 | [0x66] = { .act = { .a = send }, "\x01\x66" }, 190 | [0x67] = { .act = { .a = send }, "\x01\x67" }, 191 | [0x68] = { .act = { .a = send }, "\x01\x68" }, 192 | [0x69] = { .act = { .a = send }, "\x01\x69" }, 193 | [0x6a] = { .act = { .a = send }, "\x01\x6a" }, 194 | [0x6b] = { .act = { .a = send }, "\x01\x6b" }, 195 | [0x6c] = { .act = { .a = send }, "\x01\x6c" }, 196 | [0x6d] = { .act = { .a = send }, "\x01\x6d" }, 197 | [0x6e] = { .act = { .a = send }, "\x01\x6e" }, 198 | [0x6f] = { .act = { .a = send }, "\x01\x6f" }, 199 | 200 | [0x70] = { .act = { .a = send }, "\x01\x70" }, 201 | [0x71] = { .act = { .a = send }, "\x01\x71" }, 202 | [0x72] = { .act = { .a = send }, "\x01\x72" }, 203 | [0x73] = { .act = { .a = send }, "\x01\x73" }, 204 | [0x74] = { .act = { .a = send }, "\x01\x74" }, 205 | [0x75] = { .act = { .a = send }, "\x01\x75" }, 206 | [0x76] = { .act = { .a = send }, "\x01\x76" }, 207 | [0x77] = { .act = { .a = send }, "\x01\x77" }, 208 | [0x78] = { .act = { .a = send }, "\x01\x78" }, 209 | [0x79] = { .act = { .a = send }, "\x01\x79" }, 210 | [0x7a] = { .act = { .a = send }, "\x01\x7a" }, 211 | [0x7b] = { .act = { .a = send }, "\x01\x7b" }, 212 | [0x7c] = { .act = { .a = send }, "\x01\x7c" }, 213 | [0x7d] = { .act = { .a = send }, "\x01\x7d" }, 214 | [0x7e] = { .act = { .a = send }, "\x01\x7e" }, 215 | [0x7f] = { .act = { .a = send }, "\x01\x7f" }, 216 | }; 217 | 218 | struct handler k2[128] = { 219 | [0x00] = { .act = { .a = send }, "\x01\x00" }, 220 | [0x01] = { .act = { .a = send }, "\x01\x01" }, 221 | [0x02] = { .act = { .a = send }, "\x01\x02" }, 222 | [0x03] = { .act = { .a = send }, "\x01\x03" }, 223 | [0x04] = { .act = { .a = send }, "\x01\x04" }, 224 | [0x05] = { .act = { .a = send }, "\x01\x05" }, 225 | [0x06] = { .act = { .a = send }, "\x01\x06" }, 226 | [0x07] = { .act = { .a = send }, "\x01\x07" }, 227 | [0x08] = { .act = { .a = send }, "\x01\x08" }, 228 | [0x09] = { .act = { .a = send }, "\x01\x09" }, 229 | [0x0a] = { .act = { .a = send }, "\x01\x0a" }, 230 | [0x0b] = { .act = { .a = send }, "\x01\x0b" }, 231 | [0x0c] = { .act = { .a = send }, "\x01\x0c" }, 232 | [0x0d] = { .act = { .a = transition }, "\ncontrol" }, 233 | [0x0e] = { .act = { .a = send }, "\x01\x0e" }, 234 | [0x0f] = { .act = { .a = send }, "\x01\x0f" }, 235 | 236 | [0x10] = { .act = { .a = send }, "\x01\x10" }, 237 | [0x11] = { .act = { .a = send }, "\x01\x11" }, 238 | [0x12] = { .act = { .a = send }, "\x01\x12" }, 239 | [0x13] = { .act = { .a = send }, "\x01\x13" }, 240 | [0x14] = { .act = { .a = send }, "\x01\x14" }, 241 | [0x15] = { .act = { .a = send }, "\x01\x15" }, 242 | [0x16] = { .act = { .a = send }, "\x01\x16" }, 243 | [0x17] = { .act = { .a = send }, "\x01\x17" }, 244 | [0x18] = { .act = { .a = send }, "\x01\x18" }, 245 | [0x19] = { .act = { .a = send }, "\x01\x19" }, 246 | [0x1a] = { .act = { .a = send }, "\x01\x1a" }, 247 | [0x1b] = { .act = { .a = send }, "\x01\x1b" }, 248 | [0x1c] = { .act = { .a = send }, "\x01\x1c" }, 249 | [0x1d] = { .act = { .a = send }, "\x01\x1d" }, 250 | [0x1e] = { .act = { .a = send }, "\x01\x1e" }, 251 | [0x1f] = { .act = { .a = send }, "\x01\x1f" }, 252 | 253 | [0x20] = { .act = { .a = send }, "\x01\x20" }, 254 | [0x21] = { .act = { .a = send }, "\x01\x21" }, 255 | [0x22] = { .act = { .a = send }, "\x01\x22" }, 256 | [0x23] = { .act = { .a = send }, "\x01\x23" }, 257 | [0x24] = { .act = { .a = send }, "\x01\x24" }, 258 | [0x25] = { .act = { .a = send }, "\x01\x25" }, 259 | [0x26] = { .act = { .a = send }, "\x01\x26" }, 260 | [0x27] = { .act = { .a = send }, "\x01\x27" }, 261 | [0x28] = { .act = { .a = send }, "\x01\x28" }, 262 | [0x29] = { .act = { .a = send }, "\x01\x29" }, 263 | [0x2a] = { .act = { .a = send }, "\x01\x2a" }, 264 | [0x2b] = { .act = { .a = send }, "\x01\x2b" }, 265 | [0x2c] = { .act = { .a = send }, "\x01\x2c" }, 266 | [0x2d] = { .act = { .a = send }, "\x01\x2d" }, 267 | [0x2e] = { .act = { .a = send }, "\x01\x2e" }, 268 | [0x2f] = { .act = { .a = send }, "\x01\x2f" }, 269 | 270 | [0x30] = { .act = { .a = send }, "\x01\x30" }, 271 | [0x31] = { .act = { .a = send }, "\x01\x31" }, 272 | [0x32] = { .act = { .a = send }, "\x01\x32" }, 273 | [0x33] = { .act = { .a = send }, "\x01\x33" }, 274 | [0x34] = { .act = { .a = send }, "\x01\x34" }, 275 | [0x35] = { .act = { .a = send }, "\x01\x35" }, 276 | [0x36] = { .act = { .a = send }, "\x01\x36" }, 277 | [0x37] = { .act = { .a = send }, "\x01\x37" }, 278 | [0x38] = { .act = { .a = send }, "\x01\x38" }, 279 | [0x39] = { .act = { .a = send }, "\x01\x39" }, 280 | [0x3a] = { .act = { .a = send }, "\x01\x3a" }, 281 | [0x3b] = { .act = { .a = send }, "\x01\x3b" }, 282 | [0x3c] = { .act = { .a = send }, "\x01\x3c" }, 283 | [0x3d] = { .act = { .a = send }, "\x01\x3d" }, 284 | [0x3e] = { .act = { .a = send }, "\x01\x3e" }, 285 | [0x3f] = { .act = { .a = send }, "\x01\x3f" }, 286 | 287 | [0x40] = { .act = { .a = send }, "\x01\x40" }, 288 | [0x41] = { .act = { .a = send }, "\x01\x41" }, 289 | [0x42] = { .act = { .a = send }, "\x01\x42" }, 290 | [0x43] = { .act = { .a = send }, "\x01\x43" }, 291 | [0x44] = { .act = { .a = send }, "\x01\x44" }, 292 | [0x45] = { .act = { .a = send }, "\x01\x45" }, 293 | [0x46] = { .act = { .a = send }, "\x01\x46" }, 294 | [0x47] = { .act = { .a = send }, "\x01\x47" }, 295 | [0x48] = { .act = { .a = send }, "\x01\x48" }, 296 | [0x49] = { .act = { .a = send }, "\x01\x49" }, 297 | [0x4a] = { .act = { .a = send }, "\x01\x4a" }, 298 | [0x4b] = { .act = { .a = send }, "\x01\x4b" }, 299 | [0x4c] = { .act = { .a = send }, "\x01\x4c" }, 300 | [0x4d] = { .act = { .a = send }, "\x01\x4d" }, 301 | [0x4e] = { .act = { .a = send }, "\x01\x4e" }, 302 | [0x4f] = { .act = { .a = send }, "\x01\x4f" }, 303 | 304 | [0x50] = { .act = { .a = send }, "\x01\x50" }, 305 | [0x51] = { .act = { .a = send }, "\x01\x51" }, 306 | [0x52] = { .act = { .a = send }, "\x01\x52" }, 307 | [0x53] = { .act = { .a = send }, "\x01\x53" }, 308 | [0x54] = { .act = { .a = send }, "\x01\x54" }, 309 | [0x55] = { .act = { .a = send }, "\x01\x55" }, 310 | [0x56] = { .act = { .a = send }, "\x01\x56" }, 311 | [0x57] = { .act = { .a = send }, "\x01\x57" }, 312 | [0x58] = { .act = { .a = send }, "\x01\x58" }, 313 | [0x59] = { .act = { .a = send }, "\x01\x59" }, 314 | [0x5a] = { .act = { .a = send }, "\x01\x5a" }, 315 | [0x5b] = { .act = { .a = send }, "\x01\x5b" }, 316 | [0x5c] = { .act = { .a = send }, "\x01\x5c" }, 317 | [0x5d] = { .act = { .a = send }, "\x01\x5d" }, 318 | [0x5e] = { .act = { .a = send }, "\x01\x5e" }, 319 | [0x5f] = { .act = { .a = send }, "\x01\x5f" }, 320 | 321 | [0x60] = { .act = { .a = send }, "\x01\x60" }, 322 | [0x61] = { .act = { .a = send }, "\x01\x61" }, 323 | [0x62] = { .act = { .a = send }, "\x01\x62" }, 324 | [0x63] = { .act = { .a = send }, "\x01\x63" }, 325 | [0x64] = { .act = { .a = send }, "\x01\x64" }, 326 | [0x65] = { .act = { .a = send }, "\x01\x65" }, 327 | [0x66] = { .act = { .a = send }, "\x01\x66" }, 328 | [0x67] = { .act = { .a = send }, "\x01\x67" }, 329 | [0x68] = { .act = { .a = send }, "\x01\x68" }, 330 | [0x69] = { .act = { .a = send }, "\x01\x69" }, 331 | [0x6a] = { .act = { .a = send }, "\x01\x6a" }, 332 | [0x6b] = { .act = { .a = send }, "\x01\x6b" }, 333 | [0x6c] = { .act = { .a = send }, "\x01\x6c" }, 334 | [0x6d] = { .act = { .a = send }, "\x01\x6d" }, 335 | [0x6e] = { .act = { .a = send }, "\x01\x6e" }, 336 | [0x6f] = { .act = { .a = send }, "\x01\x6f" }, 337 | 338 | [0x70] = { .act = { .a = send }, "\x01\x70" }, 339 | [0x71] = { .act = { .a = send }, "\x01\x71" }, 340 | [0x72] = { .act = { .a = send }, "\x01\x72" }, 341 | [0x73] = { .act = { .a = send }, "\x01\x73" }, 342 | [0x74] = { .act = { .a = send }, "\x01\x74" }, 343 | [0x75] = { .act = { .a = send }, "\x01\x75" }, 344 | [0x76] = { .act = { .a = send }, "\x01\x76" }, 345 | [0x77] = { .act = { .a = send }, "\x01\x77" }, 346 | [0x78] = { .act = { .a = send }, "\x01\x78" }, 347 | [0x79] = { .act = { .a = send }, "\x01\x79" }, 348 | [0x7a] = { .act = { .a = send }, "\x01\x7a" }, 349 | [0x7b] = { .act = { .a = send }, "\x01\x7b" }, 350 | [0x7c] = { .act = { .a = send }, "\x01\x7c" }, 351 | [0x7d] = { .act = { .a = send }, "\x01\x7d" }, 352 | [0x7e] = { .act = { .a = send }, "\x01\x7e" }, 353 | [0x7f] = { .act = { .a = send }, "\x01\x7f" }, 354 | }; 355 | -------------------------------------------------------------------------------- /build-aux/package-version: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Find a version. 4 | cd $( dirname $0 )/.. || exit 1 5 | 6 | GIT_CEILING_DIRECTORIES=$(cd ..; pwd -P) 7 | export GIT_CEILING_DIRECTORIES 8 | 9 | # Take version from git if we can... 10 | v=$(git describe --always --dirty --match 'v*' 2>/dev/null) 11 | if test -n "$v"; then 12 | printf "%s" "${v#v}" | tr - . 13 | # else from the version file (eg in a dist tarball) 14 | elif v=$(cat version); test -n "$v"; then 15 | printf "%s" "$v" 16 | else 17 | printf unknown 18 | fi 19 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | # -*- Autoconf -*- 2 | # Process this file with autoconf to produce a configure script. 3 | 4 | AC_PREREQ([2.69]) 5 | AC_INIT([smtx], 6 | m4_esyscmd([./build-aux/package-version]), 7 | [william.r.pursell@gmail.com]) 8 | AM_INIT_AUTOMAKE([-Wall -Werror foreign]) 9 | AM_PROG_AR 10 | LT_INIT 11 | AC_CONFIG_MACRO_DIRS([m4]) 12 | AC_CONFIG_HEADERS([config.h]) 13 | 14 | AC_PROG_CC 15 | AC_PROG_CC_STDC 16 | AC_CHECK_HEADERS([unistd.h util.h libutil.h termios.h pty.h wchar.h wctype.h]) 17 | AC_CHECK_HEADERS([curses.h ncursesw/curses.h]) 18 | AC_CHECK_DECL([A_ITALIC],AC_DEFINE([HAVE_A_ITALIC],[1],[ ]),[],[[#include ]]) 19 | AC_TYPE_SIZE_T 20 | AC_TYPE_SSIZE_T 21 | 22 | AC_FUNC_REALLOC 23 | AC_SEARCH_LIBS([endwin],[ncursesw ncurses],[],AC_MSG_ERROR([unable to find ncurses library])) 24 | AC_SEARCH_LIBS([forkpty],[util],[],AC_MSG_ERROR([unable to find util library])) 25 | AC_CHECK_FUNC([alloc_pair],AC_DEFINE([HAVE_ALLOC_PAIR],[1],[ ])) 26 | 27 | 28 | AM_CPPFLAGS='-D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=600 -D_XOPEN_SOURCE_EXTENDED' 29 | AC_CANONICAL_HOST 30 | case "${host_os}" in 31 | darwin*) AM_CPPFLAGS="${AM_CPPFLAGS} -D_DARWIN_C_SOURCE" ;; 32 | esac 33 | 34 | 35 | AC_SUBST([AM_CPPFLAGS]) 36 | AC_CONFIG_FILES([Makefile]) 37 | AC_OUTPUT 38 | -------------------------------------------------------------------------------- /cset.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include "smtx.h" 19 | 20 | wchar_t CSET_US[0x7f]; /* "USASCII" */ 21 | 22 | /* 23 | * Avoid unicode except when NDEBUG is defined because 24 | * testing is easier. More thought needs to go here. 25 | */ 26 | #if (defined(__STDC_ISO_10646__) || defined(WCHAR_IS_UNICODE)) \ 27 | && defined(NDEBUG) 28 | 29 | wchar_t CSET_UK[0x7f] = { [L'#'] = 0x00a3 }; 30 | wchar_t CSET_GRAPH[0x7f] = { /* Graphics Set One */ 31 | [L'-'] = 0x2191, [L'a'] = 0x2592, [L'k'] = 0x2510, [L'u'] = 0x2524, 32 | [L'}'] = 0x00a3, [L'b'] = 0x2409, [L'l'] = 0x250c, [L'v'] = 0x2534, 33 | [L'~'] = 0x00b7, [L'c'] = 0x240c, [L'm'] = 0x2514, [L'w'] = 0x252c, 34 | [L'{'] = 0x03c0, [L'd'] = 0x240d, [L'n'] = 0x253c, [L'x'] = 0x2502, 35 | [L','] = 0x2190, [L'e'] = 0x240a, [L'o'] = 0x23ba, [L'y'] = 0x2264, 36 | [L'+'] = 0x2192, [L'f'] = 0x00b0, [L'p'] = 0x23bb, [L'z'] = 0x2265, 37 | [L'.'] = 0x2193, [L'g'] = 0x00b1, [L'q'] = 0x2500, [L'_'] = L' ', 38 | [L'|'] = 0x2260, [L'h'] = 0x2592, [L'r'] = 0x23bc, [L'0'] = 0x25ae, 39 | [L'>'] = 0x2265, [L'i'] = 0x2603, [L's'] = 0x23bd, 40 | [L'`'] = 0x25c6, [L'j'] = 0x2518, [L't'] = 0x251c, 41 | }; 42 | 43 | #else /* wchar_t doesn't map to Unicode */ 44 | 45 | wchar_t CSET_UK[0x7f] = { [L'#'] = L'&' }; 46 | wchar_t CSET_GRAPH[0x7f] = { /* Graphics Set One */ 47 | [L'-'] = '^', [L'a'] = L':', [L'k'] = L'+', [L'u'] = L'+', 48 | [L'}'] = L'&', [L'b'] = L' ', [L'l'] = L'+', [L'v'] = L'+', 49 | [L'~'] = L'o', [L'c'] = L' ', [L'm'] = L'+', [L'w'] = L'+', 50 | [L'{'] = L'p', [L'd'] = L' ', [L'n'] = '+', [L'x'] = L'|', 51 | [L','] = L'<', [L'e'] = L' ', [L'o'] = L'-', [L'y'] = L'<', 52 | [L'+'] = L'>', [L'f'] = L'\'', [L'p'] = L'-', [L'z'] = L'>', 53 | [L'.'] = L'v', [L'g'] = L'#', [L'q'] = L'-', [L'_'] = L' ', 54 | [L'|'] = L'!', [L'h'] = L'#', [L'r'] = L'-', [L'0'] = L'#', 55 | [L'>'] = L'>', [L'i'] = L'i', [L's'] = L'_', 56 | [L'`'] = L'+', [L'j'] = L'+', [L't'] = L'+', 57 | }; 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /handler.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include "smtx.h" 19 | 20 | void 21 | set_status(struct pty *p, const char *arg) 22 | { 23 | snprintf(p->status, sizeof p->status, "%s", arg); 24 | } 25 | 26 | static void 27 | restore_cursor(struct screen *s) 28 | { 29 | if (s->sc.gc) { 30 | s->c = s->sc; 31 | #if HAVE_ALLOC_PAIR 32 | s->c.p = alloc_pair(s->c.color[0], s->c.color[1]); 33 | #endif 34 | wattr_set(s->w, s->c.attr, s->c.p, NULL); 35 | wbkgrndset(s->w, &s->c.bkg); 36 | } 37 | } 38 | 39 | static void 40 | save_cursor(struct screen *s) 41 | { 42 | wattr_get(s->w, &s->c.attr, &s->c.p, NULL); 43 | pair_content(s->c.p, s->sc.color, s->sc.color + 1); 44 | s->sc = s->c; 45 | } 46 | 47 | static void 48 | reset_sgr(struct screen *s) 49 | { 50 | pair_content(s->c.p = COLOR_PAIR(0), s->c.color, s->c.color + 1); 51 | setcchar(&s->c.bkg, L" ", A_NORMAL, s->c.p, NULL); 52 | wattr_set(s->w, A_NORMAL, s->c.p, NULL); 53 | wbkgrndset(s->w, &s->c.bkg); 54 | } 55 | 56 | static void 57 | newline(struct screen *s, int cr) 58 | { 59 | if (cr) { 60 | s->c.xenl = s->c.x = 0; 61 | } 62 | if (s->c.y == s->scroll.bot) { 63 | scroll(s->w); 64 | } else { 65 | wmove(s->w, ++s->c.y, s->c.x); 66 | } 67 | } 68 | 69 | static void 70 | print_char(wchar_t w, struct pty *p) 71 | { 72 | if (p->s->insert) { 73 | wins_wch(p->s->w, &p->s->c.bkg); 74 | } 75 | if (p->s->c.xenl && p->s->decawm) { 76 | newline(p->s, 1); 77 | } 78 | if (w < 0x7f && p->s->c.gc[w]) { 79 | w = p->s->c.gc[w]; 80 | } 81 | if (p->s->c.x >= p->ws.ws_col - wcwidth(w)) { 82 | p->s->c.xenl = 1; 83 | wins_nwstr(p->s->w, &w, 1); 84 | } else { 85 | p->s->c.xenl = 0; 86 | waddnwstr(p->s->w, &w, 1); 87 | p->s->c.x += wcwidth(w); 88 | } 89 | p->s->c.gc = p->s->c.gs; 90 | } 91 | 92 | static short colors[] = { 93 | COLOR_BLACK, 94 | COLOR_RED, 95 | COLOR_GREEN, 96 | COLOR_YELLOW, 97 | COLOR_BLUE, 98 | COLOR_MAGENTA, 99 | COLOR_CYAN, 100 | COLOR_WHITE, 101 | }; 102 | 103 | static int attrs[] = { 104 | [1] = A_BOLD, 105 | [2] = A_DIM, 106 | #if HAVE_A_ITALIC 107 | [3] = A_ITALIC, 108 | #else 109 | [3] = 0, 110 | #endif 111 | [4] = A_UNDERLINE, 112 | [5] = A_BLINK, 113 | [7] = A_REVERSE, 114 | [8] = A_INVIS, 115 | }; 116 | 117 | void 118 | tput(struct pty *p, wchar_t w, wchar_t iw, int argc, int *argv, int handler) 119 | { 120 | int i, t1; 121 | 122 | /* First arg, defaulting to 0 or 1 */ 123 | int p0[] = { argc ? *argv : 0, argc ? *argv : 1 }; 124 | struct screen *s = p->s; /* the current SCRN buffer */ 125 | WINDOW *win = s->w; /* the current window */ 126 | 127 | const int tos = p->tos; 128 | const int y = s->c.y - tos; /* cursor position w.r.t. top of screen */ 129 | const int bot = s->scroll.bot - tos + 1; 130 | const int top = MAX(0, s->scroll.top - tos); 131 | const int dtop = tos + (p->decom ? top : 0); 132 | 133 | switch ((enum cmd)handler) { 134 | case ack: 135 | rewrite(p->fd, "\006", 1); 136 | break; 137 | case bell: 138 | beep(); 139 | break; 140 | case cr: 141 | s->c.x = 0; 142 | break; 143 | case csr: 144 | t1 = argc > 1 ? argv[1] : p->ws.ws_row; 145 | set_scroll(s, tos + p0[1] - 1, tos + t1 - 1); 146 | s->c.y = dtop; 147 | s->c.x = 0; 148 | break; 149 | case cub: 150 | s->c.x -= p0[1]; 151 | break; 152 | case cud: 153 | s->c.y += p0[1]; 154 | break; 155 | case cuf: 156 | s->c.x += p0[1]; 157 | break; 158 | case cup: 159 | s->c.y = dtop + p0[1] - 1; 160 | s->c.x = argc > 1 ? argv[1] - 1 : 0; 161 | break; 162 | case cuu: 163 | s->c.y -= p0[1]; 164 | break; 165 | case dch: 166 | for (i = 0; i < p0[1]; i++) { 167 | wdelch(win); 168 | } 169 | break; 170 | case ech: 171 | for (i = 0; i < p0[1]; i++) { 172 | mvwadd_wch(win, s->c.y, s->c.x + i, &s->c.bkg); 173 | } 174 | break; 175 | case ed: /* Fallthru */ 176 | case el: 177 | switch (p0[0]) { 178 | case 2: 179 | wmove(win, handler == el ? s->c.y : tos, 0); 180 | /* Fall Thru */ 181 | case 0: 182 | (handler == el ? wclrtoeol : wclrtobot)(win); 183 | break; 184 | case 3: 185 | if (handler == ed) { 186 | werase(win); 187 | } 188 | break; 189 | case 1: 190 | if (handler == ed) { 191 | for (i = tos; i < s->c.y; i++) { 192 | wmove(win, i, 0); 193 | wclrtoeol(win); 194 | } 195 | wmove(win, s->c.y, s->c.x); 196 | } 197 | for (i = 0; i <= s->c.x; i++) { 198 | mvwadd_wch(win, s->c.y, i, &s->c.bkg); 199 | } 200 | } 201 | break; 202 | case hpa: 203 | s->c.x = -1; 204 | /* Fallthru */ 205 | case hpr: 206 | s->c.x += p0[1]; 207 | break; 208 | case hts: 209 | p->tabs[s->c.x] = true; 210 | break; 211 | case ich: 212 | for (i = 0; i < p0[1]; i++) { 213 | wins_wch(win, &p->s->c.bkg); 214 | } 215 | break; 216 | case idl: 217 | /* We don't use insdelln here because it inserts above and 218 | not below, and has a few other edge cases. */ 219 | i = MIN(p0[1], p->ws.ws_row - 1 - y); 220 | 221 | assert( y == s->c.y - tos); 222 | assert( tos == 0 || p->ws.ws_row - 1 - y == s->maxy - s->c.y ); 223 | 224 | wsetscrreg(win, s->c.y, s->scroll.bot); 225 | wscrl(win, w == L'L' ? -i : i); 226 | wsetscrreg(win, s->scroll.top, s->scroll.bot); 227 | s->c.x = 0; 228 | break; 229 | case numkp: 230 | p->pnm = (w == L'='); 231 | break; 232 | case osc: 233 | assert( 0 ); 234 | break; 235 | case rc: 236 | if (iw == L'#' ) for (int r = 0; r < p->ws.ws_row; r++) { 237 | cchar_t e; 238 | setcchar(&e, L"E", A_NORMAL, COLOR_PAIR(0), NULL); 239 | for (int c = 0; c < p->ws.ws_col; c++) { 240 | mvwadd_wch(p->s->w, tos + r, c, &e); 241 | } 242 | } 243 | restore_cursor(s); 244 | break; 245 | case ri: 246 | if (y == top) { 247 | wsetscrreg(win, MAX(s->scroll.top, tos), s->scroll.bot); 248 | wscrl(win, -1); 249 | wsetscrreg(win, s->scroll.top, s->scroll.bot); 250 | } else { 251 | s->c.y = MAX(tos, s->c.y - 1); 252 | } 253 | break; 254 | case sc: 255 | save_cursor(s); 256 | break; 257 | case su: 258 | wscrl(win, (w == L'T' || w == L'^') ? -p0[1] : p0[1]); 259 | break; 260 | case tab: 261 | for (i = 0; i < p0[1]; i += p->tabs[s->c.x] ? 1 : 0) { 262 | s->c.x += (w == L'Z' ? -1 : +1); 263 | } 264 | break; 265 | case tbc: 266 | switch (p0[0]) { 267 | case 0: 268 | p->tabs[s->c.x] = false; 269 | break; 270 | case 3: 271 | memset(p->tabs, 0, p->ws.ws_col * sizeof *p->tabs); 272 | } 273 | break; 274 | case vis: 275 | s->vis = iw != L'6'; 276 | break; 277 | case vpa: 278 | s->c.y = tos - 1; 279 | /* Fallthru */ 280 | case vpr: 281 | s->c.y = MAX(tos + top, p0[1] + s->c.y); 282 | break; 283 | case ris: 284 | ioctl(p->fd, TIOCGWINSZ, &p->ws); 285 | p->g[0] = p->g[2] = CSET_US; 286 | p->g[1] = p->g[3] = CSET_GRAPH; 287 | p->decom = s->insert = p->lnm = false; 288 | reset_sgr(s); 289 | s->decawm = p->pnm = true; 290 | for (i = 0, s = p->s = p->scr; i < 2; i++, s++) { 291 | s->c.gs = s->c.gc = CSET_US; 292 | s->vis = 1; 293 | set_scroll(s, 0, s->rows - 1); 294 | } 295 | set_tabs(p, p->tabstop); 296 | vtreset(&p->vp); 297 | break; 298 | case mode: 299 | for (i = 0; i < argc; i++) { 300 | bool set = (w == L'h'); 301 | switch (argv[i]) { 302 | case 1: 303 | p->pnm = set; 304 | break; 305 | case 3: 306 | set_width(set ? "132" : "80"); 307 | break; 308 | case 4: 309 | s->insert = set; 310 | break; 311 | case 6: 312 | p->decom = set; 313 | s->c.x = s->c.xenl = 0; 314 | s->c.y = dtop; 315 | break; 316 | case 7: 317 | s->decawm = set; 318 | break; 319 | case 20: 320 | p->lnm = set; 321 | break; 322 | case 25: 323 | s->vis = set ? 1 : 0; 324 | break; 325 | case 34: 326 | s->vis = set ? 1 : 2; 327 | break; 328 | case 1048: 329 | (set ? save_cursor : restore_cursor)(s); 330 | break; 331 | case 1049: 332 | (set ? save_cursor : restore_cursor)(s); 333 | /* fall thru */ 334 | case 47: 335 | case 1047: 336 | /* Switch to alternate screen */ 337 | if (set && p->s == p->scr) { 338 | struct screen *alt = p->scr + 1; 339 | alt->c.x = alt->c.xenl = 0; 340 | alt->c.y = dtop; 341 | wclear(alt->w); 342 | } 343 | p->s = p->scr + !!set; 344 | } 345 | } 346 | break; 347 | case sgr: 348 | { 349 | bool doc = false; 350 | if (!argc) { 351 | reset_sgr(s); 352 | } else for (i = 0; i < argc; i++) { 353 | int k = 1, a; 354 | switch (a = argv[i]) { 355 | case 0: 356 | reset_sgr(s); 357 | break; 358 | case 1: 359 | case 2: 360 | case 3: 361 | case 4: 362 | case 5: 363 | case 7: 364 | case 8: 365 | wattron(win, attrs[a]); 366 | break; 367 | case 21: 368 | case 22: 369 | case 23: 370 | case 24: 371 | case 25: 372 | case 27: 373 | wattroff(win, attrs[a - 20]); 374 | break; 375 | case 30: 376 | case 31: 377 | case 32: 378 | case 33: 379 | case 34: 380 | case 35: 381 | case 36: 382 | case 37: 383 | k = 0; /* Fallthru */ 384 | case 40: 385 | case 41: 386 | case 42: 387 | case 43: 388 | case 44: 389 | case 45: 390 | case 46: 391 | case 47: 392 | s->c.color[k] = colors[a - ( k ? 40 : 30 )]; 393 | doc = COLORS >= 8; 394 | break; 395 | case 38: 396 | case 48: 397 | if (argc > i + 2 && argv[i + 1] == 5){ 398 | s->c.color[a == 48] = argv[i + 2]; 399 | } 400 | i += 2; 401 | doc = COLORS >= 256; 402 | break; 403 | case 39: 404 | case 49: 405 | s->c.color[a == 49] = -1; 406 | doc = true; 407 | break; 408 | case 90: 409 | case 91: 410 | case 92: 411 | case 93: 412 | case 94: 413 | case 95: 414 | case 96: 415 | case 97: 416 | k = 0; /* Fallthru */ 417 | case 100: 418 | case 101: 419 | case 102: 420 | case 103: 421 | case 104: 422 | case 105: 423 | case 106: 424 | case 107: 425 | s->c.color[k] = colors[a - ( k ? 100 : 90 )]; 426 | doc = COLORS >= 16; 427 | } 428 | } 429 | if (doc) { 430 | #if HAVE_ALLOC_PAIR 431 | s->c.p = alloc_pair(s->c.color[0], s->c.color[1]); 432 | #endif 433 | wcolor_set(win, s->c.p, NULL); 434 | setcchar(&s->c.bkg, L" ", A_NORMAL, s->c.p, NULL); 435 | wbkgrndset(win, &s->c.bkg); 436 | } 437 | } 438 | break; 439 | case pnl: 440 | case nel: 441 | case ind: 442 | newline(s, handler == pnl ? p->lnm : handler == nel); 443 | break; 444 | case cpl: 445 | s->c.y = MAX(tos + top, s->c.y - p0[1]); 446 | s->c.x = 0; 447 | break; 448 | case cnl: 449 | s->c.y = MIN(tos + bot - 1, s->c.y + p0[1]); 450 | s->c.x = 0; 451 | break; 452 | case print: 453 | s->repc = w; 454 | /* Fallthru */ 455 | case rep: 456 | if (wcwidth(w = s->repc) > 0) { 457 | for (i = 0; i < p0[1]; i++) { 458 | print_char(w, p); 459 | } 460 | } 461 | break; 462 | case scs: 463 | for (const char *s = "()*+", *c = strchr(s, iw); c; c = NULL ) 464 | switch (w) { 465 | case L'A': 466 | p->g[c-s] = CSET_UK; 467 | break; 468 | case L'B': 469 | p->g[c-s] = CSET_US; 470 | break; 471 | case L'0': 472 | p->g[c-s] = CSET_GRAPH; 473 | break; 474 | case L'1': 475 | p->g[c-s] = CSET_US; 476 | break; 477 | case L'2': 478 | p->g[c-s] = CSET_GRAPH; 479 | } 480 | break; 481 | case so: 482 | for (char *s = "\x0f\x0e}|NO", *c = strchr(s, w); c; c = NULL) { 483 | switch (c - s) { 484 | case 0: /* locking shift */ 485 | case 1: 486 | case 2: 487 | case 3: 488 | p->s->c.gs = p->s->c.gc = p->g[c - s]; 489 | break; 490 | case 4: 491 | case 5: /* non-locking shift */ 492 | p->s->c.gs = p->s->c.gc; 493 | p->s->c.gc = p->g[c - s - 2]; 494 | } 495 | } 496 | } 497 | switch (handler) { 498 | case cr: 499 | case csr: 500 | case cub: 501 | case cup: 502 | case ris: 503 | p->s->c.xenl = 0; /*Fallthru */ 504 | default: 505 | p->s->repc = 0; 506 | break; 507 | case sgr: 508 | case print: 509 | ; 510 | } 511 | p->s->c.x = MAX(0, MIN(p->s->c.x, p->ws.ws_col - 1)); 512 | p->s->c.y = MAX(0, MIN(p->s->c.y, tos + bot - 1)); 513 | p->s->maxy = MAX(p->s->c.y, p->s->maxy); 514 | p->tos = MAX(0, p->s->maxy - p->ws.ws_row + 1); 515 | wmove(p->s->w, p->s->c.y, p->s->c.x); 516 | } 517 | 518 | #define CONTROL \ 519 | [0x05] = ack, \ 520 | [0x07] = bell, \ 521 | [0x08] = cub, \ 522 | [0x09] = tab, \ 523 | [0x0a] = pnl, \ 524 | [0x0b] = pnl, \ 525 | [0x0c] = pnl, \ 526 | [0x0d] = cr, \ 527 | [0x0e] = so, \ 528 | [0x0f] = so 529 | 530 | int cons[0x80] = { 531 | CONTROL, 532 | }; 533 | 534 | int csis[0x80] = { 535 | CONTROL, 536 | [L'A'] = cuu, 537 | [L'B'] = cud, 538 | [L'C'] = cuf, 539 | [L'D'] = cub, 540 | [L'E'] = cnl, 541 | [L'F'] = cpl, 542 | [L'G'] = hpa, 543 | [L'H'] = cup, 544 | [L'I'] = tab, 545 | [L'J'] = ed, 546 | [L'K'] = el, 547 | [L'L'] = idl, 548 | [L'M'] = idl, 549 | [L'P'] = dch, 550 | [L'S'] = su, 551 | [L'T'] = su, 552 | [L'X'] = ech, 553 | [L'Z'] = tab, 554 | [L'`'] = hpa, 555 | [L'^'] = su, 556 | [L'@'] = ich, 557 | [L'a'] = hpr, 558 | [L'b'] = rep, 559 | [L'd'] = vpa, 560 | [L'e'] = vpr, 561 | [L'f'] = cup, 562 | [L'g'] = tbc, 563 | [L'h'] = mode, 564 | [L'l'] = mode, 565 | [L'm'] = sgr, 566 | [L'r'] = csr, 567 | [L's'] = sc, 568 | [L'u'] = rc, 569 | }; 570 | 571 | int escs[0x80] = { 572 | CONTROL, 573 | [L'0'] = scs, 574 | [L'1'] = scs, 575 | [L'2'] = scs, 576 | [L'7'] = sc, 577 | [L'8'] = rc, 578 | [L'A'] = scs, 579 | [L'B'] = scs, 580 | [L'D'] = ind, 581 | [L'E'] = nel, 582 | [L'H'] = hts, 583 | [L'M'] = ri, 584 | [L'N'] = so, 585 | [L'O'] = so, 586 | [L'}'] = so, 587 | [L'|'] = so, 588 | [L'c'] = ris, 589 | [L'p'] = vis, 590 | [L'='] = numkp, 591 | [L'>'] = numkp, 592 | }; 593 | 594 | #pragma GCC diagnostic ignored "-Woverride-init" 595 | int oscs[0x80] = { 596 | [0 ... 0x7f] = osc, 597 | CONTROL, 598 | [0x07] = osc, 599 | [0x0a] = osc, 600 | [0x0d] = osc, 601 | }; 602 | 603 | int gnds[0x80] = { 604 | [0 ... 0x7f] = print, 605 | CONTROL, 606 | }; 607 | -------------------------------------------------------------------------------- /smtx-main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include "smtx.h" 19 | 20 | struct state S = { 21 | .ctlkey = CTRL('g'), 22 | .rawkey = 'g', 23 | .term = "smtx", 24 | .width = 80, 25 | .binding = k1, 26 | .history = 1024, 27 | .count = -1, 28 | }; 29 | 30 | static const char * 31 | getshell(void) 32 | { 33 | const char *s = getenv("SHELL"); 34 | struct passwd *pwd = s ? NULL : getpwuid(geteuid()); 35 | return s ? s : pwd ? pwd->pw_shell : "/bin/sh"; 36 | } 37 | 38 | void 39 | set_tabs(struct pty *p, int tabstop) 40 | { 41 | typeof(*p->tabs) *n; 42 | if ((n = realloc(p->tabs, p->ws.ws_col * sizeof *n)) != NULL) { 43 | memset(p->tabs = n, 0, p->ws.ws_col * sizeof *n); 44 | for (int i = 0; i < p->ws.ws_col; i += tabstop) { 45 | p->tabs[i] = true; 46 | } 47 | } 48 | } 49 | 50 | int 51 | resize_pad(WINDOW **p, int h, int w) 52 | { 53 | int rv = 0; 54 | if (*p) { 55 | rv = wresize(*p, h, w ) == OK; 56 | } else if ((*p = newpad(h, w)) != NULL) { 57 | wtimeout(*p, 0); 58 | scrollok(*p, 1); 59 | rv = (keypad(*p, 1) == OK); 60 | } 61 | return check(rv, ENOMEM, "wresize"); /* 0 is failure */ 62 | } 63 | 64 | static struct pty * 65 | get_freepty(bool allow_hidden) 66 | { 67 | struct pty *t = S.p; 68 | while (t && (!allow_hidden || t->count) && t->fd != -1) { 69 | t = t->next; 70 | } 71 | return t ? t : calloc(1, sizeof *t); 72 | } 73 | 74 | struct pty * 75 | new_pty(int rows, int cols, bool new) 76 | { 77 | struct pty *p = get_freepty(!new); 78 | if (check(p != NULL, errno = 0, "calloc")) { 79 | if (p->s == NULL) { 80 | if (resize_pad(&p->scr[0].w, rows, cols) 81 | && resize_pad(&p->scr[1].w, rows, cols) 82 | ){ 83 | p->scr[0].rows = p->scr[1].rows = rows; 84 | *(S.tail ? &S.tail->next : &S.p) = p; 85 | S.tail = p; 86 | set_scroll(p->scr, 0, rows - 1); 87 | set_scroll(&p->scr[1], 0, rows - 1); 88 | } else { 89 | delwin(p->scr[0].w); 90 | delwin(p->scr[1].w); 91 | free(p); 92 | return NULL; 93 | } 94 | } 95 | if (p->fd < 1) { 96 | const char *sh = getshell(); 97 | p->ws.ws_row = LINES - 1; 98 | p->ws.ws_col = cols; 99 | p->tos = rows - p->ws.ws_row; 100 | p->pid = forkpty(&p->fd, p->secondary, NULL, &p->ws); 101 | if (check(p->pid != -1, 0, "forkpty") && p->pid == 0) { 102 | setsid(); 103 | signal(SIGCHLD, SIG_DFL); 104 | setenv("TERM", S.term, 1); 105 | execl(sh, sh, NULL); 106 | err(EXIT_FAILURE, "exec SHELL='%s'", sh); 107 | } 108 | set_tabs(p, p->tabstop = 8); 109 | FD_SET(p->fd, &S.fds); 110 | S.maxfd = p->fd > S.maxfd ? p->fd : S.maxfd; 111 | fcntl(p->fd, F_SETFL, O_NONBLOCK); 112 | const char *bname = strrchr(sh, '/'); 113 | bname = bname ? bname + 1 : sh; 114 | strncpy(p->status, bname, sizeof p->status - 1); 115 | } 116 | if (p->s == NULL) { 117 | p->s = &p->scr[0]; 118 | tput(p, 0, 0, 0, NULL, ris); 119 | p->vp.p = p; 120 | } 121 | } 122 | return p; 123 | } 124 | 125 | struct canvas * 126 | newcanvas(struct pty *p, struct canvas *parent) 127 | { 128 | struct canvas *n = S.unused; 129 | if (n != NULL) { 130 | S.unused = n->c[0]; 131 | } else { 132 | check((n = calloc(1 , sizeof *n)) != NULL, 0, "calloc"); 133 | } 134 | if (n) { 135 | n->c[0] = n->c[1] = NULL; 136 | n->manualscroll = n->offset.y = n->offset.x = 0; 137 | n->p = p ? p : new_pty(S.history, MAX(COLS, S.width), false); 138 | n->parent = parent; 139 | if (n->p) { 140 | n->p->count += 1; 141 | } 142 | n->split = (typeof(n->split)){1.0, 1.0}; 143 | } 144 | return n; 145 | } 146 | 147 | static void 148 | draw_window(struct canvas *n) 149 | { 150 | struct point o = n->origin; 151 | struct point e = { o.y + n->extent.y - 1, o.x + n->extent.x - 1 }; 152 | if (n->p && e.y > 0 && e.x > 0) { 153 | if (! n->manualscroll) { 154 | n->offset.x = MAX(0, n->p->s->c.x - n->extent.x + 1); 155 | } 156 | struct point off = n->offset; 157 | if (n->p->ws.ws_col < n->extent.x) { 158 | assert( n->offset.x == 0 ); 159 | pnoutrefresh(S.wbkg, 0, 0, o.y, o.x + n->p->ws.ws_col, 160 | e.y, e.x); 161 | } 162 | pnoutrefresh(n->p->s->w, off.y, off.x, o.y, o.x, e.y, e.x); 163 | } 164 | } 165 | 166 | static void 167 | fixcursor(void) /* Move the terminal cursor to the active window. */ 168 | { 169 | int y = S.f->p->s->c.y, x = S.f->p->s->c.x; 170 | int show = S.binding != ctl && S.f->extent.y 171 | && x >= S.f->offset.x && x < S.f->offset.x + S.f->extent.x 172 | && y >= S.f->offset.y && y < S.f->offset.y + S.f->extent.y; 173 | draw_window(S.f); 174 | curs_set(show ? S.f->p->s->vis : 0); 175 | } 176 | 177 | void 178 | reshape_window(struct pty *p) 179 | { 180 | check(ioctl(p->fd, TIOCSWINSZ, &p->ws) == 0, 0, "ioctl on %d", p->fd); 181 | check(kill(p->pid, SIGWINCH) == 0, 0, "send WINCH to %d", (int)p->pid); 182 | set_scroll(p->scr, 0, p->scr->rows - 1); 183 | set_scroll(p->scr + 1, p->tos, p->scr->rows - 1); 184 | } 185 | 186 | void 187 | set_scroll(struct screen *s, int top, int bottom) 188 | { 189 | wsetscrreg(s->w, s->scroll.top = top, s->scroll.bot = bottom); 190 | } 191 | 192 | 193 | void 194 | scrollbottom(struct canvas *n) 195 | { 196 | if (n && n->p && n->p->s && n->extent.y) { 197 | n->offset.y = MAX(n->p->s->maxy - n->extent.y + 1, 0); 198 | } 199 | } 200 | 201 | void 202 | reshape(struct canvas *n, int y, int x, int h, int w) 203 | { 204 | if (n) { 205 | n->origin.y = y; 206 | n->origin.x = x; 207 | int h1 = h * n->split.y; 208 | int w1 = w * n->split.x; 209 | int have_div = h1 > 0 && w1 > 0 && n->c[1]; 210 | 211 | assert(n->split.y >= 0.0); 212 | assert(n->split.y <= 1.0); 213 | assert(n->split.x >= 0.0); 214 | assert(n->split.x <= 1.0); 215 | resize_pad(&n->wdiv, n->typ ? h : h1, 1); 216 | resize_pad(&n->wtit, 1, w1); 217 | reshape(n->c[0], y + h1, x, h - h1, n->typ ? w1 : w); 218 | reshape(n->c[1], y, x + w1 + have_div, 219 | n->typ ? h : h1, w - w1 - have_div); 220 | n->extent.y = h1 > 0 ? h1 - 1 : 0; 221 | n->extent.x = w1; 222 | 223 | /* If the pty is visible in multiple canvasses, 224 | set ws.ws_row to the one with biggest extent.y */ 225 | if (n->p->fd >= 0 && n->extent.y > n->p->ws.ws_row) { 226 | n->p->ws.ws_row = n->extent.y; 227 | n->p->tos = n->p->scr->rows - n->extent.y; 228 | reshape_window(n->p); 229 | wrefresh(n->p->s->w); 230 | } 231 | scrollbottom(n); 232 | } 233 | S.reshape = 0; 234 | } 235 | 236 | void 237 | change_count(struct canvas * n, int count, int pop) 238 | { 239 | assert( count < 2 && count > -2 ); 240 | if (n && n->p) { 241 | n->p->count += count; 242 | change_count(n->c[0], count, pop); 243 | change_count(n->c[1], count, pop); 244 | if (pop && n->p->count == 0) { 245 | freecanvas(n); 246 | } 247 | } 248 | } 249 | 250 | void 251 | freecanvas(struct canvas *n) 252 | { 253 | if (n == S.f) { 254 | S.f = n->parent; 255 | } 256 | if (n->parent) { 257 | n->parent->c[n == n->parent->c[1]] = NULL; 258 | } 259 | n->c[0] = S.unused; 260 | n->c[1] = NULL; 261 | n->parent = NULL; 262 | S.unused = n; 263 | } 264 | 265 | static void 266 | draw_pane(WINDOW *w, int y, int x) 267 | { 268 | int wy, wx; 269 | getmaxyx(w, wy, wx); 270 | pnoutrefresh(w, 0, 0, y, x, y + wy - 1, x + wx - 1); 271 | } 272 | 273 | static void 274 | draw_title(struct canvas *n, int r) 275 | { 276 | assert( n->wtit ); 277 | assert( n->p ); 278 | wattrset(n->wtit, r ? A_REVERSE : A_NORMAL); 279 | mvwprintw(n->wtit, 0, 0, "%d %s ", 280 | n->p->fd > 2 ? n->p->fd - 2 : n->p->pid, 281 | n->p->status); 282 | int x = n->offset.x; 283 | int w = n->p->ws.ws_col; 284 | if (x > 0 || x + n->extent.x < w) { 285 | wprintw(n->wtit, "%d-%d/%d ", x + 1, x + n->extent.x, w); 286 | } 287 | whline(n->wtit, ACS_HLINE, n->extent.x); 288 | struct point o = n->origin; 289 | draw_pane(n->wtit, o.y + n->extent.y, o.x); 290 | } 291 | 292 | static void 293 | draw_div(struct canvas *n, int rev) 294 | { 295 | wattrset(n->wdiv, rev ? A_REVERSE : A_NORMAL); 296 | mvwvline(n->wdiv, 0, 0, ACS_VLINE, INT_MAX); 297 | draw_pane(n->wdiv, n->origin.y, n->origin.x + n->extent.x); 298 | } 299 | 300 | void 301 | draw(struct canvas *n) /* Draw a canvas. */ 302 | { 303 | if (n != NULL && n->extent.y > 0) { 304 | int rev = S.binding == ctl && n == S.f; 305 | draw(n->c[0]); 306 | if (n->c[1]) { 307 | draw_div(n, rev && !n->extent.x); 308 | draw(n->c[1]); 309 | } 310 | draw_window(n); 311 | draw_title(n, rev); 312 | } 313 | } 314 | 315 | static void 316 | wait_child(struct pty *p) 317 | { 318 | int status, k = 0; 319 | const char *fmt = "exited %d"; 320 | if (check(waitpid(p->pid, &status, WNOHANG) != -1, 0, "waitpid")) { 321 | if (WIFEXITED(status)) { 322 | k = WEXITSTATUS(status); 323 | } else if (WIFSIGNALED(status)) { 324 | fmt = "caught signal %d"; 325 | k = WTERMSIG(status); 326 | } 327 | FD_CLR(p->fd, &S.fds); 328 | check(close(p->fd) == 0, 0, "close fd %d", p->fd); 329 | snprintf(p->status, sizeof p->status, fmt, k); 330 | p->fd = -1; /* (1) */ 331 | S.reshape = 1; 332 | } 333 | } 334 | /* (1) We do not free(p) because we wish to retain error messages. 335 | * The windows will persist until the user explicitly destroys them. 336 | */ 337 | 338 | static void 339 | getinput(void) /* check stdin and all pty's for input. */ 340 | { 341 | fd_set sfds = S.fds; 342 | if (select(S.maxfd + 1, &sfds, NULL, NULL, NULL) < 0) { 343 | check(errno == EINTR, 0, "select"); 344 | return; 345 | } 346 | if (FD_ISSET(STDIN_FILENO, &sfds)) { 347 | int r; 348 | wint_t w; 349 | while (S.f && (r = wget_wch(S.f->p->s->w, &w)) != ERR) { 350 | struct handler *b = NULL; 351 | if (r == OK && w > 0 && w < 128) { 352 | b = S.binding + w; 353 | } else if (r == KEY_CODE_YES) { 354 | assert( w >= KEY_MIN && w <= KEY_MAX ); 355 | b = &code_keys[w - KEY_MIN]; 356 | } 357 | if (b) { 358 | b->arg ? b->act.a(b->arg) : b->act.v(); 359 | if (b->act.a != digit) { 360 | S.count = -1; 361 | } 362 | } 363 | } 364 | } 365 | for (struct pty *t = S.p; t; t = t->next) { 366 | if (t->fd > 0 && FD_ISSET(t->fd, &sfds)) { 367 | FD_CLR(t->fd, &sfds); 368 | char iobuf[BUFSIZ]; 369 | int oldmax = t->s->maxy; 370 | ssize_t r = read(t->fd, iobuf, sizeof iobuf); 371 | if (r > 0) { 372 | vtwrite(&t->vp, iobuf, r); 373 | t->s->delta = t->s->maxy - oldmax; 374 | } else if (errno != EINTR && errno != EWOULDBLOCK) { 375 | wait_child(t); 376 | } 377 | } 378 | } 379 | } 380 | 381 | void 382 | sendarrow(const char *k) 383 | { 384 | struct canvas *n = S.f; 385 | char buf[3] = { '\033', n->p->pnm ? 'O' : '[', *k }; 386 | rewrite(n->p->fd, buf, 3); 387 | } 388 | 389 | /* 390 | * wc_lut is built from the output of wctomb, so that for k such that 391 | * k % (1 + MB_LEN_MAX) == 0, wc_lut[k] is the value returned by 392 | * wctomb( ..., k) and wc_lut[k+1 ...] is the resultant multi-byte value. 393 | */ 394 | static char wc_lut[(KEY_MAX - KEY_MIN + 1) * ( 1 + MB_LEN_MAX )]; 395 | 396 | static void 397 | build_bindings(void) 398 | { 399 | 400 | k1[S.ctlkey] = (struct handler){ { .a = transition }, " control" }; 401 | ctl[S.ctlkey] = (struct handler){ { .a = transition }, "*enter" }; 402 | 403 | for (wchar_t k = KEY_MIN; k < KEY_MAX; k++) { 404 | int idx = k - KEY_MIN; 405 | if (code_keys[idx].arg || code_keys[idx].act.v) { 406 | continue; 407 | } 408 | assert( MB_LEN_MAX < 128 ); 409 | int i = idx * (1 + MB_LEN_MAX); 410 | int v = wctomb(wc_lut + i + 1, k); 411 | assert( v < 128 && v > -2 ); 412 | wc_lut[ i ] = v == -1 ? 0 : v; 413 | code_keys[idx].act.a = send; 414 | code_keys[idx].arg = wc_lut + i; 415 | } 416 | } 417 | 418 | static void 419 | update_offset_r(struct canvas *n) 420 | { 421 | if (n) { 422 | if (n->p && n->p->s && n->p->s->delta) { 423 | int d = n->p->s->delta; 424 | int t = n->p->s->maxy - n->extent.y + 1; 425 | if (t > 0) { 426 | n->offset.y += MIN(d, t); 427 | } 428 | } 429 | update_offset_r(n->c[0]); 430 | update_offset_r(n->c[1]); 431 | } 432 | } 433 | 434 | static void 435 | main_loop(void) 436 | { 437 | while (S.root != NULL) { 438 | if (S.reshape) { 439 | reshape(S.root, 0, 0, LINES, COLS); 440 | wrefresh(curscr); 441 | } 442 | draw(S.root); 443 | if (*S.errmsg) { 444 | mvwprintw(S.werr, 0, 0, "%s", S.errmsg); 445 | wclrtoeol(S.werr); 446 | draw_pane(S.werr, LINES - 1, 0); 447 | } 448 | fixcursor(); 449 | doupdate(); 450 | getinput(); 451 | update_offset_r(S.root); 452 | for (struct pty *p = S.p; p; p = p->next) { 453 | p->s->delta = 0; 454 | } 455 | } 456 | } 457 | 458 | void 459 | endwin_wrap(void) 460 | { 461 | (void)endwin(); 462 | } 463 | 464 | static void 465 | init(void) 466 | { 467 | signal(SIGTERM, exit); 468 | FD_ZERO(&S.fds); 469 | FD_SET(STDIN_FILENO, &S.fds); 470 | build_bindings(); 471 | atexit(endwin_wrap); 472 | initscr(); /* exits on failure */ 473 | S.history = MAX(LINES, S.history); 474 | raw(); 475 | noecho(); 476 | nonl(); 477 | timeout(0); 478 | intrflush(NULL, FALSE); 479 | start_color(); 480 | use_default_colors(); 481 | resize_pad(&S.werr, 1, COLS); 482 | resize_pad(&S.wbkg, LINES, COLS); 483 | wbkgd(S.wbkg, ACS_BULLET); 484 | wborder(S.wbkg, ACS_VLINE, ACS_BULLET, ACS_BULLET, ACS_BULLET, 485 | ACS_VLINE, ACS_BULLET, ACS_BULLET, ACS_BULLET); 486 | wattron(S.werr, A_REVERSE); 487 | S.f = S.root = newcanvas(NULL, NULL); 488 | if (S.root == NULL || S.werr == NULL || S.wbkg == NULL || !S.root->p) { 489 | endwin(); 490 | errx(EXIT_FAILURE, "Unable to create root window"); 491 | } 492 | reshape_root(); 493 | } 494 | 495 | int 496 | smtx_main() 497 | { 498 | init(); 499 | main_loop(); 500 | return EXIT_SUCCESS; 501 | } 502 | 503 | /* 504 | Parse a string to build a layout. The layout is expected 505 | to consist of coordinates of the lower right corner of each 506 | window as a percentage of the full screen. eg: 507 | 508 | .5:.5 .25:.66 .5:.66 .5:.83 .5:1 1:.25 1:1 509 | 510 | would describe a layout that looks like: 511 | +--------------------------------------+ 512 | | | | | | 513 | | | | | | 514 | | |------b | | 515 | | | | | | 516 | | | | | | 517 | -------------------a------c------d-----e 518 | | | | 519 | | | | 520 | | | | 521 | | | | 522 | +---------f----------------------------g 523 | Note that order matters. 524 | */ 525 | static struct canvas * 526 | add_canvas(const char **lp, double oy, double ox, double ey, double ex, 527 | struct pty **pp, struct canvas *parent) 528 | { 529 | double y, x; 530 | int e; 531 | struct canvas *n; 532 | struct pty *p = *pp; 533 | if( 534 | (n = newcanvas(p, parent)) == NULL 535 | || n->p == NULL 536 | || ! check(2 == sscanf(*lp, "%lf%*1[,:]%lf%n", &y, &x, &e), 537 | errno = 0, "Invalid format at %s", **lp ? *lp : "end") 538 | || ! check(y > oy && y <= ey && x > ox && x <= ex, 539 | errno = 0, "Out of bounds: %s", *lp) 540 | ){ 541 | goto fail; 542 | } 543 | *lp += e; 544 | *pp = p ? (( p->next ? p->next : S.p ) == S.f->p ) ? NULL : p->next : p; 545 | n->split.y = (y - oy) / (ey - oy); 546 | n->split.x = (x - ox) / (ex - ox); 547 | if (y == ey && x == ex) { 548 | return n; 549 | } else if (y == ey) { 550 | n->typ = 1; 551 | } else if (x == ex) { 552 | n->typ = 0; 553 | } else { 554 | double ny, nx; 555 | if (! check(2 == sscanf(*lp, "%lf%*1[,:]%lf", &ny, &nx), 556 | errno = 0, "Invalid format @ '%s'", *lp) 557 | || ! check(y <= ny || x <= nx, 0, 558 | "Out of bounds: %s", *lp) 559 | || (n->typ = y < ny, 560 | (n->c[!n->typ] = add_canvas(lp, 561 | n->typ ? y : oy, n->typ ? ox : x, 562 | n->typ ? ey : y, n->typ ? x : ex, 563 | pp, n)) == NULL )) { 564 | goto fail; 565 | } 566 | } 567 | if ((n->c[n->typ] = add_canvas(lp, 568 | n->typ ? oy : y, n->typ ? x : ox, 569 | ey, ex, pp, n)) == NULL) { 570 | goto fail; 571 | } 572 | 573 | return n; 574 | fail: 575 | change_count(n, -1, 1); 576 | return NULL; 577 | } 578 | 579 | int 580 | build_layout(const char *layout) 581 | { 582 | struct pty *p = S.f->p; 583 | change_count(S.root, -1, 0); /* (1) */ 584 | struct canvas *n = add_canvas(&layout, 0.0, 0.0, 1.0, 1.0, &p, NULL); 585 | change_count(S.root, +1, 0); 586 | if (n) { 587 | change_count(S.root, -1, 1); 588 | S.f = S.root = n; 589 | S.reshape = 1; 590 | } 591 | return S.reshape; 592 | } 593 | /* (1) Decrement the view counts so that visible ptys will be availalbe for 594 | * the new layout. 595 | */ 596 | -------------------------------------------------------------------------------- /smtx.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 - 2023 William Pursell 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "smtx.h" 18 | 19 | static void 20 | parse_args(int argc, char *const*argv) 21 | { 22 | int c; 23 | char *name = strrchr(argv[0], '/'); 24 | while ((c = getopt(argc, argv, ":c:hs:t:vw:")) != -1) { 25 | switch (c) { 26 | default: 27 | fprintf(stderr, "Unknown option: %c", optopt); 28 | exit(EXIT_FAILURE); 29 | case 'c': 30 | S.ctlkey = CTRL(S.rawkey = optarg[0]); 31 | break; 32 | case 'h': 33 | printf("usage: %s", name ? name + 1 : argv[0]); 34 | puts( 35 | " [-c ctrl-key]" 36 | " [-h]" 37 | " [-s history-size]" 38 | " [-t terminal-type]" 39 | " [-v]" 40 | " [-w width]" 41 | ); 42 | exit(EXIT_SUCCESS); 43 | case 's': 44 | S.history = strtol(optarg, NULL, 10); 45 | break; 46 | case 't': 47 | S.term = optarg; 48 | break; 49 | case 'v': 50 | printf("%s-%s\n", PACKAGE_NAME, PACKAGE_VERSION); 51 | exit(EXIT_SUCCESS); 52 | case 'w': 53 | S.width = strtol(optarg, NULL, 10); 54 | } 55 | } 56 | } 57 | 58 | /* If rv is non-zero, emit the error message */ 59 | int 60 | check(int rv, int err, const char *fmt, ...) 61 | { 62 | if (!rv) { 63 | int e = err ? err : errno; 64 | va_list ap; 65 | va_start(ap, fmt); 66 | size_t len = sizeof S.errmsg; 67 | int n = vsnprintf(S.errmsg, len, fmt, ap); 68 | if (e && n + 3 < (int)len) { 69 | strncat(S.errmsg, ": ", len - n); 70 | strncat(S.errmsg, strerror(e), len - n - 2); 71 | } 72 | va_end(ap); 73 | } 74 | return !!rv; 75 | } 76 | 77 | void 78 | rewrite(int fd, const char *b, size_t n) 79 | { 80 | const char *e = b + n; 81 | ssize_t s; 82 | if (n > 0 ) do { 83 | s = write(fd, b, e - b); 84 | b += s < 0 ? 0 : s; 85 | } while (b < e && check(s >= 0 || errno == EINTR, 0, "write %d", fd) ); 86 | } 87 | 88 | int 89 | main(int argc, char **argv) 90 | { 91 | char buf[16]; 92 | parse_args(argc, argv); 93 | snprintf(buf, sizeof buf - 1, "%d", getpid()); 94 | setenv("SMTX", buf, 1); 95 | setenv("SMTX_VERSION", VERSION, 1); 96 | unsetenv("LINES"); 97 | unsetenv("COLUMNS"); 98 | setlocale(LC_ALL, ""); 99 | return smtx_main(); 100 | } 101 | -------------------------------------------------------------------------------- /smtx.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include "config.h" 19 | #include 20 | #include 21 | #if HAVE_CURSES_H 22 | # include 23 | #elif HAVE_NCURSESW_CURSES_H 24 | # include 25 | #endif 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #if HAVE_PTY_H 32 | # include 33 | # include 34 | #elif HAVE_LIBUTIL_H 35 | # include 36 | #elif HAVE_UTIL_H 37 | # include 38 | #endif 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | 54 | #include "vtparser.h" 55 | 56 | extern int smtx_main(void); 57 | 58 | #define MIN(x, y) ((x) < (y) ? (x) : (y)) 59 | #define MAX(x, y) ((x) > (y) ? (x) : (y)) 60 | #ifndef CTRL 61 | #define CTRL(x) ((x) & 0x1f) 62 | #endif 63 | 64 | struct canvas; 65 | struct screen { 66 | int vis; /* cursor visibility */ 67 | int maxy; /* highest row in which the cursor has ever been */ 68 | int rows; /* number of rows in the window */ 69 | int delta; /* number of lines written by a vtwrite */ 70 | struct { int top; int bot; } scroll; 71 | struct { 72 | int y, x, xenl; 73 | cchar_t bkg; 74 | attr_t attr; 75 | short p; /* The color pair */ 76 | short color[2]; /* [0] == foreground, [1] == background */ 77 | wchar_t *gc, *gs; 78 | } c, sc; /* cursor/save cursor */ 79 | bool insert; 80 | wchar_t repc; /* character to be repeated */ 81 | int decawm; /* wrap-around mode */ 82 | WINDOW *w; 83 | }; 84 | struct pty { 85 | int fd, tabstop, count; 86 | struct winsize ws; 87 | int tos; /* top of screen */ 88 | pid_t pid; 89 | bool *tabs, pnm, decom, lnm; 90 | /* DECOM: When set, cursor addressing is relative to the upper left 91 | * corner of the scrolling region instead of top of screen. */ 92 | struct screen scr[2], *s; /* Primary/alternate screen */ 93 | wchar_t *g[4]; 94 | char status[32]; 95 | struct vtp vp; 96 | char secondary[PATH_MAX]; 97 | struct pty *next; 98 | }; 99 | 100 | typedef void(action)(const char *arg); 101 | typedef void(action0)(void); 102 | struct handler { 103 | union { 104 | action *a; 105 | action0 *v; 106 | } act; 107 | const char *arg; 108 | }; 109 | 110 | extern struct state S; 111 | struct mode { 112 | struct handler keys[128]; 113 | }; 114 | extern struct handler k1[128]; 115 | extern struct handler k2[128]; 116 | extern struct handler ctl[128]; 117 | extern struct handler code_keys[KEY_MAX - KEY_MIN + 1]; 118 | 119 | struct state { 120 | unsigned char ctlkey; 121 | unsigned char rawkey; 122 | int width; /* Columns in newly created ptys */ 123 | int history; /* Rows in newly created windows */ 124 | int count; /* User entered count in command mode */ 125 | const char *term; /* Name of the terminal type */ 126 | struct handler *binding; /* Current key binding */ 127 | struct canvas *root; /* Root of all canvasses in use */ 128 | struct canvas *f; /* Currently focused canvas */; 129 | struct pty *p; /* List of all pty in use */ 130 | struct pty *tail; /* Last in the list of p */ 131 | struct canvas *unused; /* Unused canvasses */ 132 | fd_set fds; 133 | int maxfd; 134 | WINDOW *werr; 135 | WINDOW *wbkg; 136 | int reshape; 137 | char errmsg[256]; 138 | }; 139 | 140 | struct point { int y, x; }; 141 | struct canvas { 142 | struct point origin; /* position of upper left corner */ 143 | struct point extent; /* relative position of lower right corner */ 144 | /* extent.y is the actual number of rows visible in the window */ 145 | int typ; /* 0: c[0] is full width, 1: c[1] is full height */ 146 | struct point offset; /* Number of lines window is scrolled */ 147 | struct pty *p; 148 | struct canvas *parent; 149 | /* 150 | A canvas contains both c[0] and c[1], and shows only the upper 151 | left corner. eg: if extent = {3, 13}, typ = 0, split = { 0.5, 0.333 } 152 | 153 | |<-wdiv 154 | p->s.w | c[1] 155 | | 156 | ----wtit-----x-------c[1]->wtit----------- 157 | 158 | c[0] 159 | 160 | -------------c[0]->wtit------------------- 161 | c[0] is the window below this, c[1] is the window to the right 162 | (in a typ==1 window, c1 is full height and c0 is partial width) 163 | (Note that w.x + c1->w.x == extent.x - 1, subtracting 1 for wdiv) 164 | */ 165 | struct canvas *c[2]; 166 | struct { double y, x; } split; 167 | int manualscroll; 168 | WINDOW *wtit; /* Window for title */ 169 | WINDOW *wdiv; /* Window for divider */ 170 | }; 171 | 172 | extern wchar_t CSET_US[0x7f]; /* "USASCII" */ 173 | extern wchar_t CSET_UK[0x7f]; /* "United Kingdom" */ 174 | extern wchar_t CSET_GRAPH[0x7f]; /* Graphics Set One */ 175 | extern int tabstop; 176 | extern int id; 177 | 178 | extern struct canvas * newcanvas(struct pty *, struct canvas *); 179 | extern void draw(struct canvas *); 180 | extern void setupevents(struct pty *); 181 | extern void rewrite(int fd, const char *b, size_t n); 182 | extern void draw(struct canvas *n); 183 | extern void freecanvas(struct canvas *n); 184 | extern void scrollbottom(struct canvas *n); 185 | extern int check(int, int, const char *, ...); 186 | extern void set_tabs(struct pty *p, int tabstop); 187 | extern int resize_pad(WINDOW **, int, int); 188 | extern void reshape_window(struct pty *); 189 | extern void reshape(struct canvas *n, int y, int x, int h, int w); 190 | void set_scroll(struct screen *s, int top, int bottom); 191 | extern void change_count(struct canvas * n, int, int); 192 | extern struct pty * new_pty(int, int, bool); 193 | 194 | extern action0 attach; 195 | extern action balance; 196 | extern action create; 197 | extern action digit; 198 | extern action0 focus; 199 | extern action0 help; 200 | extern action mov; 201 | extern action0 new_tabstop; 202 | extern action0 new_shell; 203 | extern action0 next; 204 | extern action0 prune; 205 | extern action reorient; 206 | extern action0 reshape_root; 207 | extern action resize; 208 | extern action scrollh; 209 | extern action scrolln; 210 | extern action send; 211 | extern action0 send_cr; 212 | extern action sendarrow; 213 | extern action0 set_history; 214 | extern action0 set_layout; 215 | extern action set_width; 216 | extern action0 swap; 217 | extern action transition; 218 | extern action0 transpose; 219 | extern action0 vbeep; 220 | -------------------------------------------------------------------------------- /smtx.ti: -------------------------------------------------------------------------------- 1 | smtx|Simple Modal Terminal Multiplexer, 2 | # boolean capabilities 3 | am, 4 | bce, 5 | hs, 6 | mir, 7 | msgr, 8 | xenl, 9 | # numeric capabilities 10 | colors#8, 11 | cols#80, 12 | it#8, 13 | lines#25, 14 | pairs#64, 15 | # string capabilities 16 | acsc=++\,\,--..00``aaffgghhiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, 17 | bel=\007, 18 | blink=\E[5m, 19 | bold=\E[1m, 20 | cbt=\E[Z, 21 | civis=\E[25l, 22 | clear=\E[H\E[J, 23 | cnorm=\E[25h, 24 | cr=\r, 25 | csr=\E[%i%p1%d;%p2%dr, 26 | cub1=\010, 27 | cub=\E[%p1%dD, 28 | cud1=\n, 29 | cud=\E[%p1%dB, 30 | cuf1=\E[C, 31 | cuf=\E[%p1%dC, 32 | cup=\E[%i%p1%d;%p2%dH, 33 | cuu1=\EM, 34 | cuu=\E[%p1%dA, 35 | cvvis=\E[34l, 36 | dch1=\E[P, 37 | dch=\E[%p1%dP, 38 | dim=\E[2m, 39 | dl1=\E[M, 40 | dl=\E[%p1%dM, 41 | ech=\E[%p1%dX, 42 | ed=\E[J, 43 | el1=\E[1K, 44 | el=\E[K, 45 | enacs=\E(B\E)0, 46 | fsl=\007, 47 | home=\E[H, 48 | hpa=\E[%i%p1%dG, 49 | ht=\011, 50 | hts=\EH, 51 | ich=\E[%p1%d@, 52 | il1=\E[L, 53 | il=\E[%p1%dL, 54 | ind=\n, 55 | indn=\E[%p1%dS, 56 | is2=\E)0, 57 | kbs=\177, 58 | kcbt=\E[Z, 59 | kcub1=\EOD, 60 | kcud1=\EOB, 61 | kcuf1=\EOC, 62 | kcuu1=\EOA, 63 | kdch1=\E[3~, 64 | kend=\E[4~, 65 | kf10=\E[21~, 66 | kf11=\E[23~, 67 | kf12=\E[24~, 68 | kf1=\EOP, 69 | kf2=\EOQ, 70 | kf3=\EOR, 71 | kf4=\EOS, 72 | kf5=\E[15~, 73 | kf6=\E[17~, 74 | kf7=\E[18~, 75 | kf8=\E[19~, 76 | kf9=\E[20~, 77 | khome=\E[1~, 78 | kich1=\E[2~, 79 | knp=\E[6~, 80 | kpp=\E[5~, 81 | nel=\EE, 82 | op=\E[39;49m, 83 | rc=\E8, 84 | rep=%p1%c\E[%p2%{1}%-%db, 85 | rev=\E[7m, 86 | ri=\EM, 87 | rin=\E[%p1%dT, 88 | ritm=\E[23m, 89 | rmacs=\017, 90 | rmcup=\E[1049l, 91 | rmir=\E[4l, 92 | rmkx=\E[1l\E>, 93 | rmso=\E[27m, 94 | rmul=\E[24m, 95 | rs2=\Ec, 96 | sc=\E7, 97 | setab=\E[%p1%'('%+%dm, 98 | setaf=\E[%p1%{30}%+%dm, 99 | sgr0=\E[m\017, 100 | sgr=\E[0%?%p6%t;1%;%?%p1%t;3%;%?%p2%t;4%;%?%p3%t;7%;%?%p4%t;5%;%?%p5%t;2%;m%?%p9%t\016%e\017%;, 101 | sitm=\E[3m, 102 | smacs=\016, 103 | smcup=\E[1049h, 104 | smir=\E[4h, 105 | smkx=\E[1h\E=, 106 | smso=\E[7m, 107 | smul=\E[4m, 108 | tbc=\E[3g, 109 | tsl=\E]2, 110 | u8=\006, 111 | u9=\005, 112 | vpa=\E[%i%p1%dd, 113 | 114 | 115 | smtx-256color|Simple Modal Terminal Multiplexer with 256 colors, 116 | colors#0x100, 117 | pairs#0x10000, 118 | setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, 119 | setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, 120 | use=smtx, 121 | -------------------------------------------------------------------------------- /smtx.txt: -------------------------------------------------------------------------------- 1 | 2 | = smtx (1) 3 | William Pursell 4 | :doctype: manpage 5 | :man manual: smtx manual 6 | :man source: smtx 7 | :man version: {version} 8 | 9 | == NAME 10 | 11 | smtx - A simple modal terminal multiplexor 12 | 13 | == SYNOPSIS 14 | 15 | *smtx* [-c ctrl-key] [-h] [-s history-size] [-t terminal-type] [-v] [-w width] 16 | 17 | == OPTIONS 18 | 19 | *-c*=ctrl-key:: 20 | Use alternate key to enter control mode. 21 | 22 | *-h*:: 23 | Print the usage statement and exit. 24 | 25 | *-s*=history-size:: 26 | Set the number of lines in the history buffer to be used in ptys. 27 | 28 | *-t*=term:: 29 | Assign TERM environment to this value is new shells (default is "smtx"). 30 | 31 | *-v*:: 32 | Print the version and exit. 33 | 34 | *-w*=width:: 35 | Specify the minimun width in ptys (default is 80). 36 | 37 | == DESCRIPTION 38 | 39 | smtx is a terminal multiplexor which allows ptys to be larger than the physical 40 | screen. Multiple windows can be opened into the same pty at different locations, 41 | and window layouts can be generated dynamically. Each window exists as part of 42 | a "canvas", which is a rectangular portion of the physical screen. The root canvas 43 | consists of the entire physical screen and contains a window in the upper left 44 | corner and 2 child canvases which fill the remainder of the canvas, either of which 45 | may be empty. The left child occupies the space below the window, while the right 46 | child occupies the space to the right of the window. Which child extends to the 47 | lower right portion of the canvas depends on the type of the canvas, and can be 48 | modified dynamically. The window provides a view into a pty, and the pty associated 49 | with the window can be changed dynamically. 50 | 51 | When the shell running inside a pty exits, the pty is not immediately reaped 52 | and any error messages will remain available until that canvas is pruned. 53 | To immediately close all ptys and exit, use '0x' from control mode. 54 | 55 | == EXAMPLES 56 | 57 | change the current window layout: 58 | 59 | $ printf '\033]60;.33:1 .66:.5 .66:1 1:1\007' 60 | 61 | change the title of the current window: 62 | 63 | $ printf '\033]2;new title\007' 64 | 65 | == BINDINGS 66 | 67 | The following describes the default keybindings used in control mode. 68 | 69 | * a Attach the specified pty to the focused window 70 | * C Create N new canvasses to the right of the current window 71 | * c Create N new canvasses below the current window 72 | * g Goto a visible window attached to the pty with id N 73 | * i Enter one line of text to the underlying pty 74 | * j Navigate to the primary child of the focused window 75 | * h Navigate to the left child of the focused window 76 | * k Navigate to the parent of the focused window 77 | * l Navigate to the right child of the focused window 78 | * N Spawn a new shell (may create a pty, or reuse if one is available) 79 | * n Attach the next pty to the focused window 80 | * T Recursively transpose the current canvas 81 | * W Modify the width of the currently focused pty to be N 82 | * v Use pre-defined window layout N 83 | * x Recursively prune the specified canvas 84 | 85 | == COPYING 86 | 87 | Copyright \(C) 2020 {author}. 88 | Use of this software is granted under the terms of the GPLv3 89 | -------------------------------------------------------------------------------- /test-coverage: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Copyright (c) 2020 - 2023 William Pursell 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the copyright holder nor the 14 | # names of contributors may be used to endorse or promote products 15 | # derived from this software without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS 18 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS, 21 | # COPYRIGHT HOLDERS, OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF 24 | # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | # Check coverage. If configured without --coverage in CFLAGS, 30 | # this will likely just get skipped. (eg, configure with 31 | # CFLAGS='--coverage -g -O0' to enable this test) 32 | 33 | if ! test -f test-main.gcda; then 34 | exit 77 35 | fi 36 | 37 | cat << EOF | 38 | handler 98 39 | smtx 100 40 | smtx-main 97 41 | vtparser 100 42 | action 90 43 | EOF 44 | { 45 | RV=0 46 | while read name cover; do 47 | if test -f .libs/$name.gcno; then 48 | arg='-o .libs' 49 | else 50 | unset arg 51 | fi 52 | output=$(gcov $name $arg) 53 | echo "$output" 54 | # Fail if coverage falls below the threshold 55 | # We expect output to look like: 56 | # File '../../../smtx/smtx.c' 57 | # Lines executed:100.00% of 2 58 | # Creating 'smtx.c.gcov' 59 | if ! echo "$output" | awk 'NR==2 && /^Lines executed/ && $2 >= f{ok = 1} 60 | END {exit !ok}' f="${cover}" FS='[:%]'; then 61 | RV=1 62 | echo "FAIL: Coverage for $name below $cover" >&2 63 | fi 64 | if < "$name".c.gcov sed \ 65 | -e '\@{ /\* uncovered block \*/@,'"$( : 66 | )"'\@} /\* end uncovered \*/@d' \ 67 | -e '/#####/!d' \ 68 | -e '\@/\* uncovered \*/@d' | grep .; then 69 | echo The above lines of $name.c were not executed during testing 70 | fi >&2 71 | done 72 | exit $RV 73 | } 74 | # Careful. There is some shell "magic" here. RV is being assigned 75 | # and returned from a subshell. We rely on this subshell being the last 76 | # command executed, so that its value is returned by the main shell. 77 | # Also note that this relies on a specific output from gcov that happens 78 | # to work in the version I'm running. Probably need to skip this test 79 | # until it is robustified. 80 | -------------------------------------------------------------------------------- /test-describe.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 - 2023 William Pursell 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | /* This file contains descripive functions that should only ever be 19 | * used by the test harness. Note that this is not really true: it might 20 | * be convenient to have the ability to display state in the terminal, 21 | * so we should clean this code up and expect it to remain in production. 22 | */ 23 | #include "smtx.h" 24 | 25 | static size_t 26 | describe_layout(char *d, ptrdiff_t siz, const struct canvas *c, unsigned flags, 27 | int tab) 28 | { 29 | /* Describe a layout. */ 30 | const char * const e = d + siz; 31 | int recurse = flags & 0x1; 32 | int show_id = flags & 0x4; 33 | int show_pos = flags & 0x10; 34 | int show_2nd = flags & 0x40; 35 | int human = flags & 0x80; 36 | int show_pty = flags & 0x100; 37 | 38 | char *isfocus = recurse && c == S.f ? "*" : ""; 39 | if (human) { 40 | d += snprintf(d, e - d, "%c", c->typ ? '|' : '-'); 41 | } 42 | d += snprintf(d, e - d, "%s%dx%d", isfocus, c->extent.y, c->extent.x); 43 | if (show_pos){ 44 | d += snprintf(d, e - d, "@%d,%d", c->origin.y, c->origin.x); 45 | } 46 | if (show_id && c->p) { 47 | d += snprintf(d, e - d, "(id=%d)", c->p->fd - 2); 48 | } 49 | if (c->p->s && ! c->p->s->vis) { 50 | d += snprintf(d, e - d, "!"); /* Cursor hidden */ 51 | } 52 | if (! c->p->pnm) { 53 | d += snprintf(d, e - d, "#"); /* Numeric keypad */ 54 | } 55 | if (show_2nd && c->p && c->p->fd != -1) { 56 | d += snprintf(d, e - d, "(2nd=%s)", c->p->secondary ); 57 | } 58 | if (show_pty) { 59 | d += snprintf(d, e - d, "(pri %d,%d)(2nd %d,%d)", 60 | c->p->scr[0].rows, 61 | c->p->scr[0].maxy, 62 | c->p->scr[1].rows, 63 | c->p->scr[1].maxy 64 | ); 65 | } 66 | for (int i = 0; recurse && i < 2; i ++) { 67 | if (e - d > 3 + tab && c->c[i]) { 68 | *d++ = human ? '\r' : ';'; 69 | *d++ = human ? '\n' : ' '; 70 | for (int i = 0; human && i < tab; i++) { 71 | *d++ = '\t'; 72 | } 73 | d += describe_layout(d, e - d, c->c[i], flags, tab + 1); 74 | } 75 | } 76 | return siz - ( e - d ); 77 | } 78 | 79 | static void 80 | check_attr(unsigned f, unsigned *flags, char **d, char *e, char *msg, int set, 81 | int star) 82 | { 83 | char *dest = *d; 84 | size_t len = strlen(msg); 85 | if (((set && !(*flags & f)) || (!set && (*flags & f))) 86 | && len > 0 && dest + len + 3 < e) { 87 | *dest++ = '<'; 88 | if (!set) { 89 | *dest++ = '/'; 90 | } 91 | memcpy(dest, msg, len); 92 | dest += len; 93 | if (star) { 94 | *dest++ = '*'; 95 | } 96 | *dest++ = '>'; 97 | } 98 | if (set) { 99 | *flags |= f; 100 | } else { 101 | *flags &= ~f; 102 | } 103 | *d = dest; 104 | } 105 | 106 | static char * 107 | color_name(short k) 108 | { 109 | char *n; 110 | switch (k) { 111 | case COLOR_BLACK: n = "black"; break; 112 | case COLOR_RED: n = "red"; break; 113 | case COLOR_GREEN: n = "green"; break; 114 | case COLOR_YELLOW: n = "yellow"; break; 115 | case COLOR_BLUE: n = "blue"; break; 116 | case COLOR_MAGENTA: n = "magenta"; break; 117 | case COLOR_CYAN: n = "cyan"; break; 118 | case COLOR_WHITE: n = "white"; break; 119 | default: n = ""; 120 | } 121 | return n; 122 | } 123 | 124 | static size_t 125 | describe_row(char *desc, size_t siz, int row) 126 | { 127 | int y, x; 128 | size_t i = 0; 129 | unsigned attrs = 0; 130 | unsigned fgflag = 0; 131 | unsigned bgflag = 0; 132 | int last_color_pair = 0; 133 | int offset = 0; 134 | const struct canvas *c = S.root; 135 | unsigned width = c->p->ws.ws_col; 136 | char *end = desc + siz; 137 | WINDOW *w = c->p->s->w; 138 | struct { 139 | unsigned attr; 140 | unsigned flag; 141 | char *name; 142 | } *atrp, atrs[] = { 143 | { A_BOLD, 0x1, "bold" }, 144 | { A_DIM, 0x2, "dim" }, 145 | #if HAVE_A_ITALIC 146 | { A_ITALIC, 0x4, "italic" }, 147 | #endif 148 | { A_UNDERLINE, 0x8, "ul" }, 149 | { A_BLINK, 0x10, "blink" }, 150 | { A_REVERSE, 0x20, "rev" }, 151 | { A_INVIS, 0x40, "inv" }, 152 | { 0, 0, NULL } 153 | }; 154 | 155 | if (row < c->extent.y) { 156 | row += c->offset.y; 157 | offset = c->offset.x; 158 | } else if (row == c->extent.y) { 159 | w = c->wtit; 160 | row = 0; 161 | } 162 | getyx(w, y, x); 163 | for (i = 0; i < (size_t)c->extent.x && i < width && desc < end; i++) { 164 | int p; 165 | chtype k = mvwinch(w, row, i + offset); 166 | for (atrp = atrs; atrp->flag; atrp += 1) { 167 | check_attr(atrp->flag, &attrs, &desc, end, atrp->name, 168 | ( (k & A_ATTRIBUTES) & atrp->attr ) ? 1 : 0, 0 169 | ); 170 | } 171 | p = PAIR_NUMBER(k); 172 | if (p != last_color_pair) { 173 | int put = p ? p : last_color_pair; 174 | short fg, bg; 175 | if (pair_content((short)put, &fg, &bg) == OK 176 | && bg < (short)sizeof(1u) * CHAR_BIT 177 | && fg < (short)sizeof(1u) * CHAR_BIT) { 178 | check_attr(1u << fg, &fgflag, &desc, end, 179 | color_name(fg), p, 0 180 | ); 181 | check_attr(1u << bg, &bgflag, &desc, end, 182 | color_name(bg), p, 1 183 | ); 184 | } 185 | last_color_pair = p; 186 | } 187 | *desc++ = k & A_CHARTEXT; 188 | } 189 | wmove(w, y, x); 190 | return siz - ( end - desc ); 191 | } 192 | 193 | static size_t 194 | describe_state(char *desc, size_t siz) 195 | { 196 | size_t len = 0; 197 | 198 | len += snprintf(desc, siz, "history=%d, ", S.history); 199 | if (len < siz - 1) { 200 | len += snprintf(desc + len, siz - len, "y=%d, x=%d, ", LINES, 201 | COLS); 202 | } 203 | if (len < siz - 1) { 204 | len += snprintf(desc + len, siz - len, "w=%d", S.width); 205 | } 206 | if (len > siz - 3) { 207 | len = siz - 3; 208 | } 209 | desc[len++] = '\r'; 210 | desc[len++] = '\n'; 211 | desc[len++] = '\0'; 212 | return len; 213 | } 214 | 215 | static void 216 | show_layout(const char *arg) 217 | { 218 | char buf[1024] = "layout: "; 219 | int flag = strtol(*arg == ';' ? arg + 1 : arg, NULL, 16); 220 | struct canvas *c = flag & 0x20 ? S.f : S.root; 221 | if (flag & 0x80) { 222 | strcat(buf, "\r\n"); 223 | } 224 | int w = strlen(buf); 225 | size_t s = describe_layout(buf + w, sizeof buf - w - 3, c, flag, 1); 226 | for (size_t i = w; i < w + s; i++) { 227 | assert( buf[i] != ':' ); 228 | } 229 | buf[w + s++] = ':'; 230 | buf[w + s++] = '\r'; 231 | buf[w + s++] = '\n'; 232 | rewrite(STDOUT_FILENO, buf, s + w); 233 | } 234 | 235 | static void 236 | show_row(const char *arg) 237 | { 238 | int row = strtol(arg, NULL, 10); 239 | char buf[1024]; 240 | char val[1024]; 241 | size_t s = describe_row(val, sizeof val, row); 242 | int k = snprintf(buf, sizeof buf, "row %d:(%zd)%s", row, s, val); 243 | rewrite(1, buf, k < (int)sizeof buf ? k : (int)sizeof buf); 244 | } 245 | 246 | static void 247 | show_procs(void) 248 | { 249 | char buf[1024] = "procs:\tid\tpid\tcount\ttitle\r\n"; 250 | rewrite(1, buf, strlen(buf)); 251 | for (struct pty *p = S.p; p; p = p->next) { 252 | int k = snprintf(buf, sizeof buf, "\t%d\t%d\t%d\t%s\r\n", 253 | p->fd - 2, p->pid, p->count, p->status); 254 | rewrite(1, buf, k); 255 | } 256 | } 257 | 258 | static void 259 | show_state(void) 260 | { 261 | char buf[1024]; 262 | int k = sprintf(buf, "state: "); 263 | size_t s = describe_state(buf + k, sizeof buf - k); 264 | rewrite(1, buf, s + k); 265 | } 266 | 267 | void 268 | show_status(const char *arg) 269 | { 270 | if (S.count == -1 && *arg == 'x'){ 271 | for (int i = 0; i < LINES * COLS; i++) { 272 | putchar(' '); 273 | } 274 | putchar('\r'); 275 | putchar('\n'); 276 | fflush(stdout); 277 | } 278 | if (*arg == 'x') { 279 | arg += 1; 280 | } 281 | if (*arg == 'r') { 282 | show_row(arg + 1); 283 | } else { 284 | show_procs(); 285 | show_state(); 286 | show_layout(*arg ? arg : "0x1d5"); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /test-main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 - 2023 William Pursell 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include "config.h" 18 | #include "test-unit.h" 19 | #include 20 | #include 21 | 22 | #define SKIP_TEST 77 23 | 24 | static int read_timeout = 1; /* Set to 0 when interactively debugging */ 25 | static int main_timeout = 10; 26 | static int check_test_status(int rv, int status, int pty, const char *name); 27 | static int get_secondary_fd(int fd); 28 | int ctlkey = CTRL('g'); 29 | 30 | union param { 31 | struct { unsigned flag; } hup; 32 | struct { int row; unsigned flag; } usr1; 33 | }; 34 | 35 | static void 36 | write_pty(int fd, unsigned flags, const char *wait, const char *fmt, va_list ap) 37 | { 38 | char cmd[1024]; 39 | size_t n; 40 | char *b = cmd; 41 | if (flags & 0x1) { 42 | *b++ = ctlkey; 43 | } 44 | n = vsnprintf(b, sizeof cmd - (b - cmd), fmt, ap); 45 | if (n > sizeof cmd - 4) { 46 | err(EXIT_FAILURE, "Invalid string in write_pty"); 47 | } 48 | assert( b[n] == '\0' ); 49 | if (flags & 0x2) { 50 | b[n++] = '\r'; 51 | b[n] = '\0'; 52 | } 53 | const char *e = b + n; 54 | b = cmd; 55 | while (b < e) { 56 | ssize_t s = write(fd, b, e - b); 57 | if (s < 0 && errno != EINTR) { 58 | err(EXIT_FAILURE, "write to pty"); 59 | } 60 | b += s < 0 ? 0 : s; 61 | } 62 | if (wait != NULL) { 63 | grep(fd, wait); 64 | } 65 | } 66 | 67 | void __attribute__((format(printf,3,4))) 68 | send_cmd(int fd, const char *wait, const char *fmt, ...) 69 | { 70 | va_list ap; 71 | va_start(ap, fmt); 72 | write_pty(fd, 0x3, wait, fmt, ap); 73 | va_end(ap); 74 | } 75 | 76 | /* Write text to the pty and wait until the string wait is seen */ 77 | void __attribute__((format(printf,3,4))) 78 | send_txt(int fd, const char *wait, const char *fmt, ...) 79 | { 80 | va_list ap; 81 | va_start(ap, fmt); 82 | write_pty(fd, 0x2, wait, fmt, ap); 83 | va_end(ap); 84 | } 85 | 86 | void __attribute__((format(printf,3,4))) 87 | send_raw(int fd, const char *wait, const char *fmt, ...) 88 | { 89 | va_list ap; 90 | va_start(ap, fmt); 91 | write_pty(fd, 0, wait, fmt, ap); 92 | va_end(ap); 93 | } 94 | ssize_t timed_read(int, void *, size_t, const char *); 95 | 96 | int 97 | get_layout(int fd, int flag, char *buf, size_t siz) 98 | { 99 | ssize_t s = 0; 100 | const char *end = buf + siz; 101 | int fd2 = get_secondary_fd(fd); 102 | int len = snprintf(buf, siz, "\033]62;%x\007", flag); 103 | write(fd2, buf, len); 104 | close(fd2); 105 | 106 | grep(fd, "layout: "); 107 | while (buf < end && s != -1) { 108 | s = timed_read(fd, buf, 1, "layout"); 109 | if (buf[0] == ':') { 110 | buf[0] = '\0'; 111 | break; 112 | } 113 | buf += s; 114 | } 115 | if (s == -1) { 116 | fprintf(stderr, "reading from child: %s\n", strerror(errno)); 117 | return -1; 118 | } 119 | return 0; 120 | } 121 | 122 | int 123 | get_state(int fd, char *state, size_t siz) 124 | { 125 | char buf[64]; 126 | ssize_t s; 127 | int fd2 = get_secondary_fd(fd); 128 | int len = snprintf(buf, siz, "\033]62;\007"); 129 | write(fd2, buf, len); 130 | close(fd2); 131 | grep(fd, "state: "); 132 | do s = timed_read(fd, state, siz, "state"); while (s == 0 ); 133 | if (s == -1) { 134 | fprintf(stderr, "reading from child: %s\n", strerror(errno)); 135 | return -1; 136 | } 137 | return 0; 138 | } 139 | 140 | int __attribute__((format(printf,3,4))) 141 | check_layout(int fd, int flag, const char *fmt, ...) 142 | { 143 | char buf[1024]; 144 | va_list ap; 145 | char expect[1024]; 146 | int rv = -1; 147 | 148 | va_start(ap, fmt); 149 | (void)vsnprintf(expect, sizeof expect, fmt, ap); 150 | va_end(ap); 151 | 152 | if (get_layout(fd, flag, buf, sizeof buf) == 0) { 153 | if (strcmp( buf, expect )) { 154 | fputs("unexpected layout:\nreceived: ", stderr); 155 | for (const char *b = buf; *b; b++) { 156 | fputc(isprint(*b) ? *b : '?', stderr); 157 | } 158 | fprintf(stderr, "\nexpected: %s\n", expect); 159 | } else { 160 | rv = 0; 161 | } 162 | } 163 | return rv; 164 | } 165 | 166 | static void noop(int s){ (void)s; } 167 | ssize_t 168 | timed_read(int fd, void *buf, size_t count, const char *n) 169 | { 170 | fd_set set; 171 | struct timeval t = { .tv_sec = read_timeout, .tv_usec = 0 }; 172 | struct timeval *timeout = read_timeout ? &t : NULL; 173 | struct sigaction sa; 174 | memset(&sa, 0, sizeof sa); 175 | sa.sa_flags = 0; 176 | sigemptyset(&sa.sa_mask); 177 | sa.sa_handler = noop; 178 | sigaction(SIGINT, &sa, NULL); 179 | 180 | FD_ZERO(&set); 181 | FD_SET(fd, &set); 182 | 183 | switch (select(fd + 1, &set, NULL, NULL, timeout)) { 184 | case -1: 185 | err(EXIT_FAILURE, "select %d waiting for %s", fd, n); 186 | case 0: 187 | errx(EXIT_FAILURE, "timedout waiting for %s", n); 188 | } 189 | sa.sa_handler = SIG_DFL; 190 | sigaction(SIGINT, &sa, NULL); 191 | return read(fd, buf, count); 192 | } 193 | 194 | /* 195 | * Read from fd until needle is seen or end of file. This is not 196 | * intended to check anything, but is merely a synchronization device 197 | * to delay the test until data is seen to verify that the underlying 198 | * shell has processed input. 199 | */ 200 | void 201 | grep(int fd, const char *needle) 202 | { 203 | assert( needle != NULL ); 204 | 205 | ssize_t rc; 206 | char c; 207 | const char *n = needle; 208 | while (*n != '\0') { 209 | do rc = timed_read(fd, &c, 1, needle); while (rc == 0 ); 210 | if (rc == -1) { 211 | err(EXIT_FAILURE, "read from pty"); 212 | } 213 | if (c != *n++) { 214 | n = c == *needle ? needle + 1 : needle; 215 | } 216 | } 217 | } 218 | 219 | static int 220 | get_secondary_fd(int fd) 221 | { 222 | int fd2; 223 | char path[PATH_MAX]; 224 | char *p = path; 225 | const char *end = path + sizeof path; 226 | send_cmd(fd, NULL, "1Q"); 227 | grep(fd, "layout:"); 228 | grep(fd, "2nd="); 229 | do { 230 | if (timed_read(fd, p, 1, "2nd path") != 1) { 231 | err(1, "Invalid read getting 2ndary"); 232 | } 233 | } while (*p != ')' && ++p < end -1 ); 234 | *p = '\0'; 235 | if ((fd2 = open(path, O_WRONLY)) == -1) { 236 | err(1, "%s", path); 237 | } 238 | return fd2; 239 | } 240 | 241 | int 242 | get_row(int fd, int row, char *buf, size_t siz) 243 | { 244 | size_t count = 0; 245 | char *r = buf; 246 | int fd2; 247 | 248 | fd2 = get_secondary_fd(fd); 249 | int len = snprintf(buf, siz, "\033]62;r%d\007", row - 1); 250 | write(fd2, buf, len); 251 | close(fd2); 252 | 253 | sprintf(buf, "row %d:(", row - 1); 254 | grep(fd, buf); 255 | /* We expect to see "row N:(len)" on the fd, where len is the width of 256 | * the row. The above grep discards "row N:(". Now read the length. */ 257 | 258 | while (*r != ')') { 259 | if (timed_read(fd, r, 1, "count in row validation") != 1) { 260 | err(1, "Invalid read in row validation"); 261 | } else if (*r == ')') { 262 | ; 263 | } else if (! isdigit(*r)) { 264 | err(1, "Expected count, got '%c'\n", *r); 265 | } else { 266 | count = 10 * count + *r - '0'; 267 | } 268 | } 269 | if (count > siz - 1) { 270 | err(1, "Row is too long"); 271 | } 272 | ssize_t s = timed_read(fd, buf, count, "Reading row"); 273 | if (s == -1) { 274 | fprintf(stderr, "reading from child: %s\n", strerror(errno)); 275 | return -1; 276 | } 277 | buf[s] = 0; 278 | return 0; 279 | } 280 | 281 | /* TODO: allow alternatives. That is, sometimes there is a race, and 282 | * a row has one of two values (depending on if the shell has printed 283 | * a prompt). It might be nice to check that. 284 | */ 285 | int 286 | validate_row(int fd, int row, const char *fmt, ... ) 287 | { 288 | char buf[1024]; 289 | if (get_row(fd, row, buf, sizeof buf) == -1) { 290 | return -1; 291 | } 292 | char expect[1024]; 293 | va_list ap; 294 | va_start(ap, fmt); 295 | (void)vsnprintf(expect, sizeof expect, fmt, ap); 296 | va_end(ap); 297 | 298 | int status = 0; 299 | if (strcmp( buf, expect )) { 300 | fprintf(stderr, "unexpected content in row %d\n", row); 301 | fputs("received: '", stderr); 302 | for (const char *b = buf; *b; b++) { 303 | fputc(isprint(*b) ? *b : '?', stderr); 304 | } 305 | fprintf(stderr, "'\nexpected: '%s'\n", expect); 306 | status = 1; 307 | } 308 | return status; 309 | } 310 | 311 | 312 | static void * 313 | xrealloc(void *buf, size_t num, size_t siz) 314 | { 315 | buf = realloc(buf, num * siz); 316 | if (buf == NULL) { 317 | perror("realloc"); 318 | exit(EXIT_FAILURE); 319 | } 320 | return buf; 321 | } 322 | 323 | struct st { 324 | char *name; 325 | test *f; 326 | int envc; 327 | char ** env; 328 | int argc; 329 | char ** argv; 330 | struct st *next; 331 | }; 332 | static int execute_test(struct st *, const char *); 333 | static void spawn_test(struct st *v, const char *argv0); 334 | static void 335 | new_test(char *name, test *f, struct st **h, ...) 336 | { 337 | char *arg; 338 | struct st *tmp = *h; 339 | struct st *a = *h = xrealloc(NULL, 1, sizeof *a); 340 | struct { 341 | int *c; 342 | char ***v; 343 | } cv; 344 | a->next = tmp; 345 | a->name = name; 346 | a->f = f; 347 | a->envc = 0; 348 | a->argc = 1; 349 | a->env = xrealloc(NULL, 1, sizeof *a->env); 350 | a->argv = xrealloc(NULL, 2, sizeof *a->argv); 351 | a->argv[0] = "smtx"; 352 | cv.c = &a->envc; 353 | cv.v = &a->env; 354 | va_list ap; 355 | va_start(ap, h); 356 | while ((arg = va_arg(ap, char *)) != NULL) { 357 | if (strcmp(arg, "args") == 0) { 358 | cv.c = &a->argc; 359 | cv.v = &a->argv; 360 | continue; 361 | } 362 | *cv.v = xrealloc(*cv.v, *cv.c + 2, sizeof **cv.v); 363 | (*cv.v)[(*cv.c)++] = arg; 364 | } 365 | va_end(ap); 366 | a->env[a->envc] = NULL; 367 | a->argv[a->argc] = NULL; 368 | } 369 | 370 | static int 371 | match_name(const char *a, const char *b) 372 | { 373 | const char *under = strchr(a, '_'); 374 | return strcmp(a, b) && (!under || strcmp(under + 1, b)); 375 | } 376 | 377 | static int 378 | exit_status(int status) 379 | { 380 | return WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE; 381 | } 382 | 383 | static struct st * initialize_tests(char const **); 384 | 385 | int 386 | main(int argc, char *const argv[]) 387 | { 388 | int fail_count = 0; 389 | int skip_count = 0; 390 | int total_count = 0; 391 | char const *argv0 = argv[0]; 392 | struct st *tab, *v; 393 | 394 | tab = initialize_tests(&argv0); 395 | 396 | for (v = tab; v && ( argc < 2 || *++argv ); v = v ? v->next : NULL) { 397 | const char *name = *argv; 398 | if (argc > 1) { 399 | for (v = tab; v && match_name(v->name, name); ) 400 | v = v->next; 401 | } 402 | if (v && v->f) { 403 | total_count += 1; 404 | if (strcmp(v->name, argv0)) { 405 | spawn_test(v, argv0); 406 | } else { 407 | return execute_test(v, argv0); 408 | } 409 | } else { 410 | fprintf(stderr, "unknown function: %s\n", name); 411 | fail_count += 1; 412 | } 413 | } 414 | for (int status = 0, i = 0; i < total_count; i++) { 415 | waitpid(-1, &status, 0); 416 | switch (exit_status(status)) { 417 | case EXIT_FAILURE: 418 | fail_count += 1; 419 | break; 420 | case SKIP_TEST: 421 | skip_count += 1; 422 | } 423 | } 424 | if (fail_count > 0 || skip_count > 0) { 425 | fprintf( 426 | stderr, 427 | "%d test%s total: %d skipped, %d failed\n", 428 | total_count, 429 | total_count > 1 ? "s" : "", 430 | skip_count, 431 | fail_count 432 | ); 433 | } 434 | return fail_count ? EXIT_FAILURE : EXIT_SUCCESS; 435 | } 436 | 437 | /* Initialize a child to run a test. We re-exec to force argv[0] to 438 | * the name of the test so any error messages generated using err() 439 | * will have the test name, and to make it easier to pick them out 440 | * in the output of ps. 441 | */ 442 | static void 443 | spawn_test(struct st *v, const char *argv0) 444 | { 445 | pid_t pid[3]; 446 | int status; 447 | int fd[2]; 448 | char *const args[] = { v->name, v->name, NULL }; 449 | switch (pid[0] = fork()) { 450 | case -1: 451 | err(EXIT_FAILURE, "fork"); 452 | case 0: 453 | if (pipe(fd)){ 454 | err(EXIT_FAILURE, "pipe"); 455 | } 456 | switch (pid[1] = fork()) { 457 | case -1: 458 | err(EXIT_FAILURE, "fork"); 459 | case 0: 460 | dup2(fd[1], STDERR_FILENO); 461 | close(fd[0]); 462 | close(fd[1]); 463 | execv(argv0, args); 464 | err(EXIT_FAILURE, "%s", argv0); 465 | } 466 | switch (pid[2] = fork()) { 467 | case -1: 468 | err(EXIT_FAILURE, "fork"); 469 | case 0: 470 | { 471 | int c, nl = 0; 472 | FILE *fp = fdopen(fd[0], "r"); 473 | close(fd[1]); 474 | alarm(main_timeout); 475 | while (( c = fgetc(fp)) != EOF) { 476 | if (nl) { 477 | fputs(v->name, stderr); 478 | fputs(": ", stderr); 479 | nl = 0; 480 | } 481 | fputc(c, stderr); 482 | nl = c == '\n'; 483 | 484 | } 485 | _exit(0); 486 | } 487 | } 488 | close(fd[0]); 489 | close(fd[1]); 490 | 491 | pid_t died = wait(&status); 492 | if (died == pid[1]) { 493 | if (kill(pid[2], SIGKILL) ) { 494 | perror("kill"); 495 | } 496 | wait(NULL); 497 | } else { 498 | if (kill(pid[1], SIGKILL) ) { 499 | perror("kill"); 500 | } 501 | wait(&status); 502 | if (exit_status(status)) { 503 | fprintf(stderr, "FAIL(timeout): %s\n", v->name); 504 | } 505 | } 506 | _exit(exit_status(status)); 507 | } 508 | } 509 | 510 | static void 511 | set_window_size(struct winsize *ws) 512 | { 513 | char *t = getenv("LINES"); 514 | char *c = getenv("COLUMNS"); 515 | ws->ws_row = t ? strtol(t, NULL, 10) : 24; 516 | ws->ws_col = c ? strtol(c, NULL, 10) : 80; 517 | unsetenv("LINES"); 518 | unsetenv("COLUMNS"); 519 | } 520 | 521 | static int 522 | execute_test(struct st *v, const char *name) 523 | { 524 | int fd[2]; /* primary/secondary fd of pty */ 525 | int status; 526 | struct winsize ws; 527 | int rv = 1; 528 | 529 | (void)name; 530 | assert( strcmp(name, v->name) == 0 ); 531 | unsetenv("ENV"); /* Suppress all shell initializtion */ 532 | setenv("SHELL", "/bin/sh", 1); 533 | setenv("PS1", PROMPT, 1); 534 | unsetenv("LINES"); 535 | unsetenv("COLUMNS"); 536 | for (char **a = v->env; a && *a; a += 2) { 537 | setenv(a[0], a[1], 1); 538 | } 539 | set_window_size(&ws); 540 | if (openpty(fd, fd + 1, NULL, NULL, &ws)) { 541 | err(EXIT_FAILURE, "openpty"); 542 | } 543 | switch (fork()) { 544 | case -1: 545 | err(1, "fork"); 546 | break; 547 | case 0: 548 | if (close(fd[0])) { 549 | err(EXIT_FAILURE, "close"); 550 | } 551 | if (login_tty(fd[1])) { 552 | err(EXIT_FAILURE, "login_tty"); 553 | } 554 | rv = 0; 555 | execv("./smtx", v->argv); 556 | err(EXIT_FAILURE, "execv"); 557 | default: 558 | if (close(fd[1])) { 559 | err(EXIT_FAILURE, "close secondary"); 560 | } 561 | if (getenv("XFAIL") == NULL) { 562 | grep(fd[0], PROMPT); /* Wait for shell to initialize */ 563 | } 564 | rv = v->f(fd[0]); 565 | send_cmd(fd[0], NULL, "0x"); 566 | wait(&status); 567 | } 568 | status = check_test_status(rv, status, fd[0], v->name); 569 | 570 | char *verbosity = getenv("V"); 571 | if (verbosity && strtol(verbosity, NULL, 10) > 0) { 572 | printf("%20s: %s\n", v->name, status == 77 ? "SKIP" : 573 | status != 0 ? "FAIL" : "pass" ); 574 | } 575 | return status; 576 | } 577 | 578 | static int 579 | check_test_status(int rv, int status, int pty, const char *name) 580 | { 581 | char *expect = getenv("XFAIL"); 582 | int expect_stat = expect ? strtol(expect, NULL, 10) : 0; 583 | int expected = WIFEXITED(status) && WEXITSTATUS(status) == expect_stat; 584 | 585 | if (! expected) { 586 | char iobuf[BUFSIZ]; 587 | ssize_t r = read(pty, iobuf, sizeof iobuf - 1); 588 | if (r > 0) { 589 | iobuf[r] = '\0'; 590 | for (char *s = iobuf; *s; s++) { 591 | if (isprint(*s) || *s == '\n') { 592 | fputc(*s, stderr); 593 | } 594 | } 595 | } 596 | } else if (WIFSIGNALED(status)) { 597 | fprintf(stderr, "test %s caught signal %d\n", 598 | name, WTERMSIG(status)); 599 | } 600 | switch (rv) { 601 | case SKIP_TEST: 602 | fputs("SKIPPED\n", stderr); 603 | return SKIP_TEST; 604 | case 0: 605 | return expected ? EXIT_SUCCESS : EXIT_FAILURE; 606 | default: 607 | return EXIT_FAILURE; 608 | } 609 | } 610 | 611 | char bigint[128]; 612 | 613 | static struct st * 614 | initialize_tests(char const **argv0) 615 | { 616 | struct st *tab = NULL; 617 | char *base; 618 | 619 | if ((base = strrchr(*argv0, '/')) != NULL) { 620 | *argv0 = base + 1; 621 | chdir(*argv0); 622 | } 623 | snprintf(bigint, sizeof bigint, "%d", INT_MAX); 624 | 625 | #define F(x, ...) new_test(#x, x, &tab, ##__VA_ARGS__, NULL) 626 | F(test_ack); 627 | F(test_alt); 628 | F(test_attach); 629 | F(test_bighist, "XFAIL", "1", "STATUS", "1", "args", "-s", bigint); 630 | F(test_changehist, "args", "-s", "128"); 631 | F(test_cols, "COLUMNS", "92", "args", "-w", "97"); 632 | F(test_csr); 633 | F(test_cup); 634 | F(test_cursor); 635 | F(test_dashc, "args", "-c", "l"); 636 | F(test_dasht, "args", "-t", "uninstalled_terminal_type"); 637 | F(test_dch); 638 | F(test_decaln); 639 | F(test_ech); 640 | F(test_ed); 641 | F(test_el); 642 | F(test_equalize); 643 | F(test_hpr); 644 | F(test_ich); 645 | F(test_insert); 646 | F(test_layout); 647 | F(test_layout2); 648 | F(test_layout3); 649 | F(test_layout4); 650 | F(test_lnm); 651 | F(test_mode, "COLUMNS", "140"); 652 | F(test_navigate); 653 | F(test_nel, "TERM", "smtx"); 654 | F(test_pager ,"MORE", ""); 655 | F(test_pnm); 656 | F(test_prune); 657 | F(test_repc); 658 | F(test_resend); 659 | F(test_resize); 660 | F(test_resizepty, "args", "-s", "10"); 661 | F(test_ri); 662 | F(test_row); 663 | F(test_scrollback); 664 | F(test_scrollh, "COLUMNS", "26", "args", "-w", "78"); 665 | F(test_scs); 666 | F(test_sgr); 667 | F(test_su); 668 | F(test_swap); 669 | F(test_tabstop); 670 | F(test_title); 671 | F(test_tput); 672 | F(test_transpose); 673 | F(test_utf); 674 | F(test_vis); 675 | F(test_wait); 676 | F(test_width); 677 | return tab; 678 | } 679 | -------------------------------------------------------------------------------- /test-shell: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export SHELL=/bin/sh 4 | die() { echo "$*"; exit 1; } >&2 5 | skip() { echo "$*"; exit 77; } >&2 6 | 7 | ./smtx -z && exit 1 # test invalid option 8 | ./smtx -v || exit 9 | ./smtx -h | grep -q usage: || exit 10 | toe | grep -q '^smtx ' || skip "Tests are not reliable unless smtx.ti is installed" 11 | -------------------------------------------------------------------------------- /test-unit.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 - 2023 William Pursell 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #if HAVE_PTY_H 33 | # include 34 | # include 35 | #elif HAVE_LIBUTIL_H 36 | # include 37 | #elif HAVE_UTIL_H 38 | # include 39 | #endif 40 | 41 | #ifndef CTRL 42 | #define CTRL(x) ((x) & 0x1f) 43 | #endif 44 | #define PROMPT "ps1>" 45 | 46 | extern int ctlkey; 47 | 48 | void __attribute__((format(printf,3,4))) 49 | send_txt(int fd, const char *wait, const char *fmt, ...); 50 | 51 | void __attribute__((format(printf,3,4))) 52 | send_cmd(int fd, const char *wait, const char *fmt, ...); 53 | 54 | void __attribute__((format(printf,3,4))) 55 | send_raw(int fd, const char *wait, const char *fmt, ...); 56 | 57 | int __attribute__((format(printf,3,4))) 58 | validate_row(int fd, int row, const char *fmt, ... ); 59 | 60 | int get_layout(int fd, int flag, char *layout, size_t siz); 61 | int get_state(int fd, char *state, size_t siz); 62 | int get_row(int fd, int row, char *buf, size_t siz); 63 | void grep(int fd, const char *needle); 64 | 65 | int __attribute__((format(printf,3,4))) 66 | check_layout(int fd, int flag, const char *fmt, ...); 67 | 68 | typedef int(test)(int fd); 69 | test test_ack; 70 | test test_alt; 71 | test test_attach; 72 | test test_bighist; 73 | test test_changehist; 74 | test test_cols; 75 | test test_csr; 76 | test test_cup; 77 | test test_cursor; 78 | test test_dashc; 79 | test test_dasht; 80 | test test_dch; 81 | test test_decaln; 82 | test test_ech; 83 | test test_ed; 84 | test test_el; 85 | test test_equalize; 86 | test test_hpr; 87 | test test_ich; 88 | test test_insert; 89 | test test_layout; 90 | test test_layout2; 91 | test test_layout3; 92 | test test_layout4; 93 | test test_lnm; 94 | test test_mode; 95 | test test_navigate; 96 | test test_nel; 97 | test test_pager; 98 | test test_pnm; 99 | test test_prune; 100 | test test_repc; 101 | test test_resend; 102 | test test_resize; 103 | test test_resizepty; 104 | test test_ri; 105 | test test_row; 106 | test test_scrollback; 107 | test test_scrollh; 108 | test test_scs; 109 | test test_sgr; 110 | test test_su; 111 | test test_swap; 112 | test test_transpose; 113 | test test_title; 114 | test test_tput; 115 | test test_tabstop; 116 | test test_utf; 117 | test test_vis; 118 | test test_wait; 119 | test test_width; 120 | -------------------------------------------------------------------------------- /vtparser.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #include 19 | #include 20 | #include 21 | #include "vtparser.h" 22 | 23 | struct action { 24 | void (*cb)(struct vtp *p, wchar_t w); 25 | struct state *next; 26 | }; 27 | struct state { 28 | int reset; 29 | int *lut; 30 | struct action act[0x80]; 31 | }; 32 | 33 | static struct state ground, esc_entry, esc_collect, 34 | csi_entry, csi_ignore, csi_param, csi_collect, 35 | osc_param, osc_string; 36 | 37 | static void 38 | collect(struct vtp *v, wchar_t w) 39 | { 40 | v->inter = w; 41 | } 42 | 43 | static void 44 | collect_osc(struct vtp *v, wchar_t w) 45 | { 46 | if (v->osc < v->oscbuf + sizeof v->oscbuf - 1) { 47 | *v->osc++ = wctob(w); 48 | } 49 | } 50 | 51 | static void 52 | param(struct vtp *v, wchar_t w) 53 | { 54 | v->argc = v->argc ? v->argc : 1; 55 | int *a = v->args + v->argc - 1; 56 | if (w == L';') { 57 | v->argc += 1; 58 | } else if (v->argc < MAXPARAM && *a < 9999) { 59 | *a = *a * 10 + w - '0'; 60 | } 61 | } 62 | 63 | static void 64 | handle_osc(struct vtp *v, wchar_t unused) 65 | { 66 | (void)unused; 67 | switch (v->args[0]) { 68 | case 2: set_status(v->p, v->oscbuf); break; 69 | case 60: build_layout(v->oscbuf); break; 70 | #ifndef NDEBUG 71 | case 62: show_status(v->oscbuf); break; 72 | #endif 73 | } 74 | } 75 | 76 | static void 77 | send(struct vtp *v, wchar_t w) 78 | { 79 | tput(v->p, w, v->inter, v->argc, v->args, v->s->lut[w]); 80 | } 81 | 82 | /* 83 | * State definitions built by consulting the excellent state chart created by 84 | * Paul Flo Williams: http://vt100.net/emu/dec_ansi_parser 85 | * Please note that Williams does not (AFAIK) endorse this work. 86 | */ 87 | #define LOWBITS \ 88 | [0] = {NULL, NULL}, \ 89 | [0x01 ... 0x17] = {send, NULL}, \ 90 | [0x18] = {send, &ground}, \ 91 | [0x19] = {send, NULL}, \ 92 | [0x1a] = {send, &ground}, \ 93 | [0x1b] = {NULL, &esc_entry}, \ 94 | [0x1c ... 0x1f] = {send, NULL} 95 | 96 | static struct state ground = { 97 | .reset = 1, 98 | .lut = gnds, 99 | .act = { 100 | LOWBITS, 101 | [0x20 ... 0x7f] = {send, NULL}, 102 | } 103 | }; 104 | 105 | static struct state esc_entry = { 106 | .reset = 1, 107 | .lut = escs, 108 | .act = { 109 | LOWBITS, 110 | [0x20] = {collect, &esc_collect}, /* sp */ 111 | [0x21] = {NULL, &osc_string}, /* ! */ 112 | [0x22 ... 0x2f] = {collect, &esc_collect}, /* "#$%&'()*+,-./ */ 113 | [0x30 ... 0x4f] = {send, &ground}, 114 | [0x50] = {NULL, &osc_string}, /* P */ 115 | [0x51 ... 0x57] = {send, &ground}, 116 | [0x58] = {NULL, NULL}, 117 | [0x59 ... 0x5a] = {send, &ground}, 118 | [0x5b] = {NULL, &csi_entry}, /* [ */ 119 | [0x5c] = {send, &ground}, /* \ */ 120 | [0x5d] = {NULL, &osc_param}, /* ] */ 121 | [0x5e] = {NULL, &osc_string}, /* ^ */ 122 | [0x5f] = {NULL, &osc_string}, /* _ */ 123 | [0x60 ... 0x6a] = {send, &ground}, /* `a-j */ 124 | [0x6b] = {NULL, &osc_string}, /* k */ 125 | [0x6c ... 0x7e] = {send, &ground}, /* l-z{|}~ */ 126 | [0x7f] = {NULL, NULL}, 127 | } 128 | }; 129 | 130 | static struct state esc_collect = { 131 | .reset = 0, 132 | .lut = escs, 133 | .act = { 134 | LOWBITS, 135 | [0x20 ... 0x2f] = {collect, NULL}, /* sp!"#$%&'()*+,-./ */ 136 | [0x30 ... 0x7e] = {send, &ground}, /* 0-9a-zA-z ... */ 137 | [0x7f] = {NULL, NULL}, 138 | } 139 | }; 140 | 141 | static struct state csi_entry = { 142 | .reset = 0, 143 | .lut = csis, 144 | .act = { 145 | LOWBITS, 146 | [0x20 ... 0x2f] = {collect, &csi_collect}, /* !"#$%&'()*+,-./ */ 147 | [0x30 ... 0x39] = {param, &csi_param}, /* 0 - 9 */ 148 | [0x3a] = {NULL, &csi_ignore}, /* : */ 149 | [0x3b] = {param, &csi_param}, /* ; */ 150 | [0x3c ... 0x3f] = {collect, &csi_param}, /* <=>? */ 151 | [0x40 ... 0x7e] = {send, &ground}, /* @A-Za-z[\]^_`{|}~ */ 152 | [0x7f] = {NULL, NULL}, 153 | } 154 | }; 155 | 156 | static struct state csi_ignore = { 157 | .reset = 0, 158 | .lut = csis, 159 | .act = { 160 | LOWBITS, 161 | [0x20 ... 0x3f] = {NULL, NULL}, /* !"#$%&'()*+,-./0-9... */ 162 | [0x40 ... 0x7e] = {NULL, &ground}, /* @A-Za-z[\]^_`{|}~ */ 163 | [0x7f] = {NULL, NULL}, 164 | } 165 | }; 166 | 167 | static struct state csi_param = { 168 | .reset = 0, 169 | .lut = csis, 170 | .act = { 171 | LOWBITS, 172 | [0x20 ... 0x2f] = {collect, &csi_collect}, 173 | [0x30 ... 0x39] = {param, NULL}, /* 0 - 9 */ 174 | [0x3a] = {NULL, &csi_ignore}, 175 | [0x3b] = {param, NULL}, /* ; */ 176 | [0x3c ... 0x3f] = {NULL, &csi_ignore}, 177 | [0x40 ... 0x7e] = {send, &ground}, 178 | [0x7f] = {NULL, NULL}, 179 | } 180 | }; 181 | 182 | static struct state csi_collect = { 183 | .reset = 0, 184 | .lut = csis, 185 | .act = { 186 | LOWBITS, 187 | [0x20 ... 0x2f] = {collect, NULL}, /* !"#$%&'()*+,-./ */ 188 | [0x30 ... 0x3f] = {NULL, &csi_ignore}, /* 0-9 :;<=>? */ 189 | [0x40 ... 0x7e] = {send, &ground}, 190 | [0x7f] = {NULL, NULL}, 191 | } 192 | }; 193 | 194 | #pragma GCC diagnostic ignored "-Woverride-init" 195 | static struct state osc_param = { 196 | .reset = 0, 197 | .lut = oscs, 198 | .act = { 199 | LOWBITS, 200 | [0x07] = {handle_osc, &ground}, 201 | [0x20 ... 0x7f] = {collect_osc, NULL}, 202 | [0x30 ... 0x39] = {param, NULL}, /* 0 - 9 */ 203 | [0x3b] = {param, &osc_string}, /* ; */ 204 | } 205 | }; 206 | 207 | static struct state osc_string = { 208 | .reset = 0, 209 | .lut = oscs, 210 | .act = { 211 | LOWBITS, 212 | [0x07] = {handle_osc, &ground}, 213 | [0x0a] = {handle_osc, &ground}, /* \n */ 214 | [0x0d] = {handle_osc, &ground}, /* \r */ 215 | [0x20 ... 0x7f] = {collect_osc, NULL}, 216 | } 217 | }; 218 | 219 | void 220 | vtreset(struct vtp *v) 221 | { 222 | memset(&v->inter, 0, sizeof *v - offsetof(struct vtp, inter)); 223 | v->s = &ground; 224 | v->osc = v->oscbuf; 225 | } 226 | 227 | void 228 | vtwrite(struct vtp *vp, const char *s, size_t n) 229 | { 230 | size_t r; 231 | for (const char *e = s + n; s < e; s += r) { 232 | wchar_t w; 233 | switch (r = mbrtowc(&w, s, e - s, &vp->ms)) { 234 | case -1: /* invalid character, skip it */ 235 | case -2: /* incomplete character, skip it */ 236 | w = VTPARSER_BAD_CHAR; 237 | memset(&vp->ms, 0, sizeof vp->ms); /* Fallthru */ 238 | case 0: /* literal zero, write it and advance */ 239 | r = 1; 240 | } 241 | if (w >= 0 && w < 0x80) { 242 | struct action *a = vp->s->act + w; 243 | if (a->cb) { 244 | a->cb(vp, w); 245 | } 246 | if (a->next) { 247 | if (a->next->reset) { 248 | vtreset(vp); 249 | } 250 | vp->s = a->next; 251 | } 252 | } else { 253 | tput(vp->p, w, 0, 0, NULL, print); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /vtparser.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 - 2019 Rob King 3 | * Copyright 2020 - 2023 William Pursell 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | #ifndef VTC_H 19 | #define VTC_H 20 | 21 | #include 22 | #include 23 | 24 | /* The names for handlers come from their ANSI/ECMA/DEC mnemonics. */ 25 | enum cmd { 26 | ack=1, /* Acknowledge Enquiry */ 27 | bell, /* Terminal bell. */ 28 | cnl, /* Cursor Next Line */ 29 | cpl, /* Cursor Previous Line */ 30 | cr, /* Carriage Return */ 31 | csr, /* Change Scrolling Region */ 32 | cub, /* Cursor Backward */ 33 | cud, /* Cursor Down */ 34 | cuf, /* Cursor Forward */ 35 | cup, /* Cursor Position */ 36 | cuu, /* Cursor Up */ 37 | dch, /* Delete Character */ 38 | ech, /* Erase Character */ 39 | ed, /* Erase in Display */ 40 | el, /* Erase in Line */ 41 | hpa, /* Cursor Horizontal Absolute */ 42 | hpr, /* Cursor Horizontal Relative */ 43 | hts, /* Horizontal Tab Set */ 44 | ich, /* Insert Character */ 45 | idl, /* Insert/Delete Line */ 46 | ind, /* Index */ 47 | mode, /* Set or Reset Mode */ 48 | nel, /* Next Line */ 49 | numkp, /* Application/Numeric Keypad Mode */ 50 | osc, /* Operating System Command */ 51 | pnl, /* Newline */ 52 | print, /* Print a character to the terminal */ 53 | rc, /* Restore Cursor */ 54 | rep, /* Repeat Character */ 55 | ri, /* Reverse Index (scroll back) */ 56 | ris, /* Reset to Initial State */ 57 | sc, /* Save Cursor */ 58 | scs, /* Select Character Set */ 59 | sgr, /* SGR - Select Graphic Rendition */ 60 | so, /* Switch Out/In Character Set */ 61 | su, /* Scroll Up/Down */ 62 | tab, /* Tab forwards or backwards */ 63 | tbc, /* Tabulation Clear */ 64 | vis, /* Cursor visibility */ 65 | vpa, /* Cursor Vertical Absolute */ 66 | vpr, /* Cursor Vertical Relative */ 67 | #if 0 68 | 69 | The following are unimplemented: 70 | decid, /* Send Terminal Identification */ 71 | decreqtparm, /* Request Device Parameters */ 72 | dsr, /* Device Status Report */ 73 | #endif 74 | }; 75 | 76 | /* 77 | * VTPARSER_BAD_CHAR is the character that will be displayed when 78 | * an application sends an invalid multibyte sequence to the terminal. 79 | */ 80 | #ifndef VTPARSER_BAD_CHAR 81 | #ifdef __STDC_ISO_10646__ 82 | #define VTPARSER_BAD_CHAR ((wchar_t)0xfffd) 83 | #else 84 | #define VTPARSER_BAD_CHAR L'?' 85 | #endif 86 | #endif 87 | 88 | #define MAXPARAM 16 89 | #define MAXOSC 511 90 | 91 | struct pty; 92 | struct vtp { 93 | struct pty *p; 94 | struct state *s; 95 | wchar_t inter; 96 | int argc; 97 | int args[MAXPARAM]; 98 | char oscbuf[MAXOSC + 1]; 99 | char *osc; 100 | mbstate_t ms; 101 | }; 102 | extern int cons[0x80]; 103 | extern int csis[0x80]; 104 | extern int escs[0x80]; 105 | extern int oscs[0x80]; 106 | extern int gnds[0x80]; 107 | 108 | extern void set_status(struct pty *p, const char *arg); 109 | extern void tput(struct pty *, wchar_t, wchar_t, int, int *, int); 110 | extern void vtreset(struct vtp *v); 111 | extern void vtwrite(struct vtp *vp, const char *s, size_t n); 112 | extern int build_layout(const char *); 113 | 114 | /* Debugging/test harness */ 115 | #ifndef NDEBUG 116 | extern void show_status(const char *); 117 | #endif 118 | #endif 119 | --------------------------------------------------------------------------------