dvtm

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit bb19be7f72d09ecb19ccae4920c1714cf078e128
Author: AndrewLockVI <andrew@laack.co>
Date:   Wed, 19 Feb 2025 18:29:04 -0600

Init commit

Diffstat:
A.gitignore | 5+++++
ALICENSE | 22++++++++++++++++++++++
AMakefile | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abstack.c | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 209+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.h | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.mk | 14++++++++++++++
Advtm-editor.1 | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Advtm-editor.c | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Advtm-pager.1 | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Advtm.1 | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Advtm.c | 1951+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Advtm.info | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afibonacci.c | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aforkpty-aix.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aforkpty-sunos.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afullscreen.c | 5+++++
Agrid.c | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Atestsuite.sh | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atile.c | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Atstack.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Avstack.c | 27+++++++++++++++++++++++++++
Avt.c | 1981+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Avt.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
25 files changed, 6176 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,5 @@ +dvtm-editor +dvtm-pager +dvtm-status +dvtm_ +dvtm diff --git a/LICENSE b/LICENSE @@ -0,0 +1,22 @@ +MIT/X Consortium License + + (c) 2006-2007 Anselm R. Garbe <garbeam at gmail dot com> + (c) 2007-2016 Marc André Tanner <mat at brain-dump dot org> + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile @@ -0,0 +1,64 @@ +include config.mk + +SRC = dvtm.c vt.c +BIN = dvtm dvtm-status dvtm-editor dvtm-pager +MANUALS = dvtm.1 dvtm-editor.1 dvtm-pager.1 + +VERSION = $(shell git describe --always --dirty 2>/dev/null || echo "0.15-git") +CFLAGS += -DVERSION=\"${VERSION}\" +DEBUG_CFLAGS = ${CFLAGS} -UNDEBUG -O0 -g -ggdb -Wall -Wextra -Wno-unused-parameter + +all: dvtm dvtm-editor + +config.h: + cp config.def.h config.h + +dvtm: config.h config.mk *.c *.h + ${CC} ${CFLAGS} ${SRC} ${LDFLAGS} ${LIBS} -o $@ + +dvtm-editor: dvtm-editor.c + ${CC} ${CFLAGS} $^ ${LDFLAGS} -o $@ + +man: + @for m in ${MANUALS}; do \ + echo "Generating $$m"; \ + sed -e "s/VERSION/${VERSION}/" "$$m" | mandoc -W warning -T utf8 -T xhtml -O man=%N.%S.html -O style=mandoc.css 1> "$$m.html" || true; \ + done + +debug: clean + @$(MAKE) CFLAGS='${DEBUG_CFLAGS}' + +clean: + @echo cleaning + @rm -f dvtm + @rm -f dvtm-editor + +dist: clean + @echo creating dist tarball + @git archive --prefix=dvtm-${VERSION}/ -o dvtm-${VERSION}.tar.gz HEAD + +install: all + @mkdir -p ${DESTDIR}${PREFIX}/bin + @for b in ${BIN}; do \ + echo "installing ${DESTDIR}${PREFIX}/bin/$$b"; \ + cp -f "$$b" "${DESTDIR}${PREFIX}/bin" && \ + chmod 755 "${DESTDIR}${PREFIX}/bin/$$b"; \ + done + @echo installing manual page to ${DESTDIR}${MANPREFIX}/man1 + @mkdir -p ${DESTDIR}${MANPREFIX}/man1 + @for m in ${MANUALS}; do \ + sed -e "s/VERSION/${VERSION}/" < "$$m" > "${DESTDIR}${MANPREFIX}/man1/$$m" && \ + chmod 644 "${DESTDIR}${MANPREFIX}/man1/$$m"; \ + done + @echo installing terminfo description + @TERMINFO=${TERMINFO} tic -s dvtm.info + +uninstall: + @for b in ${BIN}; do \ + echo "removing ${DESTDIR}${PREFIX}/bin/$$b"; \ + rm -f "${DESTDIR}${PREFIX}/bin/$$b"; \ + done + @echo removing manual page from ${DESTDIR}${MANPREFIX}/man1 + @rm -f ${DESTDIR}${MANPREFIX}/man1/dvtm.1 + +.PHONY: all clean dist install uninstall debug diff --git a/README.md b/README.md @@ -0,0 +1,281 @@ +# dvtm - dynamic virtual terminal manager + +[dvtm](https://www.brain-dump.org/projects/dvtm/) brings the concept +of tiling window management, popularized by X11-window managers like +[dwm](https://dwm.suckless.org) to the console. As a console window +manager it tries to make it easy to work with multiple console based +programs. + +![abduco+dvtm demo](https://raw.githubusercontent.com/martanne/dvtm/gh-pages/screencast.gif#center) + +## News + +- [dvtm-0.15](https://www.brain-dump.org/projects/dvtm/dvtm-0.15.tar.gz) + [released](https://lists.suckless.org/dev/1601/28095.html) (09.01.2016) +- [dvtm-0.14](https://www.brain-dump.org/projects/dvtm/dvtm-0.14.tar.gz) + [released](https://lists.suckless.org/dev/1502/25558.html) (19.02.2015) +- [dvtm-0.13](https://www.brain-dump.org/projects/dvtm/dvtm-0.13.tar.gz) + [released](https://lists.suckless.org/dev/1411/24449.html) (15.11.2014) +- [dvtm-0.12](https://www.brain-dump.org/projects/dvtm/dvtm-0.12.tar.gz) + [released](https://lists.suckless.org/dev/1407/22702.html) (05.07.2014) +- [dvtm-0.11](https://www.brain-dump.org/projects/dvtm/dvtm-0.11.tar.gz) + [released](https://lists.suckless.org/dev/1403/20371.html) (08.03.2014) +- [dvtm-0.10](https://www.brain-dump.org/projects/dvtm/dvtm-0.10.tar.gz) + [released](https://lists.suckless.org/dev/1312/18805.html) (28.12.2013) +- [dvtm-0.9](https://www.brain-dump.org/projects/dvtm/dvtm-0.9.tar.gz) + [released](https://lists.suckless.org/dev/1304/15112.html) (3.04.2013) +- [dvtm-0.8](https://www.brain-dump.org/projects/dvtm/dvtm-0.8.tar.gz) + [released](https://lists.suckless.org/dev/1208/12004.html) (1.08.2012) +- [dvtm-0.7](https://www.brain-dump.org/projects/dvtm/dvtm-0.7.tar.gz) + [released](https://lists.suckless.org/dev/1109/9266.html) (4.09.2011) +- [dvtm-0.6](https://www.brain-dump.org/projects/dvtm/dvtm-0.6.tar.gz) + [released](https://lists.suckless.org/dev/1010/6146.html) (8.10.2010) +- [dvtm-0.5.2](https://www.brain-dump.org/projects/dvtm/dvtm-0.5.2.tar.gz) + [released](https://lists.suckless.org/dev/0907/0520.html) (7.07.2009) +- [dvtm-0.5.1](https://www.brain-dump.org/projects/dvtm/dvtm-0.5.1.tar.gz) + [released](https://lists.suckless.org/dwm/0902/7405.html) (8.02.2009) +- [dvtm-0.5](https://www.brain-dump.org/projects/dvtm/dvtm-0.5.tar.gz) + [released](https://lists.suckless.org/dwm/0901/7354.html) (26.01.2009) +- [dvtm-0.4.1](https://www.brain-dump.org/projects/dvtm/dvtm-0.4.1.tar.gz) + [released](https://lists.suckless.org/dwm/0805/5672.html) (10.05.2008) +- [dvtm-0.4](https://www.brain-dump.org/projects/dvtm/dvtm-0.4.tar.gz) + [released](https://lists.suckless.org/dwm/0802/4850.html) (17.02.2008) +- [dvtm-0.3](https://www.brain-dump.org/projects/dvtm/dvtm-0.3.tar.gz) + [released](https://lists.suckless.org/dwm/0801/4735.html) (12.01.2008) +- [dvtm-0.2](https://www.brain-dump.org/projects/dvtm/dvtm-0.2.tar.gz) + [released](https://lists.suckless.org/dwm/0712/4677.html) (29.12.2007) +- [dvtm-0.1](https://www.brain-dump.org/projects/dvtm/dvtm-0.1.tar.gz) + [released](https://lists.suckless.org/dwm/0712/4632.html) (21.12.2007) +- [dvtm-0.01](https://www.brain-dump.org/projects/dvtm/dvtm-0.01.tar.gz) + [released](https://lists.suckless.org/dwm/0712/4424.html) (08.12.2007) + +## Download + +Either Download the latest [source tarball](https://github.com/martanne/dvtm/releases), +compile (you will need curses headers) and install it + + $EDITOR config.mk && $EDITOR config.def.h && make && sudo make install + +or use one of the distribution provided +[binary packages](https://repology.org/project/dvtm/packages). + +## Why dvtm? The philosophy behind + +dvtm strives to adhere to the +[Unix philosophy](http://www.catb.org/esr/writings/taoup/html/ch01s06.html). +It tries to do one thing, *dynamic* window management on the console, +and to do it well. + +As such dvtm does *not* implement [session management](#faq) but instead +delegates this task to a separate tool called +[abduco](https://www.brain-dump.org/projects/abduco/). + +Similarly dvtm's copy mode is implemented by piping the scroll back buffer +content to an external editor and only storing whatever the editor writes +to `stdout`. Hence the selection process is delegated to the editor +where powerful features such as regular expression search are available. + +As a result dvtm's source code is relatively small +([~4000 lines of C](https://www.ohloh.net/p/dvtm/analyses/latest/languages_summary)), +simple and therefore easy to hack on. + +## Quickstart + +All of dvtm keybindings start with a common modifier which from now +on is refered to as `MOD`. By default `MOD` is set to `CTRL+g` however +this can be changed at runttime with the `-m` command line option. +For example setting `MOD` to `CTRL-b` is accomplished by starting +`dvtm -m ^b`. + +### Windows + +New windows are created with `MOD+c` and closed with `MOD+x`. +To switch among the windows use `MOD+j` and `MOD+k` or `MOD+[1..9]` +where the digit corresponds to the window number which is displayed +in the title bar. Windows can be minimized and restored with `MOD+.`. +Input can be directed to all visible window by pressing `MOD+a`, +issuing the same key combination again restores normal behaviour +i.e. only the currently focused window will receive input. + +### Layouts + +Visible Windows are arranged by a layout. Each layout consists of a +master and a tile area. Typically the master area occupies the largest +part of the screen and is intended for the currently most important +window. The size of the master area can be shrunk with `MOD+h` +and enlarged with `MOD-l` respectively. Windows can be zoomed into +the master area with `MOD+Enter`. The number of windows in the +master area can be increased and decreased with `MOD+i` and `MOD+d`. + +By default dvtm comes with 4 different layouts which can be cycled +through via `MOD+Space` + + * vertical stack: master area on the left half, other clients + stacked on the right + * bottom stack: master area on the top half, other clients stacked below + * grid: every window gets an equally sized portion of the screen + * fullscreen: only the selected window is shown and occupies the + whole available display area `MOD+m` + +Further layouts are included in the source tarball but disabled by +default. + +### Tagging + +Each window has a non empty set of tags [1..n] associated with it. A view +consists of a number of tags. The current view includes all windows +which are tagged with the currently active tags. The following key +bindings are used to manipulate the tagsets. + +- `MOD-0` view all windows with any tag +- `Mod-v-Tab` toggles to the previously selected tags +- `MOD-v-[1..n]` view all windows with nth tag +- `Mod-V-[1..n]` add/remove all windows with nth tag to/from the view +- `Mod-t-[1..n]` apply nth tag to focused window +- `Mod-T-[1..n]` add/remove nth tag to/from focused window + +### Statusbar + +dvtm can be instructed to read and display status messages from a named +pipe. As an example the +[`dvtm-status` script](https://raw.githubusercontent.com/martanne/dvtm/master/dvtm-status) +is provided which shows the current time. + +### Copymode ### + +`MOD+e` pipes the whole scroll buffer content to an external editor. +What ever the editor writes to `stdout` is remembered by dvtm and can +later be pasted with `MOD+p`. + +In order for this to work the editor needs to be usable as a filter +and should use `stderr` for its user interface. Examples where this is +the case include `sandy(1)` and [vis](https://www.brain-dump.org/projects/vis). + + $ echo Hello World | vis - | cat + +## Patches + +There exist a number of out of tree patches which customize dvtm's +behaviour: + + - [pertag](http://waxandwane.org/dvtm.html) (see also the corresponding + [mailing list post](https://lists.suckless.org/hackers/1510/8186.html)) + +## FAQ + +### Detach / reattach functionality + +dvtm doesn't have session support built in. Use +[abduco](https://www.brain-dump.org/projects/abduco/) instead. + + $ abduco -c dvtm-session + +Detach using `CTRL-\` and later reattach with + + $ abduco -a dvtm-session + +### Copy / Paste does not work under X + +If you have mouse support enabled, which is the case with the +default settings, you need to hold down shift while selecting +and inserting text. In case you don't like this behaviour either +run dvtm with the `-M` command line argument, disable it at run +time with `MOD+M` or modify `config.def.h` to disable it completely +at compile time. You will however no longer be able to perform +other mouse actions like selecting windows etc. + +### How to change the key bindings? + +The configuration of dvtm is done by creating a custom `config.h` +and (re)compiling the source code. See the default `config.def.h` +as an example, adapting it to your preference should be straightforward. +You basically define a set of layouts and keys which dvtm will use. +There are some pre defined macros to ease configuration. + +### WARNING: terminal is not fully functional + +This means you haven't installed the `dvtm.info` terminfo description +which can be done with `tic -s dvtm.info`. If for some reason you +can't install new terminfo descriptions set the `DVTM_TERM` environment +variable to a known terminal when starting `dvtm` as in + + $ DVTM_TERM=rxvt dvtm + +This will instruct dvtm to use rxvt as `$TERM` value within its windows. + +### How to set the window title? + +The window title can be changed by means of a +[xterm extension](https://tldp.org/HOWTO/Xterm-Title-3.html#ss3.2) +terminal escape sequence + + $ echo -ne "\033]0;Your title here\007" + +So for example in `bash` if you want to display the current working +directory in the window title this can be accomplished by means of +the following section in your startup files. + + # If this is an xterm set the title to user@host:dir + case "$TERM" in + dvtm*|xterm*|rxvt*) + PROMPT_COMMAND='echo -ne "\033]0;${USER}@${HOSTNAME}: ${PWD/$HOME/~}\007"' + ;; + *) + ;; + esac + +Other shells provide similar functionality, zsh as an example has a +[precmd function](http://zsh.sourceforge.net/Doc/Release/Functions.html#Hook-Functions) +which can be used to achieve the same effect. + +### Something is wrong with the displayed colors + +Make sure you have set `$TERM` correctly for example if you want to +use 256 color profiles you probably have to append `-256color` to +your regular terminal name. Also due to limitations of ncurses by +default you can only use 255 color pairs simultaneously. If you +need more than 255 different color pairs at the same time, then you +have to rebuild ncurses with + + $ ./configure ... --enable-ext-colors + +Note that this changes the ABI and therefore sets SONAME of the +library to 6 (i.e. you have to link against `libncursesw.so.6`). + +### Some characters are displayed like garbage + +Make sure you compiled dvtm against a unicode aware curses library +(in case of ncurses this would be `libncursesw`). Also make sure +that your locale settings contain UTF-8. + +### The numeric keypad does not work with Putty + +Disable [application keypad mode](https://the.earth.li/~sgtatham/putty/0.64/htmldoc/Chapter4.html#config-features-application) +in the Putty configuration under `Terminal => Features => Disable application keypad mode`. + +### Unicode characters do not work within Putty + +You have to tell Putty in which +[character encoding](https://the.earth.li/~sgtatham/putty/0.64/htmldoc/Chapter4.html#config-translation) +the received data is. Set the dropdown box under `Window => Translation` +to UTF-8. In order to get proper line drawing characters you proabably +also want to set the TERM environment variable to `putty` or `putty-256color`. +If that still doesn't do the trick then try running dvtm with the +following ncurses related environment variable set `NCURSES_NO_UTF8_ACS=1`. + +## Development + +You can always fetch the current code base from the git repository +located at [Github](https://github.com/martanne/dvtm/) or +[Sourcehut](https://git.sr.ht/~martanne/dvtm). + +If you have comments, suggestions, ideas, a bug report, a patch or +something else related to dvtm then write to the +[suckless developer mailing list](https://suckless.org/community) +or contact me directly. + +## License + +dvtm reuses some code of dwm and is released under the same +[MIT/X11 license](https://raw.githubusercontent.com/martanne/dvtm/master/LICENSE). +The terminal emulation part is licensed under the ISC license. diff --git a/bstack.c b/bstack.c @@ -0,0 +1,58 @@ +static void bstack(void) +{ + unsigned int i, n, nx, ny, nw, nh, m, mw, mh, tw; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + + m = MAX(1, MIN(n, screen.nmaster)); + mh = n == m ? wah : screen.mfact * wah; + mw = waw / m; + tw = n == m ? 0 : waw / (n - m); + nx = wax; + ny = way; + + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + if (i < m) { /* master */ + if (i > 0) { + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + nx++; + } + nh = mh; + nw = (i < m - 1) ? mw : (wax + waw) - nx; + } else { /* tile window */ + if (i == m) { + nx = wax; + ny += mh; + nh = (way + wah) - ny; + } + if (i > m) { + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + nx++; + } + nw = (i < n - 1) ? tw : (wax + waw) - nx; + } + resize(c, nx, ny, nw, nh); + nx += nw; + i++; + } + + /* Fill in nmaster intersections */ + if (n > m) { + nx = wax; + for (i = 0; i < m; i++) { + if (i > 0) { + mvaddch(ny, nx, ACS_PLUS); + nx++; + } + nw = (i < m - 1) ? mw : (wax + waw) - nx; + nx += nw; + } + } +} diff --git a/config.def.h b/config.def.h @@ -0,0 +1,209 @@ +/* valid curses attributes are listed below they can be ORed + * + * A_NORMAL Normal display (no highlight) + * A_STANDOUT Best highlighting mode of the terminal. + * A_UNDERLINE Underlining + * A_REVERSE Reverse video + * A_BLINK Blinking + * A_DIM Half bright + * A_BOLD Extra bright or bold + * A_PROTECT Protected mode + * A_INVIS Invisible or blank mode + */ + +enum { + DEFAULT, + BLUE, +}; + +static Color colors[] = { + [DEFAULT] = { .fg = -1, .bg = -1, .fg256 = -1, .bg256 = -1, }, + [BLUE] = { .fg = COLOR_BLUE, .bg = -1, .fg256 = 68, .bg256 = -1, }, +}; + +#define COLOR(c) COLOR_PAIR(colors[c].pair) +/* curses attributes for the currently focused window */ +#define SELECTED_ATTR (COLOR(BLUE) | A_NORMAL) +/* curses attributes for normal (not selected) windows */ +#define NORMAL_ATTR (COLOR(DEFAULT) | A_NORMAL) +/* curses attributes for a window with pending urgent flag */ +#define URGENT_ATTR NORMAL_ATTR +/* curses attributes for the status bar */ +#define BAR_ATTR (COLOR(BLUE) | A_NORMAL) +/* characters for beginning and end of status bar message */ +#define BAR_BEGIN '[' +#define BAR_END ']' +/* status bar (command line option -s) position */ +#define BAR_POS BAR_TOP /* BAR_BOTTOM, BAR_OFF */ +/* whether status bar should be hidden if only one client exists */ +#define BAR_AUTOHIDE true +/* master width factor [0.1 .. 0.9] */ +#define MFACT 0.5 +/* number of clients in master area */ +#define NMASTER 1 +/* scroll back buffer size in lines */ +#define SCROLL_HISTORY 500 +/* printf format string for the tag in the status bar */ +#define TAG_SYMBOL "[%s]" +/* curses attributes for the currently selected tags */ +#define TAG_SEL (COLOR(BLUE) | A_BOLD) +/* curses attributes for not selected tags which contain no windows */ +#define TAG_NORMAL (COLOR(DEFAULT) | A_NORMAL) +/* curses attributes for not selected tags which contain windows */ +#define TAG_OCCUPIED (COLOR(BLUE) | A_NORMAL) +/* curses attributes for not selected tags which with urgent windows */ +#define TAG_URGENT (COLOR(BLUE) | A_NORMAL | A_BLINK) + +const char tags[][8] = { "1", "2", "3", "4", "5" }; + +#include "tile.c" +#include "grid.c" +#include "bstack.c" +#include "fullscreen.c" + +/* by default the first layout entry is used */ +static Layout layouts[] = { + { "[]=", tile }, + { "+++", grid }, + { "TTT", bstack }, + { "[ ]", fullscreen }, +}; + +#define MOD CTRL('g') +#define TAGKEYS(KEY,TAG) \ + { { MOD, 'v', KEY, }, { view, { tags[TAG] } } }, \ + { { MOD, 't', KEY, }, { tag, { tags[TAG] } } }, \ + { { MOD, 'V', KEY, }, { toggleview, { tags[TAG] } } }, \ + { { MOD, 'T', KEY, }, { toggletag, { tags[TAG] } } }, + +/* you can specifiy at most 3 arguments */ +static KeyBinding bindings[] = { + { { MOD, 'c', }, { create, { NULL } } }, + { { MOD, 'C', }, { create, { NULL, NULL, "$CWD" } } }, + { { MOD, 'x', 'x', }, { killclient, { NULL } } }, + { { MOD, 'j', }, { focusnext, { NULL } } }, + { { MOD, 'J', }, { focusdown, { NULL } } }, + { { MOD, 'K', }, { focusup, { NULL } } }, + { { MOD, 'H', }, { focusleft, { NULL } } }, + { { MOD, 'L', }, { focusright, { NULL } } }, + { { MOD, 'k', }, { focusprev, { NULL } } }, + { { MOD, 'f', }, { setlayout, { "[]=" } } }, + { { MOD, 'g', }, { setlayout, { "+++" } } }, + { { MOD, 'b', }, { setlayout, { "TTT" } } }, + { { MOD, 'm', }, { setlayout, { "[ ]" } } }, + { { MOD, ' ', }, { setlayout, { NULL } } }, + { { MOD, 'i', }, { incnmaster, { "+1" } } }, + { { MOD, 'd', }, { incnmaster, { "-1" } } }, + { { MOD, 'h', }, { setmfact, { "-0.05" } } }, + { { MOD, 'l', }, { setmfact, { "+0.05" } } }, + { { MOD, '.', }, { toggleminimize, { NULL } } }, + { { MOD, 's', }, { togglebar, { NULL } } }, + { { MOD, 'S', }, { togglebarpos, { NULL } } }, + { { MOD, 'M', }, { togglemouse, { NULL } } }, + { { MOD, '\n', }, { zoom , { NULL } } }, + { { MOD, '\r', }, { zoom , { NULL } } }, + { { MOD, '1', }, { focusn, { "1" } } }, + { { MOD, '2', }, { focusn, { "2" } } }, + { { MOD, '3', }, { focusn, { "3" } } }, + { { MOD, '4', }, { focusn, { "4" } } }, + { { MOD, '5', }, { focusn, { "5" } } }, + { { MOD, '6', }, { focusn, { "6" } } }, + { { MOD, '7', }, { focusn, { "7" } } }, + { { MOD, '8', }, { focusn, { "8" } } }, + { { MOD, '9', }, { focusn, { "9" } } }, + { { MOD, '\t', }, { focuslast, { NULL } } }, + { { MOD, 'q', 'q', }, { quit, { NULL } } }, + { { MOD, 'a', }, { togglerunall, { NULL } } }, + { { MOD, CTRL('L'), }, { redraw, { NULL } } }, + { { MOD, 'r', }, { redraw, { NULL } } }, + { { MOD, 'e', }, { copymode, { "dvtm-editor" } } }, + { { MOD, 'E', }, { copymode, { "dvtm-pager" } } }, + { { MOD, '/', }, { copymode, { "dvtm-pager", "/" } } }, + { { MOD, 'p', }, { paste, { NULL } } }, + { { MOD, KEY_PPAGE, }, { scrollback, { "-1" } } }, + { { MOD, KEY_NPAGE, }, { scrollback, { "1" } } }, + { { MOD, '?', }, { create, { "man dvtm", "dvtm help" } } }, + { { MOD, MOD, }, { send, { (const char []){MOD, 0} } } }, + { { KEY_SPREVIOUS, }, { scrollback, { "-1" } } }, + { { KEY_SNEXT, }, { scrollback, { "1" } } }, + { { MOD, '0', }, { view, { NULL } } }, + { { MOD, KEY_F(1), }, { view, { tags[0] } } }, + { { MOD, KEY_F(2), }, { view, { tags[1] } } }, + { { MOD, KEY_F(3), }, { view, { tags[2] } } }, + { { MOD, KEY_F(4), }, { view, { tags[3] } } }, + { { MOD, KEY_F(5), }, { view, { tags[4] } } }, + { { MOD, 'v', '0' }, { view, { NULL } } }, + { { MOD, 'v', '\t', }, { viewprevtag, { NULL } } }, + { { MOD, 't', '0' }, { tag, { NULL } } }, + TAGKEYS( '1', 0) + TAGKEYS( '2', 1) + TAGKEYS( '3', 2) + TAGKEYS( '4', 3) + TAGKEYS( '5', 4) +}; + +static const ColorRule colorrules[] = { + { "", A_NORMAL, &colors[DEFAULT] }, /* default */ +}; + +/* possible values for the mouse buttons are listed below: + * + * BUTTON1_PRESSED mouse button 1 down + * BUTTON1_RELEASED mouse button 1 up + * BUTTON1_CLICKED mouse button 1 clicked + * BUTTON1_DOUBLE_CLICKED mouse button 1 double clicked + * BUTTON1_TRIPLE_CLICKED mouse button 1 triple clicked + * BUTTON2_PRESSED mouse button 2 down + * BUTTON2_RELEASED mouse button 2 up + * BUTTON2_CLICKED mouse button 2 clicked + * BUTTON2_DOUBLE_CLICKED mouse button 2 double clicked + * BUTTON2_TRIPLE_CLICKED mouse button 2 triple clicked + * BUTTON3_PRESSED mouse button 3 down + * BUTTON3_RELEASED mouse button 3 up + * BUTTON3_CLICKED mouse button 3 clicked + * BUTTON3_DOUBLE_CLICKED mouse button 3 double clicked + * BUTTON3_TRIPLE_CLICKED mouse button 3 triple clicked + * BUTTON4_PRESSED mouse button 4 down + * BUTTON4_RELEASED mouse button 4 up + * BUTTON4_CLICKED mouse button 4 clicked + * BUTTON4_DOUBLE_CLICKED mouse button 4 double clicked + * BUTTON4_TRIPLE_CLICKED mouse button 4 triple clicked + * BUTTON_SHIFT shift was down during button state change + * BUTTON_CTRL control was down during button state change + * BUTTON_ALT alt was down during button state change + * ALL_MOUSE_EVENTS report all button state changes + * REPORT_MOUSE_POSITION report mouse movement + */ + +#ifdef NCURSES_MOUSE_VERSION +# define CONFIG_MOUSE /* compile in mouse support if we build against ncurses */ +#endif + +#define ENABLE_MOUSE true /* whether to enable mouse events by default */ + +#ifdef CONFIG_MOUSE +static Button buttons[] = { + { BUTTON1_CLICKED, { mouse_focus, { NULL } } }, + { BUTTON1_DOUBLE_CLICKED, { mouse_fullscreen, { "[ ]" } } }, + { BUTTON2_CLICKED, { mouse_zoom, { NULL } } }, + { BUTTON3_CLICKED, { mouse_minimize, { NULL } } }, +}; +#endif /* CONFIG_MOUSE */ + +static Cmd commands[] = { + /* create [cmd]: create a new window, run `cmd` in the shell if specified */ + { "create", { create, { NULL } } }, + /* focus <win_id>: focus the window whose `DVTM_WINDOW_ID` is `win_id` */ + { "focus", { focusid, { NULL } } }, + /* tag <win_id> <tag> [tag ...]: add +tag, remove -tag or set tag of the window with the given identifier */ + { "tag", { tagid, { NULL } } }, +}; + +/* gets executed when dvtm is started */ +static Action actions[] = { + { create, { NULL } }, +}; + +static char const * const keytable[] = { + /* add your custom key escape sequences */ +}; diff --git a/config.h b/config.h @@ -0,0 +1,221 @@ +/* valid curses attributes are listed below they can be ORed + * + * A_NORMAL Normal display (no highlight) + * A_STANDOUT Best highlighting mode of the terminal. + * A_UNDERLINE Underlining + * A_REVERSE Reverse video + * A_BLINK Blinking + * A_DIM Half bright + * A_BOLD Extra bright or bold + * A_PROTECT Protected mode + * A_INVIS Invisible or blank mode + */ + +enum { + DEFAULT, + BLUE, +}; + +static Color colors[] = { + [DEFAULT] = { .fg = -1, .bg = -1, .fg256 = -1, .bg256 = -1, }, + [BLUE] = { .fg = COLOR_BLUE, .bg = -1, .fg256 = 68, .bg256 = -1, }, +}; + +#define COLOR(c) COLOR_PAIR(colors[c].pair) +/* curses attributes for the currently focused window */ +#define SELECTED_ATTR (COLOR(BLUE) | A_NORMAL) +/* curses attributes for normal (not selected) windows */ +#define NORMAL_ATTR (COLOR(DEFAULT) | A_NORMAL) +/* curses attributes for a window with pending urgent flag */ +#define URGENT_ATTR NORMAL_ATTR +/* curses attributes for the status bar */ +#define BAR_ATTR (COLOR(BLUE) | A_NORMAL) +/* characters for beginning and end of status bar message */ +#define BAR_BEGIN '[' +#define BAR_END ']' +/* status bar (command line option -s) position */ +#define BAR_POS BAR_TOP /* BAR_BOTTOM, BAR_OFF */ +/* whether status bar should be hidden if only one client exists */ +#define BAR_AUTOHIDE false +/* master width factor [0.1 .. 0.9] */ +#define MFACT 0.5 +/* number of clients in master area */ +#define NMASTER 1 +/* scroll back buffer size in lines */ +#define SCROLL_HISTORY 500 +/* printf format string for the tag in the status bar */ +#define TAG_SYMBOL "[%s]" +/* curses attributes for the currently selected tags */ +#define TAG_SEL (COLOR(BLUE) | A_BOLD) +/* curses attributes for not selected tags which contain no windows */ +#define TAG_NORMAL (COLOR(DEFAULT) | A_NORMAL) +/* curses attributes for not selected tags which contain windows */ +#define TAG_OCCUPIED (COLOR(BLUE) | A_NORMAL) +/* curses attributes for not selected tags which with urgent windows */ +#define TAG_URGENT (COLOR(BLUE) | A_NORMAL | A_BLINK) + +const char tags[][10] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; + +#include "tile.c" +#include "grid.c" +#include "bstack.c" +#include "fullscreen.c" + +/* by default the first layout entry is used */ +static Layout layouts[] = { + { "[]=", tile }, + { "+++", grid }, + { "TTT", bstack }, + { "[ ]", fullscreen }, +}; + +#define MOD CTRL('s') +#define TAGKEYS(KEY,TAG) \ + { { MOD, 'v', KEY, }, { view, { tags[TAG] } } }, \ + { { MOD, 't', KEY, }, { tag, { tags[TAG] } } }, \ + { { MOD, 'V', KEY, }, { toggleview, { tags[TAG] } } }, \ + { { MOD, 'T', KEY, }, { toggletag, { tags[TAG] } } }, + +/* you can specifiy at most 3 arguments */ +static KeyBinding bindings[] = { + { { MOD, '\r', }, { create, { NULL } } }, + //{ { MOD, '\n', }, { create, { NULL, NULL, "$CWD" } } }, + { { MOD, 'x', }, { killclient, { NULL } } }, + { { MOD, KEY_DOWN, }, { focusnext, { NULL } } }, + //{ { MOD, 'J', }, { focusdown, { NULL } } }, + //{ { MOD, 'K', }, { focusup, { NULL } } }, + //{ { MOD, 'H', }, { focusleft, { NULL } } }, + //{ { MOD, 'L', }, { focusright, { NULL } } }, + { { MOD, KEY_UP, }, { focusprev, { NULL } } }, + { { MOD, 'v', }, { setlayout, { "[]=" } } }, + { { MOD, 'g', }, { setlayout, { "+++" } } }, + { { MOD, 'b', }, { setlayout, { "TTT" } } }, + { { MOD, 'm', }, { setlayout, { "[ ]" } } }, + { { MOD, ' ', }, { setlayout, { NULL } } }, + { { MOD, 'i', }, { incnmaster, { "+1" } } }, + { { MOD, 'd', }, { incnmaster, { "-1" } } }, + { { MOD, KEY_LEFT, }, { setmfact, { "-0.05" } } }, + { { MOD, KEY_RIGHT, }, { setmfact, { "+0.05" } } }, + { { MOD, '.', }, { toggleminimize, { NULL } } }, + { { MOD, 's', }, { togglebar, { NULL } } }, + { { MOD, 'S', }, { togglebarpos, { NULL } } }, + { { MOD, 'M', }, { togglemouse, { NULL } } }, + //{ { MOD, Shift, '\r', }, { zoom , { NULL } } }, + { { MOD, 'T', }, { zoom , { NULL } } }, + { { MOD, '\r', }, { zoom, { NULL } } }, + { { MOD, '1', }, { view, { "1" } } }, + { { MOD, '2', }, { view, { "2" } } }, + { { MOD, '3', }, { view, { "3" } } }, + { { MOD, '4', }, { view, { "4" } } }, + { { MOD, '5', }, { view, { "5" } } }, + { { MOD, '6', }, { view, { "6" } } }, + { { MOD, '7', }, { view, { "7" } } }, + { { MOD, '8', }, { view, { "8" } } }, + { { MOD, '9', }, { view, { "9" } } }, + { { MOD, '0', }, { view, { "10" } } }, + { { MOD, '\t', }, { focuslast, { NULL } } }, + { { MOD, 'Q', }, { quit, { NULL } } }, + { { MOD, 'a', }, { togglerunall, { NULL } } }, + { { MOD, CTRL('L'), }, { redraw, { NULL } } }, + { { MOD, 'r', }, { redraw, { NULL } } }, + { { MOD, 'e', }, { copymode, { "dvtm-editor" } } }, + { { MOD, 'E', }, { copymode, { "dvtm-pager" } } }, + { { MOD, '/', }, { copymode, { "dvtm-pager", "/" } } }, + { { MOD, 'p', }, { paste, { NULL } } }, + { { MOD, KEY_PPAGE, }, { scrollback, { "-1" } } }, + { { MOD, KEY_NPAGE, }, { scrollback, { "1" } } }, + { { MOD, '?', }, { create, { "man dvtm", "dvtm help" } } }, + { { MOD, MOD, }, { send, { (const char []){MOD, 0} } } }, + { { KEY_SPREVIOUS, }, { scrollback, { "-1" } } }, + { { KEY_SNEXT, }, { scrollback, { "1" } } }, + //{ { MOD, '0', }, { view, { NULL } } }, + { { MOD, KEY_F(1), }, { view, { tags[0] } } }, + { { MOD, KEY_F(2), }, { view, { tags[1] } } }, + { { MOD, KEY_F(3), }, { view, { tags[2] } } }, + { { MOD, KEY_F(4), }, { view, { tags[3] } } }, + { { MOD, KEY_F(5), }, { view, { tags[4] } } }, + { { MOD, KEY_F(6), }, { view, { tags[5] } } }, + { { MOD, KEY_F(7), }, { view, { tags[6] } } }, + { { MOD, KEY_F(8), }, { view, { tags[7] } } }, + { { MOD, KEY_F(9), }, { view, { tags[8] } } }, + { { MOD, KEY_F(10), }, { view, { tags[9] } } }, + //{ { MOD, 'v', '0' }, { view, { NULL } } }, + //{ { MOD, 'v', '\t', }, { viewprevtag, { NULL } } }, + //{ { MOD, 't', '0' }, { tag, { NULL } } }, + TAGKEYS( '1', 0) + TAGKEYS( '2', 1) + TAGKEYS( '3', 2) + TAGKEYS( '4', 3) + TAGKEYS( '5', 4) + TAGKEYS( '6', 5) + TAGKEYS( '7', 6) + TAGKEYS( '8', 7) + TAGKEYS( '9', 8) + TAGKEYS( '10', 9) +}; + +static const ColorRule colorrules[] = { + { "", A_NORMAL, &colors[DEFAULT] }, /* default */ +}; + +/* possible values for the mouse buttons are listed below: + * + * BUTTON1_PRESSED mouse button 1 down + * BUTTON1_RELEASED mouse button 1 up + * BUTTON1_CLICKED mouse button 1 clicked + * BUTTON1_DOUBLE_CLICKED mouse button 1 double clicked + * BUTTON1_TRIPLE_CLICKED mouse button 1 triple clicked + * BUTTON2_PRESSED mouse button 2 down + * BUTTON2_RELEASED mouse button 2 up + * BUTTON2_CLICKED mouse button 2 clicked + * BUTTON2_DOUBLE_CLICKED mouse button 2 double clicked + * BUTTON2_TRIPLE_CLICKED mouse button 2 triple clicked + * BUTTON3_PRESSED mouse button 3 down + * BUTTON3_RELEASED mouse button 3 up + * BUTTON3_CLICKED mouse button 3 clicked + * BUTTON3_DOUBLE_CLICKED mouse button 3 double clicked + * BUTTON3_TRIPLE_CLICKED mouse button 3 triple clicked + * BUTTON4_PRESSED mouse button 4 down + * BUTTON4_RELEASED mouse button 4 up + * BUTTON4_CLICKED mouse button 4 clicked + * BUTTON4_DOUBLE_CLICKED mouse button 4 double clicked + * BUTTON4_TRIPLE_CLICKED mouse button 4 triple clicked + * BUTTON_SHIFT shift was down during button state change + * BUTTON_CTRL control was down during button state change + * BUTTON_ALT alt was down during button state change + * ALL_MOUSE_EVENTS report all button state changes + * REPORT_MOUSE_POSITION report mouse movement + */ + +#ifdef NCURSES_MOUSE_VERSION +# define CONFIG_MOUSE /* compile in mouse support if we build against ncurses */ +#endif + +#define ENABLE_MOUSE true /* whether to enable mouse events by default */ + +#ifdef CONFIG_MOUSE +static Button buttons[] = { + { BUTTON1_CLICKED, { mouse_focus, { NULL } } }, + { BUTTON1_DOUBLE_CLICKED, { mouse_fullscreen, { "[ ]" } } }, + { BUTTON2_CLICKED, { mouse_zoom, { NULL } } }, + { BUTTON3_CLICKED, { mouse_minimize, { NULL } } }, +}; +#endif /* CONFIG_MOUSE */ + +static Cmd commands[] = { + /* create [cmd]: create a new window, run `cmd` in the shell if specified */ + { "create", { create, { NULL } } }, + /* focus <win_id>: focus the window whose `DVTM_WINDOW_ID` is `win_id` */ + { "focus", { focusid, { NULL } } }, + /* tag <win_id> <tag> [tag ...]: add +tag, remove -tag or set tag of the window with the given identifier */ + { "tag", { tagid, { NULL } } }, +}; + +/* gets executed when dvtm is started */ +static Action actions[] = { + { create, { NULL } }, +}; + +static char const * const keytable[] = { + /* add your custom key escape sequences */ +}; diff --git a/config.mk b/config.mk @@ -0,0 +1,14 @@ +# Customize below to fit your system + +PREFIX ?= /usr/local +MANPREFIX = ${PREFIX}/share/man +# specify your systems terminfo directory +# leave empty to install into your home folder +TERMINFO := ${DESTDIR}${PREFIX}/share/terminfo + +INCS = -I. +LIBS = -lc -lutil -lncursesw +CPPFLAGS = -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700 -D_XOPEN_SOURCE_EXTENDED +CFLAGS += -std=c99 ${INCS} -DNDEBUG ${CPPFLAGS} + +CC ?= cc diff --git a/dvtm-editor.1 b/dvtm-editor.1 @@ -0,0 +1,88 @@ +.Dd December 27, 2016 +.Dt DVTM-EDITOR 1 +.Os dvtm VERSION +.Sh NAME +.Nm dvtm-editor +.Nd make a text editor act as a filter +. +. +.Sh SYNOPSIS +. +.Nm +ARGS... +. +. +.Sh DESCRIPTION +. +The +.Nm +is a file buffering utility used by the +.Xr dvtm 1 +terminal multiplexer to implement its copy mode. It reads the standard input and saves it to a temporary +file, then opens an editor according to the +.Sx "ENVIRONMENT VARIABLES" . +.Pp +If the invoked editor terminates with a non-zero exit status or +the file modification time remains unchanged, +.Nm +does not output anything. Otherwise, it outputs the content of the modified temporary +file to stdout. +.Pp +All command line arguments are forwarded verbatim. +.Xr dvtm 1 +uses this to adjust the initial view port by passing +.Sy +n , +meaning the start of line +.Sy n +should be displayed. +. +. +.Sh ENVIRONMENT VARIABLES +. +.Nm +will try to find the user editor by checking these variables in order: +. +.Bl -tag -width indent +.It Ev DVTM_EDITOR +Permitting to invoke an editor specific to dvtm, or set particular flags. +. +.It Ev VISUAL , Ev EDITOR +Falling back to global defaults: +.Ev VISUAL +and +.Ev EDITOR . +.El +.Pp +If no editor is found, +.Xr vi 1 +is used. +. +. +.Sh FILES +. +The temporary files are created according to the template: +.Pa /tmp/dvtm-editor.XXXXXX . +.Pp +.Pa /dev/tty +is opened to obtain a controlling tty which is used for the standard input/output +streams of the invoked editor. +. +.Sh NOTES +. +Using +.Xr vis 1 +as editor is particularly convenient because +.Ic :wq! +in visual mode will reduce the file to the currently active selection(s). +. +.Sh SEE ALSO +. +.Xr vi 1 , +.Xr dvtm 1 , +.Xr dvtm-pager 1 +. +. +.Sh AUTHOR +. +dvtm is written by +.An Marc André Tanner Aq Mt mat at brain-dump.org diff --git a/dvtm-editor.c b/dvtm-editor.c @@ -0,0 +1,180 @@ +/* Invoke $EDITOR as a filter. + * + * Copyright (c) 2016 Dmitry Bogatov <KAction@gnu.org> + * Copyright (c) 2017 Marc André Tanner <mat@brain-dump.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static void error(const char *msg, ...) { + va_list ap; + va_start(ap, msg); + vfprintf(stderr, msg, ap); + va_end(ap); + if (errno) + fprintf(stderr, ": %s", strerror(errno)); + fprintf(stderr, "\n"); +} + +int main(int argc, char *argv[]) +{ + int exit_status = EXIT_FAILURE, tmp_write = -1; + + const char *editor = getenv("DVTM_EDITOR"); + if (!editor) + editor = getenv("VISUAL"); + if (!editor) + editor = getenv("EDITOR"); + if (!editor) + editor = "vi"; + + char tempname[] = "/tmp/dvtm-editor.XXXXXX"; + if ((tmp_write = mkstemp(tempname)) == -1) { + error("failed to open temporary file `%s'", tempname); + goto err; + } + + /* POSIX does not mandates modes of temporary file. */ + if (fchmod(tmp_write, 0600) == -1) { + error("failed to change mode of temporary file `%s'", tempname); + goto err; + } + + char buffer[2048]; + ssize_t bytes; + while ((bytes = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0) { + do { + ssize_t written = write(tmp_write, buffer, bytes); + if (written == -1) { + error("failed to write data to temporary file `%s'", + tempname); + goto err; + } + bytes -= written; + } while (bytes > 0); + } + + if (fsync(tmp_write) == -1) { + error("failed to fsync temporary file `%s'", tempname); + goto err; + } + + struct stat stat_before; + if (fstat(tmp_write, &stat_before) == -1) { + error("failed to stat newly created temporary file `%s'", tempname); + goto err; + } + + if (close(tmp_write) == -1) { + error("failed to close temporary file `%s'", tempname); + goto err; + } + + pid_t pid = fork(); + if (pid == -1) { + error("failed to fork editor process"); + goto err; + } else if (pid == 0) { + int tty = open("/dev/tty", O_RDWR); + if (tty == -1) { + error("failed to open /dev/tty"); + _exit(1); + } + + if (dup2(tty, STDIN_FILENO) == -1) { + error("failed to set tty as stdin"); + _exit(1); + } + + if (dup2(tty, STDOUT_FILENO) == -1) { + error("failed to set tty as stdout"); + _exit(1); + } + + if (dup2(tty, STDERR_FILENO) == -1) { + error("failed to set tty as stderr"); + _exit(1); + } + + close(tty); + + const char *editor_argv[argc+2]; + editor_argv[0] = editor; + for (int i = 1; i < argc; i++) + editor_argv[i] = argv[i]; + editor_argv[argc] = tempname; + editor_argv[argc+1] = NULL; + + execvp(editor, (char* const*)editor_argv); + error("failed to exec editor process `%s'", editor); + _exit(127); + } + + int status; + if (waitpid(pid, &status, 0) == -1) { + error("waitpid failed"); + goto err; + } + if (!WIFEXITED(status)) { + error("editor invocation failed"); + goto err; + } + if ((status = WEXITSTATUS(status)) != 0) { + error("editor terminated with exit status: %d", status); + goto err; + } + + int tmp_read = open(tempname, O_RDONLY); + if (tmp_read == -1) { + error("failed to open for reading of edited temporary file `%s'", + tempname); + goto err; + } + + struct stat stat_after; + if (fstat(tmp_read, &stat_after) == -1) { + error("failed to stat edited temporary file `%s'", tempname); + goto err; + } + + if (stat_before.st_mtime == stat_after.st_mtime) + goto ok; /* no modifications */ + + while ((bytes = read(tmp_read, buffer, sizeof(buffer))) > 0) { + do { + ssize_t written = write(STDOUT_FILENO, buffer, bytes); + if (written == -1) { + error("failed to write data to stdout"); + goto err; + } + bytes -= written; + } while (bytes > 0); + } + +ok: + exit_status = EXIT_SUCCESS; +err: + if (tmp_write != -1) + unlink(tempname); + return exit_status; +} diff --git a/dvtm-pager.1 b/dvtm-pager.1 @@ -0,0 +1,64 @@ +.Dd January 03, 2017 +.Dt DVTM-PAGER 1 +.Os dvtm VERSION +.Sh NAME +.Nm dvtm-pager +.Nd select apropriate pager for dvtm +. +. +.Sh SYNOPSIS +. +.Nm +ARGS... +. +. +.Sh DESCRIPTION +. +.Nm +is an utility used by the +.Xr dvtm 1 +terminal multiplexer to display its scrollback history using a suitable pager. +.Pp +The invoked pager is expected to display the data sent to its standard input. +The data stream might contain ANSI color escape sequence. +All command line arguments are forwarded verbatim. +.Xr dvtm 1 +uses this to adjust the initial view port by passing +.Sy +n , +meaning the start of line +.Sy n +should be displayed. +. +. +.Sh ENVIRONMENT VARIABLES +. +.Nm +will try to find the preferred pager by checking these variables in order: +. +.Bl -tag -width indent +.It Ev DVTM_PAGER +Permitting to invoke a pager specific to dvtm, or set particular flags such as +.Fl R +for +.Xr less 1 . +. +.It Ev PAGER +Falling back to the default pager. +.El +.Pp +If none of these variables are set, +.Xr less 1 +is used. +. +. +.Sh SEE ALSO +. +.Xr less 1 , +.Xr dvtm 1 , +.Xr dvtm-editor 1 +. +. +.Sh AUTHOR +. +dvtm is written by +.An Marc André Tanner Aq Mt mat at brain-dump.org diff --git a/dvtm.1 b/dvtm.1 @@ -0,0 +1,321 @@ +.Dd December 27, 2016 +.Dt DVTM 1 +.Os dvtm VERSION +.Sh NAME +.Nm dvtm +.Nd dynamic virtual terminal manager +. +. +.Sh SYNOPSIS +. +.Nm +.Op Fl v +.Op Fl M +.Op Fl m Ar modifier +.Op Fl d Ar delay +.Op Fl h Ar lines +.Op Fl t Ar title +.Op Fl s Ar status-fifo +.Op Fl c Ar cmd-fifo +.Op Ar command Ar ... +. +. +.Sh DESCRIPTION +. +.Nm +is a dynamic tiling window manager for the console. +.Pp +As a console window manager it tries to make it easy to work with multiple +console based applications. +. +.Bl -tag -width 8 +.It Fl v +Print version information to standard output and exit. +. +.It Fl M +Toggle default mouse grabbing upon startup. Use this to allow normal mouse operation +under X. +. +.It Fl m Ar modifier +Set command modifier at runtime. +. +.It Fl d Ar delay +Set the delay ncurses waits before deciding if a character that might be +part of an escape sequence is actually part of an escape sequence. +. +.It Fl h Ar lines +Set the scrollback history buffer size at runtime. +. +.It Fl t Ar title +Set a static terminal +.Ar title +and don't change it to the one of the currently focused window. +. +.It Fl s Ar status-fifo +Open or create the named pipe +.Pa status-fifo +read its content and display it in the statusbar. See the +.Xr dvtm-status 1 +script for an usage example. +. +.It Fl c Ar cmd-fifo +Open or create the named pipe +.Pa cmd-fifo +and look for commands to execute which were defined in +.Pa config.h . +. +.It Ar command Ar ... +Execute +.Ar command +(s), each in a separate window. +.El +. +. +.Sh USAGE +. +.Ss Keyboard commands +. +Each keybinding begins with +.Ic Mod +which defaults to +.Ic ^g +but can be changed in +.Pa config.h +or with the +.Fl m +command line option. +. +.Bl -tag -width 8 +.It Ic Mod-c +Create a new shell window. +. +.It Ic Mod-C +Create a new shell window using the current working directory of the focused window. +. +.It Ic Mod-x-x +Close focused window. +. +.It Ic Mod-l +Increases the master area width about 5% (all except grid and +fullscreen layout). +. +.It Ic Mod-h +Decreases the master area width about 5% (all except grid and +fullscreen layout). +. +.It Ic Mod-i +Increase number of windows displayed in the master area. +. +.It Ic Mod-d +Decrease number of windows displayed in the master area. +. +.It Ic Mod-j +Focus next window. +. +.It Ic Mod-k +Focus previous window. +. +.It Ic Mod-J +Focus window below. +. +.It Ic Mod-K +Focus window above. +. +.It Ic Mod-H +Focus window to the left. +. +.It Ic Mod-L +Focus window to the right. +. +.It Ic Mod-[0..9] +Focus the [0..9]-th window. +. +.It Ic Mod-Tab +Focus previously selected window. +. +.It Ic Mod-. +Toggle minimization of current window. +. +.It Ic Mod-m +Maximize current window (change to fullscreen layout). +. +.It Ic Shift-PageUp +.It Ic Mod-PageUp +Scroll up. +. +.It Ic Shift-PageDown +.It Ic Mod-PageDown +Scroll down. +. +.It Ic Mod-Space +Toggle between defined layouts (affects all windows). +. +.It Ic Mod-Enter +Zooms/cycles current window to/from master area. +. +.It Ic Mod-f +Change to vertical stack tiling layout. +. +.It Ic Mod-b +Change to bottom stack tiling layout. +. +.It Ic Mod-g +Change to grid layout. +. +.It Ic Mod-s +Show/hide the status bar. +. +.It Ic Mod-S +Toggle position of the status bar between top and bottom. +. +.It Ic Mod-r +. +.It Ic Mod-^L +Redraw whole screen. +. +.It Ic Mod-a +Toggle keyboard multiplexing mode, if activated keypresses are sent to all +visible windows. +. +.It Ic Mod-M +Toggle dvtm mouse grabbing. +. +.It Ic Mod-e +Enter copy mode (see section below for further information). +. +.It Ic Mod-/ +Enter copy mode and start searching forward (assumes a vi-like editor). +. +.It Ic Mod-p +Paste last copied text from copy mode at current cursor position. +. +.It Ic Mod-? +Show this manual page. +. +.It Ic Mod-Mod +Send the Mod key. +. +.It Ic Mod-F[1..n] +.It Ic Mod-v-[1..n] +View all windows with n-th tag. +. +.It Ic Mod-0 +View all windows with any tag. +. +.It Ic Mod-v-Tab +Toggles to the previously selected tags. +. +.It Ic Mod-V-[1..n] +Add/remove all windows with nth tag to/from the view. +. +.It Ic Mod-t-[1..n] +Apply nth tag to focused window. +. +.It Ic Mod-T-[1..n] +Add/remove nth tag to/from focused window. +. +.It Ic Mod-q-q +Quit dvtm. +.El +. +. +.Ss Mouse commands +. +By default dvtm captures mouse events to provide the actions listed below. +Unfortunately this interferes with the standard X copy and paste mechanism. +To work around this you need to hold down +.Ic Shift +while selecting or pasting text. +Alternatively you can disable mouse support at compile time, start dvtm with the +.Fl M +flag or toggle mouse support during runtime with +.Ic Mod-M . +. +.Bl -tag -width 8 +.It Ic Button1 click +Focus window. +. +.It Ic Button1 double click +Focus window and toggle maximization. +. +.It Ic Button2 click +Zoom/cycle current window to/from master area. +. +.It Ic Button3 click +Toggle minimization of current window. +.El +. +. +.Ss Copy mode +. +Copy mode gives easy access to past output by piping it to +.Xr dvtm-editor 1 , +opening an editor. +What the editor writes will be stored in an internal register and can be pasted +into other clients (via +.Ic Mod-p ). +. +. +.Sh ENVIRONMENT VARIABLES +. +.Bl -tag -width 8 +.It Ev DVTM +Each process spawned by dvtm will have this variable set to the dvtm version +it is running under. +. +.It Ev DVTM_WINDOW_ID +Each process also has access to its constant and unique window id. +. +.It Ev DVTM_CMD_FIFO +If the -c command line argument was specified upon dvtm startup, this variable +will be set to the file name of the named pipe. Thus allowing the process +to send commands back to dvtm. +. +.It Ev DVTM_TERM +By default dvtm uses its own terminfo file and therefore sets +.Ev TERM=dvtm +within the client windows. This can be overridden by setting the +.Ev DVTM_TERM +environment variable to a valid terminal name before launching dvtm. +. +.It Ev DVTM_EDITOR +When entering the copymode dvtm pipes the whole scroll back buffer to +.Xr dvtm-editor 1 +which opens the content in +.Ev DVTM_EDITOR , +with fallbacks to +.Ev VISUAL , +.Ev EDITOR +and +.Xr vi 1 +.Pa config.h +is used instead. +.El +. +. +.Sh EXAMPLE +. +See the +.Xr dvtm-status 1 +script as an example of how to display text in the status bar. +. +. +.Sh FILES +. +.Nm +is customized by creating a custom +.Pa config.h +and (re)compiling the source code. +This keeps it fast, secure and simple. +. +. +.Sh SEE ALSO +. +.Xr abduco 1 , +.Xr dvtm-status 1 +. +. +.Sh AUTHOR +. +dvtm is written +.An Marc André Tanner Aq Mt mat at brain-dump.org diff --git a/dvtm.c b/dvtm.c @@ -0,0 +1,1951 @@ +/* + * The initial "port" of dwm to curses was done by + * + * © 2007-2016 Marc André Tanner <mat at brain-dump dot org> + * + * It is highly inspired by the original X11 dwm and + * reuses some code of it which is mostly + * + * © 2006-2007 Anselm R. Garbe <garbeam at gmail dot com> + * + * See LICENSE for details. + */ +#include <stdlib.h> +#include <unistd.h> +#include <stdint.h> +#include <wchar.h> +#include <limits.h> +#include <libgen.h> +#include <sys/select.h> +#include <sys/stat.h> +#include <sys/ioctl.h> +#include <sys/wait.h> +#include <sys/time.h> +#include <sys/types.h> +#include <fcntl.h> +#include <curses.h> +#include <stdio.h> +#include <stdarg.h> +#include <signal.h> +#include <locale.h> +#include <string.h> +#include <unistd.h> +#include <stdbool.h> +#include <errno.h> +#include <pwd.h> +#if defined __CYGWIN__ || defined __sun +# include <termios.h> +#endif +#include "vt.h" + +#ifdef PDCURSES +int ESCDELAY; +#endif + +#ifndef NCURSES_REENTRANT +# define set_escdelay(d) (ESCDELAY = (d)) +#endif + +typedef struct { + float mfact; + unsigned int nmaster; + int history; + int w; + int h; + volatile sig_atomic_t need_resize; +} Screen; + +typedef struct { + const char *symbol; + void (*arrange)(void); +} Layout; + +typedef struct Client Client; +struct Client { + WINDOW *window; + Vt *term; + Vt *editor, *app; + int editor_fds[2]; + volatile sig_atomic_t editor_died; + const char *cmd; + char title[255]; + int order; + pid_t pid; + unsigned short int id; + unsigned short int x; + unsigned short int y; + unsigned short int w; + unsigned short int h; + bool has_title_line; + bool minimized; + bool urgent; + volatile sig_atomic_t died; + Client *next; + Client *prev; + Client *snext; + unsigned int tags; +}; + +typedef struct { + short fg; + short bg; + short fg256; + short bg256; + short pair; +} Color; + +typedef struct { + const char *title; + attr_t attrs; + Color *color; +} ColorRule; + +#define ALT(k) ((k) + (161 - 'a')) +#if defined CTRL && defined _AIX + #undef CTRL +#endif +#ifndef CTRL + #define CTRL(k) ((k) & 0x1F) +#endif +#define CTRL_ALT(k) ((k) + (129 - 'a')) + +#define MAX_ARGS 8 + +typedef struct { + void (*cmd)(const char *args[]); + const char *args[3]; +} Action; + +#define MAX_KEYS 3 + +typedef unsigned int KeyCombo[MAX_KEYS]; + +typedef struct { + KeyCombo keys; + Action action; +} KeyBinding; + +typedef struct { + mmask_t mask; + Action action; +} Button; + +typedef struct { + const char *name; + Action action; +} Cmd; + +enum { BAR_TOP, BAR_BOTTOM, BAR_OFF }; + +typedef struct { + int fd; + int pos, lastpos; + bool autohide; + unsigned short int h; + unsigned short int y; + char text[512]; + const char *file; +} StatusBar; + +typedef struct { + int fd; + const char *file; + unsigned short int id; +} CmdFifo; + +typedef struct { + char *data; + size_t len; + size_t size; +} Register; + +typedef struct { + char *name; + const char *argv[4]; + bool filter; + bool color; +} Editor; + +#define LENGTH(arr) (sizeof(arr) / sizeof((arr)[0])) +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define TAGMASK ((1 << LENGTH(tags)) - 1) + +#ifdef NDEBUG + #define debug(format, args...) +#else + #define debug eprint +#endif + +/* commands for use by keybindings */ +static void create(const char *args[]); +static void copymode(const char *args[]); +static void focusn(const char *args[]); +static void focusid(const char *args[]); +static void focusnext(const char *args[]); +static void focusnextnm(const char *args[]); +static void focusprev(const char *args[]); +static void focusprevnm(const char *args[]); +static void focuslast(const char *args[]); +static void focusup(const char *args[]); +static void focusdown(const char *args[]); +static void focusleft(const char *args[]); +static void focusright(const char *args[]); +static void killclient(const char *args[]); +static void paste(const char *args[]); +static void quit(const char *args[]); +static void redraw(const char *args[]); +static void scrollback(const char *args[]); +static void send(const char *args[]); +static void setlayout(const char *args[]); +static void incnmaster(const char *args[]); +static void setmfact(const char *args[]); +static void startup(const char *args[]); +static void tag(const char *args[]); +static void tagid(const char *args[]); +static void togglebar(const char *args[]); +static void togglebarpos(const char *args[]); +static void toggleminimize(const char *args[]); +static void togglemouse(const char *args[]); +static void togglerunall(const char *args[]); +static void toggletag(const char *args[]); +static void toggleview(const char *args[]); +static void viewprevtag(const char *args[]); +static void view(const char *args[]); +static void zoom(const char *args[]); + +/* commands for use by mouse bindings */ +static void mouse_focus(const char *args[]); +static void mouse_fullscreen(const char *args[]); +static void mouse_minimize(const char *args[]); +static void mouse_zoom(const char *args[]); + +/* functions and variables available to layouts via config.h */ +static Client* nextvisible(Client *c); +static void focus(Client *c); +static void resize(Client *c, int x, int y, int w, int h); +extern Screen screen; +static unsigned int waw, wah, wax, way; +static Client *clients = NULL; +static char *title; +static KeyCombo keys; + +#include "config.h" + +/* global variables */ +static const char *dvtm_name = "dvtm"; +Screen screen = { .mfact = MFACT, .nmaster = NMASTER, .history = SCROLL_HISTORY }; +static Client *stack = NULL; +static Client *sel = NULL; +static Client *lastsel = NULL; +static Client *msel = NULL; +static unsigned int seltags; +static unsigned int tagset[2] = { 1, 1 }; +static bool mouse_events_enabled = ENABLE_MOUSE; +static Layout *layout = layouts; +static StatusBar bar = { .fd = -1, .lastpos = BAR_POS, .pos = BAR_POS, .autohide = BAR_AUTOHIDE, .h = 1 }; +static CmdFifo cmdfifo = { .fd = -1 }; +static const char *shell; +static Register copyreg; +static volatile sig_atomic_t running = true; +static bool runinall = false; + +static void +eprint(const char *errstr, ...) { + va_list ap; + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); +} + +static void +error(const char *errstr, ...) { + va_list ap; + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +static bool +isarrange(void (*func)()) { + return func == layout->arrange; +} + +static bool +isvisible(Client *c) { + return c->tags & tagset[seltags]; +} + +static bool +is_content_visible(Client *c) { + if (!c) + return false; + if (isarrange(fullscreen)) + return sel == c; + return isvisible(c) && !c->minimized; +} + +static Client* +nextvisible(Client *c) { + for (; c && !isvisible(c); c = c->next); + return c; +} + +static void +updatebarpos(void) { + bar.y = 0; + wax = 0; + way = 0; + wah = screen.h; + waw = screen.w; + if (bar.pos == BAR_TOP) { + wah -= bar.h; + way += bar.h; + } else if (bar.pos == BAR_BOTTOM) { + wah -= bar.h; + bar.y = wah; + } +} + +static void +hidebar(void) { + if (bar.pos != BAR_OFF) { + bar.lastpos = bar.pos; + bar.pos = BAR_OFF; + } +} + +static void +showbar(void) { + if (bar.pos == BAR_OFF) + bar.pos = bar.lastpos; +} + +static void +drawbar(void) { + int sx, sy, x, y, width; + unsigned int occupied = 0, urgent = 0; + if (bar.pos == BAR_OFF) + return; + + for (Client *c = clients; c; c = c->next) { + occupied |= c->tags; + if (c->urgent) + urgent |= c->tags; + } + + getyx(stdscr, sy, sx); + attrset(BAR_ATTR); + move(bar.y, 0); + + for (unsigned int i = 0; i < LENGTH(tags); i++){ + if (tagset[seltags] & (1 << i)) + attrset(TAG_SEL); + else if (urgent & (1 << i)) + attrset(TAG_URGENT); + else if (occupied & (1 << i)) + attrset(TAG_OCCUPIED); + else + attrset(TAG_NORMAL); + printw(TAG_SYMBOL, tags[i]); + } + + attrset(runinall ? TAG_SEL : TAG_NORMAL); + addstr(layout->symbol); + attrset(TAG_NORMAL); + + for (unsigned int i = 0; i < MAX_KEYS && keys[i]; i++) { + if (keys[i] < ' ') + printw("^%c", 'A' - 1 + keys[i]); + else + printw("%c", keys[i]); + } + + getyx(stdscr, y, x); + (void)y; + int maxwidth = screen.w - x - 2; + + addch(BAR_BEGIN); + attrset(BAR_ATTR); + + wchar_t wbuf[sizeof bar.text]; + size_t numchars = mbstowcs(wbuf, bar.text, sizeof bar.text); + + if (numchars != (size_t)-1 && (width = wcswidth(wbuf, maxwidth)) != -1) { + int pos; + for (pos = 0; pos + width < maxwidth; pos++) + addch(' '); + + for (size_t i = 0; i < numchars; i++) { + pos += wcwidth(wbuf[i]); + if (pos > maxwidth) + break; + addnwstr(wbuf+i, 1); + } + + clrtoeol(); + } + + attrset(TAG_NORMAL); + mvaddch(bar.y, screen.w - 1, BAR_END); + attrset(NORMAL_ATTR); + move(sy, sx); + wnoutrefresh(stdscr); +} + +static int +show_border(void) { + return (bar.pos != BAR_OFF) || (clients && clients->next); +} + +static void +draw_border(Client *c) { + char t = '\0'; + int x, y, maxlen, attrs = NORMAL_ATTR; + + if (!show_border()) + return; + if (sel != c && c->urgent) + attrs = URGENT_ATTR; + if (sel == c || (runinall && !c->minimized)) + attrs = SELECTED_ATTR; + + wattrset(c->window, attrs); + getyx(c->window, y, x); + mvwhline(c->window, 0, 0, ACS_HLINE, c->w); + maxlen = c->w - 10; + if (maxlen < 0) + maxlen = 0; + if ((size_t)maxlen < sizeof(c->title)) { + t = c->title[maxlen]; + c->title[maxlen] = '\0'; + } + + mvwprintw(c->window, 0, 2, "[%s%s#%d]", + *c->title ? c->title : "", + *c->title ? " | " : "", + c->order); + if (t) + c->title[maxlen] = t; + wmove(c->window, y, x); +} + +static void +draw_content(Client *c) { + vt_draw(c->term, c->window, c->has_title_line, 0); +} + +static void +draw(Client *c) { + if (is_content_visible(c)) { + redrawwin(c->window); + draw_content(c); + } + if (!isarrange(fullscreen) || sel == c) + draw_border(c); + wnoutrefresh(c->window); +} + +static void +draw_all(void) { + if (!nextvisible(clients)) { + sel = NULL; + curs_set(0); + erase(); + drawbar(); + doupdate(); + return; + } + + if (!isarrange(fullscreen)) { + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c != sel) + draw(c); + } + } + /* as a last step the selected window is redrawn, + * this has the effect that the cursor position is + * accurate + */ + if (sel) + draw(sel); +} + +static void +arrange(void) { + unsigned int m = 0, n = 0; + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + c->order = ++n; + if (c->minimized) + m++; + } + erase(); + attrset(NORMAL_ATTR); + if (bar.fd == -1 && bar.autohide) { + if ((!clients || !clients->next) && n == 1) + hidebar(); + else + showbar(); + updatebarpos(); + } + if (m && !isarrange(fullscreen)) + wah--; + layout->arrange(); + if (m && !isarrange(fullscreen)) { + unsigned int i = 0, nw = waw / m, nx = wax; + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) { + resize(c, nx, way+wah, ++i == m ? waw - nx : nw, 1); + nx += nw; + } + } + wah++; + } + focus(NULL); + wnoutrefresh(stdscr); + drawbar(); + draw_all(); +} + +static void +attach(Client *c) { + if (clients) + clients->prev = c; + c->next = clients; + c->prev = NULL; + clients = c; + for (int o = 1; c; c = nextvisible(c->next), o++) + c->order = o; +} + +static void +attachafter(Client *c, Client *a) { /* attach c after a */ + if (c == a) + return; + if (!a) + for (a = clients; a && a->next; a = a->next); + + if (a) { + if (a->next) + a->next->prev = c; + c->next = a->next; + c->prev = a; + a->next = c; + for (int o = a->order; c; c = nextvisible(c->next)) + c->order = ++o; + } +} + +static void +attachstack(Client *c) { + c->snext = stack; + stack = c; +} + +static void +detach(Client *c) { + Client *d; + if (c->prev) + c->prev->next = c->next; + if (c->next) { + c->next->prev = c->prev; + for (d = nextvisible(c->next); d; d = nextvisible(d->next)) + --d->order; + } + if (c == clients) + clients = c->next; + c->next = c->prev = NULL; +} + +static void +settitle(Client *c) { + char *term, *t = title; + if (!t && sel == c && *c->title) + t = c->title; + if (t && (term = getenv("TERM")) && !strstr(term, "linux")) { + printf("\033]0;%s\007", t); + fflush(stdout); + wnoutrefresh(c->window); + } +} + +static void +detachstack(Client *c) { + Client **tc; + for (tc = &stack; *tc && *tc != c; tc = &(*tc)->snext); + *tc = c->snext; +} + +static void +focus(Client *c) { + if (!c) + for (c = stack; c && !isvisible(c); c = c->snext); + if (sel == c) + return; + lastsel = sel; + sel = c; + if (lastsel) { + lastsel->urgent = false; + if (!isarrange(fullscreen)) { + draw_border(lastsel); + wnoutrefresh(lastsel->window); + } + } + + if (c) { + detachstack(c); + attachstack(c); + settitle(c); + c->urgent = false; + if (isarrange(fullscreen)) { + draw(c); + } else { + draw_border(c); + wnoutrefresh(c->window); + } + } + curs_set(c && !c->minimized && vt_cursor_visible(c->term)); +} + +static void +applycolorrules(Client *c) { + const ColorRule *r = colorrules; + short fg = r->color->fg, bg = r->color->bg; + attr_t attrs = r->attrs; + + for (unsigned int i = 1; i < LENGTH(colorrules); i++) { + r = &colorrules[i]; + if (strstr(c->title, r->title)) { + attrs = r->attrs; + fg = r->color->fg; + bg = r->color->bg; + break; + } + } + + vt_default_colors_set(c->term, attrs, fg, bg); +} + +static void +term_title_handler(Vt *term, const char *title) { + Client *c = (Client *)vt_data_get(term); + if (title) + strncpy(c->title, title, sizeof(c->title) - 1); + c->title[title ? sizeof(c->title) - 1 : 0] = '\0'; + settitle(c); + if (!isarrange(fullscreen) || sel == c) + draw_border(c); + applycolorrules(c); +} + +static void +term_urgent_handler(Vt *term) { + Client *c = (Client *)vt_data_get(term); + c->urgent = true; + printf("\a"); + fflush(stdout); + drawbar(); + if (!isarrange(fullscreen) && sel != c && isvisible(c)) + draw_border(c); +} + +static void +move_client(Client *c, int x, int y) { + if (c->x == x && c->y == y) + return; + debug("moving, x: %d y: %d\n", x, y); + if (mvwin(c->window, y, x) == ERR) { + eprint("error moving, x: %d y: %d\n", x, y); + } else { + c->x = x; + c->y = y; + } +} + +static void +resize_client(Client *c, int w, int h) { + bool has_title_line = show_border(); + bool resize_window = c->w != w || c->h != h; + if (resize_window) { + debug("resizing, w: %d h: %d\n", w, h); + if (wresize(c->window, h, w) == ERR) { + eprint("error resizing, w: %d h: %d\n", w, h); + } else { + c->w = w; + c->h = h; + } + } + if (resize_window || c->has_title_line != has_title_line) { + c->has_title_line = has_title_line; + vt_resize(c->app, h - has_title_line, w); + if (c->editor) + vt_resize(c->editor, h - has_title_line, w); + } +} + +static void +resize(Client *c, int x, int y, int w, int h) { + resize_client(c, w, h); + move_client(c, x, y); +} + +static Client* +get_client_by_coord(unsigned int x, unsigned int y) { + if (y < way || y >= way+wah) + return NULL; + if (isarrange(fullscreen)) + return sel; + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (x >= c->x && x < c->x + c->w && y >= c->y && y < c->y + c->h) { + debug("mouse event, x: %d y: %d client: %d\n", x, y, c->order); + return c; + } + } + return NULL; +} + +static void +sigchld_handler(int sig) { + int errsv = errno; + int status; + pid_t pid; + + while ((pid = waitpid(-1, &status, WNOHANG)) != 0) { + if (pid == -1) { + if (errno == ECHILD) { + /* no more child processes */ + break; + } + eprint("waitpid: %s\n", strerror(errno)); + break; + } + + debug("child with pid %d died\n", pid); + + for (Client *c = clients; c; c = c->next) { + if (c->pid == pid) { + c->died = true; + break; + } + if (c->editor && vt_pid_get(c->editor) == pid) { + c->editor_died = true; + break; + } + } + } + + errno = errsv; +} + +static void +sigwinch_handler(int sig) { + screen.need_resize = true; +} + +static void +sigterm_handler(int sig) { + running = false; +} + +static void +resize_screen(void) { + struct winsize ws; + + if (ioctl(0, TIOCGWINSZ, &ws) == -1) { + getmaxyx(stdscr, screen.h, screen.w); + } else { + screen.w = ws.ws_col; + screen.h = ws.ws_row; + } + + debug("resize_screen(), w: %d h: %d\n", screen.w, screen.h); + + resizeterm(screen.h, screen.w); + wresize(stdscr, screen.h, screen.w); + updatebarpos(); + clear(); + arrange(); +} + +static KeyBinding* +keybinding(KeyCombo keys, unsigned int keycount) { + for (unsigned int b = 0; b < LENGTH(bindings); b++) { + for (unsigned int k = 0; k < keycount; k++) { + if (keys[k] != bindings[b].keys[k]) + break; + if (k == keycount - 1) + return &bindings[b]; + } + } + return NULL; +} + +static unsigned int +bitoftag(const char *tag) { + unsigned int i; + if (!tag) + return ~0; + for (i = 0; (i < LENGTH(tags)) && strcmp(tags[i], tag); i++); + return (i < LENGTH(tags)) ? (1 << i) : 0; +} + +static void +tagschanged() { + bool allminimized = true; + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (!c->minimized) { + allminimized = false; + break; + } + } + if (allminimized && nextvisible(clients)) { + focus(NULL); + toggleminimize(NULL); + } + arrange(); +} + +static void +tag(const char *args[]) { + if (!sel) + return; + sel->tags = bitoftag(args[0]) & TAGMASK; + tagschanged(); +} + +static void +tagid(const char *args[]) { + if (!args[0] || !args[1]) + return; + + const int win_id = atoi(args[0]); + for (Client *c = clients; c; c = c->next) { + if (c->id == win_id) { + unsigned int ntags = c->tags; + for (unsigned int i = 1; i < MAX_ARGS && args[i]; i++) { + if (args[i][0] == '+') + ntags |= bitoftag(args[i]+1); + else if (args[i][0] == '-') + ntags &= ~bitoftag(args[i]+1); + else + ntags = bitoftag(args[i]); + } + ntags &= TAGMASK; + if (ntags) { + c->tags = ntags; + tagschanged(); + } + return; + } + } +} + +static void +toggletag(const char *args[]) { + if (!sel) + return; + unsigned int newtags = sel->tags ^ (bitoftag(args[0]) & TAGMASK); + if (newtags) { + sel->tags = newtags; + tagschanged(); + } +} + +static void +toggleview(const char *args[]) { + unsigned int newtagset = tagset[seltags] ^ (bitoftag(args[0]) & TAGMASK); + if (newtagset) { + tagset[seltags] = newtagset; + tagschanged(); + } +} + +static void +view(const char *args[]) { + unsigned int newtagset = bitoftag(args[0]) & TAGMASK; + if (tagset[seltags] != newtagset && newtagset) { + seltags ^= 1; /* toggle sel tagset */ + tagset[seltags] = newtagset; + tagschanged(); + } +} + +static void +viewprevtag(const char *args[]) { + seltags ^= 1; + tagschanged(); +} + +static void +keypress(int code) { + int key = -1; + unsigned int len = 1; + char buf[8] = { '\e' }; + + if (code == '\e') { + /* pass characters following escape to the underlying app */ + nodelay(stdscr, TRUE); + for (int t; len < sizeof(buf) && (t = getch()) != ERR; len++) { + if (t > 255) { + key = t; + break; + } + buf[len] = t; + } + nodelay(stdscr, FALSE); + } + + for (Client *c = runinall ? nextvisible(clients) : sel; c; c = nextvisible(c->next)) { + if (is_content_visible(c)) { + c->urgent = false; + if (code == '\e') + vt_write(c->term, buf, len); + else + vt_keypress(c->term, code); + if (key != -1) + vt_keypress(c->term, key); + } + if (!runinall) + break; + } +} + +static void +mouse_setup(void) { +#ifdef CONFIG_MOUSE + mmask_t mask = 0; + + if (mouse_events_enabled) { + mask = BUTTON1_CLICKED | BUTTON2_CLICKED; + for (unsigned int i = 0; i < LENGTH(buttons); i++) + mask |= buttons[i].mask; + } + mousemask(mask, NULL); +#endif /* CONFIG_MOUSE */ +} + +static bool +checkshell(const char *shell) { + if (shell == NULL || *shell == '\0' || *shell != '/') + return false; + if (!strcmp(strrchr(shell, '/')+1, dvtm_name)) + return false; + if (access(shell, X_OK)) + return false; + return true; +} + +static const char * +getshell(void) { + const char *shell = getenv("SHELL"); + struct passwd *pw; + + if (checkshell(shell)) + return shell; + if ((pw = getpwuid(getuid())) && checkshell(pw->pw_shell)) + return pw->pw_shell; + return "/bin/sh"; +} + +static void +setup(void) { + shell = getshell(); + setlocale(LC_CTYPE, ""); + initscr(); + start_color(); + noecho(); + nonl(); + keypad(stdscr, TRUE); + mouse_setup(); + raw(); + vt_init(); + vt_keytable_set(keytable, LENGTH(keytable)); + for (unsigned int i = 0; i < LENGTH(colors); i++) { + if (COLORS == 256) { + if (colors[i].fg256) + colors[i].fg = colors[i].fg256; + if (colors[i].bg256) + colors[i].bg = colors[i].bg256; + } + colors[i].pair = vt_color_reserve(colors[i].fg, colors[i].bg); + } + resize_screen(); + struct sigaction sa; + memset(&sa, 0, sizeof sa); + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = sigwinch_handler; + sigaction(SIGWINCH, &sa, NULL); + sa.sa_handler = sigchld_handler; + sigaction(SIGCHLD, &sa, NULL); + sa.sa_handler = sigterm_handler; + sigaction(SIGTERM, &sa, NULL); + sa.sa_handler = SIG_IGN; + sigaction(SIGPIPE, &sa, NULL); +} + +static void +destroy(Client *c) { + if (sel == c) + focusnextnm(NULL); + detach(c); + detachstack(c); + if (sel == c) { + Client *next = nextvisible(clients); + if (next) { + focus(next); + toggleminimize(NULL); + } else { + sel = NULL; + } + } + if (lastsel == c) + lastsel = NULL; + werase(c->window); + wnoutrefresh(c->window); + vt_destroy(c->term); + delwin(c->window); + if (!clients && LENGTH(actions)) { + if (!strcmp(c->cmd, shell)) + quit(NULL); + else + create(NULL); + } + free(c); + arrange(); +} + +static void +cleanup(void) { + while (clients) + destroy(clients); + vt_shutdown(); + endwin(); + free(copyreg.data); + if (bar.fd > 0) + close(bar.fd); + if (bar.file) + unlink(bar.file); + if (cmdfifo.fd > 0) + close(cmdfifo.fd); + if (cmdfifo.file) + unlink(cmdfifo.file); +} + +static char *getcwd_by_pid(Client *c) { + if (!c) + return NULL; + char buf[32]; + snprintf(buf, sizeof buf, "/proc/%d/cwd", c->pid); + return realpath(buf, NULL); +} + +static void +create(const char *args[]) { + const char *pargs[4] = { shell, NULL }; + char buf[8], *cwd = NULL; + const char *env[] = { + "DVTM_WINDOW_ID", buf, + NULL + }; + + if (args && args[0]) { + pargs[1] = "-c"; + pargs[2] = args[0]; + pargs[3] = NULL; + } + Client *c = calloc(1, sizeof(Client)); + if (!c) + return; + c->tags = tagset[seltags]; + c->id = ++cmdfifo.id; + snprintf(buf, sizeof buf, "%d", c->id); + + if (!(c->window = newwin(wah, waw, way, wax))) { + free(c); + return; + } + + c->term = c->app = vt_create(screen.h, screen.w, screen.history); + if (!c->term) { + delwin(c->window); + free(c); + return; + } + + if (args && args[0]) { + c->cmd = args[0]; + char name[PATH_MAX]; + strncpy(name, args[0], sizeof(name)); + name[sizeof(name)-1] = '\0'; + strncpy(c->title, basename(name), sizeof(c->title)); + } else { + c->cmd = shell; + } + + if (args && args[1]) + strncpy(c->title, args[1], sizeof(c->title)); + c->title[sizeof(c->title)-1] = '\0'; + + if (args && args[2]) + cwd = !strcmp(args[2], "$CWD") ? getcwd_by_pid(sel) : (char*)args[2]; + c->pid = vt_forkpty(c->term, shell, pargs, cwd, env, NULL, NULL); + if (args && args[2] && !strcmp(args[2], "$CWD")) + free(cwd); + vt_data_set(c->term, c); + vt_title_handler_set(c->term, term_title_handler); + vt_urgent_handler_set(c->term, term_urgent_handler); + applycolorrules(c); + c->x = wax; + c->y = way; + debug("client with pid %d forked\n", c->pid); + attach(c); + focus(c); + arrange(); +} + +static void +copymode(const char *args[]) { + if (!args || !args[0] || !sel || sel->editor) + return; + + bool colored = strstr(args[0], "pager") != NULL; + + if (!(sel->editor = vt_create(sel->h - sel->has_title_line, sel->w, 0))) + return; + + int *to = &sel->editor_fds[0]; + int *from = strstr(args[0], "editor") ? &sel->editor_fds[1] : NULL; + sel->editor_fds[0] = sel->editor_fds[1] = -1; + + const char *argv[3] = { args[0], NULL, NULL }; + char argline[32]; + int line = vt_content_start(sel->app); + snprintf(argline, sizeof(argline), "+%d", line); + argv[1] = argline; + + char *cwd = getcwd_by_pid(sel); + if (vt_forkpty(sel->editor, args[0], argv, cwd, NULL, to, from) < 0) { + vt_destroy(sel->editor); + sel->editor = NULL; + return; + } + + sel->term = sel->editor; + + if (sel->editor_fds[0] != -1) { + char *buf = NULL; + size_t len = vt_content_get(sel->app, &buf, colored); + char *cur = buf; + while (len > 0) { + ssize_t res = write(sel->editor_fds[0], cur, len); + if (res < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + break; + } + cur += res; + len -= res; + } + free(buf); + close(sel->editor_fds[0]); + sel->editor_fds[0] = -1; + } + + if (args[1]) + vt_write(sel->editor, args[1], strlen(args[1])); +} + +static void +focusn(const char *args[]) { + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->order == atoi(args[0])) { + focus(c); + if (c->minimized) + toggleminimize(NULL); + return; + } + } +} + +static void +focusid(const char *args[]) { + if (!args[0]) + return; + + const int win_id = atoi(args[0]); + for (Client *c = clients; c; c = c->next) { + if (c->id == win_id) { + focus(c); + if (c->minimized) + toggleminimize(NULL); + if (!isvisible(c)) { + c->tags |= tagset[seltags]; + tagschanged(); + } + return; + } + } +} + +static void +focusnext(const char *args[]) { + Client *c; + if (!sel) + return; + for (c = sel->next; c && !isvisible(c); c = c->next); + if (!c) + for (c = clients; c && !isvisible(c); c = c->next); + if (c) + focus(c); +} + +static void +focusnextnm(const char *args[]) { + if (!sel) + return; + Client *c = sel; + do { + c = nextvisible(c->next); + if (!c) + c = nextvisible(clients); + } while (c->minimized && c != sel); + focus(c); +} + +static void +focusprev(const char *args[]) { + Client *c; + if (!sel) + return; + for (c = sel->prev; c && !isvisible(c); c = c->prev); + if (!c) { + for (c = clients; c && c->next; c = c->next); + for (; c && !isvisible(c); c = c->prev); + } + if (c) + focus(c); +} + +static void +focusprevnm(const char *args[]) { + if (!sel) + return; + Client *c = sel; + do { + for (c = c->prev; c && !isvisible(c); c = c->prev); + if (!c) { + for (c = clients; c && c->next; c = c->next); + for (; c && !isvisible(c); c = c->prev); + } + } while (c && c != sel && c->minimized); + focus(c); +} + +static void +focuslast(const char *args[]) { + if (lastsel) + focus(lastsel); +} + +static void +focusup(const char *args[]) { + if (!sel) + return; + /* avoid vertical separator, hence +1 in x direction */ + Client *c = get_client_by_coord(sel->x + 1, sel->y - 1); + if (c) + focus(c); + else + focusprev(args); +} + +static void +focusdown(const char *args[]) { + if (!sel) + return; + Client *c = get_client_by_coord(sel->x, sel->y + sel->h); + if (c) + focus(c); + else + focusnext(args); +} + +static void +focusleft(const char *args[]) { + if (!sel) + return; + Client *c = get_client_by_coord(sel->x - 2, sel->y); + if (c) + focus(c); + else + focusprev(args); +} + +static void +focusright(const char *args[]) { + if (!sel) + return; + Client *c = get_client_by_coord(sel->x + sel->w + 1, sel->y); + if (c) + focus(c); + else + focusnext(args); +} + +static void +killclient(const char *args[]) { + if (!sel) + return; + debug("killing client with pid: %d\n", sel->pid); + kill(-sel->pid, SIGKILL); +} + +static void +paste(const char *args[]) { + if (sel && copyreg.data) + vt_write(sel->term, copyreg.data, copyreg.len); +} + +static void +quit(const char *args[]) { + cleanup(); + exit(EXIT_SUCCESS); +} + +static void +redraw(const char *args[]) { + for (Client *c = clients; c; c = c->next) { + if (!c->minimized) { + vt_dirty(c->term); + wclear(c->window); + wnoutrefresh(c->window); + } + } + resize_screen(); +} + +static void +scrollback(const char *args[]) { + if (!is_content_visible(sel)) + return; + + if (!args[0] || atoi(args[0]) < 0) + vt_scroll(sel->term, -sel->h/2); + else + vt_scroll(sel->term, sel->h/2); + + draw(sel); + curs_set(vt_cursor_visible(sel->term)); +} + +static void +send(const char *args[]) { + if (sel && args && args[0]) + vt_write(sel->term, args[0], strlen(args[0])); +} + +static void +setlayout(const char *args[]) { + unsigned int i; + + if (!args || !args[0]) { + if (++layout == &layouts[LENGTH(layouts)]) + layout = &layouts[0]; + } else { + for (i = 0; i < LENGTH(layouts); i++) + if (!strcmp(args[0], layouts[i].symbol)) + break; + if (i == LENGTH(layouts)) + return; + layout = &layouts[i]; + } + arrange(); +} + +static void +incnmaster(const char *args[]) { + int delta; + + if (isarrange(fullscreen) || isarrange(grid)) + return; + /* arg handling, manipulate nmaster */ + if (args[0] == NULL) { + screen.nmaster = NMASTER; + } else if (sscanf(args[0], "%d", &delta) == 1) { + if (args[0][0] == '+' || args[0][0] == '-') + screen.nmaster += delta; + else + screen.nmaster = delta; + if (screen.nmaster < 1) + screen.nmaster = 1; + } + arrange(); +} + +static void +setmfact(const char *args[]) { + float delta; + + if (isarrange(fullscreen) || isarrange(grid)) + return; + /* arg handling, manipulate mfact */ + if (args[0] == NULL) { + screen.mfact = MFACT; + } else if (sscanf(args[0], "%f", &delta) == 1) { + if (args[0][0] == '+' || args[0][0] == '-') + screen.mfact += delta; + else + screen.mfact = delta; + if (screen.mfact < 0.1) + screen.mfact = 0.1; + else if (screen.mfact > 0.9) + screen.mfact = 0.9; + } + arrange(); +} + +static void +startup(const char *args[]) { + for (unsigned int i = 0; i < LENGTH(actions); i++) + actions[i].cmd(actions[i].args); +} + +static void +togglebar(const char *args[]) { + if (bar.pos == BAR_OFF) + showbar(); + else + hidebar(); + bar.autohide = false; + updatebarpos(); + redraw(NULL); +} + +static void +togglebarpos(const char *args[]) { + switch (bar.pos == BAR_OFF ? bar.lastpos : bar.pos) { + case BAR_TOP: + bar.pos = BAR_BOTTOM; + break; + case BAR_BOTTOM: + bar.pos = BAR_TOP; + break; + } + updatebarpos(); + redraw(NULL); +} + +static void +toggleminimize(const char *args[]) { + Client *c, *m, *t; + unsigned int n; + if (!sel) + return; + /* the last window can't be minimized */ + if (!sel->minimized) { + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + if (n == 1) + return; + } + sel->minimized = !sel->minimized; + m = sel; + /* check whether the master client was minimized */ + if (sel == nextvisible(clients) && sel->minimized) { + c = nextvisible(sel->next); + detach(c); + attach(c); + focus(c); + detach(m); + for (; c && (t = nextvisible(c->next)) && !t->minimized; c = t); + attachafter(m, c); + } else if (m->minimized) { + /* non master window got minimized move it above all other + * minimized ones */ + focusnextnm(NULL); + detach(m); + for (c = nextvisible(clients); c && (t = nextvisible(c->next)) && !t->minimized; c = t); + attachafter(m, c); + } else { /* window is no longer minimized, move it to the master area */ + vt_dirty(m->term); + detach(m); + attach(m); + } + arrange(); +} + +static void +togglemouse(const char *args[]) { + mouse_events_enabled = !mouse_events_enabled; + mouse_setup(); +} + +static void +togglerunall(const char *args[]) { + runinall = !runinall; + drawbar(); + draw_all(); +} + +static void +zoom(const char *args[]) { + Client *c; + + if (!sel) + return; + if (args && args[0]) + focusn(args); + if ((c = sel) == nextvisible(clients)) + if (!(c = nextvisible(c->next))) + return; + detach(c); + attach(c); + focus(c); + if (c->minimized) + toggleminimize(NULL); + arrange(); +} + +/* commands for use by mouse bindings */ +static void +mouse_focus(const char *args[]) { + focus(msel); + if (msel->minimized) + toggleminimize(NULL); +} + +static void +mouse_fullscreen(const char *args[]) { + mouse_focus(NULL); + setlayout(isarrange(fullscreen) ? NULL : args); +} + +static void +mouse_minimize(const char *args[]) { + focus(msel); + toggleminimize(NULL); +} + +static void +mouse_zoom(const char *args[]) { + focus(msel); + zoom(NULL); +} + +static Cmd * +get_cmd_by_name(const char *name) { + for (unsigned int i = 0; i < LENGTH(commands); i++) { + if (!strcmp(name, commands[i].name)) + return &commands[i]; + } + return NULL; +} + +static void +handle_cmdfifo(void) { + int r; + char *p, *s, cmdbuf[512], c; + Cmd *cmd; + + r = read(cmdfifo.fd, cmdbuf, sizeof cmdbuf - 1); + if (r <= 0) { + cmdfifo.fd = -1; + return; + } + + cmdbuf[r] = '\0'; + p = cmdbuf; + while (*p) { + /* find the command name */ + for (; *p == ' ' || *p == '\n'; p++); + for (s = p; *p && *p != ' ' && *p != '\n'; p++); + if ((c = *p)) + *p++ = '\0'; + if (*s && (cmd = get_cmd_by_name(s)) != NULL) { + bool quote = false; + int argc = 0; + const char *args[MAX_ARGS], *arg; + memset(args, 0, sizeof(args)); + /* if arguments were specified in config.h ignore the one given via + * the named pipe and thus skip everything until we find a new line + */ + if (cmd->action.args[0] || c == '\n') { + debug("execute %s", s); + cmd->action.cmd(cmd->action.args); + while (*p && *p != '\n') + p++; + continue; + } + /* no arguments were given in config.h so we parse the command line */ + while (*p == ' ') + p++; + arg = p; + for (; (c = *p); p++) { + switch (*p) { + case '\\': + /* remove the escape character '\\' move every + * following character to the left by one position + */ + switch (p[1]) { + case '\\': + case '\'': + case '\"': { + char *t = p+1; + do { + t[-1] = *t; + } while (*t++); + } + } + break; + case '\'': + case '\"': + quote = !quote; + break; + case ' ': + if (!quote) { + case '\n': + /* remove trailing quote if there is one */ + if (*(p - 1) == '\'' || *(p - 1) == '\"') + *(p - 1) = '\0'; + *p++ = '\0'; + /* remove leading quote if there is one */ + if (*arg == '\'' || *arg == '\"') + arg++; + if (argc < MAX_ARGS) + args[argc++] = arg; + + while (*p == ' ') + ++p; + arg = p--; + } + break; + } + + if (c == '\n' || *p == '\n') { + if (!*p) + p++; + debug("execute %s", s); + for(int i = 0; i < argc; i++) + debug(" %s", args[i]); + debug("\n"); + cmd->action.cmd(args); + break; + } + } + } + } +} + +static void +handle_mouse(void) { +#ifdef CONFIG_MOUSE + MEVENT event; + unsigned int i; + if (getmouse(&event) != OK) + return; + msel = get_client_by_coord(event.x, event.y); + + if (!msel) + return; + + debug("mouse x:%d y:%d cx:%d cy:%d mask:%d\n", event.x, event.y, event.x - msel->x, event.y - msel->y, event.bstate); + + vt_mouse(msel->term, event.x - msel->x, event.y - msel->y, event.bstate); + + for (i = 0; i < LENGTH(buttons); i++) { + if (event.bstate & buttons[i].mask) + buttons[i].action.cmd(buttons[i].action.args); + } + + msel = NULL; +#endif /* CONFIG_MOUSE */ +} + +static void +handle_statusbar(void) { + char *p; + int r; + switch (r = read(bar.fd, bar.text, sizeof bar.text - 1)) { + case -1: + strncpy(bar.text, strerror(errno), sizeof bar.text - 1); + bar.text[sizeof bar.text - 1] = '\0'; + bar.fd = -1; + break; + case 0: + bar.fd = -1; + break; + default: + bar.text[r] = '\0'; + p = bar.text + r - 1; + for (; p >= bar.text && *p == '\n'; *p-- = '\0'); + for (; p >= bar.text && *p != '\n'; --p); + if (p >= bar.text) + memmove(bar.text, p + 1, strlen(p)); + drawbar(); + } +} + +static void +handle_editor(Client *c) { + if (!copyreg.data && (copyreg.data = malloc(screen.history))) + copyreg.size = screen.history; + copyreg.len = 0; + while (c->editor_fds[1] != -1 && copyreg.len < copyreg.size) { + ssize_t len = read(c->editor_fds[1], copyreg.data + copyreg.len, copyreg.size - copyreg.len); + if (len == -1) { + if (errno == EINTR) + continue; + break; + } + if (len == 0) + break; + copyreg.len += len; + if (copyreg.len == copyreg.size) { + copyreg.size *= 2; + if (!(copyreg.data = realloc(copyreg.data, copyreg.size))) { + copyreg.size = 0; + copyreg.len = 0; + } + } + } + c->editor_died = false; + c->editor_fds[1] = -1; + vt_destroy(c->editor); + c->editor = NULL; + c->term = c->app; + vt_dirty(c->term); + draw_content(c); + wnoutrefresh(c->window); +} + +static int +open_or_create_fifo(const char *name, const char **name_created) { + struct stat info; + int fd; + + do { + if ((fd = open(name, O_RDWR|O_NONBLOCK)) == -1) { + if (errno == ENOENT && !mkfifo(name, S_IRUSR|S_IWUSR)) { + *name_created = name; + continue; + } + error("%s\n", strerror(errno)); + } + } while (fd == -1); + + if (fstat(fd, &info) == -1) + error("%s\n", strerror(errno)); + if (!S_ISFIFO(info.st_mode)) + error("%s is not a named pipe\n", name); + return fd; +} + +static void +usage(void) { + cleanup(); + eprint("usage: dvtm [-v] [-M] [-m mod] [-d delay] [-h lines] [-t title] " + "[-s status-fifo] [-c cmd-fifo] [cmd...]\n"); + exit(EXIT_FAILURE); +} + +static bool +parse_args(int argc, char *argv[]) { + bool init = false; + const char *name = argv[0]; + + if (name && (name = strrchr(name, '/'))) + dvtm_name = name + 1; + if (!getenv("ESCDELAY")) + set_escdelay(100); + for (int arg = 1; arg < argc; arg++) { + if (argv[arg][0] != '-') { + const char *args[] = { argv[arg], NULL, NULL }; + if (!init) { + setup(); + init = true; + } + create(args); + continue; + } + if (argv[arg][1] != 'v' && argv[arg][1] != 'M' && (arg + 1) >= argc) + usage(); + switch (argv[arg][1]) { + case 'v': + puts("dvtm-"VERSION" © 2007-2016 Marc André Tanner"); + exit(EXIT_SUCCESS); + case 'M': + mouse_events_enabled = !mouse_events_enabled; + break; + case 'm': { + char *mod = argv[++arg]; + if (mod[0] == '^' && mod[1]) + *mod = CTRL(mod[1]); + for (unsigned int b = 0; b < LENGTH(bindings); b++) + if (bindings[b].keys[0] == MOD) + bindings[b].keys[0] = *mod; + break; + } + case 'd': + set_escdelay(atoi(argv[++arg])); + if (ESCDELAY < 50) + set_escdelay(50); + else if (ESCDELAY > 1000) + set_escdelay(1000); + break; + case 'h': + screen.history = atoi(argv[++arg]); + break; + case 't': + title = argv[++arg]; + break; + case 's': + bar.fd = open_or_create_fifo(argv[++arg], &bar.file); + updatebarpos(); + break; + case 'c': { + char *fifo; + cmdfifo.fd = open_or_create_fifo(argv[++arg], &cmdfifo.file); + if (!(fifo = realpath(argv[arg], NULL))) + error("%s\n", strerror(errno)); + setenv("DVTM_CMD_FIFO", fifo, 1); + free(fifo); + break; + } + default: + usage(); + } + } + return init; +} + +int +main(int argc, char *argv[]) { + unsigned int key_index = 0; + memset(keys, 0, sizeof(keys)); + sigset_t emptyset, blockset; + + setenv("DVTM", VERSION, 1); + if (!parse_args(argc, argv)) { + setup(); + startup(NULL); + } + + sigemptyset(&emptyset); + sigemptyset(&blockset); + sigaddset(&blockset, SIGWINCH); + sigaddset(&blockset, SIGCHLD); + sigprocmask(SIG_BLOCK, &blockset, NULL); + + while (running) { + int r, nfds = 0; + fd_set rd; + + if (screen.need_resize) { + resize_screen(); + screen.need_resize = false; + } + + FD_ZERO(&rd); + FD_SET(STDIN_FILENO, &rd); + + if (cmdfifo.fd != -1) { + FD_SET(cmdfifo.fd, &rd); + nfds = cmdfifo.fd; + } + + if (bar.fd != -1) { + FD_SET(bar.fd, &rd); + nfds = MAX(nfds, bar.fd); + } + + for (Client *c = clients; c; ) { + if (c->editor && c->editor_died) + handle_editor(c); + if (!c->editor && c->died) { + Client *t = c->next; + destroy(c); + c = t; + continue; + } + int pty = c->editor ? vt_pty_get(c->editor) : vt_pty_get(c->app); + FD_SET(pty, &rd); + nfds = MAX(nfds, pty); + c = c->next; + } + + doupdate(); + r = pselect(nfds + 1, &rd, NULL, NULL, NULL, &emptyset); + + if (r < 0) { + if (errno == EINTR) + continue; + perror("select()"); + exit(EXIT_FAILURE); + } + + if (FD_ISSET(STDIN_FILENO, &rd)) { + int code = getch(); + if (code >= 0) { + keys[key_index++] = code; + KeyBinding *binding = NULL; + if (code == KEY_MOUSE) { + key_index = 0; + handle_mouse(); + } else if ((binding = keybinding(keys, key_index))) { + unsigned int key_length = MAX_KEYS; + while (key_length > 1 && !binding->keys[key_length-1]) + key_length--; + if (key_index == key_length) { + binding->action.cmd(binding->action.args); + key_index = 0; + memset(keys, 0, sizeof(keys)); + } + } else { + key_index = 0; + memset(keys, 0, sizeof(keys)); + keypress(code); + } + drawbar(); + if (is_content_visible(sel)) + wnoutrefresh(sel->window); + } + if (r == 1) /* no data available on pty's */ + continue; + } + + if (cmdfifo.fd != -1 && FD_ISSET(cmdfifo.fd, &rd)) + handle_cmdfifo(); + + if (bar.fd != -1 && FD_ISSET(bar.fd, &rd)) + handle_statusbar(); + + for (Client *c = clients; c; c = c->next) { + if (FD_ISSET(vt_pty_get(c->term), &rd)) { + if (vt_process(c->term) < 0 && errno == EIO) { + if (c->editor) + c->editor_died = true; + else + c->died = true; + continue; + } + } + + if (c != sel && is_content_visible(c)) { + draw_content(c); + wnoutrefresh(c->window); + } + } + + if (is_content_visible(sel)) { + draw_content(sel); + curs_set(vt_cursor_visible(sel->term)); + wnoutrefresh(sel->window); + } + } + + cleanup(); + return 0; +} diff --git a/dvtm.info b/dvtm.info @@ -0,0 +1,136 @@ +dvtm|dynamic virtual terminal manager, + am, + eo, + mir, + msgr, + xenl, + colors#8, + cols#80, + it#8, + lines#24, + ncv@, + pairs#64, + acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + bel=^G, + blink=\E[5m, + bold=\E[1m, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?25h, + cr=^M, + csr=\E[%i%p1%d;%p2%dr, + cub=\E[%p1%dD, + cub1=^H, + cud=\E[%p1%dB, + cud1=^J, + cuf=\E[%p1%dC, + cuf1=\E[C, + cup=\E[%i%p1%d;%p2%dH, + cuu=\E[%p1%dA, + cuu1=\E[A, + dl=\E[%p1%dM, + dl1=\E[M, + ed=\E[J, + el=\E[K, + el1=\E[1K, + enacs=\E(B\E)0, + home=\E[H, + hpa=\E[%i%p1%dG, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + ich1=\E[@, + il=\E[%p1%dL, + il1=\E[L, + ind=^J, + is1=\E[?47l\E=\E[?1l, + is2=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l, + kDC=\E[3$, + kEND=\E[8$, + kHOM=\E[7$, + kIC=\E[2$, + kLFT=\E[d, + kNXT=\E[6$, + kPRV=\E[5$, + kRIT=\E[c, + ka1=\EOw, + ka3=\EOy, + kb2=\EOu, + kbs=\177, + kc1=\EOq, + kc3=\EOs, + kcbt=\E[Z, + kcub1=\E[D, + kcud1=\E[B, + kcuf1=\E[C, + kcuu1=\E[A, + kdch1=\E[3~, + kel=\E[8\^, + kend=\E[8~, + kent=\EOM, + kf0=\E[21~, + kf1=\E[11~, + kf2=\E[12~, + kf3=\E[13~, + kf4=\E[14~, + kf5=\E[15~, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[25~, + kf14=\E[26~, + kf15=\E[28~, + kf16=\E[29~, + kf17=\E[31~, + kf18=\E[32~, + kf19=\E[33~, + kf20=\E[34~, + kf21=\E[23$, + kf22=\E[24$ + kfnd=\E[1~, + khome=\E[7~, + kich1=\E[2~, + kind=\E[a, + kmous=\E[M, + knp=\E[6~, + kpp=\E[5~, + kri=\E[b, + kslt=\E[4~, + op=\E[39;49m, + rc=\E8, + rev=\E[7m, + ri=\EM, + ritm=\E[23m, + rmacs=^O, + rmcup=\E[2J\E[?47l\E8, + rmir=\E[4l, + rmso=\E[27m, + rmul=\E[24m, + rs1=\E>\E[?1;3;4;5;6l\E[?7h\E[m\E[r\E[2J\E[H, + rs2=\E[r\E[m\E[2J\E[H\E[?7h\E[?1;3;4;6l\E[4l\E>\E[?1000l\E[?25h, + s0ds=\E(B, + s1ds=\E(0, + sc=\E7, + setab=\E[4%p1%dm, + setaf=\E[3%p1%dm, + sgr=\E[0%?%p6%t;1%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;m%?%p9%t\016%e\017%;, + sgr0=\E[m\017, + sitm=\E[3m, + smacs=^N, + smcup=\E7\E[?47h, + smir=\E[4h, + smso=\E[7m, + smul=\E[4m, + tbc=\E[3g, + vpa=\E[%i%p1%dd, + +dvtm-256color|dynamic virtual terminal manager with 256 colors, + use=dvtm, + colors#256, + pairs#32767, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m, diff --git a/fibonacci.c b/fibonacci.c @@ -0,0 +1,94 @@ +static void fibonacci(int s) +{ + unsigned int nx, ny, nw, nnw, nh, nnh, i, n, mod; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + + /* initial position and dimensions */ + nx = wax; + ny = way; + nw = (n == 1) ? waw : screen.mfact * waw; + /* don't waste space dviding by 2 doesn't work for odd numbers + * plus we need space for the border too. therefore set up these + * variables for the next new width/height + */ + nnw = waw - nw - 1; + nnh = nh = wah; + + /* set the mod factor, 2 for dwindle, 4 for spiral */ + mod = s ? 4 : 2; + + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + /* dwindle: even case, spiral: case 0 */ + if (i % mod == 0) { + if (i) { + if (s) { + nh = nnh; + ny -= nh; + } else { + ny += nh; + nh = nnh; + } + /* don't adjust the width for the last client */ + if (i < n - 1) { + nw /= 2; + nnw -= nw + 1; + } + mvaddch(ny, nx - 1, ACS_LTEE); + } + } else if (i % mod == 1) { /* dwindle: odd case, spiral: case 1 */ + nx += nw; + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + ++nx; + nw = nnw; + /* don't adjust the height for the last client */ + if (i < n - 1) { + nh /= 2; + nnh -= nh; + } + } else if (i % mod == 2 && s) { /* spiral: case 2 */ + ny += nh; + nh = nnh; + /* don't adjust the width for the last client */ + if (i < n - 1) { + nw /= 2; + nnw -= nw + 1; + nx += nnw; + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + ++nx; + } else { + mvaddch(ny, nx - 1, ACS_LTEE); + } + } else if (s) { /* spiral: case 3 */ + nw = nnw; + nx -= nw + 1; /* border */ + /* don't adjust the height for the last client */ + if (i < n - 1) { + nh /= 2; + nnh -= nh; + ny += nnh; + } + mvaddch(ny, nx - 1, ACS_LTEE); + } + + resize(c, nx, ny, nw, nh); + i++; + } +} + +static void spiral(void) +{ + fibonacci(1); +} + +static void dwindle(void) +{ + fibonacci(0); +} diff --git a/forkpty-aix.c b/forkpty-aix.c @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2009 Nicholas Marriott <nicm@users.sourceforge.net> + * Copyright (c) 2012 Ross Palmer Mohn <rpmohn@waxandwane.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <stdlib.h> +#include <stropts.h> +#include <unistd.h> +#include <paths.h> + +pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws) +{ + int slave, fd; + char *path; + pid_t pid; + struct termios tio2; + + if ((*master = open("/dev/ptc", O_RDWR|O_NOCTTY)) == -1) + return -1; + + if ((path = ttyname(*master)) == NULL) + goto out; + if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1) + goto out; + + switch (pid = fork()) { + case -1: + goto out; + case 0: + close(*master); + + fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); + if (fd >= 0) { + ioctl(fd, TIOCNOTTY, NULL); + close(fd); + } + + setsid(); + + fd = open(_PATH_TTY, O_RDWR|O_NOCTTY); + if (fd >= 0) + return -1; + + fd = open(path, O_RDWR); + if (fd < 0) + return -1; + close(fd); + + fd = open("/dev/tty", O_WRONLY); + if (fd < 0) + return -1; + close(fd); + + if (tcgetattr(slave, &tio2) != 0) + return -1; + if (tio != NULL) + memcpy(tio2.c_cc, tio->c_cc, sizeof tio2.c_cc); + tio2.c_cc[VERASE] = '\177'; + if (tcsetattr(slave, TCSAFLUSH, &tio2) == -1) + return -1; + if (ioctl(slave, TIOCSWINSZ, ws) == -1) + return -1; + + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + if (slave > 2) + close(slave); + return 0; + } + + close(slave); + return pid; + +out: + if (*master != -1) + close(*master); + if (slave != -1) + close(slave); + return -1; +} diff --git a/forkpty-sunos.c b/forkpty-sunos.c @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2008 Nicholas Marriott <nicm@users.sourceforge.net> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <fcntl.h> +#include <stdlib.h> +#include <strings.h> +#include <stropts.h> +#include <unistd.h> + +#ifndef TTY_NAME_MAX +#define TTY_NAME_MAX TTYNAME_MAX +#endif + +pid_t forkpty(int *master, char *name, struct termios *tio, struct winsize *ws) +{ + int slave; + char *path; + pid_t pid; + + if ((*master = open("/dev/ptmx", O_RDWR|O_NOCTTY)) == -1) + return -1; + if (grantpt(*master) != 0) + goto out; + if (unlockpt(*master) != 0) + goto out; + + if ((path = ptsname(*master)) == NULL) + goto out; + if (name != NULL) + strlcpy(name, path, TTY_NAME_MAX); + if ((slave = open(path, O_RDWR|O_NOCTTY)) == -1) + goto out; + + switch (pid = fork()) { + case -1: + goto out; + case 0: + close(*master); + + setsid(); +#ifdef TIOCSCTTY + if (ioctl(slave, TIOCSCTTY, NULL) == -1) + return -1; +#endif + + if (ioctl(slave, I_PUSH, "ptem") == -1) + return -1; + if (ioctl(slave, I_PUSH, "ldterm") == -1) + return -1; + + if (tio != NULL && tcsetattr(slave, TCSAFLUSH, tio) == -1) + return -1; + if (ioctl(slave, TIOCSWINSZ, ws) == -1) + return -1; + + dup2(slave, 0); + dup2(slave, 1); + dup2(slave, 2); + if (slave > 2) + close(slave); + return 0; + } + + close(slave); + return pid; + +out: + if (*master != -1) + close(*master); + if (slave != -1) + close(slave); + return -1; +} diff --git a/fullscreen.c b/fullscreen.c @@ -0,0 +1,5 @@ +static void fullscreen(void) +{ + for (Client *c = nextvisible(clients); c; c = nextvisible(c->next)) + resize(c, wax, way, waw, wah); +} diff --git a/grid.c b/grid.c @@ -0,0 +1,50 @@ +static void grid(void) +{ + unsigned int i, n, nx, ny, nw, nh, aw, ah, cols, rows; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + /* grid dimensions */ + for (cols = 0; cols <= n / 2; cols++) + if (cols * cols >= n) + break; + rows = (cols && (cols - 1) * cols >= n) ? cols - 1 : cols; + /* window geoms (cell height/width) */ + nh = wah / (rows ? rows : 1); + nw = waw / (cols ? cols : 1); + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + /* if there are less clients in the last row than normal adjust the + * split rate to fill the empty space */ + if (rows > 1 && i == (rows * cols) - cols && (n - i) <= (n % cols)) + nw = waw / (n - i); + nx = (i % cols) * nw + wax; + ny = (i / cols) * nh + way; + /* adjust height/width of last row/column's windows */ + ah = (i >= cols * (rows - 1)) ? wah - nh * rows : 0; + /* special case if there are less clients in the last row */ + if (rows > 1 && i == n - 1 && (n - i) < (n % cols)) + /* (n % cols) == number of clients in the last row */ + aw = waw - nw * (n % cols); + else + aw = ((i + 1) % cols == 0) ? waw - nw * cols : 0; + if (i % cols) { + mvvline(ny, nx, ACS_VLINE, nh + ah); + /* if we are on the first row, or on the last one and there are fewer clients + * than normal whose border does not match the line above, print a top tree char + * otherwise a plus sign. */ + if (i <= cols + || (i >= rows * cols - cols && n % cols + && (cols - (n % cols)) % 2)) + mvaddch(ny, nx, ACS_TTEE); + else + mvaddch(ny, nx, ACS_PLUS); + nx++, aw--; + } + resize(c, nx, ny, nw + aw, nh + ah); + i++; + } +} diff --git a/testsuite.sh b/testsuite.sh @@ -0,0 +1,61 @@ +#!/bin/sh + +MOD="" # CTRL+g +ESC="" # \e +DVTM="./dvtm" +export DVTM_EDITOR="vis" +LOG="dvtm.log" +TEST_LOG="$0.log" +UTF8_TEST_URL="http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-demo.txt" + +[ ! -z "$1" ] && DVTM="$1" +[ ! -x "$DVTM" ] && echo "usage: $0 path-to-dvtm-binary" && exit 1 + +dvtm_input() { + printf "$1" +} + +dvtm_cmd() { + printf "${MOD}$1" + sleep 1 +} + +sh_cmd() { + printf "$1\n" + sleep 1 +} + +test_copymode() { # requires wget, diff, vis + local FILENAME="UTF-8-demo.txt" + local COPY="$FILENAME.copy" + [ ! -e "$FILENAME" ] && (wget "$UTF8_TEST_URL" -O "$FILENAME" > /dev/null 2>&1 || return 1) + sleep 1 + sh_cmd "cat $FILENAME" + dvtm_cmd 'e' + dvtm_input "?UTF-8 encoded\n" + dvtm_input '^kvG1k$' + dvtm_input ":wq!\n" + sleep 1 + sh_cmd "cat <<'EOF' > $COPY" + dvtm_cmd 'p' + sh_cmd 'EOF' + while [ ! -r "$COPY" ]; do sleep 1; done; + dvtm_input "exit\n" + diff -u "$FILENAME" "$COPY" 1>&2 + local RESULT=$? + rm -f "$COPY" + return $RESULT +} + +if ! which vis > /dev/null 2>&1 ; then + echo "vis not found, skiping copymode test" + exit 0 +fi + +{ + echo "Testing $DVTM" 1>&2 + $DVTM -v 1>&2 + test_copymode && echo "copymode: OK" 1>&2 || echo "copymode: FAIL" 1>&2; +} 2> "$TEST_LOG" | $DVTM -m ^g 2> $LOG + +cat "$TEST_LOG" && rm "$TEST_LOG" $LOG diff --git a/tile.c b/tile.c @@ -0,0 +1,49 @@ +static void tile(void) +{ + unsigned int i, n, nx, ny, nw, nh, m, mw, mh, th; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + + m = MAX(1, MIN(n, screen.nmaster)); + mw = n == m ? waw : screen.mfact * waw; + mh = wah / m; + th = n == m ? 0 : wah / (n - m); + nx = wax; + ny = way; + + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + if (i < m) { /* master */ + nw = mw; + nh = (i < m - 1) ? mh : (way + wah) - ny; + } else { /* tile window */ + if (i == m) { + ny = way; + nx += mw; + mvvline(ny, nx, ACS_VLINE, wah); + mvaddch(ny, nx, ACS_TTEE); + nx++; + nw = waw - mw -1; + } + nh = (i < n - 1) ? th : (way + wah) - ny; + if (i > m) + mvaddch(ny, nx - 1, ACS_LTEE); + } + resize(c, nx, ny, nw, nh); + ny += nh; + i++; + } + + /* Fill in nmaster intersections */ + if (n > m) { + ny = way + mh; + for (i = 1; i < m; i++) { + mvaddch(ny, nx - 1, ((ny - 1) % th ? ACS_RTEE : ACS_PLUS)); + ny += mh; + } + } +} diff --git a/tstack.c b/tstack.c @@ -0,0 +1,45 @@ +static void tstack(void) +{ + unsigned int i, n, nx, ny, nw, nh, m, mw, mh, tw; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + + m = MAX(1, MIN(n, screen.nmaster)); + mh = n == m ? wah : screen.mfact * wah; + mw = waw / m; + tw = n == m ? 0 : waw / (n - m); + nx = wax; + ny = way + wah - mh; + + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + if (i < m) { /* master */ + if (i > 0) { + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + nx++; + } + nh = mh; + nw = (i < m - 1) ? mw : (wax + waw) - nx; + } else { /* tile window */ + if (i == m) { + nx = wax; + ny = way; + nh = (way + wah) - ny - mh; + } + if (i > m) { + mvvline(ny, nx, ACS_VLINE, nh); + mvaddch(ny, nx, ACS_TTEE); + nx++; + } + nw = (i < n - 1) ? tw : (wax + waw) - nx; + } + resize(c, nx, ny, nw, nh); + nx += nw; + i++; + } +} diff --git a/vstack.c b/vstack.c @@ -0,0 +1,27 @@ +/* A vertical stack layout, all windows have the full screen width. */ +static void vstack(void) +{ + unsigned int i, n, ny, nh, m, mh, th; + Client *c; + + for (n = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) + if (!c->minimized) + n++; + + m = MAX(1, MIN(n, screen.nmaster)); + mh = (n == m ? wah : screen.mfact * wah); + th = n == m ? 0 : (wah - mh) / (n - m); + ny = way; + + for (i = 0, c = nextvisible(clients); c; c = nextvisible(c->next)) { + if (c->minimized) + continue; + if (i < m) /* master */ + nh = (i < m - 1) ? mh / m : (way + mh) - ny; + else /* tile window */ + nh = (i < n - 1) ? th : (way + wah) - ny; + resize(c, wax, ny, waw, nh); + ny += nh; + i++; + } +} diff --git a/vt.c b/vt.c @@ -0,0 +1,1981 @@ +/* + * Copyright © 2004 Bruno T. C. de Oliveira + * Copyright © 2006 Pierre Habouzit + * Copyright © 2008-2016 Marc André Tanner + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#include <stdlib.h> +#include <stdint.h> +#include <unistd.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <langinfo.h> +#include <limits.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <stddef.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/types.h> +#include <termios.h> +#include <wchar.h> +#if defined(__linux__) || defined(__CYGWIN__) +# include <pty.h> +#elif defined(__FreeBSD__) || defined(__DragonFly__) +# include <libutil.h> +#elif defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) +# include <util.h> +#endif + +#include "vt.h" + +#ifdef _AIX +# include "forkpty-aix.c" +#elif defined __sun +# include "forkpty-sunos.c" +#endif + +#ifndef NCURSES_ATTR_SHIFT +# define NCURSES_ATTR_SHIFT 8 +#endif + +#ifndef NCURSES_ACS +# ifdef PDCURSES +# define NCURSES_ACS(c) (acs_map[(unsigned char)(c)]) +# else /* BSD curses */ +# define NCURSES_ACS(c) (_acs_map[(unsigned char)(c)]) +# endif +#endif + +#ifdef NCURSES_VERSION +# ifndef NCURSES_EXT_COLORS +# define NCURSES_EXT_COLORS 0 +# endif +# if !NCURSES_EXT_COLORS +# define MAX_COLOR_PAIRS MIN(COLOR_PAIRS, 256) +# endif +#endif +#ifndef MAX_COLOR_PAIRS +# define MAX_COLOR_PAIRS COLOR_PAIRS +#endif + +#if defined _AIX && defined CTRL +# undef CTRL +#endif +#ifndef CTRL +# define CTRL(k) ((k) & 0x1F) +#endif + +#define IS_CONTROL(ch) !((ch) & 0xffffff60UL) +#define MIN(x, y) ((x) < (y) ? (x) : (y)) +#define LENGTH(arr) (sizeof(arr) / sizeof((arr)[0])) + +static bool is_utf8, has_default_colors; +static short color_pairs_reserved, color_pairs_max, color_pair_current; +static short *color2palette, default_fg, default_bg; +static char vt_term[32]; + +typedef struct { + wchar_t text; + attr_t attr; + short fg; + short bg; +} Cell; + +typedef struct { + Cell *cells; + unsigned dirty:1; +} Row; + +/* Buffer holding the current terminal window content (as an array) as well + * as the scroll back buffer content (as a circular/ring buffer). + * + * If new content is added to terminal the view port slides down and the + * previously top most line is moved into the scroll back buffer at postion + * scroll_index. This index will eventually wrap around and thus overwrite + * the oldest lines. + * + * In the scenerio below a scroll up has been performed. That is 'scroll_above' + * lines still lie above the current view port. Further scrolling up will show + * them. Similarly 'scroll_below' is the amount of lines below the current + * viewport. + * + * The function buffer_boundary sets the row pointers to the start/end range + * of the section delimiting the region before/after the viewport. The functions + * buffer_row_{first,last} return the first/last logical row. And + * buffer_row_{next,prev} allows to iterate over the logical lines in either + * direction. + * + * scroll back buffer + * + * scroll_buf->+----------------+-----+ + * | | | ^ \ + * | before | | | | + * current terminal content | viewport | | | | + * | | | | + * +----------------+-----+\ | | | s > scroll_above + * ^ | | i | \ | | i | c | + * | | | n | \ | | n | r | + * | | v | \ | | v | o | + * r | | i | \ | | i | l / + * o | viewport | s | >|<- scroll_index | s | l \ + * w | | i | / | | i | | + * s | | b | / | after | b | s > scroll_below + * | | l | / | viewport | l | i | + * v | | e | / | | e | z / + * +----------------+-----+/ | unused | | e + * <- maxcols -> | scroll back | | + * <- cols -> | buffer | | | + * | | | | + * | | | v + * roll_buf + scroll_size->+----------------+-----+ + * <- maxcols -> + * <- cols -> + */ +typedef struct { + Row *lines; /* array of Row pointers of size 'rows' */ + Row *curs_row; /* row on which the cursor currently resides */ + Row *scroll_buf; /* a ring buffer holding the scroll back content */ + Row *scroll_top; /* row in lines where scrolling region starts */ + Row *scroll_bot; /* row in lines where scrolling region ends */ + bool *tabs; /* a boolean flag for each column whether it is a tab */ + int scroll_size; /* maximal capacity of scroll back buffer (in lines) */ + int scroll_index; /* current index into the ring buffer */ + int scroll_above; /* number of lines above current viewport */ + int scroll_below; /* number of lines below current viewport */ + int rows, cols; /* current dimension of buffer */ + int maxcols; /* allocated cells (maximal cols over time) */ + attr_t curattrs, savattrs; /* current and saved attributes for cells */ + int curs_col; /* current cursor column (zero based) */ + int curs_srow, curs_scol; /* saved cursor row/colmn (zero based) */ + short curfg, curbg; /* current fore and background colors */ + short savfg, savbg; /* saved colors */ +} Buffer; + +struct Vt { + Buffer buffer_normal; /* normal screen buffer */ + Buffer buffer_alternate; /* alternate screen buffer */ + Buffer *buffer; /* currently active buffer (one of the above) */ + attr_t defattrs; /* attributes to use for normal/empty cells */ + short deffg, defbg; /* colors to use for back normal/empty cells (white/black) */ + int pty; /* master side pty file descriptor */ + pid_t pid; /* process id of the process running in this vt */ + /* flags */ + unsigned seen_input:1; + unsigned insert:1; + unsigned escaped:1; + unsigned curshid:1; + unsigned curskeymode:1; + unsigned bell:1; + unsigned relposmode:1; + unsigned mousetrack:1; + unsigned graphmode:1; + unsigned savgraphmode:1; + bool charsets[2]; + /* buffers and parsing state */ + char rbuf[BUFSIZ]; + char ebuf[BUFSIZ]; + unsigned int rlen, elen; + int srow, scol; /* last known offset to display start row, start column */ + char title[256]; /* xterm style window title */ + vt_title_handler_t title_handler; /* hook which is called when title changes */ + vt_urgent_handler_t urgent_handler; /* hook which is called upon bell */ + void *data; /* user supplied data */ +}; + +static const char *keytable[KEY_MAX+1] = { + [KEY_ENTER] = "\r", + ['\n'] = "\n", + /* for the arrow keys the CSI / SS3 sequences are not stored here + * because they depend on the current cursor terminal mode + */ + [KEY_UP] = "A", + [KEY_DOWN] = "B", + [KEY_RIGHT] = "C", + [KEY_LEFT] = "D", +#ifdef KEY_SUP + [KEY_SUP] = "\e[1;2A", +#endif +#ifdef KEY_SDOWN + [KEY_SDOWN] = "\e[1;2B", +#endif + [KEY_SRIGHT] = "\e[1;2C", + [KEY_SLEFT] = "\e[1;2D", + [KEY_BACKSPACE] = "\177", + [KEY_IC] = "\e[2~", + [KEY_DC] = "\e[3~", + [KEY_PPAGE] = "\e[5~", + [KEY_NPAGE] = "\e[6~", + [KEY_HOME] = "\e[7~", + [KEY_END] = "\e[8~", + [KEY_BTAB] = "\e[Z", + [KEY_SUSPEND] = "\x1A", /* Ctrl+Z gets mapped to this */ + [KEY_F(1)] = "\e[11~", + [KEY_F(2)] = "\e[12~", + [KEY_F(3)] = "\e[13~", + [KEY_F(4)] = "\e[14~", + [KEY_F(5)] = "\e[15~", + [KEY_F(6)] = "\e[17~", + [KEY_F(7)] = "\e[18~", + [KEY_F(8)] = "\e[19~", + [KEY_F(9)] = "\e[20~", + [KEY_F(10)] = "\e[21~", + [KEY_F(11)] = "\e[23~", + [KEY_F(12)] = "\e[24~", + [KEY_F(13)] = "\e[23~", + [KEY_F(14)] = "\e[24~", + [KEY_F(15)] = "\e[25~", + [KEY_F(16)] = "\e[26~", + [KEY_F(17)] = "\e[28~", + [KEY_F(18)] = "\e[29~", + [KEY_F(19)] = "\e[31~", + [KEY_F(20)] = "\e[32~", + [KEY_F(21)] = "\e[33~", + [KEY_F(22)] = "\e[34~", + [KEY_RESIZE] = "", +#ifdef KEY_EVENT + [KEY_EVENT] = "", +#endif +}; + +static void puttab(Vt *t, int count); +static void process_nonprinting(Vt *t, wchar_t wc); +static void send_curs(Vt *t); + +__attribute__ ((const)) +static attr_t build_attrs(attr_t curattrs) +{ + return ((curattrs & ~A_COLOR) | COLOR_PAIR(curattrs & 0xff)) + >> NCURSES_ATTR_SHIFT; +} + +static void row_set(Row *row, int start, int len, Buffer *t) +{ + Cell cell = { + .text = L'\0', + .attr = t ? build_attrs(t->curattrs) : 0, + .fg = t ? t->curfg : -1, + .bg = t ? t->curbg : -1, + }; + + for (int i = start; i < len + start; i++) + row->cells[i] = cell; + row->dirty = true; +} + +static void row_roll(Row *start, Row *end, int count) +{ + int n = end - start; + + count %= n; + if (count < 0) + count += n; + + if (count) { + char buf[count * sizeof(Row)]; + memcpy(buf, start, count * sizeof(Row)); + memmove(start, start + count, (n - count) * sizeof(Row)); + memcpy(end - count, buf, count * sizeof(Row)); + for (Row *row = start; row < end; row++) + row->dirty = true; + } +} + +static void buffer_clear(Buffer *b) +{ + Cell cell = { + .text = L'\0', + .attr = A_NORMAL, + .fg = -1, + .bg = -1, + }; + + for (int i = 0; i < b->rows; i++) { + Row *row = b->lines + i; + for (int j = 0; j < b->cols; j++) { + row->cells[j] = cell; + row->dirty = true; + } + } +} + +static void buffer_free(Buffer *b) +{ + for (int i = 0; i < b->rows; i++) + free(b->lines[i].cells); + free(b->lines); + for (int i = 0; i < b->scroll_size; i++) + free(b->scroll_buf[i].cells); + free(b->scroll_buf); + free(b->tabs); +} + +static void buffer_scroll(Buffer *b, int s) +{ + /* work in screenfuls */ + int ssz = b->scroll_bot - b->scroll_top; + if (s > ssz) { + buffer_scroll(b, ssz); + buffer_scroll(b, s - ssz); + return; + } + if (s < -ssz) { + buffer_scroll(b, -ssz); + buffer_scroll(b, s + ssz); + return; + } + + b->scroll_above += s; + if (b->scroll_above >= b->scroll_size) + b->scroll_above = b->scroll_size; + + if (s > 0 && b->scroll_size) { + for (int i = 0; i < s; i++) { + Row tmp = b->scroll_top[i]; + b->scroll_top[i] = b->scroll_buf[b->scroll_index]; + b->scroll_buf[b->scroll_index] = tmp; + + b->scroll_index++; + if (b->scroll_index == b->scroll_size) + b->scroll_index = 0; + } + } + row_roll(b->scroll_top, b->scroll_bot, s); + if (s < 0 && b->scroll_size) { + for (int i = (-s) - 1; i >= 0; i--) { + b->scroll_index--; + if (b->scroll_index == -1) + b->scroll_index = b->scroll_size - 1; + + Row tmp = b->scroll_top[i]; + b->scroll_top[i] = b->scroll_buf[b->scroll_index]; + b->scroll_buf[b->scroll_index] = tmp; + b->scroll_top[i].dirty = true; + } + } +} + +static void buffer_resize(Buffer *b, int rows, int cols) +{ + Row *lines = b->lines; + + if (b->rows != rows) { + if (b->curs_row >= lines + rows) { + /* scroll up instead of simply chopping off bottom */ + buffer_scroll(b, (b->curs_row - b->lines) - rows + 1); + } + while (b->rows > rows) { + free(lines[b->rows - 1].cells); + b->rows--; + } + + lines = realloc(lines, sizeof(Row) * rows); + } + + if (b->maxcols < cols) { + for (int row = 0; row < b->rows; row++) { + lines[row].cells = realloc(lines[row].cells, sizeof(Cell) * cols); + if (b->cols < cols) + row_set(lines + row, b->cols, cols - b->cols, NULL); + lines[row].dirty = true; + } + Row *sbuf = b->scroll_buf; + for (int row = 0; row < b->scroll_size; row++) { + sbuf[row].cells = realloc(sbuf[row].cells, sizeof(Cell) * cols); + if (b->cols < cols) + row_set(sbuf + row, b->cols, cols - b->cols, NULL); + } + b->tabs = realloc(b->tabs, sizeof(*b->tabs) * cols); + for (int col = b->cols; col < cols; col++) + b->tabs[col] = !(col & 7); + b->maxcols = cols; + b->cols = cols; + } else if (b->cols != cols) { + for (int row = 0; row < b->rows; row++) + lines[row].dirty = true; + b->cols = cols; + } + + int deltarows = 0; + if (b->rows < rows) { + while (b->rows < rows) { + lines[b->rows].cells = calloc(b->maxcols, sizeof(Cell)); + row_set(lines + b->rows, 0, b->maxcols, b); + b->rows++; + } + + /* prepare for backfill */ + if (b->curs_row >= b->scroll_bot - 1) { + deltarows = b->lines + rows - b->curs_row - 1; + if (deltarows > b->scroll_above) + deltarows = b->scroll_above; + } + } + + b->curs_row += lines - b->lines; + b->scroll_top = lines; + b->scroll_bot = lines + rows; + b->lines = lines; + + /* perform backfill */ + if (deltarows > 0) { + buffer_scroll(b, -deltarows); + b->curs_row += deltarows; + } +} + +static bool buffer_init(Buffer *b, int rows, int cols, int scroll_size) +{ + b->curattrs = A_NORMAL; /* white text over black background */ + b->curfg = b->curbg = -1; + if (scroll_size < 0) + scroll_size = 0; + if (scroll_size && !(b->scroll_buf = calloc(scroll_size, sizeof(Row)))) + return false; + b->scroll_size = scroll_size; + buffer_resize(b, rows, cols); + return true; +} + +static void buffer_boundry(Buffer *b, Row **bs, Row **be, Row **as, Row **ae) { + if (bs) + *bs = NULL; + if (be) + *be = NULL; + if (as) + *as = NULL; + if (ae) + *ae = NULL; + if (!b->scroll_size) + return; + + if (b->scroll_above) { + if (bs) + *bs = &b->scroll_buf[(b->scroll_index - b->scroll_above + b->scroll_size) % b->scroll_size]; + if (be) + *be = &b->scroll_buf[(b->scroll_index-1 + b->scroll_size) % b->scroll_size]; + } + if (b->scroll_below) { + if (as) + *as = &b->scroll_buf[b->scroll_index]; + if (ae) + *ae = &b->scroll_buf[(b->scroll_index + b->scroll_below-1) % b->scroll_size]; + } +} + +static Row *buffer_row_first(Buffer *b) { + Row *bstart; + if (!b->scroll_size || !b->scroll_above) + return b->lines; + buffer_boundry(b, &bstart, NULL, NULL, NULL); + return bstart; +} + +static Row *buffer_row_last(Buffer *b) { + Row *aend; + if (!b->scroll_size || !b->scroll_below) + return b->lines + b->rows - 1; + buffer_boundry(b, NULL, NULL, NULL, &aend); + return aend; +} + +static Row *buffer_row_next(Buffer *b, Row *row) +{ + Row *before_start, *before_end, *after_start, *after_end; + Row *first = b->lines, *last = b->lines + b->rows - 1; + + if (!row) + return NULL; + + buffer_boundry(b, &before_start, &before_end, &after_start, &after_end); + + if (row >= first && row < last) + return ++row; + if (row == last) + return after_start; + if (row == before_end) + return first; + if (row == after_end) + return NULL; + if (row == &b->scroll_buf[b->scroll_size - 1]) + return b->scroll_buf; + return ++row; +} + +static Row *buffer_row_prev(Buffer *b, Row *row) +{ + Row *before_start, *before_end, *after_start, *after_end; + Row *first = b->lines, *last = b->lines + b->rows - 1; + + if (!row) + return NULL; + + buffer_boundry(b, &before_start, &before_end, &after_start, &after_end); + + if (row > first && row <= last) + return --row; + if (row == first) + return before_end; + if (row == before_start) + return NULL; + if (row == after_start) + return last; + if (row == b->scroll_buf) + return &b->scroll_buf[b->scroll_size - 1]; + return --row; +} + +static void cursor_clamp(Vt *t) +{ + Buffer *b = t->buffer; + Row *lines = t->relposmode ? b->scroll_top : b->lines; + int rows = t->relposmode ? b->scroll_bot - b->scroll_top : b->rows; + + if (b->curs_row < lines) + b->curs_row = lines; + if (b->curs_row >= lines + rows) + b->curs_row = lines + rows - 1; + if (b->curs_col < 0) + b->curs_col = 0; + if (b->curs_col >= b->cols) + b->curs_col = b->cols - 1; +} + +static void cursor_line_down(Vt *t) +{ + Buffer *b = t->buffer; + row_set(b->curs_row, b->cols, b->maxcols - b->cols, NULL); + b->curs_row++; + if (b->curs_row < b->scroll_bot) + return; + + vt_noscroll(t); + + b->curs_row = b->scroll_bot - 1; + buffer_scroll(b, 1); + row_set(b->curs_row, 0, b->cols, b); +} + +static void cursor_save(Vt *t) +{ + Buffer *b = t->buffer; + b->curs_srow = b->curs_row - b->lines; + b->curs_scol = b->curs_col; +} + +static void cursor_restore(Vt *t) +{ + Buffer *b = t->buffer; + b->curs_row = b->lines + b->curs_srow; + b->curs_col = b->curs_scol; + cursor_clamp(t); +} + +static void attributes_save(Vt *t) +{ + Buffer *b = t->buffer; + b->savattrs = b->curattrs; + b->savfg = b->curfg; + b->savbg = b->curbg; + t->savgraphmode = t->graphmode; +} + +static void attributes_restore(Vt *t) +{ + Buffer *b = t->buffer; + b->curattrs = b->savattrs; + b->curfg = b->savfg; + b->curbg = b->savbg; + t->graphmode = t->savgraphmode; +} + +static void new_escape_sequence(Vt *t) +{ + t->escaped = true; + t->elen = 0; + t->ebuf[0] = '\0'; +} + +static void cancel_escape_sequence(Vt *t) +{ + t->escaped = false; + t->elen = 0; + t->ebuf[0] = '\0'; +} + +static bool is_valid_csi_ender(int c) +{ + return (c >= 'a' && c <= 'z') + || (c >= 'A' && c <= 'Z') + || (c == '@' || c == '`'); +} + +/* interprets a 'set attribute' (SGR) CSI escape sequence */ +static void interpret_csi_sgr(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + if (pcount == 0) { + /* special case: reset attributes */ + b->curattrs = A_NORMAL; + b->curfg = b->curbg = -1; + return; + } + + for (int i = 0; i < pcount; i++) { + switch (param[i]) { + case 0: + b->curattrs = A_NORMAL; + b->curfg = b->curbg = -1; + break; + case 1: + b->curattrs |= A_BOLD; + break; + case 2: + b->curattrs |= A_DIM; + break; +#ifdef A_ITALIC + case 3: + b->curattrs |= A_ITALIC; + break; +#endif + case 4: + b->curattrs |= A_UNDERLINE; + break; + case 5: + b->curattrs |= A_BLINK; + break; + case 7: + b->curattrs |= A_REVERSE; + break; + case 8: + b->curattrs |= A_INVIS; + break; + case 22: + b->curattrs &= ~(A_BOLD | A_DIM); + break; +#ifdef A_ITALIC + case 23: + b->curattrs &= ~A_ITALIC; + break; +#endif + case 24: + b->curattrs &= ~A_UNDERLINE; + break; + case 25: + b->curattrs &= ~A_BLINK; + break; + case 27: + b->curattrs &= ~A_REVERSE; + break; + case 28: + b->curattrs &= ~A_INVIS; + break; + case 30 ... 37: /* fg */ + b->curfg = param[i] - 30; + break; + case 38: + if ((i + 2) < pcount && param[i + 1] == 5) { + b->curfg = param[i + 2]; + i += 2; + } + break; + case 39: + b->curfg = -1; + break; + case 40 ... 47: /* bg */ + b->curbg = param[i] - 40; + break; + case 48: + if ((i + 2) < pcount && param[i + 1] == 5) { + b->curbg = param[i + 2]; + i += 2; + } + break; + case 49: + b->curbg = -1; + break; + case 90 ... 97: /* hi fg */ + b->curfg = param[i] - 82; + break; + case 100 ... 107: /* hi bg */ + b->curbg = param[i] - 92; + break; + default: + break; + } + } +} + +/* interprets an 'erase display' (ED) escape sequence */ +static void interpret_csi_ed(Vt *t, int param[], int pcount) +{ + Row *row, *start, *end; + Buffer *b = t->buffer; + + attributes_save(t); + b->curattrs = A_NORMAL; + b->curfg = b->curbg = -1; + + if (pcount && param[0] == 2) { + start = b->lines; + end = b->lines + b->rows; + } else if (pcount && param[0] == 1) { + start = b->lines; + end = b->curs_row; + row_set(b->curs_row, 0, b->curs_col + 1, b); + } else { + row_set(b->curs_row, b->curs_col, b->cols - b->curs_col, b); + start = b->curs_row + 1; + end = b->lines + b->rows; + } + + for (row = start; row < end; row++) + row_set(row, 0, b->cols, b); + + attributes_restore(t); +} + +/* interprets a 'move cursor' (CUP) escape sequence */ +static void interpret_csi_cup(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + Row *lines = t->relposmode ? b->scroll_top : b->lines; + + if (pcount == 0) { + b->curs_row = lines; + b->curs_col = 0; + } else if (pcount == 1) { + b->curs_row = lines + param[0] - 1; + b->curs_col = 0; + } else { + b->curs_row = lines + param[0] - 1; + b->curs_col = param[1] - 1; + } + + cursor_clamp(t); +} + +/* Interpret the 'relative mode' sequences: CUU, CUD, CUF, CUB, CNL, + * CPL, CHA, HPR, VPA, VPR, HPA */ +static void interpret_csi_c(Vt *t, char verb, int param[], int pcount) +{ + Buffer *b = t->buffer; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + switch (verb) { + case 'A': + b->curs_row -= n; + break; + case 'B': + case 'e': + b->curs_row += n; + break; + case 'C': + case 'a': + b->curs_col += n; + break; + case 'D': + b->curs_col -= n; + break; + case 'E': + b->curs_row += n; + b->curs_col = 0; + break; + case 'F': + b->curs_row -= n; + b->curs_col = 0; + break; + case 'G': + case '`': + b->curs_col = n - 1; + break; + case 'd': + b->curs_row = b->lines + n - 1; + break; + } + + cursor_clamp(t); +} + +/* Interpret the 'erase line' escape sequence */ +static void interpret_csi_el(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + switch (pcount ? param[0] : 0) { + case 1: + row_set(b->curs_row, 0, b->curs_col + 1, b); + break; + case 2: + row_set(b->curs_row, 0, b->cols, b); + break; + default: + row_set(b->curs_row, b->curs_col, b->cols - b->curs_col, b); + break; + } +} + +/* Interpret the 'insert blanks' sequence (ICH) */ +static void interpret_csi_ich(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + Row *row = b->curs_row; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + if (b->curs_col + n > b->cols) + n = b->cols - b->curs_col; + + for (int i = b->cols - 1; i >= b->curs_col + n; i--) + row->cells[i] = row->cells[i - n]; + + row_set(row, b->curs_col, n, b); +} + +/* Interpret the 'delete chars' sequence (DCH) */ +static void interpret_csi_dch(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + Row *row = b->curs_row; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + if (b->curs_col + n > b->cols) + n = b->cols - b->curs_col; + + for (int i = b->curs_col; i < b->cols - n; i++) + row->cells[i] = row->cells[i + n]; + + row_set(row, b->cols - n, n, b); +} + +/* Interpret an 'insert line' sequence (IL) */ +static void interpret_csi_il(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + if (b->curs_row + n >= b->scroll_bot) { + for (Row *row = b->curs_row; row < b->scroll_bot; row++) + row_set(row, 0, b->cols, b); + } else { + row_roll(b->curs_row, b->scroll_bot, -n); + for (Row *row = b->curs_row; row < b->curs_row + n; row++) + row_set(row, 0, b->cols, b); + } +} + +/* Interpret a 'delete line' sequence (DL) */ +static void interpret_csi_dl(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + if (b->curs_row + n >= b->scroll_bot) { + for (Row *row = b->curs_row; row < b->scroll_bot; row++) + row_set(row, 0, b->cols, b); + } else { + row_roll(b->curs_row, b->scroll_bot, n); + for (Row *row = b->scroll_bot - n; row < b->scroll_bot; row++) + row_set(row, 0, b->cols, b); + } +} + +/* Interpret an 'erase characters' (ECH) sequence */ +static void interpret_csi_ech(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + int n = (pcount && param[0] > 0) ? param[0] : 1; + + if (b->curs_col + n > b->cols) + n = b->cols - b->curs_col; + + row_set(b->curs_row, b->curs_col, n, b); +} + +/* Interpret a 'set scrolling region' (DECSTBM) sequence */ +static void interpret_csi_decstbm(Vt *t, int param[], int pcount) +{ + Buffer *b = t->buffer; + int new_top, new_bot; + + switch (pcount) { + case 0: + b->scroll_top = b->lines; + b->scroll_bot = b->lines + b->rows; + break; + case 2: + new_top = param[0] - 1; + new_bot = param[1]; + + /* clamp to bounds */ + if (new_top < 0) + new_top = 0; + if (new_top >= b->rows) + new_top = b->rows - 1; + if (new_bot < 0) + new_bot = 0; + if (new_bot >= b->rows) + new_bot = b->rows; + + /* check for range validity */ + if (new_top < new_bot) { + b->scroll_top = b->lines + new_top; + b->scroll_bot = b->lines + new_bot; + } + break; + default: + return; /* malformed */ + } + b->curs_row = b->scroll_top; + b->curs_col = 0; +} + +static void interpret_csi_mode(Vt *t, int param[], int pcount, bool set) +{ + for (int i = 0; i < pcount; i++) { + switch (param[i]) { + case 4: /* insert/replace mode */ + t->insert = set; + break; + } + } +} + +static void interpret_csi_priv_mode(Vt *t, int param[], int pcount, bool set) +{ + for (int i = 0; i < pcount; i++) { + switch (param[i]) { + case 1: /* set application/normal cursor key mode (DECCKM) */ + t->curskeymode = set; + break; + case 6: /* set origin to relative/absolute (DECOM) */ + t->relposmode = set; + break; + case 25: /* make cursor visible/invisible (DECCM) */ + t->curshid = !set; + break; + case 1049: /* combine 1047 + 1048 */ + case 47: /* use alternate/normal screen buffer */ + case 1047: + if (!set) + buffer_clear(&t->buffer_alternate); + t->buffer = set ? &t->buffer_alternate : &t->buffer_normal; + vt_dirty(t); + if (param[i] != 1049) + break; + /* fall through */ + case 1048: /* save/restore cursor */ + if (set) + cursor_save(t); + else + cursor_restore(t); + break; + case 1000: /* enable/disable normal mouse tracking */ + t->mousetrack = set; + break; + } + } +} + +static void interpret_csi(Vt *t) +{ + Buffer *b = t->buffer; + int csiparam[16]; + unsigned int param_count = 0; + const char *p = t->ebuf + 1; + char verb = t->ebuf[t->elen - 1]; + + /* parse numeric parameters */ + for (p += (t->ebuf[1] == '?'); *p; p++) { + if (IS_CONTROL(*p)) { + process_nonprinting(t, *p); + } else if (*p == ';') { + if (param_count >= LENGTH(csiparam)) + return; /* too long! */ + csiparam[param_count++] = 0; + } else if (isdigit((unsigned char)*p)) { + if (param_count == 0) + csiparam[param_count++] = 0; + csiparam[param_count - 1] *= 10; + csiparam[param_count - 1] += *p - '0'; + } + } + + if (t->ebuf[1] == '?') { + switch (verb) { + case 'h': + case 'l': /* private set/reset mode */ + interpret_csi_priv_mode(t, csiparam, param_count, verb == 'h'); + break; + } + return; + } + + /* delegate handling depending on command character (verb) */ + switch (verb) { + case 'h': + case 'l': /* set/reset mode */ + interpret_csi_mode(t, csiparam, param_count, verb == 'h'); + break; + case 'm': /* set attribute */ + interpret_csi_sgr(t, csiparam, param_count); + break; + case 'J': /* erase display */ + interpret_csi_ed(t, csiparam, param_count); + break; + case 'H': + case 'f': /* move cursor */ + interpret_csi_cup(t, csiparam, param_count); + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'e': + case 'a': + case 'd': + case '`': /* relative move */ + interpret_csi_c(t, verb, csiparam, param_count); + break; + case 'K': /* erase line */ + interpret_csi_el(t, csiparam, param_count); + break; + case '@': /* insert characters */ + interpret_csi_ich(t, csiparam, param_count); + break; + case 'P': /* delete characters */ + interpret_csi_dch(t, csiparam, param_count); + break; + case 'L': /* insert lines */ + interpret_csi_il(t, csiparam, param_count); + break; + case 'M': /* delete lines */ + interpret_csi_dl(t, csiparam, param_count); + break; + case 'X': /* erase chars */ + interpret_csi_ech(t, csiparam, param_count); + break; + case 'S': /* SU: scroll up */ + vt_scroll(t, param_count ? -csiparam[0] : -1); + break; + case 'T': /* SD: scroll down */ + vt_scroll(t, param_count ? csiparam[0] : 1); + break; + case 'Z': /* CBT: cursor backward tabulation */ + puttab(t, param_count ? -csiparam[0] : -1); + break; + case 'g': /* TBC: tabulation clear */ + switch (param_count ? csiparam[0] : 0) { + case 0: + b->tabs[b->curs_col] = false; + break; + case 3: + memset(b->tabs, 0, sizeof(*b->tabs) * b->maxcols); + break; + } + break; + case 'r': /* set scrolling region */ + interpret_csi_decstbm(t, csiparam, param_count); + break; + case 's': /* save cursor location */ + cursor_save(t); + break; + case 'u': /* restore cursor location */ + cursor_restore(t); + break; + case 'n': /* query cursor location */ + if (param_count == 1 && csiparam[0] == 6) + send_curs(t); + break; + default: + break; + } +} + +/* Interpret an 'index' (IND) sequence */ +static void interpret_csi_ind(Vt *t) +{ + Buffer *b = t->buffer; + if (b->curs_row < b->lines + b->rows - 1) + b->curs_row++; +} + +/* Interpret a 'reverse index' (RI) sequence */ +static void interpret_csi_ri(Vt *t) +{ + Buffer *b = t->buffer; + if (b->curs_row > b->scroll_top) + b->curs_row--; + else { + row_roll(b->scroll_top, b->scroll_bot, -1); + row_set(b->scroll_top, 0, b->cols, b); + } +} + +/* Interpret a 'next line' (NEL) sequence */ +static void interpret_csi_nel(Vt *t) +{ + Buffer *b = t->buffer; + if (b->curs_row < b->lines + b->rows - 1) { + b->curs_row++; + b->curs_col = 0; + } +} + +/* Interpret a 'select character set' (SCS) sequence */ +static void interpret_csi_scs(Vt *t) +{ + /* ESC ( sets G0, ESC ) sets G1 */ + t->charsets[!!(t->ebuf[0] == ')')] = (t->ebuf[1] == '0'); + t->graphmode = t->charsets[0]; +} + +/* Interpret an 'operating system command' (OSC) sequence */ +static void interpret_osc(Vt *t) +{ + /* ESC ] command ; data BEL + * ESC ] command ; data ESC \\ + * Note that BEL or ESC \\ have already been replaced with NUL. + */ + char *data = NULL; + int command = strtoul(t->ebuf + 1, &data, 10); + if (data && *data == ';') { + switch (command) { + case 0: /* icon name and window title */ + case 2: /* window title */ + if (t->title_handler) + t->title_handler(t, data+1); + break; + case 1: /* icon name */ + break; + default: +#ifndef NDEBUG + fprintf(stderr, "unknown OSC command: %d\n", command); +#endif + break; + } + } +} + +static void try_interpret_escape_seq(Vt *t) +{ + char lastchar = t->ebuf[t->elen - 1]; + + if (!*t->ebuf) + return; + + switch (*t->ebuf) { + case '#': /* ignore DECDHL, DECSWL, DECDWL, DECHCP, DECFPP */ + if (t->elen == 2) { + if (lastchar == '8') { /* DECALN */ + interpret_csi_ed(t, (int []){ 2 }, 1); + goto handled; + } + goto cancel; + } + break; + case '(': + case ')': + if (t->elen == 2) { + interpret_csi_scs(t); + goto handled; + } + break; + case ']': /* OSC - operating system command */ + if (lastchar == '\a' || + (lastchar == '\\' && t->elen >= 2 && t->ebuf[t->elen - 2] == '\e')) { + t->elen -= lastchar == '\a' ? 1 : 2; + t->ebuf[t->elen] = '\0'; + interpret_osc(t); + goto handled; + } + break; + case '[': /* CSI - control sequence introducer */ + if (is_valid_csi_ender(lastchar)) { + interpret_csi(t); + goto handled; + } + break; + case '7': /* DECSC: save cursor and attributes */ + attributes_save(t); + cursor_save(t); + goto handled; + case '8': /* DECRC: restore cursor and attributes */ + attributes_restore(t); + cursor_restore(t); + goto handled; + case 'D': /* IND: index */ + interpret_csi_ind(t); + goto handled; + case 'M': /* RI: reverse index */ + interpret_csi_ri(t); + goto handled; + case 'E': /* NEL: next line */ + interpret_csi_nel(t); + goto handled; + case 'H': /* HTS: horizontal tab set */ + t->buffer->tabs[t->buffer->curs_col] = true; + goto handled; + default: + goto cancel; + } + + if (t->elen + 1 >= sizeof(t->ebuf)) { +cancel: +#ifndef NDEBUG + fprintf(stderr, "cancelled: \\033"); + for (unsigned int i = 0; i < t->elen; i++) { + if (isprint(t->ebuf[i])) { + fputc(t->ebuf[i], stderr); + } else { + fprintf(stderr, "\\%03o", t->ebuf[i]); + } + } + fputc('\n', stderr); +#endif +handled: + cancel_escape_sequence(t); + } +} + +static void puttab(Vt *t, int count) +{ + Buffer *b = t->buffer; + int direction = count >= 0 ? 1 : -1; + for (int col = b->curs_col + direction; count; col += direction) { + if (col < 0) { + b->curs_col = 0; + break; + } + if (col >= b->cols) { + b->curs_col = b->cols - 1; + break; + } + if (b->tabs[col]) { + b->curs_col = col; + count -= direction; + } + } +} + +static void process_nonprinting(Vt *t, wchar_t wc) +{ + Buffer *b = t->buffer; + switch (wc) { + case '\e': /* ESC */ + new_escape_sequence(t); + break; + case '\a': /* BEL */ + if (t->urgent_handler) + t->urgent_handler(t); + break; + case '\b': /* BS */ + if (b->curs_col > 0) + b->curs_col--; + break; + case '\t': /* HT */ + puttab(t, 1); + break; + case '\r': /* CR */ + b->curs_col = 0; + break; + case '\v': /* VT */ + case '\f': /* FF */ + case '\n': /* LF */ + cursor_line_down(t); + break; + case '\016': /* SO: shift out, invoke the G1 character set */ + t->graphmode = t->charsets[1]; + break; + case '\017': /* SI: shift in, invoke the G0 character set */ + t->graphmode = t->charsets[0]; + break; + } +} + +static void is_utf8_locale(void) +{ + const char *cset = nl_langinfo(CODESET); + if (!cset) + cset = "ANSI_X3.4-1968"; + is_utf8 = !strcmp(cset, "UTF-8"); +} + +static wchar_t get_vt100_graphic(char c) +{ + static char vt100_acs[] = "`afgjklmnopqrstuvwxyz{|}~"; + + /* + * 5f-7e standard vt100 + * 40-5e rxvt extension for extra curses acs chars + */ + static uint16_t const vt100_utf8[62] = { + 0x2191, 0x2193, 0x2192, 0x2190, 0x2588, 0x259a, 0x2603, // 41-47 + 0, 0, 0, 0, 0, 0, 0, 0, // 48-4f + 0, 0, 0, 0, 0, 0, 0, 0, // 50-57 + 0, 0, 0, 0, 0, 0, 0, 0x0020, // 58-5f + 0x25c6, 0x2592, 0x2409, 0x240c, 0x240d, 0x240a, 0x00b0, 0x00b1, // 60-67 + 0x2424, 0x240b, 0x2518, 0x2510, 0x250c, 0x2514, 0x253c, 0x23ba, // 68-6f + 0x23bb, 0x2500, 0x23bc, 0x23bd, 0x251c, 0x2524, 0x2534, 0x252c, // 70-77 + 0x2502, 0x2264, 0x2265, 0x03c0, 0x2260, 0x00a3, 0x00b7, // 78-7e + }; + + if (is_utf8) + return vt100_utf8[c - 0x41]; + else if (strchr(vt100_acs, c)) + return NCURSES_ACS(c); + return '\0'; +} + +static void put_wc(Vt *t, wchar_t wc) +{ + int width = 0; + + if (!t->seen_input) { + t->seen_input = 1; + kill(-t->pid, SIGWINCH); + } + + if (t->escaped) { + if (t->elen + 1 < sizeof(t->ebuf)) { + t->ebuf[t->elen] = wc; + t->ebuf[++t->elen] = '\0'; + try_interpret_escape_seq(t); + } else { + cancel_escape_sequence(t); + } + } else if (IS_CONTROL(wc)) { + process_nonprinting(t, wc); + } else { + if (t->graphmode) { + if (wc >= 0x41 && wc <= 0x7e) { + wchar_t gc = get_vt100_graphic(wc); + if (gc) + wc = gc; + } + width = 1; + } else if ((width = wcwidth(wc)) < 1) { + width = 1; + } + Buffer *b = t->buffer; + Cell blank_cell = { L'\0', build_attrs(b->curattrs), b->curfg, b->curbg }; + if (width == 2 && b->curs_col == b->cols - 1) { + b->curs_row->cells[b->curs_col++] = blank_cell; + b->curs_row->dirty = true; + } + + if (b->curs_col >= b->cols) { + b->curs_col = 0; + cursor_line_down(t); + } + + if (t->insert) { + Cell *src = b->curs_row->cells + b->curs_col; + Cell *dest = src + width; + size_t len = b->cols - b->curs_col - width; + memmove(dest, src, len * sizeof *dest); + } + + b->curs_row->cells[b->curs_col] = blank_cell; + b->curs_row->cells[b->curs_col++].text = wc; + b->curs_row->dirty = true; + if (width == 2) + b->curs_row->cells[b->curs_col++] = blank_cell; + } +} + +int vt_process(Vt *t) +{ + int res; + unsigned int pos = 0; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + if (t->pty < 0) { + errno = EINVAL; + return -1; + } + + res = read(t->pty, t->rbuf + t->rlen, sizeof(t->rbuf) - t->rlen); + if (res < 0) + return -1; + + t->rlen += res; + while (pos < t->rlen) { + wchar_t wc; + ssize_t len; + + len = (ssize_t)mbrtowc(&wc, t->rbuf + pos, t->rlen - pos, &ps); + if (len == -2) { + t->rlen -= pos; + memmove(t->rbuf, t->rbuf + pos, t->rlen); + return 0; + } + + if (len == -1) { + len = 1; + wc = t->rbuf[pos]; + } + + pos += len ? len : 1; + put_wc(t, wc); + } + + t->rlen -= pos; + memmove(t->rbuf, t->rbuf + pos, t->rlen); + return 0; +} + +void vt_default_colors_set(Vt *t, attr_t attrs, short fg, short bg) +{ + t->defattrs = attrs; + t->deffg = fg; + t->defbg = bg; +} + +Vt *vt_create(int rows, int cols, int scroll_size) +{ + if (rows <= 0 || cols <= 0) + return NULL; + + Vt *t = calloc(1, sizeof(Vt)); + if (!t) + return NULL; + + t->pty = -1; + t->deffg = t->defbg = -1; + t->buffer = &t->buffer_normal; + + if (!buffer_init(&t->buffer_normal, rows, cols, scroll_size) || + !buffer_init(&t->buffer_alternate, rows, cols, 0)) { + free(t); + return NULL; + } + + return t; +} + +void vt_resize(Vt *t, int rows, int cols) +{ + struct winsize ws = { .ws_row = rows, .ws_col = cols }; + + if (rows <= 0 || cols <= 0) + return; + + vt_noscroll(t); + buffer_resize(&t->buffer_normal, rows, cols); + buffer_resize(&t->buffer_alternate, rows, cols); + cursor_clamp(t); + ioctl(t->pty, TIOCSWINSZ, &ws); + kill(-t->pid, SIGWINCH); +} + +void vt_destroy(Vt *t) +{ + if (!t) + return; + buffer_free(&t->buffer_normal); + buffer_free(&t->buffer_alternate); + close(t->pty); + free(t); +} + +void vt_dirty(Vt *t) +{ + Buffer *b = t->buffer; + for (Row *row = b->lines, *end = row + b->rows; row < end; row++) + row->dirty = true; +} + +void vt_draw(Vt *t, WINDOW *win, int srow, int scol) +{ + Buffer *b = t->buffer; + + if (srow != t->srow || scol != t->scol) { + vt_dirty(t); + t->srow = srow; + t->scol = scol; + } + + for (int i = 0; i < b->rows; i++) { + Row *row = b->lines + i; + + if (!row->dirty) + continue; + + wmove(win, srow + i, scol); + Cell *cell = NULL; + for (int j = 0; j < b->cols; j++) { + Cell *prev_cell = cell; + cell = row->cells + j; + if (!prev_cell || cell->attr != prev_cell->attr + || cell->fg != prev_cell->fg + || cell->bg != prev_cell->bg) { + if (cell->attr == A_NORMAL) + cell->attr = t->defattrs; + if (cell->fg == -1) + cell->fg = t->deffg; + if (cell->bg == -1) + cell->bg = t->defbg; + wattrset(win, cell->attr << NCURSES_ATTR_SHIFT); + wcolor_set(win, vt_color_get(t, cell->fg, cell->bg), NULL); + } + + if (is_utf8 && cell->text >= 128) { + char buf[MB_CUR_MAX + 1]; + size_t len = wcrtomb(buf, cell->text, NULL); + if (len > 0) { + waddnstr(win, buf, len); + if (wcwidth(cell->text) > 1) + j++; + } + } else { + waddch(win, cell->text > ' ' ? cell->text : ' '); + } + } + + int x, y; + getyx(win, y, x); + (void)y; + if (x && x < b->cols - 1) + whline(win, ' ', b->cols - x); + + row->dirty = false; + } + + wmove(win, srow + b->curs_row - b->lines, scol + b->curs_col); +} + +void vt_scroll(Vt *t, int rows) +{ + Buffer *b = t->buffer; + if (!b->scroll_size) + return; + if (rows < 0) { /* scroll back */ + if (rows < -b->scroll_above) + rows = -b->scroll_above; + } else { /* scroll forward */ + if (rows > b->scroll_below) + rows = b->scroll_below; + } + buffer_scroll(b, rows); + b->scroll_below -= rows; +} + +void vt_noscroll(Vt *t) +{ + int scroll_below = t->buffer->scroll_below; + if (scroll_below) + vt_scroll(t, scroll_below); +} + +pid_t vt_forkpty(Vt *t, const char *p, const char *argv[], const char *cwd, const char *env[], int *to, int *from) +{ + int vt2ed[2], ed2vt[2]; + struct winsize ws; + ws.ws_row = t->buffer->rows; + ws.ws_col = t->buffer->cols; + ws.ws_xpixel = ws.ws_ypixel = 0; + + if (to && pipe(vt2ed)) { + *to = -1; + to = NULL; + } + if (from && pipe(ed2vt)) { + *from = -1; + from = NULL; + } + + pid_t pid = forkpty(&t->pty, NULL, NULL, &ws); + if (pid < 0) + return -1; + + if (pid == 0) { + setsid(); + + sigset_t emptyset; + sigemptyset(&emptyset); + sigprocmask(SIG_SETMASK, &emptyset, NULL); + + if (to) { + close(vt2ed[1]); + dup2(vt2ed[0], STDIN_FILENO); + close(vt2ed[0]); + } + + if (from) { + close(ed2vt[0]); + dup2(ed2vt[1], STDOUT_FILENO); + close(ed2vt[1]); + } + + int maxfd = sysconf(_SC_OPEN_MAX); + for (int fd = 3; fd < maxfd; fd++) + if (close(fd) == -1 && errno == EBADF) + break; + + for (const char **envp = env; envp && envp[0]; envp += 2) + setenv(envp[0], envp[1], 1); + setenv("TERM", vt_term, 1); + + if (cwd) { + int err = chdir(cwd); + if (err) { + fprintf(stderr, "\nchdir() failed. "); + perror(cwd); + exit(1); + } + } + + struct sigaction sa; + memset(&sa, 0, sizeof sa); + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sa.sa_handler = SIG_DFL; + sigaction(SIGPIPE, &sa, NULL); + + execvp(p, (char *const *)argv); + fprintf(stderr, "\nexecv() failed.\nCommand: '%s'\n", argv[0]); + exit(1); + } + + if (to) { + close(vt2ed[0]); + *to = vt2ed[1]; + } + + if (from) { + close(ed2vt[1]); + *from = ed2vt[0]; + } + + return t->pid = pid; +} + +int vt_pty_get(Vt *t) +{ + return t->pty; +} + +ssize_t vt_write(Vt *t, const char *buf, size_t len) +{ + ssize_t ret = len; + + while (len > 0) { + ssize_t res = write(t->pty, buf, len); + if (res < 0) { + if (errno != EAGAIN && errno != EINTR) + return -1; + continue; + } + buf += res; + len -= res; + } + + return ret; +} + +static void send_curs(Vt *t) +{ + Buffer *b = t->buffer; + char keyseq[16]; + snprintf(keyseq, sizeof keyseq, "\e[%d;%dR", (int)(b->curs_row - b->lines), b->curs_col); + vt_write(t, keyseq, strlen(keyseq)); +} + +void vt_keypress(Vt *t, int keycode) +{ + vt_noscroll(t); + + if (keycode >= 0 && keycode <= KEY_MAX && keytable[keycode]) { + switch (keycode) { + case KEY_UP: + case KEY_DOWN: + case KEY_RIGHT: + case KEY_LEFT: { + char keyseq[3] = { '\e', (t->curskeymode ? 'O' : '['), keytable[keycode][0] }; + vt_write(t, keyseq, sizeof keyseq); + break; + } + default: + vt_write(t, keytable[keycode], strlen(keytable[keycode])); + } + } else if (keycode <= UCHAR_MAX) { + char c = keycode; + vt_write(t, &c, 1); + } else { +#ifndef NDEBUG + fprintf(stderr, "unhandled key %#o\n", keycode); +#endif + } +} + +void vt_mouse(Vt *t, int x, int y, mmask_t mask) +{ +#ifdef NCURSES_MOUSE_VERSION + char seq[6] = { '\e', '[', 'M' }, state = 0, button = 0; + + if (!t->mousetrack) + return; + + if (mask & (BUTTON1_PRESSED | BUTTON1_CLICKED)) + button = 0; + else if (mask & (BUTTON2_PRESSED | BUTTON2_CLICKED)) + button = 1; + else if (mask & (BUTTON3_PRESSED | BUTTON3_CLICKED)) + button = 2; + else if (mask & (BUTTON1_RELEASED | BUTTON2_RELEASED | BUTTON3_RELEASED)) + button = 3; + + if (mask & BUTTON_SHIFT) + state |= 4; + if (mask & BUTTON_ALT) + state |= 8; + if (mask & BUTTON_CTRL) + state |= 16; + + seq[3] = 32 + button + state; + seq[4] = 32 + x; + seq[5] = 32 + y; + + vt_write(t, seq, sizeof seq); + + if (mask & (BUTTON1_CLICKED | BUTTON2_CLICKED | BUTTON3_CLICKED)) { + /* send a button release event */ + button = 3; + seq[3] = 32 + button + state; + vt_write(t, seq, sizeof seq); + } +#endif /* NCURSES_MOUSE_VERSION */ +} + +static unsigned int color_hash(short fg, short bg) +{ + if (fg == -1) + fg = COLORS; + if (bg == -1) + bg = COLORS + 1; + return fg * (COLORS + 2) + bg; +} + +short vt_color_get(Vt *t, short fg, short bg) +{ + if (fg >= COLORS) + fg = (t ? t->deffg : default_fg); + if (bg >= COLORS) + bg = (t ? t->defbg : default_bg); + + if (!has_default_colors) { + if (fg == -1) + fg = (t && t->deffg != -1 ? t->deffg : default_fg); + if (bg == -1) + bg = (t && t->defbg != -1 ? t->defbg : default_bg); + } + + if (!color2palette || (fg == -1 && bg == -1)) + return 0; + unsigned int index = color_hash(fg, bg); + if (color2palette[index] == 0) { + short oldfg, oldbg; + for (;;) { + if (++color_pair_current >= color_pairs_max) + color_pair_current = color_pairs_reserved + 1; + pair_content(color_pair_current, &oldfg, &oldbg); + unsigned int old_index = color_hash(oldfg, oldbg); + if (color2palette[old_index] >= 0) { + if (init_pair(color_pair_current, fg, bg) == OK) { + color2palette[old_index] = 0; + color2palette[index] = color_pair_current; + } + break; + } + } + } + + short color_pair = color2palette[index]; + return color_pair >= 0 ? color_pair : -color_pair; +} + +short vt_color_reserve(short fg, short bg) +{ + if (!color2palette || fg >= COLORS || bg >= COLORS) + return 0; + if (!has_default_colors && fg == -1) + fg = default_fg; + if (!has_default_colors && bg == -1) + bg = default_bg; + if (fg == -1 && bg == -1) + return 0; + unsigned int index = color_hash(fg, bg); + if (color2palette[index] >= 0) { + if (init_pair(color_pairs_reserved + 1, fg, bg) == OK) + color2palette[index] = -(++color_pairs_reserved); + } + short color_pair = color2palette[index]; + return color_pair >= 0 ? color_pair : -color_pair; +} + +static void init_colors(void) +{ + pair_content(0, &default_fg, &default_bg); + if (default_fg == -1) + default_fg = COLOR_WHITE; + if (default_bg == -1) + default_bg = COLOR_BLACK; + has_default_colors = (use_default_colors() == OK); + color_pairs_max = MIN(MAX_COLOR_PAIRS, SHRT_MAX); + if (COLORS) + color2palette = calloc((COLORS + 2) * (COLORS + 2), sizeof(short)); + /* + * XXX: On undefined color-pairs NetBSD curses pair_content() set fg + * and bg to default colors while ncurses set them respectively to + * 0 and 0. Initialize all color-pairs in order to have consistent + * behaviour despite the implementation used. + */ + for (short i = 1; i < color_pairs_max; i++) + init_pair(i, 0, 0); + vt_color_reserve(COLOR_WHITE, COLOR_BLACK); +} + +void vt_init(void) +{ + init_colors(); + is_utf8_locale(); + char *term = getenv("DVTM_TERM"); + if (!term) + term = "dvtm"; + snprintf(vt_term, sizeof vt_term, "%s%s", term, COLORS >= 256 ? "-256color" : ""); +} + +void vt_keytable_set(const char * const keytable_overlay[], int count) +{ + for (int k = 0; k < count && k < KEY_MAX; k++) { + const char *keyseq = keytable_overlay[k]; + if (keyseq) + keytable[k] = keyseq; + } +} + +void vt_shutdown(void) +{ + free(color2palette); +} + +void vt_title_handler_set(Vt *t, vt_title_handler_t handler) +{ + t->title_handler = handler; +} + +void vt_urgent_handler_set(Vt *t, vt_urgent_handler_t handler) +{ + t->urgent_handler = handler; +} + +void vt_data_set(Vt *t, void *data) +{ + t->data = data; +} + +void *vt_data_get(Vt *t) +{ + return t->data; +} + +bool vt_cursor_visible(Vt *t) +{ + return t->buffer->scroll_below ? false : !t->curshid; +} + +pid_t vt_pid_get(Vt *t) +{ + return t->pid; +} + +size_t vt_content_get(Vt *t, char **buf, bool colored) +{ + Buffer *b = t->buffer; + int lines = b->scroll_above + b->scroll_below + b->rows + 1; + size_t size = lines * ((b->cols + 1) * ((colored ? 64 : 0) + MB_CUR_MAX)); + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + + if (!(*buf = malloc(size))) + return 0; + + char *s = *buf; + Cell *prev_cell = NULL; + + for (Row *row = buffer_row_first(b); row; row = buffer_row_next(b, row)) { + size_t len = 0; + char *last_non_space = s; + for (int col = 0; col < b->cols; col++) { + Cell *cell = row->cells + col; + if (colored) { + int esclen = 0; + if (!prev_cell || cell->attr != prev_cell->attr) { + attr_t attr = cell->attr << NCURSES_ATTR_SHIFT; + esclen = sprintf(s, "\033[0%s%s%s%s%s%sm", + attr & A_BOLD ? ";1" : "", + attr & A_DIM ? ";2" : "", + attr & A_UNDERLINE ? ";4" : "", + attr & A_BLINK ? ";5" : "", + attr & A_REVERSE ? ";7" : "", + attr & A_INVIS ? ";8" : ""); + if (esclen > 0) + s += esclen; + } + if (!prev_cell || cell->fg != prev_cell->fg || cell->attr != prev_cell->attr) { + if (cell->fg == -1) + esclen = sprintf(s, "\033[39m"); + else + esclen = sprintf(s, "\033[38;5;%dm", cell->fg); + if (esclen > 0) + s += esclen; + } + if (!prev_cell || cell->bg != prev_cell->bg || cell->attr != prev_cell->attr) { + if (cell->bg == -1) + esclen = sprintf(s, "\033[49m"); + else + esclen = sprintf(s, "\033[48;5;%dm", cell->bg); + if (esclen > 0) + s += esclen; + } + prev_cell = cell; + } + if (cell->text) { + len = wcrtomb(s, cell->text, &ps); + if (len > 0) + s += len; + last_non_space = s; + } else if (len) { + len = 0; + } else { + *s++ = ' '; + } + } + + s = last_non_space; + *s++ = '\n'; + } + + return s - *buf; +} + +int vt_content_start(Vt *t) +{ + return t->buffer->scroll_above; +} diff --git a/vt.h b/vt.h @@ -0,0 +1,66 @@ +/* + * Copyright © 2004 Bruno T. C. de Oliveira + * Copyright © 2006 Pierre Habouzit + * Copyright © 2008-2013 Marc André Tanner + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ +#ifndef VT_H +#define VT_H + +#include <curses.h> +#include <stdbool.h> +#include <sys/types.h> + +#ifndef NCURSES_MOUSE_VERSION +#define mmask_t unsigned long +#endif + +typedef struct Vt Vt; +typedef void (*vt_title_handler_t)(Vt*, const char *title); +typedef void (*vt_urgent_handler_t)(Vt*); + +void vt_init(void); +void vt_shutdown(void); + +void vt_keytable_set(char const * const keytable_overlay[], int count); +void vt_default_colors_set(Vt*, attr_t attrs, short fg, short bg); +void vt_title_handler_set(Vt*, vt_title_handler_t); +void vt_urgent_handler_set(Vt*, vt_urgent_handler_t); +void vt_data_set(Vt*, void *); +void *vt_data_get(Vt*); + +Vt *vt_create(int rows, int cols, int scroll_buf_sz); +void vt_resize(Vt*, int rows, int cols); +void vt_destroy(Vt*); +pid_t vt_forkpty(Vt*, const char *p, const char *argv[], const char *cwd, const char *env[], int *to, int *from); +int vt_pty_get(Vt*); +bool vt_cursor_visible(Vt*); + +int vt_process(Vt *); +void vt_keypress(Vt *, int keycode); +ssize_t vt_write(Vt*, const char *buf, size_t len); +void vt_mouse(Vt*, int x, int y, mmask_t mask); +void vt_dirty(Vt*); +void vt_draw(Vt*, WINDOW *win, int startrow, int startcol); +short vt_color_get(Vt*, short fg, short bg); +short vt_color_reserve(short fg, short bg); + +void vt_scroll(Vt*, int rows); +void vt_noscroll(Vt*); + +pid_t vt_pid_get(Vt*); +size_t vt_content_get(Vt*, char **s, bool colored); +int vt_content_start(Vt*); + +#endif /* VT_H */