1
0
Fork 0
forked from entailz/toes

tabs and tab overview

This commit is contained in:
entailz 2026-05-12 23:33:02 -07:00
commit d07c2a5cc9
244 changed files with 72046 additions and 0 deletions

View file

@ -0,0 +1,55 @@
image: alpine/edge
packages:
- musl-dev
- eudev-libs
- eudev-dev
- linux-headers
- meson
- ninja
- gcc
- scdoc
- wayland-dev
- wayland-protocols
- freetype-dev
- fontconfig-dev
- harfbuzz-dev
- utf8proc-dev
- pixman-dev
- libxkbcommon-dev
- ncurses
- python3
- py3-pip
- check-dev
- ttf-hack
- font-noto-emoji
sources:
- https://git.sr.ht/~dnkl/foot
# triggers:
# - action: email
# condition: failure
# to: <committer>
tasks:
- fcft: |
cd foot/subprojects
git clone https://codeberg.org/dnkl/fcft.git
cd ../..
- debug: |
mkdir -p bld/debug
meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug
ninja -C bld/debug -k0
meson test -C bld/debug --print-errorlogs
- release: |
mkdir -p bld/release
meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release
ninja -C bld/release -k0
meson test -C bld/release --print-errorlogs
- codespell: |
python3 -m venv codespell-venv
source codespell-venv/bin/activate
pip install codespell
cd foot
~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd
deactivate

View file

@ -0,0 +1,43 @@
image: alpine/edge
arch: x86
packages:
- musl-dev
- eudev-libs
- eudev-dev
- linux-headers
- meson
- ninja
- gcc
- scdoc
- wayland-dev
- wayland-protocols
- freetype-dev
- fontconfig-dev
- harfbuzz-dev
- utf8proc-dev
- pixman-dev
- libxkbcommon-dev
- ncurses
- check-dev
- ttf-hack
- font-noto-emoji
sources:
- https://git.sr.ht/~dnkl/foot
# triggers:
# - action: email
# condition: failure
# to: <committer>
tasks:
- debug: |
mkdir -p bld/debug
meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug
ninja -C bld/debug -k0
meson test -C bld/debug --print-errorlogs
- release: |
mkdir -p bld/release
meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release
ninja -C bld/release -k0
meson test -C bld/release --print-errorlogs

49
.builds/freebsd-x64.yml Normal file
View file

@ -0,0 +1,49 @@
image: freebsd/latest
packages:
- evdev-proto
- libepoll-shim
- meson
- ninja
- pkgconf
- scdoc
- wayland
- wayland-protocols
- freetype2
- fontconfig
- harfbuzz
- utf8proc
- pixman
- libxkbcommon
- check
- hack-font
- noto-emoji
sources:
- https://codeberg.org/dnkl/foot.git
# triggers:
# - action: email
# condition: failure
# to: <committer>
tasks:
- fcft: |
cd foot/subprojects
git clone https://codeberg.org/dnkl/tllist.git
git clone https://codeberg.org/dnkl/fcft.git
cd ../..
- debug: |
mkdir -p bld/debug
meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug
ninja -C bld/debug -k0
meson test -C bld/debug --print-errorlogs
bld/debug/foot --version
bld/debug/footclient --version
- release: |
mkdir -p bld/release
meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release
ninja -C bld/release -k0
meson test -C bld/release --print-errorlogs
bld/release/foot --version
bld/release/footclient --version

17
.editorconfig Normal file
View file

@ -0,0 +1,17 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4
max_line_length = 70
[{meson.build,PKGBUILD}]
indent_size = 2
[*.scd]
indent_style = tab
trim_trailing_whitespace = false

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/bld/
/build/
/build-pgo/
/pkg/
/src/
/.cache/
/subprojects/*/

0
.gitmodules vendored Normal file
View file

139
.woodpecker.yaml Normal file
View file

@ -0,0 +1,139 @@
# -*- yaml -*-
steps:
- name: pychecks
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
image: alpine:edge
commands:
- apk add openssl
- apk add python3
- apk add py3-pip
- python3 -m venv venv
- source venv/bin/activate
- python -m pip install --upgrade pip
- pip install codespell
- pip install mypy
- pip install ruff
- codespell
- mypy
- ruff check
- deactivate
- name: subprojects
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
image: alpine:edge
commands:
- apk add git
- mkdir -p subprojects && cd subprojects
- git clone https://codeberg.org/dnkl/tllist.git
- git clone https://codeberg.org/dnkl/fcft.git
- cd ..
- name: x64
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
depends_on: [subprojects]
image: alpine:edge
commands:
- apk update
- apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses
- apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev
- apk add wayland-dev wayland-protocols
- apk add git
- apk add check-dev
- apk add ttf-hack font-noto-emoji
# Debug
- mkdir -p bld/debug-x64
- cd bld/debug-x64
- meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
# Release (gcc)
- mkdir -p bld/release-x64
- cd bld/release-x64
- meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
# Release (clang)
- mkdir -p bld/release-x64-clang
- cd bld/release-x64-clang
- CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
# no grapheme clustering
- apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev
- mkdir -p bld/debug
- cd bld/debug
- meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
- name: x86
when:
- event: [manual, pull_request]
- event: [push, tag]
branch: [master, releases/*]
depends_on: [subprojects]
image: i386/alpine:edge
commands:
- apk update
- apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses
- apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev
- apk add wayland-dev wayland-protocols
- apk add git
- apk add check-dev
- apk add ttf-hack font-noto-emoji
# Debug
- mkdir -p bld/debug-x86
- cd bld/debug-x86
- meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
# Release (gcc)
- mkdir -p bld/release-x86
- cd bld/release-x86
- meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..
# Release (clang)
- mkdir -p bld/release-x86-clang
- cd bld/release-x86-clang
- CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../..
- ninja -v -k0
- ninja -v test
- ./foot --version
- ./footclient --version
- cd ../..

3703
CHANGELOG.md Normal file

File diff suppressed because it is too large Load diff

83
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,83 @@
# Foot Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
Participants in the foot community are expected to uphold the described
standards not only in official community spaces (issue trackers, IRC channels,
etc.) but in all public spaces. The Code of Conduct however does acknowledge
that people are fallible and that it is possible to truly correct a past
pattern of unacceptable behavior. That is to say, the scope of the Code of
Conduct does not necessarily extend into the distant past.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement
at [daniel@ekloef.se](mailto:daniel@ekloef.se). All complaints will
be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
The consequences for Code of Conduct violations will be decided upon and
enforced by community leaders. These may include a formal warning, a temporary
ban from community spaces, a permanent ban from community spaces, etc.
There are no hard and fast rules for exactly what behavior in which space will
result in what consequences, it is up to the community leaders to enforce the
Code of Conduct in a way that they believe best promotes a healthy community.
## Attribution
This Code of Conduct is adapted from the
[Contributor Covenant](https://www.contributor-covenant.org/),
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.

476
INSTALL.md Normal file
View file

@ -0,0 +1,476 @@
# Installing
1. [Overview](#overview)
1. [Requirements](#requirements)
1. [Running](#running)
1. [Building](#building)
1. [Other](#other)
1. [Setup](#setup)
1. [Options](#options)
1. [Release build](#release-build)
1. [Size optimized](#size-optimized)
1. [Performance optimized, non-PGO](#performance-optimized-non-pgo)
1. [Performance optimized, PGO](#performance-optimized-pgo)
1. [Partial PGO](#partial-pgo)
1. [Full PGO](#full-pgo)
1. [Use the generated PGO data](#use-the-generated-pgo-data)
1. [Profile Guided Optimization](#profile-guided-optimization)
1. [Debug build](#debug-build)
1. [Terminfo](#terminfo)
1. [Running the new build](#running-the-new-build)
## Overview
foot makes use of a couple of libraries I have developed:
[tllist](https://codeberg.org/dnkl/tllist) and
[fcft](https://codeberg.org/dnkl/fcft). As such, they will most likely
not have been installed already. You can either install them as system
libraries or build them as _subprojects_ in foot.
When building foot, they will first be searched for as system
libraries. If **found**, foot will link dynamically against them.
If **not** found, meson will attempt to download and build them as
subprojects.
## Requirements
### Running
* UTF-8 locale
* fontconfig
* freetype
* pixman
* wayland (_client_ and _cursor_ libraries)
* xkbcommon
* utf8proc (_optional_, needed for grapheme clustering)
* libutempter (_optional_, needed for utmp logging on Linux)
* ulog (_optional_, needed for utmp logging on FreeBSD)
* [fcft](https://codeberg.org/dnkl/fcft) [^1]
[^1]: can also be built as subprojects, in which case they are
statically linked.
If you are packaging foot, you may also want to consider adding the
following **optional** dependencies:
* libnotify: desktop notifications by default uses `notify-send`.
* xdg-utils: URLs are by default launched with `xdg-open`.
* bash-completion: If you want completion for positional arguments.
### Building
In addition to the dev variant of the packages above, you need:
* meson
* ninja
* wayland protocols
* ncurses (needed to generate terminfo)
* scdoc (for man page generation, not needed if documentation is disabled)
* llvm (for PGO builds with Clang)
* [tllist](https://codeberg.org/dnkl/tllist) [^1]
* systemd (optional, foot will install systemd unit files if detected)
A note on compilers; in general, foot runs **much** faster when
compiled with gcc instead of clang. A profile-guided gcc build can be
more than twice as fast as a clang build.
**Note** GCC 10.1 has a performance regression that severely affects
foot when doing PGO builds and building with `-O2`; it is about 30-40%
slower compared to GCC 9.3.
The work around is simple: make sure you build with `-O3`. This is the
default with `meson --buildtype=release`, but e.g. `makepkg` can
override it (`makepkg` uses `-O2` by default).
## Other
Foot uses _meson_. If you are unfamiliar with it, the official
[tutorial](https://mesonbuild.com/Tutorial.html) might be a good
starting point.
A note on terminfo; the terminfo database exposes terminal
capabilities to the applications running inside the terminal. As such,
it is important that the terminfo used reflects the actual
terminal. Using the `xterm-256color` terminfo will, in many cases,
work, but I still recommend using foot's own terminfo. There are two
reasons for this:
* foot's terminfo contains a couple of non-standard capabilities,
used by e.g. tmux.
* New capabilities added to the `xterm-256color` terminfo could
potentially break foot.
* There may be future additions or changes to foot's terminfo.
As of ncurses 2021-07-31, ncurses includes a version of foot's
terminfo. **The recommendation is to use those**, and only install the
terminfo definitions from this git repo if the system's ncurses
predates 2021-07-31.
But, note that the foot terminfo definitions in ncurses' lack the
non-standard capabilities. This mostly affects tmux; without them,
`terminal-overrides` must be configured to enable truecolor
support. For this reason, it _is_ possible to install "our" terminfo
definitions as well, either in a non-default location, or under a
different name.
Both have their set of issues. When installing to a non-default
location, foot will set the environment variable `TERMINFO` in the
child process. However, there are many situations where this simply
does not work. See https://codeberg.org/dnkl/foot/issues/695 for
details.
Installing them under a different name generally works well, but will
break applications that check if `$TERM == foot`.
Hence the recommendation to simply use ncurses' terminfo definitions
if available.
If packaging "our" terminfo definitions, I recommend doing that as a
separate package, to allow them to be installed on remote systems
without having to install foot itself.
### Setup
To build, first, create a build directory, and switch to it:
```sh
mkdir -p bld/release && cd bld/release
```
### Options
Available compile-time options:
| Option | Type | Default | Description | Extra dependencies |
|--------------------------------------|---------|-------------------------|---------------------------------------------------------------------------------|---------------------|
| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc |
| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | None |
| `-Dime` | bool | `true` | Enables IME support | None |
| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc |
| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) |
| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None |
| `-Dterminfo-base-name` | string | `-Ddefault-terminfo` | Base name of the generated terminfo files | None |
| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None |
| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None |
| `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog |
| `-Dutmp-default-helper-path` | string | `auto` | Default path to utmp helper binary. `auto` selects path based on `utmp-backend` | None |
Documentation includes the man pages, readme, changelog and license
files.
`-Ddefault-terminfo`: I strongly recommend leaving the default
value. Use this option if you plan on installing the terminfo files
under a different name. Setting this changes the default value of
`$TERM`, and the names of the terminfo files (if
`-Dterminfo=enabled`).
If you want foot to use the terminfo files from ncurses, but still
package foot's own terminfo files under a different name, you can use
the `-Dterminfo-base-name` option. Many distributions use the name
`foot-extra`, and thus it might be a good idea to reuse that:
```sh
meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra
```
(or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway).
Finally, `-Dcustom-terminfo-install-location` enables foot's terminfo
to co-exist with ncurses' version, without changing the terminfo
names. The idea is that you install foot's terminfo to a non-standard
location, for example `/usr/share/foot/terminfo`. Use
`-Dcustom-terminfo-install-location` to tell foot where the terminfo
is. Foot will set the environment variable `TERMINFO` to this value
(with `${prefix}` added). The value is **relative to ${prefix}**.
Note that there are several issues with this approach:
https://codeberg.org/dnkl/foot/issues/695.
If left unset, foot will **not** set or modify `TERMINFO`.
`-Dterminfo` can be used to disable building the terminfo definitions
in the meson build. It does **not** change the default value of
`TERM`, and it does **not** disable `TERMINFO`, if
`-Dcustom-terminfo-install-location` has been set. Use this if
packaging the terminfo definitions in a separate package (and the
build script isn't shared with the 'foot' package).
Example:
```sh
meson --prefix=/usr -Dcustom-terminfo-install-location=lib/foot/terminfo
```
The above tells foot its terminfo definitions will be installed to
`/usr/lib/foot/terminfo`. This is the value foot will set the
`TERMINFO` environment variable to.
If `-Dterminfo` is enabled (the default), then the terminfo files will
be built as part of the regular build process, and installed to the
specified location.
Packagers may want to set `-Dterminfo=disabled`, and manually build
and [install the terminfo](#terminfo) files instead.
### Release build
Below are instructions for building foot either [size
optimized](#size-optimized), [performance
optimized](performance-optimized-non-pgo), or performance
optimized using [PGO](#performance-optimized-pgo).
PGO - _Profile Guided Optimization_ - is a way to optimize a program
better than `-O3` can, and is done by compiling foot twice: first to
generate an instrumented version which is used to run a payload that
exercises the performance critical parts of foot, and then a second
time to rebuild foot using the generated profiling data to guide
optimization.
In addition to being faster, PGO builds also tend to be smaller than
regular `-O3` builds.
#### Size optimized
To optimize for size (i.e. produce a small binary):
```sh
export CFLAGS="$CFLAGS -Os"
meson --buildtype=release --prefix=/usr -Db_lto=true ../..
ninja
ninja test
ninja install
```
#### Performance optimized, non-PGO
To do a regular, non-PGO build optimized for performance:
```sh
export CFLAGS="$CFLAGS -O3"
meson --buildtype=release --prefix=/usr -Db_lto=true ../..
ninja
ninja test
ninja install
```
Use `-O2` instead of `-O3` if you prefer a slightly smaller (and
slower!) binary.
#### Performance optimized, PGO
There are a lot more steps involved in a PGO build, and for this
reason there are a number of helper scripts available.
`pgo/pgo.sh` is a standalone script that pieces together the other
scripts in the `pgo` directory to do a complete PGO build. This script
is intended to be used when doing manual builds.
Note that all "full" PGO builds (which `auto` will prefer, if
possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is
**not** done automatically.
Example:
```sh
cd foot
./pgo/pgo.sh auto . /tmp/foot-pgo-build-output
```
(run `./pgo/pgo.sh` to get help on usage)
It supports a couple of different PGO builds; partial (covered in
detail below), full (also covered in detail below), and (full)
headless builds using Sway or cage.
Packagers may want to use it as inspiration, but may choose to support
only a specific build type; e.g. full/headless with Sway.
To do a manual PGO build, instead of using the script(s) mentioned
above, detailed instructions follows:
First, configure the build directory:
```sh
export CFLAGS="$CFLAGS -O3"
meson --buildtype=release --prefix=/usr -Db_lto=true ../..
```
It is **very** important `-O3` is being used here, as GCC-10.1.x and
later have a regression where PGO with `-O2` is **much** slower.
Clang users **must** add `-Wno-ignored-optimization-argument` to
`CFLAGS`.
Then, tell meson we want to _generate_ profiling data, and build:
```sh
meson configure -Db_pgo=generate
ninja
ninja test
```
Next, we need to actually generate the profiling data.
There are two ways to do this: a [partial PGO build using a PGO
helper](#partial-pgo) binary, or a [full PGO build](#full-pgo) by
running the real foot binary. The latter has slightly better results
(i.e. results in a faster binary), but must be run in a Wayland
session.
A full PGO build also tends to be smaller than a partial build.
##### Partial PGO
This method uses a PGO helper binary that links against the VT parser
only. It is similar to a mock test; it instantiates a dummy terminal
instance and then directly calls the VT parser with stimuli.
It explicitly does **not** include the Wayland backend and as such, it
does not require a running Wayland session. The downside is that not
all code paths in foot is exercised. In particular, the **rendering**
code is not. As a result, the final binary built using this method is
slightly slower than when doing a [full PGO](#full-pgo) build.
We will use the `pgo` binary along with input corpus generated by
`scripts/generate-alt-random-writes.py`:
```sh
./utils/xtgettcap
./footclient --version
./foot --version
tmp_file=$(mktemp)
../../scripts/generate-alt-random-writes \
--rows=67 \
--cols=135 \
--scroll \
--scroll-region \
--colors-regular \
--colors-bright \
--colors-256 \
--colors-rgb \
--attr-bold \
--attr-italic \
--attr-underline \
--sixel \
${tmp_file}
./pgo ${tmp_file} ${tmp_file} ${tmp_file}
rm ${tmp_file}
```
The first step, running `./foot --version` and `./footclient
--version` etc, might seem unnecessary, but is needed to ensure we
have _some_ profiling data for functions not covered by the PGO helper
binary, for **all** binaries. Without this, the final link phase will
fail.
The snippet above then creates an (empty) temporary file. Then, it
runs a script that generates random escape sequences (if you cat
`${tmp_file}` in a terminal, you'll see random colored characters all
over the screen). Finally, we feed the randomly generated escape
sequences to the PGO helper. This is what generates the profiling data
used in the next step.
You are now ready to [use the generated PGO
data](#use-the-generated-pgo-data).
##### Full PGO
This method requires a running Wayland session.
We will use the script `scripts/generate-alt-random-writes.py`:
```sh
./utils/xtgettcap
./footclient --version
foot_tmp_file=$(mktemp)
./foot \
--config=/dev/null \
--override tweak.grapheme-shaping=no \
--term=xterm \
sh -c "<path-to-generate-alt-random-writes.py> --scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel ${foot_tmp_file} && cat ${foot_tmp_file}"
rm ${foot_tmp_file}
```
You should see a foot window open up, with random colored text. The
window should close after ~1-2s.
The first step, `./utils/xtgettcap && ./footclient --version`
might seem unnecessary, but is needed to ensure we have _some_
profiling data for **all** binaries we build. Without this, the final
link phase will fail.
##### Use the generated PGO data
Now that we have _generated_ PGO data, we need to rebuild foot. This
time telling meson (and ultimately gcc/clang) to _use_ the PGO data.
If using Clang, now do (this requires _llvm_ to have been installed):
```sh
llvm-profdata merge default_*profraw --output=default.profdata
```
Next, tell meson to _use_ the profile data we just generated, and rebuild:
```sh
meson configure -Db_pgo=use
ninja
ninja test
```
Continue reading in [Running the new build](#running-the-new-build)
### Debug build
```sh
meson --buildtype=debug ../..
ninja
ninja test
```
### Terminfo
By default, building foot also builds the terminfo files. If packaging
the terminfo files in a separate package, it might be easier to simply
disable the terminfo files in the regular build, and compile the
terminfo files manually instead.
To build the terminfo files, run:
```sh
sed 's/@default_terminfo@/foot/g' foot.info | \
tic -o <output-directory> -x -e foot,foot-direct -
```
Where _"output-directory"_ **must** match the value passed to
`-Dcustom-terminfo-install-location` in the foot build. If
`-Dcustom-terminfo-install-location` has not been set, `-o
<output-directory>` can simply be omitted.
Or, if packaging:
```sh
tic -o ${DESTDIR}/usr/share/terminfo ...
```
### Running the new build
You can now run it directly from the build directory:
```sh
./foot
```
Or, if you did not install the terminfo definitions:
```sh
./foot --term xterm-256color
```

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Daniel Eklöf
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.

1
README.md Normal file
View file

@ -0,0 +1 @@
toes

35
async.c Normal file
View file

@ -0,0 +1,35 @@
#include "async.h"
#include <stdint.h>
#include <errno.h>
#include <unistd.h>
#define LOG_MODULE "async"
#define LOG_ENABLE_DBG 0
#include "log.h"
enum async_write_status
async_write(int fd, const void *_data, size_t len, size_t *idx)
{
const uint8_t *const data = _data;
size_t left = len - *idx;
while (left > 0) {
ssize_t ret = write(fd, &data[*idx], left);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return ASYNC_WRITE_REMAIN;
return ASYNC_WRITE_ERR;
}
LOG_DBG("wrote %zd bytes of %zu (%zu left) to FD=%d",
ret, left, left - ret, fd);
*idx += ret;
left -= ret;
}
return ASYNC_WRITE_DONE;
}

24
async.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <stddef.h>
enum async_write_status {ASYNC_WRITE_DONE, ASYNC_WRITE_REMAIN, ASYNC_WRITE_ERR};
/*
* Primitive that writes data to a NONBLOCK:ing FD.
*
* _data: points to the beginning of the buffer
* len: total size of the data buffer
* idx: pointer to byte offset into data buffer - writing starts here.
*
* Thus, the total amount of data to write is (len - *idx). *idx is
* updated such that it points to the next unwritten byte in the data
* buffer.
*
* I.e. if the return value is:
* - ASYNC_WRITE_DONE, then the *idx == len.
* - ASYNC_WRITE_REMAIN, then *idx < len
* - ASYNC_WRITE_ERR, there was an error, and no data was written
*/
enum async_write_status async_write(
int fd, const void *data, size_t len, size_t *idx);

172
base64.c Normal file
View file

@ -0,0 +1,172 @@
#include "base64.h"
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
#define LOG_MODULE "base64"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "debug.h"
enum {
P = 1 << 6, // Padding byte (=)
I = 1 << 7, // Invalid byte ([^A-Za-z0-9+/=])
};
static const uint8_t reverse_lookup[256] = {
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, 62, I, I, I, 63,
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, I, I, I, P, I, I,
I, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, I, I, I, I, I,
I, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I,
I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I
};
static const char lookup[64] = {
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/"
};
char *
base64_decode(const char *s, size_t *size)
{
const size_t len = strlen(s);
if (unlikely(len % 4 != 0)) {
errno = EINVAL;
return NULL;
}
char *ret = malloc(len / 4 * 3 + 1);
if (unlikely(ret == NULL))
return NULL;
if (unlikely(size != NULL))
*size = len / 4 * 3;
for (size_t i = 0, o = 0; i < len; i += 4, o += 3) {
unsigned a = reverse_lookup[(unsigned char)s[i + 0]];
unsigned b = reverse_lookup[(unsigned char)s[i + 1]];
unsigned c = reverse_lookup[(unsigned char)s[i + 2]];
unsigned d = reverse_lookup[(unsigned char)s[i + 3]];
unsigned u = a | b | c | d;
if (unlikely(u & I))
goto invalid;
if (unlikely(u & P)) {
if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P))))
goto invalid;
if (unlikely(size != NULL)) {
if (c & P)
*size = len / 4 * 3 - 2;
else
*size = len / 4 * 3 - 1;
}
c &= 63;
d &= 63;
}
uint32_t v = a << 18 | b << 12 | c << 6 | d << 0;
char x = (v >> 16) & 0xff;
char y = (v >> 8) & 0xff;
char z = (v >> 0) & 0xff;
LOG_DBG("%c%c%c", x, y, z);
ret[o + 0] = x;
ret[o + 1] = y;
ret[o + 2] = z;
}
ret[len / 4 * 3] = '\0';
return ret;
invalid:
free(ret);
errno = EINVAL;
return NULL;
}
char *
base64_encode(const uint8_t *data, size_t size)
{
xassert(size % 3 == 0);
if (unlikely(size % 3 != 0))
return NULL;
char *ret = malloc(size / 3 * 4 + 1);
if (unlikely(ret == NULL))
return NULL;
for (size_t i = 0, o = 0; i < size; i += 3, o += 4) {
int x = data[i + 0];
int y = data[i + 1];
int z = data[i + 2];
uint32_t v = x << 16 | y << 8 | z << 0;
unsigned a = (v >> 18) & 0x3f;
unsigned b = (v >> 12) & 0x3f;
unsigned c = (v >> 6) & 0x3f;
unsigned d = (v >> 0) & 0x3f;
char c0 = lookup[a];
char c1 = lookup[b];
char c2 = lookup[c];
char c3 = lookup[d];
ret[o + 0] = c0;
ret[o + 1] = c1;
ret[o + 2] = c2;
ret[o + 3] = c3;
LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3);
}
ret[size / 3 * 4] = '\0';
return ret;
}
void
base64_encode_final(const uint8_t *data, size_t size, char result[4])
{
xassert(size > 0);
xassert(size < 3);
uint32_t v = 0;
if (size >= 1)
v |= data[0] << 16;
if (size >= 2)
v |= data[1] << 8;
unsigned a = (v >> 18) & 0x3f;
unsigned b = (v >> 12) & 0x3f;
unsigned c = (v >> 6) & 0x3f;
char c0 = lookup[a];
char c1 = lookup[b];
char c2 = size == 2 ? lookup[c] : '=';
char c3 = '=';
result[0] = c0;
result[1] = c1;
result[2] = c2;
result[3] = c3;
LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3);
}

8
base64.h Normal file
View file

@ -0,0 +1,8 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
char *base64_decode(const char *s, size_t *out_len);
char *base64_encode(const uint8_t *data, size_t size);
void base64_encode_final(const uint8_t *data, size_t size, char result[4]);

3450
box-drawing.c Normal file

File diff suppressed because it is too large Load diff

7
box-drawing.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include <uchar.h>
#include <fcft/fcft.h>
struct terminal;
struct fcft_glyph *box_drawing(const struct terminal *term, char32_t wc);

432
char32.c Normal file
View file

@ -0,0 +1,432 @@
#include "char32.h"
#include <stdlib.h>
#include <string.h>
#include <locale.h>
#include <wctype.h>
#include <wchar.h>
#if defined __has_include
#if __has_include (<stdc-predef.h>)
#include <stdc-predef.h>
#endif
#endif
#define LOG_MODULE "char32"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "debug.h"
#include "macros.h"
#include "xmalloc.h"
/*
* For now, assume we can map directly to the corresponding wchar_t
* functions. This is true if:
*
* - both data types have the same size
* - both use the same encoding (though we require that encoding to be UTF-32)
*/
_Static_assert(
sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch");
#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__
#error "char32_t does not use UTF-32"
#endif
#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__)
#error "wchar_t does not use UTF-32"
#endif
UNITTEST
{
xassert(c32len(U"") == 0);
xassert(c32len(U"foobar") == 6);
}
UNITTEST
{
xassert(c32cmp(U"foobar", U"foobar") == 0);
xassert(c32cmp(U"foo", U"foobar") < 0);
xassert(c32cmp(U"foobar", U"foo") > 0);
xassert(c32cmp(U"a", U"b") < 0);
xassert(c32cmp(U"b", U"a") > 0);
}
UNITTEST
{
xassert(c32ncmp(U"foo", U"foot", 3) == 0);
xassert(c32ncmp(U"foot", U"FOOT", 4) > 0);
xassert(c32ncmp(U"a", U"b", 1) < 0);
xassert(c32ncmp(U"bb", U"aa", 2) > 0);
}
UNITTEST
{
char32_t copy[16];
char32_t *ret = c32ncpy(copy, U"foobar", 16);
xassert(ret == copy);
xassert(copy[0] == U'f');
xassert(copy[1] == U'o');
xassert(copy[2] == U'o');
xassert(copy[3] == U'b');
xassert(copy[4] == U'a');
xassert(copy[5] == U'r');
unsigned char zeroes[(16 - 6) * sizeof(copy[0])] = {0};
xassert(memcmp(&copy[6], zeroes, sizeof(zeroes)) == 0);
}
UNITTEST
{
char32_t copy[16];
memset(copy, 0x55, sizeof(copy));
char32_t *ret = c32cpy(copy, U"foobar");
xassert(ret == copy);
xassert(copy[0] == U'f');
xassert(copy[1] == U'o');
xassert(copy[2] == U'o');
xassert(copy[3] == U'b');
xassert(copy[4] == U'a');
xassert(copy[5] == U'r');
xassert(copy[6] == U'\0');
unsigned char fives[(16 - 6 - 1) * sizeof(copy[0])];
memset(fives, 0x55, sizeof(fives));
xassert(memcmp(&copy[7], fives, sizeof(fives)) == 0);
}
UNITTEST
{
xassert(c32casecmp(U"foobar", U"FOOBAR") == 0);
xassert(c32casecmp(U"foo", U"FOOO") < 0);
xassert(c32casecmp(U"FOOO", U"foo") > 0);
xassert(c32casecmp(U"a", U"B") < 0);
xassert(c32casecmp(U"B", U"a") > 0);
}
UNITTEST
{
xassert(c32ncasecmp(U"foo", U"FOObar", 3) == 0);
xassert(c32ncasecmp(U"foo", U"FOOO", 4) < 0);
xassert(c32ncasecmp(U"FOOO", U"foo", 4) > 0);
xassert(c32ncasecmp(U"a", U"BB", 1) < 0);
xassert(c32ncasecmp(U"BB", U"a", 1) > 0);
}
UNITTEST
{
char32_t dst[32] = U"foobar";
char32_t *ret = c32ncat(dst, U"12345678XXXXXXXXX", 8);
xassert(ret == dst);
xassert(c32cmp(dst, U"foobar12345678") == 0);
}
UNITTEST
{
char32_t dst[32] = U"foobar";
char32_t *ret = c32cat(dst, U"12345678");
xassert(ret == dst);
xassert(c32cmp(dst, U"foobar12345678") == 0);
}
UNITTEST
{
xassert(!isc32upper(U'a'));
xassert(isc32upper(U'A'));
xassert(!isc32upper(U'a'));
}
UNITTEST
{
xassert(hasc32upper(U"abc1A"));
xassert(!hasc32upper(U"abc1_aaa"));
xassert(!hasc32upper(U""));
}
UNITTEST
{
char32_t *c = xc32dup(U"foobar");
xassert(c32cmp(c, U"foobar") == 0);
free(c);
c = xc32dup(U"");
xassert(c32cmp(c, U"") == 0);
free(c);
}
size_t
mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len)
{
mbstate_t ps = {0};
char32_t *out = dst;
const char *in = src;
size_t consumed = 0;
size_t chars = 0;
size_t rc;
while ((out == NULL || chars < len) &&
consumed < nms &&
(rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0)
{
switch (rc) {
case 0:
goto done;
case (size_t)-1:
case (size_t)-2:
case (size_t)-3:
goto err;
}
in += rc;
consumed += rc;
chars++;
if (out != NULL)
out++;
}
done:
return chars;
err:
return (size_t)-1;
}
UNITTEST
{
const char input[] = "foobarzoo";
char32_t c32[32];
size_t ret = mbsntoc32(NULL, input, sizeof(input), 0);
xassert(ret == 9);
memset(c32, 0x55, sizeof(c32));
ret = mbsntoc32(c32, input, sizeof(input), 32);
xassert(ret == 9);
xassert(c32[0] == U'f');
xassert(c32[1] == U'o');
xassert(c32[2] == U'o');
xassert(c32[3] == U'b');
xassert(c32[4] == U'a');
xassert(c32[5] == U'r');
xassert(c32[6] == U'z');
xassert(c32[7] == U'o');
xassert(c32[8] == U'o');
xassert(c32[9] == U'\0');
xassert(c32[10] == 0x55555555);
memset(c32, 0x55, sizeof(c32));
ret = mbsntoc32(c32, input, 1, 32);
xassert(ret == 1);
xassert(c32[0] == U'f');
xassert(c32[1] == 0x55555555);
memset(c32, 0x55, sizeof(c32));
ret = mbsntoc32(c32, input, sizeof(input), 1);
xassert(ret == 1);
xassert(c32[0] == U'f');
xassert(c32[1] == 0x55555555);
}
UNITTEST
{
const char input[] = "foobarzoo";
char32_t c32[32];
size_t ret = mbstoc32(NULL, input, 0);
xassert(ret == 9);
memset(c32, 0x55, sizeof(c32));
ret = mbstoc32(c32, input, 32);
xassert(ret == 9);
xassert(c32[0] == U'f');
xassert(c32[1] == U'o');
xassert(c32[2] == U'o');
xassert(c32[3] == U'b');
xassert(c32[4] == U'a');
xassert(c32[5] == U'r');
xassert(c32[6] == U'z');
xassert(c32[7] == U'o');
xassert(c32[8] == U'o');
xassert(c32[9] == U'\0');
xassert(c32[10] == 0x55555555);
memset(c32, 0x55, sizeof(c32));
ret = mbstoc32(c32, input, 1);
xassert(ret == 1);
xassert(c32[0] == U'f');
xassert(c32[1] == 0x55555555);
}
char32_t *
ambstoc32(const char *src)
{
if (src == NULL)
return NULL;
const size_t src_len = strlen(src);
char32_t *ret = xmalloc((src_len + 1) * sizeof(ret[0]));
mbstate_t ps = {0};
char32_t *out = ret;
const char *in = src;
const char *const end = src + src_len + 1;
size_t chars = 0;
size_t rc;
while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) {
switch (rc) {
case (size_t)-1:
case (size_t)-2:
case (size_t)-3:
goto err;
}
in += rc;
out++;
chars++;
}
*out = U'\0';
ret = xrealloc(ret, (chars + 1) * sizeof(ret[0]));
return ret;
err:
free(ret);
return NULL;
}
UNITTEST
{
const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8");
if (!locale)
locale = setlocale(LC_CTYPE, "C.UTF-8");
if (!locale)
return;
char32_t *hello = ambstoc32(u8"hello");
xassert(hello != NULL);
xassert(hello[0] == U'h');
xassert(hello[1] == U'e');
xassert(hello[2] == U'l');
xassert(hello[3] == U'l');
xassert(hello[4] == U'o');
xassert(hello[5] == U'\0');
free(hello);
char32_t *swedish = ambstoc32(u8"åäö");
xassert(swedish != NULL);
xassert(swedish[0] == U'å');
xassert(swedish[1] == U'ä');
xassert(swedish[2] == U'ö');
xassert(swedish[3] == U'\0');
free(swedish);
char32_t *emoji = ambstoc32(u8"👨‍👩‍👧‍👦");
xassert(emoji != NULL);
xassert(emoji[0] == U'👨');
xassert(emoji[1] == U'');
xassert(emoji[2] == U'👩');
xassert(emoji[3] == U'');
xassert(emoji[4] == U'👧');
xassert(emoji[5] == U'');
xassert(emoji[6] == U'👦');
xassert(emoji[7] == U'\0');
free(emoji);
xassert(ambstoc32(NULL) == NULL);
xassert(setlocale(LC_CTYPE, "C") != NULL);
}
char *
ac32tombs(const char32_t *src)
{
if (src == NULL)
return NULL;
const size_t src_len = c32len(src);
size_t allocated = src_len + 1;
char *ret = xmalloc(allocated);
mbstate_t ps = {0};
char *out = ret;
const char32_t *const end = src + src_len + 1;
size_t bytes = 0;
char mb[MB_CUR_MAX];
for (const char32_t *in = src; in < end; in++) {
size_t rc = c32rtomb(mb, *in, &ps);
switch (rc) {
case (size_t)-1:
goto err;
}
if (bytes + rc > allocated) {
allocated *= 2;
ret = xrealloc(ret, allocated);
out = &ret[bytes];
}
for (size_t i = 0; i < rc; i++, out++)
*out = mb[i];
bytes += rc;
}
xassert(ret[bytes - 1] == '\0');
ret = xrealloc(ret, bytes);
return ret;
err:
free(ret);
return NULL;
}
UNITTEST
{
const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8");
if (!locale)
locale = setlocale(LC_CTYPE, "C.UTF-8");
if (!locale)
return;
char *s = ac32tombs(U"foobar");
xassert(s != NULL);
xassert(strcmp(s, "foobar") == 0);
free(s);
s = ac32tombs(U"åäö");
xassert(s != NULL);
xassert(strcmp(s, u8"åäö") == 0);
free(s);
s = ac32tombs(U"👨‍👩‍👧‍👦");
xassert(s != NULL);
xassert(strcmp(s, u8"👨‍👩‍👧‍👦") == 0);
free(s);
xassert(ac32tombs(NULL) == NULL);
xassert(setlocale(LC_CTYPE, "C") != NULL);
}

115
char32.h Normal file
View file

@ -0,0 +1,115 @@
#pragma once
#include <stdbool.h>
#include <uchar.h>
#include <stddef.h>
#include <stdarg.h>
#include <string.h>
#include <wchar.h>
#include <wctype.h>
#if defined(FOOT_GRAPHEME_CLUSTERING)
#include <utf8proc.h>
#endif
static inline size_t c32len(const char32_t *s) {
return wcslen((const wchar_t *)s);
}
static inline int c32cmp(const char32_t *s1, const char32_t *s2) {
return wcscmp((const wchar_t *)s1, (const wchar_t *)s2);
}
static inline int c32ncmp(const char32_t *s1, const char32_t *s2, size_t n) {
return wcsncmp((const wchar_t *)s1, (const wchar_t *)s2, n);
}
static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) {
return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n);
}
static inline char32_t *c32cpy(char32_t *dst, const char32_t *src) {
return (char32_t *)wcscpy((wchar_t *)dst, (const wchar_t *)src);
}
static inline char32_t *c32ncat(char32_t *dst, const char32_t *src, size_t n) {
return (char32_t *)wcsncat((wchar_t *)dst, (const wchar_t *)src, n);
}
static inline char32_t *c32cat(char32_t *dst, const char32_t *src) {
return (char32_t *)wcscat((wchar_t *)dst, (const wchar_t *)src);
}
static inline char32_t *c32dup(const char32_t *s) {
return (char32_t *)wcsdup((const wchar_t *)s);
}
static inline char32_t *c32chr(const char32_t *s, char32_t c) {
return (char32_t *)wcschr((const wchar_t *)s, c);
}
static inline int c32casecmp(const char32_t *s1, const char32_t *s2) {
return wcscasecmp((const wchar_t *)s1, (const wchar_t *)s2);
}
static inline int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n) {
return wcsncasecmp((const wchar_t *)s1, (const wchar_t *)s2, n);
}
static inline char32_t toc32lower(char32_t c) {
return (char32_t)towlower((wint_t)c);
}
static inline char32_t toc32upper(char32_t c) {
return (char32_t)towupper((wint_t)c);
}
static inline bool isc32upper(char32_t c32) {
return iswupper((wint_t)c32);
}
static inline bool isc32space(char32_t c32) {
return iswspace((wint_t)c32);
}
static inline bool isc32print(char32_t c32) {
return iswprint((wint_t)c32);
}
static inline bool isc32graph(char32_t c32) {
return iswgraph((wint_t)c32);
}
static inline bool hasc32upper(const char32_t *s) {
for (int i = 0; s[i] != '\0'; i++) {
if (isc32upper(s[i])) return true;
}
return false;
}
static inline int c32width(char32_t c) {
#if defined(FOOT_GRAPHEME_CLUSTERING)
return utf8proc_charwidth((utf8proc_int32_t)c);
#else
return wcwidth((wchar_t)c);
#endif
}
static inline int c32swidth(const char32_t *s, size_t n) {
#if defined(FOOT_GRAPHEME_CLUSTERING)
int width = 0;
for (size_t i = 0; i < n; i++)
width += utf8proc_charwidth((utf8proc_int32_t)s[i]);
return width;
#else
return wcswidth((const wchar_t *)s, n);
#endif
}
size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len);
char32_t *ambstoc32(const char *src);
char *ac32tombs(const char32_t *src);
static inline size_t mbstoc32(char32_t *dst, const char *src, size_t len) {
return mbsntoc32(dst, src, strlen(src) + 1, len);
}

45
client-protocol.h Normal file
View file

@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
struct client_string {
uint16_t len;
/* char str[static len]; */
};
struct client_data {
bool hold:1;
bool no_wait:1;
bool xdga_token:1;
uint8_t reserved:5;
uint8_t token_len;
uint16_t cwd_len;
uint16_t override_count;
uint16_t argc;
uint16_t env_count;
/* char cwd[static cwd_len]; */
/* char token[static token_len]; */
/* struct client_string overrides[static override_count]; */
/* struct client_string argv[static argc]; */
/* struct client_string envp[static env_count]; */
} __attribute__((packed));
_Static_assert(sizeof(struct client_data) == 10, "protocol struct size error");
enum client_ipc_code {
FOOT_IPC_SIGUSR,
};
struct client_ipc_hdr {
enum client_ipc_code ipc_code;
uint8_t size;
} __attribute__((packed));
struct client_ipc_sigusr {
int signo;
} __attribute__((packed));

604
client.c Normal file
View file

@ -0,0 +1,604 @@
#include <errno.h>
#include <getopt.h>
#include <limits.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <tllist.h>
#define LOG_MODULE "foot-client"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "client-protocol.h"
#include "debug.h"
#include "foot-features.h"
#include "macros.h"
#include "util.h"
#include "xmalloc.h"
extern char **environ;
struct string {
size_t len;
char *str;
};
typedef tll(struct string) string_list_t;
static volatile sig_atomic_t aborted = 0;
static volatile sig_atomic_t sigusr = 0;
static void
sigint_handler(int signo)
{
aborted = 1;
}
static void
sigusr_handler(int signo)
{
sigusr = signo;
}
static ssize_t
sendall(int sock, const void *_buf, size_t len)
{
const uint8_t *buf = _buf;
size_t left = len;
while (left > 0) {
ssize_t r = send(sock, buf, left, MSG_NOSIGNAL);
if (r < 0) {
if (errno == EINTR)
continue;
return r;
}
buf += r;
left -= r;
}
return len;
}
static void
print_usage(const char *prog_name)
{
static const char options[] =
"\nOptions:\n"
" -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n"
" -T,--title=TITLE initial window title (foot)\n"
" -a,--app-id=ID window application ID (foot)\n"
" --toplevel-tag=TAG set a custom toplevel tag\n"
" -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n"
" -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n"
" -m,--maximized start in maximized mode\n"
" -F,--fullscreen start in fullscreen mode\n"
" -L,--login-shell start shell as a login shell\n"
" -D,--working-directory=DIR directory to start in (CWD)\n"
" -s,--server-socket=PATH path to the server UNIX domain socket (default=$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)\n"
" -H,--hold remain open after child process exits\n"
" -N,--no-wait detach the client process from the running terminal, exiting immediately\n"
" -o,--override=[section.]key=value override configuration option\n"
" -E, --client-environment exec shell using footclient's environment, instead of the server's\n"
" -d,--log-level={info|warning|error|none} log level (warning)\n"
" -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n"
" -v,--version show the version number and quit\n"
" -e ignored (for compatibility with xterm -e)\n";
printf("Usage: %s [OPTIONS...]\n", prog_name);
printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name);
puts(options);
}
static bool NOINLINE
push_string(string_list_t *string_list, const char *s, uint64_t *total_len)
{
size_t len = strlen(s) + 1;
if (len >= 1 << (8 * sizeof(uint16_t))) {
LOG_ERR("string length overflow");
return false;
}
struct string o = {len, xstrdup(s)};
tll_push_back(*string_list, o);
*total_len += sizeof(struct client_string) + o.len;
return true;
}
static void
free_string_list(string_list_t *string_list)
{
tll_foreach(*string_list, it) {
free(it->item.str);
tll_remove(*string_list, it);
}
}
static bool
send_string_list(int fd, const string_list_t *string_list)
{
tll_foreach(*string_list, it) {
const struct client_string s = {it->item.len};
if (sendall(fd, &s, sizeof(s)) < 0 ||
sendall(fd, it->item.str, s.len) < 0)
{
LOG_ERRNO("failed to send setup packet to server");
return false;
}
}
return true;
}
enum {
TOPLEVEL_TAG_OPTION = CHAR_MAX + 1,
};
int
main(int argc, char *const *argv)
{
/* Custom exit code, to enable users to differentiate between foot
* itself failing, and the client application failing */
static const int foot_exit_failure = -36;
int ret = foot_exit_failure;
const char *const prog_name = argc > 0 ? argv[0] : "<nullptr>";
static const struct option longopts[] = {
{"term", required_argument, NULL, 't'},
{"title", required_argument, NULL, 'T'},
{"app-id", required_argument, NULL, 'a'},
{"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION},
{"window-size-pixels", required_argument, NULL, 'w'},
{"window-size-chars", required_argument, NULL, 'W'},
{"maximized", no_argument, NULL, 'm'},
{"fullscreen", no_argument, NULL, 'F'},
{"login-shell", no_argument, NULL, 'L'},
{"working-directory", required_argument, NULL, 'D'},
{"server-socket", required_argument, NULL, 's'},
{"hold", no_argument, NULL, 'H'},
{"no-wait", no_argument, NULL, 'N'},
{"override", required_argument, NULL, 'o'},
{"client-environment", no_argument, NULL, 'E'},
{"log-level", required_argument, NULL, 'd'},
{"log-colorize", optional_argument, NULL, 'l'},
{"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 'h'},
{NULL, no_argument, NULL, 0},
};
const char *custom_cwd = NULL;
const char *server_socket_path = NULL;
enum log_class log_level = LOG_CLASS_WARNING;
enum log_colorize log_colorize = LOG_COLORIZE_AUTO;
bool hold = false;
bool client_environment = false;
/* Used to format overrides */
bool no_wait = false;
/* For XDG activation */
const char *token = getenv("XDG_ACTIVATION_TOKEN");
bool xdga_token = token != NULL;
size_t token_len = xdga_token ? strlen(token) + 1 : 0;
char buf[1024];
/* Total packet length, not (yet) including overrides or argv[] */
uint64_t total_len = 0;
/* malloc:ed and needs to be in scope of all goto's */
int fd = -1;
char *_cwd = NULL;
struct client_string *cargv = NULL;
string_list_t overrides = tll_init();
string_list_t envp = tll_init();
while (true) {
int c = getopt_long(argc, argv, "+t:T:a:w:W:mFLD:s:HNo:Ed:l::veh", longopts, NULL);
if (c == -1)
break;
switch (c) {
case 't':
snprintf(buf, sizeof(buf), "term=%s", optarg);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
case 'T':
snprintf(buf, sizeof(buf), "title=%s", optarg);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
case 'a':
snprintf(buf, sizeof(buf), "app-id=%s", optarg);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
case TOPLEVEL_TAG_OPTION:
snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
case 'L':
if (!push_string(&overrides, "login-shell=yes", &total_len))
goto err;
break;
case 'D': {
struct stat st;
if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) {
fprintf(stderr, "error: %s: not a directory\n", optarg);
goto err;
}
custom_cwd = optarg;
break;
}
case 'w': {
unsigned width, height;
if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg);
goto err;
}
snprintf(buf, sizeof(buf), "initial-window-size-pixels=%ux%u", width, height);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
}
case 'W': {
unsigned width, height;
if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg);
goto err;
}
snprintf(buf, sizeof(buf), "initial-window-size-chars=%ux%u", width, height);
if (!push_string(&overrides, buf, &total_len))
goto err;
break;
}
case 'm':
if (!push_string(&overrides, "initial-window-mode=maximized", &total_len))
goto err;
break;
case 'F':
if (!push_string(&overrides, "initial-window-mode=fullscreen", &total_len))
goto err;
break;
case 's':
server_socket_path = optarg;
break;
case 'H':
hold = true;
break;
case 'N':
no_wait = true;
break;
case 'o':
if (!push_string(&overrides, optarg, &total_len))
goto err;
break;
case 'E':
client_environment = true;
break;
case 'd': {
int lvl = log_level_from_string(optarg);
if (unlikely(lvl < 0)) {
fprintf(
stderr,
"-d,--log-level: %s: argument must be one of %s\n",
optarg,
log_level_string_hint());
goto err;
}
log_level = lvl;
break;
}
case 'l':
if (optarg == NULL || streq(optarg, "auto"))
log_colorize = LOG_COLORIZE_AUTO;
else if (streq(optarg, "never"))
log_colorize = LOG_COLORIZE_NEVER;
else if (streq(optarg, "always"))
log_colorize = LOG_COLORIZE_ALWAYS;
else {
fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg);
goto err;
}
break;
case 'v':
print_version_and_features("footclient ");
ret = EXIT_SUCCESS;
goto err;
case 'h':
print_usage(prog_name);
ret = EXIT_SUCCESS;
goto err;
case 'e':
break;
case '?':
goto err;
}
}
if (argc > 0) {
argc -= optind;
argv += optind;
}
log_init(log_colorize, false, LOG_FACILITY_USER, log_level);
fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
LOG_ERRNO("failed to create socket");
goto err;
}
struct sockaddr_un addr = {.sun_family = AF_UNIX};
if (server_socket_path != NULL) {
strncpy(addr.sun_path, server_socket_path, sizeof(addr.sun_path) - 1);
if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
LOG_ERR("%s: failed to connect (is 'foot --server' running?)", server_socket_path);
goto err;
}
} else {
bool connected = false;
const char *xdg_runtime = getenv("XDG_RUNTIME_DIR");
if (xdg_runtime != NULL) {
const char *wayland_display = getenv("WAYLAND_DISPLAY");
if (wayland_display != NULL) {
snprintf(addr.sun_path, sizeof(addr.sun_path),
"%s/foot-%s.sock", xdg_runtime, wayland_display);
connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0);
}
if (!connected) {
LOG_WARN("%s: failed to connect, will now try %s/foot.sock",
addr.sun_path, xdg_runtime);
snprintf(addr.sun_path, sizeof(addr.sun_path),
"%s/foot.sock", xdg_runtime);
connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0);
}
if (!connected)
LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path);
}
if (!connected) {
strncpy(addr.sun_path, "/tmp/foot.sock", sizeof(addr.sun_path) - 1);
if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) {
LOG_ERRNO("failed to connect (is 'foot --server' running?)");
goto err;
}
}
}
const char *cwd = custom_cwd;
if (cwd == NULL) {
size_t buf_len = 1024;
do {
_cwd = xrealloc(_cwd, buf_len);
errno = 0;
if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) {
LOG_ERRNO("failed to get current working directory");
goto err;
}
buf_len *= 2;
} while (errno == ERANGE);
cwd = _cwd;
}
const char *pwd = getenv("PWD");
if (pwd != NULL) {
char *resolved_path_cwd = realpath(cwd, NULL);
char *resolved_path_pwd = realpath(pwd, NULL);
if (resolved_path_cwd != NULL &&
resolved_path_pwd != NULL &&
streq(resolved_path_cwd, resolved_path_pwd))
{
/*
* The resolved path of $PWD matches the resolved path of
* the *actual* working directory - use $PWD.
*
* This makes a difference when $PWD refers to a symlink.
*/
cwd = pwd;
}
free(resolved_path_cwd);
free(resolved_path_pwd);
}
if (client_environment) {
for (char **e = environ; *e != NULL; e++) {
if (!push_string(&envp, *e, &total_len))
goto err;
}
}
/* String lengths, including NULL terminator */
const size_t cwd_len = strlen(cwd) + 1;
const size_t override_count = tll_length(overrides);
const size_t env_count = tll_length(envp);
const struct client_data data = {
.hold = hold,
.no_wait = no_wait,
.xdga_token = xdga_token,
.token_len = token_len,
.cwd_len = cwd_len,
.override_count = override_count,
.argc = argc,
.env_count = env_count,
};
/* Total packet length, not (yet) including argv[] */
total_len += sizeof(data) + cwd_len + token_len;
/* Add argv[] size to total packet length */
cargv = xmalloc(argc * sizeof(cargv[0]));
for (size_t i = 0; i < argc; i++) {
const size_t arg_len = strlen(argv[i]) + 1;
if (arg_len >= 1 << (8 * sizeof(cargv[i].len))) {
LOG_ERR("argv length overflow");
goto err;
}
cargv[i].len = arg_len;
total_len += sizeof(cargv[i]) + cargv[i].len;
}
/* Check for size overflows */
if (total_len >= 1llu << (8 * sizeof(uint32_t)) ||
cwd_len >= 1 << (8 * sizeof(data.cwd_len)) ||
token_len >= 1 << (8 * sizeof(data.token_len)) ||
override_count > (size_t)(unsigned int)data.override_count ||
argc > (int)(unsigned int)data.argc ||
env_count > (size_t)(unsigned int)data.env_count)
{
LOG_ERR("size overflow");
goto err;
}
/* Send everything except argv[] */
if (sendall(fd, &(uint32_t){total_len}, sizeof(uint32_t)) < 0 ||
sendall(fd, &data, sizeof(data)) < 0 ||
sendall(fd, cwd, cwd_len) < 0)
{
LOG_ERRNO("failed to send setup packet to server");
goto err;
}
/* Send XDGA token, if we have one */
if (xdga_token) {
if (sendall(fd, token, token_len) != token_len)
{
LOG_ERRNO("failed to send xdg activation token to server");
goto err;
}
}
/* Send overrides */
if (!send_string_list(fd, &overrides))
goto err;
/* Send argv[] */
for (size_t i = 0; i < argc; i++) {
if (sendall(fd, &cargv[i], sizeof(cargv[i])) < 0 ||
sendall(fd, argv[i], cargv[i].len) < 0)
{
LOG_ERRNO("failed to send setup packet (argv) to server");
goto err;
}
}
/* Send environment */
if (!send_string_list(fd, &envp))
goto err;
struct sigaction sa_int = {.sa_handler = &sigint_handler};
struct sigaction sa_usr = {.sa_handler = &sigusr_handler};
sigemptyset(&sa_int.sa_mask);
sigemptyset(&sa_usr.sa_mask);
if (sigaction(SIGINT, &sa_int, NULL) < 0 ||
sigaction(SIGTERM, &sa_int, NULL) < 0 ||
sigaction(SIGUSR1, &sa_usr, NULL) < 0 ||
sigaction(SIGUSR2, &sa_usr, NULL) < 0)
{
LOG_ERRNO("failed to register signal handlers");
goto err;
}
int exit_code;
ssize_t rcvd = -1;
while (true) {
rcvd = recv(fd, &exit_code, sizeof(exit_code), 0);
const int got_sigusr = sigusr;
sigusr = 0;
if (rcvd < 0 && errno == EINTR) {
if (aborted)
break;
else if (got_sigusr != 0) {
LOG_DBG("sending sigusr %d to server", got_sigusr);
struct {
struct client_ipc_hdr hdr;
struct client_ipc_sigusr sigusr;
} ipc = {
.hdr = {
.ipc_code = FOOT_IPC_SIGUSR,
.size = sizeof(struct client_ipc_sigusr),
},
.sigusr = {
.signo = got_sigusr,
},
};
ssize_t count = send(fd, &ipc, sizeof(ipc), 0);
if (count < 0) {
LOG_ERRNO("failed to send SIGUSR IPC to server");
goto err;
} else if ((size_t)count != sizeof(ipc)) {
LOG_ERR("failed to send SIGUSR IPC to server");
goto err;
}
}
continue;
}
break;
}
if (rcvd == -1 && errno == EINTR)
xassert(aborted);
else if (rcvd != sizeof(exit_code))
LOG_ERRNO("failed to read server response");
else
ret = exit_code;
err:
free_string_list(&envp);
free_string_list(&overrides);
free(cargv);
free(_cwd);
if (fd != -1)
close(fd);
log_deinit();
return ret;
}

115
commands.c Normal file
View file

@ -0,0 +1,115 @@
#include "commands.h"
#define LOG_MODULE "commands"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "grid.h"
#include "render.h"
#include "selection.h"
#include "terminal.h"
#include "url-mode.h"
#include "util.h"
void
cmd_scrollback_up(struct terminal *term, int rows)
{
if (term->grid == &term->alt)
return;
if (urls_mode_is_active(term))
return;
const struct grid *grid = term->grid;
const int view = grid->view;
const int grid_rows = grid->num_rows;
/* The view row number in scrollback relative coordinates. This is
* the maximum number of rows we're allowed to scroll */
int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows);
int view_sb_rel =
grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view);
rows = min(rows, view_sb_rel);
if (rows == 0)
return;
int new_view = (view + grid_rows) - rows;
new_view &= grid_rows - 1;
xassert(new_view != view);
xassert(grid->rows[new_view] != NULL);
#if defined(_DEBUG)
for (int r = 0; r < term->rows; r++)
xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL);
#endif
LOG_DBG("scrollback UP: %d -> %d (offset = %d, rows = %d)",
view, new_view, offset, grid_rows);
selection_view_up(term, new_view);
term->grid->view = new_view;
if (rows < term->rows) {
term_damage_scroll(
term, DAMAGE_SCROLL_REVERSE_IN_VIEW,
(struct scroll_region){0, term->rows}, rows);
term_damage_rows_in_view(term, 0, rows - 1);
} else
term_damage_view(term);
render_refresh_urls(term);
render_refresh(term);
}
void
cmd_scrollback_down(struct terminal *term, int rows)
{
if (term->grid == &term->alt)
return;
if (urls_mode_is_active(term))
return;
const struct grid *grid = term->grid;
const int offset = grid->offset;
const int view = grid->view;
const int grid_rows = grid->num_rows;
const int screen_rows = term->rows;
const int scrollback_end = offset;
/* Number of rows to scroll, without going past the scrollback end */
int max_rows = 0;
if (view <= scrollback_end)
max_rows = scrollback_end - view;
else
max_rows = offset + (grid_rows - view);
rows = min(rows, max_rows);
if (rows == 0)
return;
int new_view = (view + rows) & (grid_rows - 1);
xassert(new_view != view);
xassert(grid->rows[new_view] != NULL);
#if defined(_DEBUG)
for (int r = 0; r < term->rows; r++)
xassert(grid->rows[(new_view + r) & (grid_rows - 1)] != NULL);
#endif
LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, rows = %d)",
view, new_view, offset, grid_rows);
selection_view_down(term, new_view);
term->grid->view = new_view;
if (rows < term->rows) {
term_damage_scroll(
term, DAMAGE_SCROLL_IN_VIEW,
(struct scroll_region){0, term->rows}, rows);
term_damage_rows_in_view(term, term->rows - rows, screen_rows - 1);
} else
term_damage_view(term);
render_refresh_urls(term);
render_refresh(term);
}

6
commands.h Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include "terminal.h"
void cmd_scrollback_up(struct terminal *term, int rows);
void cmd_scrollback_down(struct terminal *term, int rows);

90
completions/bash/foot Normal file
View file

@ -0,0 +1,90 @@
# Bash completion script for foot
_foot()
{
COMPREPLY=()
local cur prev flags word commands match previous_words i offset
flags=(
"--app-id"
"--toplevel-tag"
"--check-config"
"--config"
"--font"
"--fullscreen"
"--help"
"--hold"
"--log-colorize"
"--log-level"
"--log-no-syslog"
"--login-shell"
"--maximized"
"--override"
"--print-pid"
"--pty"
"--server"
"--term"
"--title"
"--version"
"--window-size-pixels"
"--window-size-chars"
"--working-directory"
)
flags="${flags[@]}"
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
# Check if positional argument is completed
previous_words=( "${COMP_WORDS[@]}" )
unset previous_words[-1]
commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|foot)$' | sort -u)
i=0
for word in "${previous_words[@]}" ; do
match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null)
if [[ ! -z "$match" ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
(( i++ ))
continue
fi
# Positional argument found
offset=$i
fi
(( i++ ))
done
if [[ ! -z "$offset" ]] ; then
# Depends on bash_completion being available
declare -F _command_offset >/dev/null || return 1
_command_offset $offset
return 0
elif [[ ${cur} == --* ]] ; then
COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) )
return 0
fi
case "$prev" in
--config|--print-pid|--server|-[cps])
compopt -o default ;;
--working-directory|-D)
compopt -o dirnames ;;
--term|-t)
command -v toe > /dev/null || return 1
COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 !~ /[+]/ {print $1}')" -- ${cur}) ) ;;
--font|-f)
command -v fc-list > /dev/null || return 1
COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;;
--log-level|-d)
COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;;
--log-colorize|-l)
COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;;
--app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC])
# Don't autocomplete for these flags
: ;;
*)
# Complete commands from $PATH
COMPREPLY=( $(compgen -c -- ${cur}) ) ;;
esac
return 0
}
complete -F _foot foot

View file

@ -0,0 +1,82 @@
# Bash completion script for footclient
_footclient()
{
COMPREPLY=()
local cur prev flags word commands match previous_words i offset
flags=(
"--app-id"
"--toplevel-tag"
"--fullscreen"
"--help"
"--hold"
"--login-shell"
"--log-level"
"--log-colorize"
"--maximized"
"--override"
"--client-environment"
"--server-socket"
"--term"
"--title"
"--version"
"--window-size-pixels"
"--window-size-chars"
"--working-directory"
)
flags="${flags[@]}"
cur=${COMP_WORDS[COMP_CWORD]}
prev=${COMP_WORDS[COMP_CWORD-1]}
# Check if positional argument is completed
previous_words=( "${COMP_WORDS[@]}" )
unset previous_words[-1]
commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|footclient)$' | sort -u)
i=0
for word in "${previous_words[@]}" ; do
match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null)
if [[ ! -z "$match" ]] ; then
if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then
(( i++ ))
continue
fi
# Positional argument found
offset=$i
fi
(( i++ ))
done
if [[ ! -z "$offset" ]] ; then
# Depends on bash_completion being available
declare -F _command_offset >/dev/null || return 1
_command_offset $offset
return 0
elif [[ ${cur} == --* ]] ; then
COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) )
return 0
fi
case "$prev" in
--server-socket|-s)
compopt -o default ;;
--working-directory|-D)
compopt -o dirnames ;;
--term|-t)
command -v toe > /dev/null || return 1
COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;;
--log-level|-d)
COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;;
--log-colorize|-l)
COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;;
--app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw])
# Don't autocomplete for these flags
: ;;
*)
# Complete commands from $PATH
COMPREPLY=( $(compgen -c -- ${cur}) ) ;;
esac
return 0
}
complete -F _footclient footclient

View file

@ -0,0 +1,24 @@
complete -c foot -x -a "(__fish_complete_subcommand)"
complete -c foot -r -s c -l config -d "path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)"
complete -c foot -s C -l check-config -d "verify configuration and exit with 0 if ok, otherwise exit with 1"
complete -c foot -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE"
complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/\n/g' | sort | uniq)" -d "font name and style in fontconfig format (monospace)"
complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)"
complete -c foot -x -s T -l title -d "initial window title"
complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)"
complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to"
complete -c foot -s m -l maximized -d "start in maximized mode"
complete -c foot -s F -l fullscreen -d "start in fullscreen mode"
complete -c foot -s L -l login-shell -d "start shell as a login shell"
complete -c foot -F -s D -l working-directory -d "initial working directory for the client application (CWD)"
complete -c foot -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)"
complete -c foot -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)"
complete -c foot -F -s s -l server -d "run as server; open terminals by running footclient"
complete -c foot -s H -l hold -d "remain open after child process exits"
complete -c foot -r -s p -l print-pid -d "print PID to this file or FD when up and running (server mode only)"
complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)"
complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr"
complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)"
complete -c foot -r -l pty -d "display an existing pty instead of creating one"
complete -c foot -s v -l version -d "show the version number and quit"
complete -c foot -s h -l help -d "show help message and quit"

View file

@ -0,0 +1,20 @@
complete -c footclient -x -a "(__fish_complete_subcommand)"
complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)"
complete -c footclient -x -s T -l title -d "initial window title"
complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)"
complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to"
complete -c footclient -s m -l maximized -d "start in maximized mode"
complete -c footclient -s F -l fullscreen -d "start in fullscreen mode"
complete -c footclient -s L -l login-shell -d "start shell as a login shell"
complete -c footclient -F -s D -l working-directory -d "initial working directory for the client application (CWD)"
complete -c footclient -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)"
complete -c footclient -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)"
complete -c footclient -F -s s -l server-socket -d "override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)"
complete -c footclient -s H -l hold -d "remain open after child process exits"
complete -c footclient -s N -l no-wait -d "detach the client process from the running terminal, exiting immediately"
complete -c footclient -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE"
complete -c footclient -s E -l client-environment -d "child process inherits footclient's environment, instead of the server's"
complete -c footclient -x -s d -l log-level -a "info warning error none" -d "log-level (info)"
complete -c footclient -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr"
complete -c footclient -s v -l version -d "show the version number and quit"
complete -c footclient -s h -l help -d "show help message and quit"

9
completions/meson.build Normal file
View file

@ -0,0 +1,9 @@
zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions')
fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d')
bash_install_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions')
install_data('zsh/_foot', install_dir: zsh_install_dir)
install_data('zsh/_footclient', install_dir: zsh_install_dir)
install_data('fish/foot.fish', install_dir: fish_install_dir)
install_data('fish/footclient.fish', install_dir: fish_install_dir)
install_data('bash/foot', install_dir: bash_install_dir)
install_data('bash/footclient', install_dir: bash_install_dir)

41
completions/zsh/_foot Normal file
View file

@ -0,0 +1,41 @@
#compdef foot
_arguments \
-s -S -C \
'(-c --config)'{-c,--config}'[path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)]:config:_files' \
'(-C --check-config)'{-C,--check-config}'[verify configuration and exit with 0 if ok, otherwise exit with 1]' \
'(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \
'(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \
'(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \
'(-T --title)'{-T,--title}'[initial window title]:()' \
'(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \
'--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \
'(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \
'(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \
'(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \
'(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \
'(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \
'(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \
'(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \
'(-H --hold)'{-H,--hold}'[remain open after child process exits]' \
'(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \
'--pty=[display an existing pty instead of creating one]:pty:_files' \
'(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \
'(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \
'(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \
'(-v --version)'{-v,--version}'[show the version number and quit]' \
'(-h --help)'{-h,--help}'[show help message and quit]' \
':command: _command_names -e' \
'*::command arguments: _dispatch ${words[1]} ${words[1]}'
case ${state} in
fonts)
IFS=$'\n'
_values -s , 'font families' $(fc-list : family | sed 's/,/\n/g' | sort | uniq)
unset IFS
;;
terms)
_values 'terminal definitions' /usr/share/terminfo/**/*(.:t)
;;
esac

View file

@ -0,0 +1,31 @@
#compdef footclient
_arguments \
-s -S -C \
'(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \
'(-T --title)'{-T,--title}'[initial window title]:()' \
'(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \
'--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \
'(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \
'(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \
'(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \
'(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \
'(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \
'(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \
'(-s --server-socket)'{-s,--server-socket}'[override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)]:server:_files' \
'(-H --hold)'{-H,--hold}'[remain open after child process exits]' \
'(-N --no-wait)'{-N,--no-wait}'[detach the client process from the running terminal, exiting immediately]' \
'(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \
'(-E --client-environment)'{-E,--client-environment}"[child process inherits footclient's environment, instead of the server's]" \
'(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \
'(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \
'(-v --version)'{-v,--version}'[show the version number and quit]' \
'(-h --help)'{-h,--help}'[show help message and quit]' \
':command: _command_names -e' \
'*::command arguments: _dispatch ${words[1]} ${words[1]}'
case ${state} in
terms)
_values 'terminal definitions' /usr/share/terminfo/**/*(.:t)
;;
esac

149
composed.c Normal file
View file

@ -0,0 +1,149 @@
#include "composed.h"
#include <stdlib.h>
#include <stdbool.h>
#include "debug.h"
#include "terminal.h"
uint32_t
composed_key_from_chars(const uint32_t chars[], size_t count)
{
if (count == 0)
return 0;
uint32_t key = chars[0];
for (size_t i = 1; i < count; i++)
key = composed_key_from_key(key, chars[i]);
return key;
}
uint32_t
composed_key_from_key(uint32_t prev_key, uint32_t next_char)
{
unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO);
/* Rotate old key 8 bits */
uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8));
/* xor with new char */
new_key ^= next_char;
/* Multiply with magic hash constant */
new_key *= 2654435761ul;
/* And mask, to ensure the new value is within range */
new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO;
return new_key;
}
UNITTEST
{
const char32_t chars[] = U"abcdef";
uint32_t k1 = composed_key_from_key(chars[0], chars[1]);
uint32_t k2 = composed_key_from_chars(chars, 2);
xassert(k1 == k2);
uint32_t k3 = composed_key_from_key(k2, chars[2]);
uint32_t k4 = composed_key_from_chars(chars, 3);
xassert(k3 == k4);
}
const struct composed *
composed_lookup(struct composed *root, uint32_t key)
{
struct composed *node = root;
while (node != NULL) {
if (key == node->key)
return node;
node = key < node->key ? node->left : node->right;
}
return NULL;
}
const struct composed *
composed_lookup_without_collision(struct composed *root, uint32_t *key,
const char32_t *prefix_text, size_t prefix_len,
char32_t wc, int forced_width)
{
while (true) {
const struct composed *cc = composed_lookup(root, *key);
if (cc == NULL)
return NULL;
bool match = cc->count == prefix_len + 1 &&
cc->forced_width == forced_width &&
cc->chars[prefix_len] == wc;
if (match) {
for (size_t i = 0; i < prefix_len; i++) {
if (cc->chars[i] != prefix_text[i]) {
match = false;
break;
}
}
}
if (match)
return cc;
(*key)++;
*key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO;
/* TODO: this will loop infinitely if the composed table is full */
}
return NULL;
}
void
composed_insert(struct composed **root, struct composed *node)
{
node->left = node->right = NULL;
if (*root == NULL) {
*root = node;
return;
}
uint32_t key = node->key;
struct composed *prev = NULL;
struct composed *n = *root;
while (n != NULL) {
xassert(n->key != node->key);
prev = n;
n = key < n->key ? n->left : n->right;
}
xassert(prev != NULL);
xassert(n == NULL);
if (key < prev->key) {
xassert(prev->left == NULL);
prev->left = node;
} else {
xassert(prev->right == NULL);
prev->right = node;
}
}
void
composed_free(struct composed *root)
{
if (root == NULL)
return;
composed_free(root->left);
composed_free(root->right);
free(root->chars);
free(root);
}

25
composed.h Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include <stdint.h>
#include <uchar.h>
struct composed {
char32_t *chars;
struct composed *left;
struct composed *right;
uint32_t key;
uint8_t count;
uint8_t width;
uint8_t forced_width;
};
uint32_t composed_key_from_chars(const uint32_t chars[], size_t count);
uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char);
const struct composed *composed_lookup(struct composed *root, uint32_t key);
const struct composed *composed_lookup_without_collision(
struct composed *root, uint32_t *key,
const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width);
void composed_insert(struct composed **root, struct composed *node);
void composed_free(struct composed *root);

4366
config.c Normal file

File diff suppressed because it is too large Load diff

523
config.h Normal file
View file

@ -0,0 +1,523 @@
#pragma once
#include <regex.h>
#include <stdbool.h>
#include <stdint.h>
#include <uchar.h>
#include <xkbcommon/xkbcommon.h>
#include <tllist.h>
#include <fcft/fcft.h>
#include "user-notification.h"
#define DEFINE_LIST(type) \
type##_list { \
size_t count; \
type *arr; \
}
/* If px != 0 then px is valid, otherwise pt is valid */
struct pt_or_px {
int16_t px;
float pt;
};
struct font_size_adjustment {
struct pt_or_px pt_or_px;
float percent;
};
enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM, CURSOR_HOLLOW };
enum cursor_unfocused_style {
CURSOR_UNFOCUSED_UNCHANGED,
CURSOR_UNFOCUSED_HOLLOW,
CURSOR_UNFOCUSED_NONE
};
enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS};
struct config_font {
char *pattern;
float pt_size;
int px_size;
};
DEFINE_LIST(struct config_font);
#if 0
struct config_key_modifiers {
bool shift;
bool alt;
bool ctrl;
bool super;
};
#endif
struct argv {
char **args;
};
enum binding_aux_type {
BINDING_AUX_NONE,
BINDING_AUX_PIPE,
BINDING_AUX_TEXT,
BINDING_AUX_REGEX,
};
struct binding_aux {
enum binding_aux_type type;
bool master_copy;
union {
struct argv pipe;
struct {
uint8_t *data;
size_t len;
} text;
char *regex_name;
};
};
enum key_binding_type {
KEY_BINDING,
MOUSE_BINDING,
};
typedef tll(char *) config_modifier_list_t;
struct config_key_binding {
int action; /* One of the various bind_action_* enums from wayland.h */
//struct config_key_modifiers modifiers;
config_modifier_list_t modifiers;
union {
/* Key bindings */
struct {
xkb_keysym_t sym;
} k;
/* Mouse bindings */
struct {
int button;
int count;
} m;
};
struct binding_aux aux;
/* For error messages in collision handling */
const char *path;
int lineno;
};
DEFINE_LIST(struct config_key_binding);
typedef tll(char *) config_override_t;
struct config_spawn_template {
struct argv argv;
};
struct env_var {
char *name;
char *value;
};
typedef tll(struct env_var) env_var_list_t;
struct custom_regex {
char *name;
char *regex;
regex_t preg;
struct config_spawn_template launch;
};
struct color_theme {
uint32_t fg;
uint32_t bg;
uint32_t flash;
uint32_t flash_alpha;
uint32_t table[256];
uint16_t alpha;
uint32_t selection_fg;
uint32_t selection_bg;
uint32_t url;
uint32_t dim[8];
uint32_t sixel[16];
enum {
DIM_BLEND_TOWARDS_BLACK,
DIM_BLEND_TOWARDS_WHITE,
} dim_blend_towards;
enum {
ALPHA_MODE_DEFAULT,
ALPHA_MODE_MATCHING,
ALPHA_MODE_ALL
} alpha_mode;
struct {
uint32_t text;
uint32_t cursor;
} cursor;
struct {
uint32_t fg;
uint32_t bg;
} jump_label;
struct {
uint32_t fg;
uint32_t bg;
} scrollback_indicator;
struct {
struct {
uint32_t fg;
uint32_t bg;
} no_match;
struct {
uint32_t fg;
uint32_t bg;
} match;
} search_box;
struct {
bool cursor:1;
bool jump_label:1;
bool scrollback_indicator:1;
bool url:1;
bool search_box_no_match:1;
bool search_box_match:1;
uint8_t dim;
} use_custom;
bool blur;
};
enum which_color_theme {
COLOR_THEME_DARK,
COLOR_THEME_LIGHT,
COLOR_THEME_1, /* Deprecated */
COLOR_THEME_2, /* Deprecated */
};
enum shm_bit_depth {
SHM_BITS_AUTO,
SHM_BITS_8,
SHM_BITS_10,
SHM_BITS_16,
};
enum center_when {
CENTER_INVALID,
CENTER_NEVER,
CENTER_FULLSCREEN,
CENTER_MAXIMIZED_AND_FULLSCREEN,
CENTER_ALWAYS,
};
enum underline_style {
UNDERLINE_NONE,
UNDERLINE_SINGLE, /* Legacy underline */
UNDERLINE_DOUBLE,
UNDERLINE_CURLY,
UNDERLINE_DOTTED,
UNDERLINE_DASHED,
};
struct config {
char *conf_path;
char *term;
char *shell;
char *title;
char *app_id;
char *toplevel_tag;
char32_t *word_delimiters;
bool login_shell;
bool locked_title;
struct {
enum conf_size_type type;
uint32_t width;
uint32_t height;
} size;
unsigned pad_left;
unsigned pad_top;
unsigned pad_right;
unsigned pad_bottom;
enum center_when center_when;
bool resize_by_cells;
bool resize_keep_grid;
uint16_t resize_delay_ms;
struct {
float amount;
} dim;
struct {
bool enabled;
bool palette_based;
float amount;
} bold_in_bright;
enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode;
bool dpi_aware;
bool gamma_correct;
bool uppercase_regex_insert;
struct config_font_list fonts[4];
struct font_size_adjustment font_size_adjustment;
/* Custom font metrics (-1 = use real font metrics) */
struct pt_or_px line_height;
struct pt_or_px letter_spacing;
/* Adjusted letter x/y offsets */
struct pt_or_px horizontal_letter_offset;
struct pt_or_px vertical_letter_offset;
bool use_custom_underline_offset;
struct pt_or_px underline_offset;
struct pt_or_px underline_thickness;
struct pt_or_px strikeout_thickness;
bool box_drawings_uses_font_glyphs;
bool can_shape_grapheme;
struct {
enum {
OSC52_DISABLED,
OSC52_COPY_ENABLED,
OSC52_PASTE_ENABLED,
OSC52_ENABLED,
} osc52;
} security;
struct {
bool urgent;
bool notify;
bool flash;
bool system_bell;
struct config_spawn_template command;
bool command_focused;
} bell;
struct {
uint32_t lines;
struct {
enum {
SCROLLBACK_INDICATOR_POSITION_NONE,
SCROLLBACK_INDICATOR_POSITION_FIXED,
SCROLLBACK_INDICATOR_POSITION_RELATIVE
} position;
enum {
SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE,
SCROLLBACK_INDICATOR_FORMAT_LINENO,
SCROLLBACK_INDICATOR_FORMAT_TEXT,
} format;
char32_t *text;
} indicator;
float multiplier;
} scrollback;
struct {
char32_t *label_letters;
struct config_spawn_template launch;
enum {
OSC8_UNDERLINE_URL_MODE,
OSC8_UNDERLINE_ALWAYS,
} osc8_underline;
enum underline_style style;
char *regex;
regex_t preg;
} url;
tll(struct custom_regex) custom_regexes;
struct color_theme colors_dark;
struct color_theme colors_light;
enum which_color_theme initial_color_theme;
struct {
enum cursor_style style;
enum cursor_unfocused_style unfocused_style;
struct {
bool enabled;
uint32_t rate_ms;
} blink;
struct pt_or_px beam_thickness;
struct pt_or_px underline_thickness;
} cursor;
struct {
bool hide_when_typing;
bool alternate_scroll_mode;
//struct config_key_modifiers selection_override_modifiers;
config_modifier_list_t selection_override_modifiers;
} mouse;
struct {
/* Bindings for "normal" mode */
struct config_key_binding_list key;
struct config_key_binding_list mouse;
/*
* Special modes
*/
/* While searching (not - action to *start* a search is in the
* 'key' bindings above */
struct config_key_binding_list search;
/* While showing URL jump labels */
struct config_key_binding_list url;
} bindings;
struct {
enum { CONF_CSD_PREFER_NONE, CONF_CSD_PREFER_SERVER, CONF_CSD_PREFER_CLIENT } preferred;
uint16_t title_height;
uint16_t border_width;
uint16_t border_width_visible;
uint16_t button_width;
bool hide_when_maximized;
bool double_click_to_maximize;
struct {
bool title_set:1;
bool buttons_set:1;
bool minimize_set:1;
bool maximize_set:1;
bool close_set:1;
bool border_set:1;
uint32_t title;
uint32_t buttons;
uint32_t minimize;
uint32_t maximize;
uint32_t quit; /* 'close' collides with #define in epoll-shim */
uint32_t border;
} color;
struct config_font_list font;
} csd;
uint16_t render_worker_count;
char *server_socket_path;
bool presentation_timings;
bool hold_at_exit;
enum {
SELECTION_TARGET_NONE,
SELECTION_TARGET_PRIMARY,
SELECTION_TARGET_CLIPBOARD,
SELECTION_TARGET_BOTH
} selection_target;
struct {
struct config_spawn_template command;
struct config_spawn_template command_action_arg;
struct config_spawn_template close;
bool inhibit_when_focused;
} desktop_notifications;
env_var_list_t env_vars;
char *utmp_helper_path;
struct {
enum fcft_scaling_filter fcft_filter;
bool overflowing_glyphs;
bool grapheme_shaping;
enum {
GRAPHEME_WIDTH_WCSWIDTH,
GRAPHEME_WIDTH_DOUBLE,
GRAPHEME_WIDTH_MAX,
} grapheme_width_method;
enum {
RENDER_TIMER_NONE,
RENDER_TIMER_OSD,
RENDER_TIMER_LOG,
RENDER_TIMER_BOTH
} render_timer;
bool damage_whole_window;
uint32_t delayed_render_lower_ns;
uint32_t delayed_render_upper_ns;
off_t max_shm_pool_size;
float box_drawing_base_thickness;
bool box_drawing_solid_shades;
bool font_monospace_warn;
bool sixel;
enum shm_bit_depth surface_bit_depth;
uint32_t min_stride_alignment;
bool preapply_damage;
} tweak;
struct {
uint32_t long_press_delay;
} touch;
struct {
bool enabled;
enum {
CONF_TABS_POSITION_TOP,
CONF_TABS_POSITION_BOTTOM,
} position;
enum {
CONF_TABS_STYLE_ROUNDED,
CONF_TABS_STYLE_SQUARE,
} style;
enum {
CONF_TABS_LAYOUT_SPAN,
CONF_TABS_LAYOUT_FLOATING,
} layout;
uint16_t height; /* pill height; bar = height + margin in floating mode */
uint16_t tab_width; /* max tab width in floating mode */
uint16_t tab_padding; /* gap between tabs in floating mode */
uint16_t label_padding; /* horizontal padding around the label inside each tab pill */
uint16_t margin; /* edge gap in floating mode (added to bar height) */
uint16_t corner_radius;
bool inherit_cwd;
char *unread_indicator; /* UTF-8 string drawn before label when unread; NULL or empty disables */
struct {
uint32_t bg;
uint32_t fg;
uint32_t active_bg;
uint32_t active_fg;
uint32_t unread_fg;
} colors;
} tabs;
user_notifications_t notifications;
};
bool config_override_apply(struct config *conf, config_override_t *overrides,
bool errors_are_fatal);
bool config_load(
struct config *conf, const char *path,
user_notifications_t *initial_user_notifications,
config_override_t *overrides, bool errors_are_fatal,
bool as_server);
void config_free(struct config *conf);
struct config *config_clone(const struct config *old);
bool config_font_parse(const char *pattern, struct config_font *font);
void config_font_list_destroy(struct config_font_list *font_list);
#if 0
struct seat;
xkb_mod_mask_t
conf_modifiers_to_mask(
const struct seat *seat, const struct config_key_modifiers *modifiers);
#endif
bool check_if_font_is_monospaced(
const char *pattern, user_notifications_t *notifications);

2234
csi.c Normal file

File diff suppressed because it is too large Load diff

6
csi.h Normal file
View file

@ -0,0 +1,6 @@
#pragma once
#include <stdbool.h>
#include "terminal.h"
void csi_dispatch(struct terminal *term, uint8_t final);

130
cursor-shape.c Normal file
View file

@ -0,0 +1,130 @@
#include <stdlib.h>
#include <string.h>
#define LOG_MODULE "cursor-shape"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "cursor-shape.h"
#include "debug.h"
#include "util.h"
const char *const *
cursor_shape_to_string(enum cursor_shape shape)
{
static const char *const table[][CURSOR_SHAPE_COUNT]= {
[CURSOR_SHAPE_NONE] = {NULL},
[CURSOR_SHAPE_HIDDEN] = {"hidden", NULL},
[CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL},
[CURSOR_SHAPE_POINTER] = {"pointer", "hand1", NULL},
[CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL},
[CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL},
[CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL},
[CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = {"sw-resize", "bottom_left_corner", NULL},
[CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = {"se-resize", "bottom_right_corner", NULL},
[CURSOR_SHAPE_LEFT_SIDE] = {"w-resize", "left_side", NULL},
[CURSOR_SHAPE_RIGHT_SIDE] = {"e-resize", "right_side", NULL},
[CURSOR_SHAPE_TOP_SIDE] = {"n-resize", "top_side", NULL},
[CURSOR_SHAPE_BOTTOM_SIDE] = {"s-resize", "bottom_side", NULL},
};
xassert(shape <= ALEN(table));
return table[shape];
}
enum wp_cursor_shape_device_v1_shape
cursor_shape_to_server_shape(enum cursor_shape shape)
{
static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = {
[CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT,
[CURSOR_SHAPE_POINTER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER,
[CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT,
[CURSOR_SHAPE_TOP_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE,
[CURSOR_SHAPE_TOP_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE,
[CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE,
[CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE,
[CURSOR_SHAPE_LEFT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE,
[CURSOR_SHAPE_RIGHT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE,
[CURSOR_SHAPE_TOP_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE,
[CURSOR_SHAPE_BOTTOM_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE,
};
xassert(shape <= ALEN(table));
xassert(table[shape] != 0);
return table[shape];
}
enum wp_cursor_shape_device_v1_shape
cursor_string_to_server_shape(const char *xcursor, int bound_version)
{
if (xcursor == NULL)
return 0;
static const char *const table[][2] = {
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT] = {"default", "left_ptr"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CONTEXT_MENU] = {"context-menu"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP] = {"help", "question_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER] = {"pointer", "hand"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS] = {"progress", "left_ptr_watch"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT] = {"wait", "watch"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CELL] = {"cell"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR] = {"crosshair", "cross"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT] = {"text", "xterm"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_VERTICAL_TEXT] = {"vertical-text"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALIAS] = {"alias", "dnd-link"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY] = {"copy", "dnd-copy"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move", "dnd-move"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP] = {"no-drop", "dnd-no-drop"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED] = {"not-allowed", "crossed_circle"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB] = {"grab", "hand1"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING] = {"grabbing"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE] = {"e-resize", "right_side"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE] = {"n-resize", "top_side"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE] = {"ne-resize", "top_right_corner"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE] = {"nw-resize", "top_left_corner"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE] = {"s-resize", "bottom_side"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE] = {"se-resize", "bottom_right_corner"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE] = {"sw-resize", "bottom_left_corner"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE] = {"w-resize", "left_side"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE] = {"ew-resize", "sb_h_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE] = {"ns-resize", "sb_v_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE] = {"nesw-resize", "fd_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE] = {"nwse-resize", "bd_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE] = {"col-resize", "sb_h_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE] = {"row-resize", "sb_v_double_arrow"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL] = {"all-scroll", "fleur"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN] = {"zoom-in"},
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT] = {"zoom-out"},
#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK] = {"dnd-ask"},
#endif
#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) /* 1.42 */
[WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE] = {"all-resize"},
#endif
};
for (size_t i = 0; i < ALEN(table); i++) {
#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION)
if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK &&
bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION)
{
continue;
}
#endif
#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION)
if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE &&
bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION)
{
continue;
}
#endif
for (size_t j = 0; j < ALEN(table[i]); j++) {
if (table[i][j] != NULL && streq(xcursor, table[i][j])) {
return i;
}
}
}
return 0;
}

30
cursor-shape.h Normal file
View file

@ -0,0 +1,30 @@
#pragma once
#include <cursor-shape-v1.h>
enum cursor_shape {
CURSOR_SHAPE_NONE,
CURSOR_SHAPE_CUSTOM,
CURSOR_SHAPE_HIDDEN,
CURSOR_SHAPE_LEFT_PTR,
CURSOR_SHAPE_POINTER,
CURSOR_SHAPE_TEXT,
CURSOR_SHAPE_TOP_LEFT_CORNER,
CURSOR_SHAPE_TOP_RIGHT_CORNER,
CURSOR_SHAPE_BOTTOM_LEFT_CORNER,
CURSOR_SHAPE_BOTTOM_RIGHT_CORNER,
CURSOR_SHAPE_LEFT_SIDE,
CURSOR_SHAPE_RIGHT_SIDE,
CURSOR_SHAPE_TOP_SIDE,
CURSOR_SHAPE_BOTTOM_SIDE,
CURSOR_SHAPE_COUNT,
};
const char *const *cursor_shape_to_string(enum cursor_shape shape);
enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape(
enum cursor_shape shape);
enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape(
const char *xcursor, int bound_version);

533
dcs.c Normal file
View file

@ -0,0 +1,533 @@
#include "dcs.h"
#include <string.h>
#define LOG_MODULE "dcs"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "foot-terminfo.h"
#include "sixel.h"
#include "util.h"
#include "vt.h"
#include "xmalloc.h"
#include "xsnprintf.h"
static bool
ensure_size(struct terminal *term, size_t required_size)
{
if (required_size <= term->vt.dcs.size)
return true;
uint8_t *new_data = realloc(term->vt.dcs.data, required_size);
if (new_data == NULL) {
LOG_ERRNO("failed to increase size of DCS buffer");
return false;
}
term->vt.dcs.data = new_data;
term->vt.dcs.size = required_size;
return true;
}
/* Decode hex-encoded string *inline*. NULL terminates */
static char *
hex_decode(const char *s, size_t len)
{
if (len % 2)
return NULL;
char *hex = xmalloc(len / 2 + 1);
char *o = hex;
/* TODO: error checking */
for (size_t i = 0; i < len; i += 2) {
uint8_t nib1 = hex2nibble(*s); s++;
uint8_t nib2 = hex2nibble(*s); s++;
if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID)
goto err;
*o = nib1 << 4 | nib2; o++;
}
*o = '\0';
return hex;
err:
free(hex);
return NULL;
}
UNITTEST
{
/* Verify table is sorted */
const char *p = terminfo_capabilities;
size_t left = sizeof(terminfo_capabilities);
const char *last_cap = NULL;
while (left > 0) {
const char *cap = p;
const char *val = cap + strlen(cap) + 1;
size_t size = strlen(cap) + 1 + strlen(val) + 1;;
xassert(size <= left);
p += size;
left -= size;
if (last_cap != NULL)
xassert(strcmp(last_cap, cap) < 0);
last_cap = cap;
}
}
static bool
lookup_capability(const char *name, const char **value)
{
const char *p = terminfo_capabilities;
size_t left = sizeof(terminfo_capabilities);
while (left > 0) {
const char *cap = p;
const char *val = cap + strlen(cap) + 1;
size_t size = strlen(cap) + 1 + strlen(val) + 1;;
xassert(size <= left);
p += size;
left -= size;
int r = strcmp(cap, name);
if (r == 0) {
*value = val;
return true;
} else if (r > 0)
break;
}
*value = NULL;
return false;
}
static void
xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len)
{
char *name = hex_decode(hex_cap_name, len);
if (name == NULL) {
LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability");
return;
}
const char *value;
bool valid_capability = lookup_capability(name, &value);
xassert(!valid_capability || value != NULL);
LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s",
name, (int)len, hex_cap_name,
valid_capability ? value : "<invalid>");
if (!valid_capability)
goto err;
if (value[0] == '\0') {
/* Boolean */
term_to_slave(term, "\033P1+r", 5);
term_to_slave(term, hex_cap_name, len);
term_to_slave(term, "\033\\", 2);
goto out;
}
/*
* Reply format:
* \EP 1 + r cap=value \E\\
* Where 'cap' and 'value are hex encoded ascii strings
*/
char *reply = xmalloc(
5 + /* DCS 1 + r (\EP1+r) */
len + /* capability name, hex encoded */
1 + /* '=' */
strlen(value) * 2 + /* capability value, hex encoded */
2 + /* ST (\E\\) */
1);
int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name);
for (const char *c = value; *c != '\0'; c++) {
uint8_t nib1 = (uint8_t)*c >> 4;
uint8_t nib2 = (uint8_t)*c & 0xf;
reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++;
reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++;
}
reply[idx] = '\033'; idx++;
reply[idx] = '\\'; idx++;
term_to_slave(term, reply, idx);
free(reply);
goto out;
err:
term_to_slave(term, "\033P0+r", 5);
term_to_slave(term, hex_cap_name, len);
term_to_slave(term, "\033\\", 2);
out:
free(name);
}
static void
xtgettcap_put(struct terminal *term, uint8_t c)
{
struct vt *vt = &term->vt;
/* Grow buffer expontentially */
if (vt->dcs.idx >= vt->dcs.size) {
size_t new_size = vt->dcs.size * 2;
if (new_size == 0)
new_size = 128;
if (!ensure_size(term, new_size))
return;
}
vt->dcs.data[vt->dcs.idx++] = c;
}
static void
xtgettcap_unhook(struct terminal *term)
{
size_t left = term->vt.dcs.idx;
const char *const end = (const char *)&term->vt.dcs.data[left];
const char *p = (const char *)term->vt.dcs.data;
if (p == NULL) {
/* Request is empty; send an error reply, without any capabilities */
term_to_slave(term, "\033P0+r\033\\", 7);
return;
}
while (true) {
const char *sep = memchr(p, ';', left);
size_t cap_len;
if (sep == NULL) {
/* Last capability */
cap_len = end - p;
} else {
cap_len = sep - p;
}
xtgettcap_reply(term, p, cap_len);
left -= cap_len + 1;
p += cap_len + 1;
if (sep == NULL)
break;
}
}
static void NOINLINE
append_sgr_attr_n(char **reply, size_t *len, const char *attr, size_t n)
{
size_t new_len = *len + n + 1;
*reply = xrealloc(*reply, new_len);
memcpy(&(*reply)[*len], attr, n);
(*reply)[new_len - 1] = ';';
*len = new_len;
}
static void
decrqss_put(struct terminal *term, uint8_t c)
{
/* Largest request we support is two bytes */
if (!ensure_size(term, 2))
return;
struct vt *vt = &term->vt;
if (vt->dcs.idx >= 2)
return;
vt->dcs.data[vt->dcs.idx++] = c;
}
static void
decrqss_unhook(struct terminal *term)
{
const uint8_t *query = term->vt.dcs.data;
const size_t n = term->vt.dcs.idx;
/*
* A note on the Ps parameter in the reply: many DEC manual
* instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim
* that 0 means "request is valid", and 1 means "request is
* invalid".
*
* However, this appears to be a typo; actual hardware inverts the
* response (as does XTerm and mlterm):
* https://github.com/hackerb9/vt340test/issues/13
*/
if (n == 1 && query[0] == 'r') {
/* DECSTBM - Set Top and Bottom Margins */
char reply[64];
size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\",
term->scroll_region.start + 1,
term->scroll_region.end);
term_to_slave(term, reply, len);
}
else if (n == 1 && query[0] == 'm') {
/* SGR - Set Graphic Rendition */
char *reply = NULL;
size_t len = 0;
#define append_sgr_attr(num_as_str) \
append_sgr_attr_n(&reply, &len, num_as_str, sizeof(num_as_str) - 1)
/* Always present, both in the example from the VT510 manual
* (https://vt100.net/docs/vt510-rm/DECRPSS), and in XTerm and
* mlterm */
append_sgr_attr("0");
struct attributes *a = &term->vt.attrs;
if (a->bold)
append_sgr_attr("1");
if (a->dim)
append_sgr_attr("2");
if (a->italic)
append_sgr_attr("3");
if (a->underline) {
if (term->vt.underline.style > UNDERLINE_SINGLE) {
char value[4];
size_t val_len =
xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style);
append_sgr_attr_n(&reply, &len, value, val_len);
} else
append_sgr_attr("4");
}
if (a->blink)
append_sgr_attr("5");
if (a->reverse)
append_sgr_attr("7");
if (a->conceal)
append_sgr_attr("8");
if (a->strikethrough)
append_sgr_attr("9");
switch (a->fg_src) {
case COLOR_DEFAULT:
break;
case COLOR_BASE16: {
char value[4];
size_t val_len = xsnprintf(
value, sizeof(value), "%u",
a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
case COLOR_BASE256: {
char value[16];
size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
case COLOR_RGB: {
uint8_t r = a->fg >> 16;
uint8_t g = a->fg >> 8;
uint8_t b = a->fg >> 0;
char value[32];
size_t val_len = xsnprintf(
value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
}
switch (a->bg_src) {
case COLOR_DEFAULT:
break;
case COLOR_BASE16: {
char value[4];
size_t val_len = xsnprintf(
value, sizeof(value), "%u",
a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
case COLOR_BASE256: {
char value[16];
size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
case COLOR_RGB: {
uint8_t r = a->bg >> 16;
uint8_t g = a->bg >> 8;
uint8_t b = a->bg >> 0;
char value[32];
size_t val_len = xsnprintf(
value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
}
switch (term->vt.underline.color_src) {
case COLOR_DEFAULT:
case COLOR_BASE16:
break;
case COLOR_BASE256: {
char value[16];
size_t val_len = xsnprintf(
value, sizeof(value), "58:5:%u", term->vt.underline.color);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
case COLOR_RGB: {
uint8_t r = term->vt.underline.color >> 16;
uint8_t g = term->vt.underline.color >> 8;
uint8_t b = term->vt.underline.color >> 0;
char value[32];
size_t val_len = xsnprintf(
value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b);
append_sgr_attr_n(&reply, &len, value, val_len);
break;
}
}
#undef append_sgr_attr_n
reply[len - 1] = 'm';
term_to_slave(term, "\033P1$r", 5);
term_to_slave(term, reply, len);
term_to_slave(term, "\033\\", 2);
free(reply);
}
else if (n == 2 && memcmp(query, " q", 2) == 0) {
/* DECSCUSR - Set Cursor Style */
int mode;
switch (term->cursor_style) {
case CURSOR_HOLLOW: /* FALLTHROUGH */
case CURSOR_BLOCK: mode = 2; break;
case CURSOR_UNDERLINE: mode = 4; break;
case CURSOR_BEAM: mode = 6; break;
default: BUG("invalid cursor style"); break;
}
if (term->cursor_blink.deccsusr)
mode--;
char reply[16];
size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode);
term_to_slave(term, reply, len);
}
else {
static const char err[] = "\033P0$r\033\\";
term_to_slave(term, err, sizeof(err) - 1);
}
}
void
dcs_hook(struct terminal *term, uint8_t final)
{
LOG_DBG("hook: %c (intermediate(s): %.2s, param=%d)", final,
(const char *)&term->vt.private, vt_param_get(term, 0, 0));
xassert(term->vt.dcs.data == NULL);
xassert(term->vt.dcs.size == 0);
xassert(term->vt.dcs.put_handler == NULL);
xassert(term->vt.dcs.unhook_handler == NULL);
switch (term->vt.private) {
case 0:
switch (final) {
case 'q': {
if (!term->conf->tweak.sixel) {
break;
}
int p1 = vt_param_get(term, 0, 0);
int p2 = vt_param_get(term, 1, 0);
int p3 = vt_param_get(term, 2, 0);
term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3);
term->vt.dcs.unhook_handler = &sixel_unhook;
break;
}
}
break;
case '$':
switch (final) {
case 'q':
term->vt.dcs.put_handler = &decrqss_put;
term->vt.dcs.unhook_handler = &decrqss_unhook;
break;
}
break;
case '=':
switch (final) {
case 's':
/* BSU/ESU: https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */
switch (vt_param_get(term, 0, 0)) {
case 1:
term->vt.dcs.unhook_handler = &term_enable_app_sync_updates;
return;
case 2:
term->vt.dcs.unhook_handler = &term_disable_app_sync_updates;
return;
}
break;
}
break;
case '+':
switch (final) {
case 'q': /* XTGETTCAP */
term->vt.dcs.put_handler = &xtgettcap_put;
term->vt.dcs.unhook_handler = &xtgettcap_unhook;
break;
}
break;
}
}
void
dcs_put(struct terminal *term, uint8_t c)
{
/* LOG_DBG("PUT: %c", c); */
if (term->vt.dcs.put_handler != NULL)
term->vt.dcs.put_handler(term, c);
}
void
dcs_unhook(struct terminal *term)
{
if (term->vt.dcs.unhook_handler != NULL)
term->vt.dcs.unhook_handler(term);
term->vt.dcs.unhook_handler = NULL;
term->vt.dcs.put_handler = NULL;
free(term->vt.dcs.data);
term->vt.dcs.data = NULL;
term->vt.dcs.size = 0;
term->vt.dcs.idx = 0;
}

8
dcs.h Normal file
View file

@ -0,0 +1,8 @@
#pragma once
#include <stdbool.h>
#include "terminal.h"
void dcs_hook(struct terminal *term, uint8_t final);
void dcs_put(struct terminal *term, uint8_t c);
void dcs_unhook(struct terminal *term);

47
debug.c Normal file
View file

@ -0,0 +1,47 @@
#include "debug.h"
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "log.h"
#if defined(__SANITIZE_ADDRESS__) || HAS_FEATURE(address_sanitizer)
#include <sanitizer/asan_interface.h>
#define ASAN_ENABLED 1
#endif
static void
print_stack_trace(void)
{
#ifdef ASAN_ENABLED
fputs("\nStack trace:\n", stderr);
__sanitizer_print_stack_trace();
#endif
}
noreturn void
fatal_error(const char *file, int line, const char *msg, int err)
{
log_msg(LOG_CLASS_ERROR, "debug", file, line, "%s: %s", msg, strerror(err));
print_stack_trace();
fflush(stderr);
abort();
}
noreturn void
bug(const char *file, int line, const char *func, const char *fmt, ...)
{
char buf[4096];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
const char *msg = likely(n >= 0) ? buf : "??";
log_msg(LOG_CLASS_ERROR, "debug", file, line, "BUG in %s(): %s", func, msg);
print_stack_trace();
fflush(stderr);
abort();
}

32
debug.h Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include "macros.h"
#define FATAL_ERROR(...) fatal_error(__FILE__, __LINE__, __VA_ARGS__)
#ifdef NDEBUG
#define BUG(...) UNREACHABLE()
#else
#define BUG(...) bug(__FILE__, __LINE__, __func__, __VA_ARGS__)
#endif
#define xassert(x) do { \
IGNORE_WARNING("-Wtautological-compare") \
if (unlikely(!(x))) { \
BUG("assertion failed: '%s'", #x); \
} \
UNIGNORE_WARNINGS \
} while (0)
#ifndef static_assert
#if __STDC_VERSION__ >= 201112L
#define static_assert(x, msg) _Static_assert((x), msg)
#elif GNUC_AT_LEAST(4, 6) || HAS_EXTENSION(c_static_assert)
#define static_assert(x, msg) __extension__ _Static_assert((x), msg)
#else
#define static_assert(x, msg)
#endif
#endif
noreturn void fatal_error(const char *file, int line, const char *msg, int err) COLD;
noreturn void bug(const char *file, int line, const char *func, const char *fmt, ...) PRINTF(4) COLD;

81
doc/benchmark.md Normal file
View file

@ -0,0 +1,81 @@
# Benchmarks
## vtebench
All benchmarks are done using [vtebench](https://github.com/alacritty/vtebench):
```sh
./target/release/vtebench -b ./benchmarks --dat /tmp/<terminal>
```
## 2022-05-12
### System
CPU: i9-9900
RAM: 64GB
Graphics: Radeon RX 5500XT
### Terminal configuration
Geometry: 2040x1884
Font: Fantasque Sans Mono 10.00pt/23px
Scrollback: 10000 lines
### Results
| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 |
|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:|
| cursor motion | 10.40 | 14.07 | 24.97 | 23.38 | 1622.86 |
| dense cells | 29.58 | 45.46 | 97.45 | 10828.00 | 2323.00 |
| light cells | 4.34 | 4.40 | 12.84 | 12.17 | 49.81 |
| scrollling | 135.31 | 116.35 | 121.69 | 108.30 | 4041.33 |
| scrolling bottom region | 118.19 | 109.70 | 105.26 | 118.80 | 3875.00 |
| scrolling bottom small region | 132.41 | 122.11 | 122.83 | 151.30 | 3839.67 |
| scrolling fullscreen | 5.70 | 5.66 | 10.92 | 12.09 | 124.25 |
| scrolling top region | 144.19 | 121.78 | 135.81 | 159.24 | 3858.33 |
| scrolling top small region | 135.95 | 119.01 | 115.46 | 216.55 | 3872.67 |
| unicode | 11.56 | 10.92 | 15.94 | 1012.27 | 4779.33 |
## 2022-05-12
### System
CPU: i5-8250U
RAM: 8GB
Graphics: Intel UHD Graphics 620
### Terminal configuration
Geometry: 945x1020
Font: Dina:pixelsize=12
Scrollback=10000 lines
### Results
| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 |
|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:|
| cursor motion | 15.03 | 16.74 | 23.22 | 24.14 | 1381.63 |
| dense cells | 43.56 | 54.10 | 89.43 | 1807.17 | 1945.50 |
| light cells | 7.96 | 9.66 | 20.19 | 21.31 | 122.44 |
| scrollling | 146.02 | 150.47 | 129.22 | 129.84 | 10140.00 |
| scrolling bottom region | 138.36 | 137.42 | 117.06 | 141.87 | 10136.00 |
| scrolling bottom small region | 137.40 | 134.66 | 128.97 | 208.77 | 9930.00 |
| scrolling fullscreen | 11.66 | 12.02 | 19.69 | 21.96 | 315.80 |
| scrolling top region | 143.81 | 133.47 | 132.51 | 475.81 | 10267.00 |
| scrolling top small region | 133.72 | 135.32 | 145.10 | 314.13 | 10074.00 |
| unicode | 20.89 | 21.78 | 26.11 | 5687.00 | 15740.00 |

813
doc/foot-ctlseqs.7.scd Normal file
View file

@ -0,0 +1,813 @@
foot-ctlseqs(7)
# NAME
foot-ctlseqs - terminal control sequences supported by foot
# DESCRIPTION
This document describes all the control sequences supported by foot.
- Control characters
- Sequences beginning with ESC
- CSI - Control Sequence Introducer
- SGR
- Indexed and RGB colors (256-color palette and 24-bit colors)
- Private modes
- Window manipulation
- Other
- OSC - Operating System Command
- DCS - Device Control String
# Control characters
[[ *Sequence*
:[ *Name*
:< *Description*
| \\a
: BEL
: Depends on what *bell* in *foot.ini*(5) is set to.
| \\b
: BS
: Backspace; move the cursor left one step. Wrap if _bw_ is enabled.
| \\t
: HT
: Horizontal tab; move the cursor to the next tab stop.
| \\n
: LF
: Line feed; move the cursor down one step, or scroll content up if
at the bottom line.
| \\v
: VT
: Vertical tab; identical to _LF_.
| \\f
: FF
: Form feed; identical to _LF_.
| \\r
: CR
: Carriage ret; move the cursor to the leftmost column.
| \\x0E
: SO
: Shift out; select the _G1_ character set.
| \\x0F
: SI
: Shift in; select the _G0_ character set.
# Sequences beginning with ESC
Note: this table excludes sequences where ESC is part of a 7-bit
equivalent to 8-bit C1 controls.
[[ *Sequence*
:[ *Name*
:[ *Origin*
:< *Description*
| \\E 7
: DECSC
: VT100
: Save cursor position.
| \\E 8
: DECRC
: VT100
: Restore cursor position.
| \\E c
: RIS
: VT100
: Reset terminal to initial state.
| \\E D
: IND
: VT100
: Line feed; move the cursor down one step, or scroll content up if
at the bottom margin.
| \\E E
: NEL
: VT100
: Next line; move the cursor down one step, and to the first
column. Content is scrolled up if at the bottom line.
| \\E H
: HTS
: VT100
: Set one horizontal tab stop at the current position.
| \\E M
: RI
: VT100
: Reverse index; move the cursor up one step, or scroll content down
if at the top margin.
| \\E N
: SS2
: VT220
: Single shift select of G2 character set (affects next character only).
| \\E O
: SS3
: VT220
: Single shift select of G3 character set (affects next character only).
| \\E =
: DECKPAM
: VT100
: Switch keypad to _application_ mode.
| \\E >
: DECKPNM
: VT100
: Switch keypad to _numeric_ mode.
| \\E ( _C_
: SCS
: VT100
: Designate G0 character set. Supported values for _C_ are: *0* (DEC
Special Character and Line Drawing Set), and *B* (USASCII).
| \\E ) _C_
: SCS
: VT100
: Designate G1 character set. Same supported values for _C_ as in _G0_.
| \\E \* _C_
: SCS
: VT220
: Designate G2 character set. Same supported values for _C_ as in _G0_.
| \\E + _C_
: SCS
: VT220
: Designate G3 character set. Same supported values for _C_ as in _G0_.
# CSI
All sequences begin with *\\E[*, sometimes abbreviated "CSI". Spaces
are used in the sequence strings to make them easier to read, but are
not actually part of the string (i.e. *\\E[ 1 m* is really *\\E[1m*).
## SGR
All SGR sequences are in the form *\\E[* _N_ *m*, where _N_ is a decimal
number - the _parameter_. Multiple parameters can be combined in a
single CSI sequence by separating them with semicolons: *\\E[ 1;2;3
m*.
[[ *Parameter*
:< *Description*
| 0
: Reset all attributes
| 1
: Bold
| 2
: Dim
| 3
: Italic
| 4
: Underline, including styled underlines
| 5
: Blink
| 7
: Reverse video; swap foreground and background colors
| 8
: Conceal; text is not visible, but is copiable
| 9
: Crossed-out/strike
| 21
: Double underline
| 22
: Disable *bold* and *dim*
| 23
: Disable italic
| 24
: Disable underline
| 25
: Disable blink
| 27
: Disable reverse video
| 28
: Disable conceal
| 29
: Disable crossed-out
| 30-37
: Select foreground color (using *regularN* in *foot.ini*(5))
| 38
: Select foreground color, see "indexed and RGB colors" below
| 39
: Use the default foreground color (*foreground* in *foot.ini*(5))
| 40-47
: Select background color (using *regularN* in *foot.ini*(5))
| 48
: Select background color, see "indexed and RGB colors" below
| 49
: Use the default background color (*background* in *foot.ini*(5))
| 58
: Select underline color, see "indexed and RGB colors" below
| 59
: Use the default underline color
| 90-97
: Select foreground color (using *brightN* in *foot.ini*(5))
| 100-107
: Select background color (using *brightN* in *foot.ini*(5))
## Indexed and RGB colors (256-color palette and 24-bit colors)
Foot supports both the new sub-parameter based variants, and the older
parameter based variants for setting foreground and background colors.
Indexed colors:
- *\\E[ 38 : 5 :* _idx_ *m*
- *\\E[ 38 ; 5 ;* _idx_ *m*
RGB colors:
- *\\E[ 38 : 2 :* _cs_ *:* _r_ *:* _g_ *:* _b_ *m*
- *\\E[ 38 : 2 :* _r_ *:* _g_ *:* _b_ *m*
- *\\E[ 38 ; 2 ;* _r_ *;* _g_ *;* _b_ *m*
The first variant is the "correct" one (and foot also recognizes, but
ignores, the optional _tolerance_ parameters).
The second one is allowed since many programs "forget" the color space
ID, _cs_.
The sub-parameter based variants are preferred, and are what foot's
*terminfo*(5) entry uses.
## Private Modes
There are several Boolean-like "modes" that affect certain aspects
of the terminal's behavior. These modes can be manipulated with the
following 4 escape sequences:
[[ *Sequence*
:[ *Name*
:< *Description*
| \\E[ ? _Pm_ h
: DECSET
: Enable private mode
| \\E[ ? _Pm_ l
: DECRST
: Disable private mode
| \\E[ ? _Pm_ s
: XTSAVE
: Save private mode
| \\E[ ? _Pm_ r
: XTRESTORE
: Restore private mode
The _Pm_ parameter in the above sequences denotes a numerical ID
that corresponds to one of the following modes:
[[ *Parameter*
:[ *Origin*
:< *Description*
| 1
: VT100
: Cursor keys mode (DECCKM)
| 5
: VT100
: Reverse video (DECSCNM)
| 6
: VT100
: Origin mode (DECOM)
| 7
: VT100
: Auto-wrap mode (DECAWM)
| 12
: AT&T 610
: Cursor blink
| 25
: VT220
: Cursor visibility (DECTCEM)
| 45
: xterm
: Reverse-wraparound mode
| 47
: xterm
: Same as 1047 (see below)
| 66
: VT320
: Numeric keypad mode (DECNKM); same as DECKPAM/DECKPNM when enabled/disabled
| 1000
: xterm
: Send mouse x/y on button press/release
| 1001
: xterm
: Use hilite mouse tracking
| 1002
: xterm
: Use cell motion mouse tracking
| 1003
: xterm
: Use all motion mouse tracking
| 1004
: xterm
: Send FocusIn/FocusOut events
| 1006
: xterm
: SGR mouse mode
| 1007
: xterm
: Alternate scroll mode
| 1015
: urxvt
: urxvt mouse mode
| 1016
: xterm
: SGR-Pixels mouse mode
| 1034
: xterm
: 8-bit Meta mode
| 1035
: xterm
: Num Lock modifier (see xterm numLock option)
| 1036
: xterm
: Send ESC when Meta modifies a key (see xterm metaSendsEscape option)
| 1042
: xterm
: Perform action for BEL character (see *bell* in *foot.ini*(5))
| 1047
: xterm
: Use alternate screen buffer
| 1048
: xterm
: Save/restore cursor (DECSET=save, DECRST=restore)
| 1049
: xterm
: Equivalent to 1048 and 1047 combined
| 1070
: xterm
: Use private color registers for each sixel
| 2004
: xterm
: Wrap pasted text with start/end delimiters (bracketed paste mode)
| 2026
: terminal-wg
: Application synchronized updates mode
| 2027
: contour
: Grapheme cluster processing
| 2031
: contour
: Request color theme updates
| 2048
: TODO
: In-band window resize notifications
| 8452
: xterm
: Position cursor to the right of sixels, instead of on the next line
| 737769
: foot
: Input Method Editor (IME) mode
## Window manipulation
Foot implements a sub-set of XTerm's (originally dtterm's) window
manipulation sequences. The generic format is:
*\\E[ *_Ps_* ; *_Ps_* ; *_Ps_* t*
[[ *Parameter 1*
:[ *Parameter 2*
:< *Description*
| 11
: -
: Report if window is iconified. Foot always reports *1* - not iconified.
| 13
: -
: Report window position. Foot always reports (0,0), due to Wayland
limitations.
| 13
: 2
: Report text area position. Foot always reports (0,0) due to Wayland
limitations.
| 14
: -
: Report text area size, in pixels. Foot reports the grid size,
excluding the margins.
| 14
: 2
: Report window size, in pixels. Foot reports the grid size plus the
margins.
| 15
: -
: Report the screen size, in pixels.
| 16
: -
: Report the cell size, in pixels.
| 18
: -
: Report text area size, in characters.
| 19
: -
: Report screen size, in characters.
| 20
: -
: Report icon label.
| 22
: -
: Push window title+icon.
| 22
: 1
: Push window icon.
| 22
: 2
: Push window title.
| 23
: -
: Pop window title+icon.
| 23
: 1
: Pop window icon.
| 23
: 2
: Pop window title.
## Other
[[ *Parameter*
:[ *Name*
:[ *Origin*
:< *Description*
| \\E[ _Ps_ c
: DA
: VT100
: Send primary device attributes. Foot responds with "I'm a VT220 with
sixel and ANSI color support".
| \\E[ _Ps_ A
: CUU
: VT100
: Cursor up - move cursor up _Ps_ times.
| \\E[ _Ps_ B
: CUD
: VT100
: Cursor down - move cursor down _Ps_ times.
| \\E[ _Ps_ C
: CUF
: VT100
: Cursor forward - move cursor to the right _Ps_ times.
| \\E[ _Ps_ D
: CUB
: VT100
: Cursor backward - move cursor to the left _Ps_ times.
| \\E[ _Ps_ g
: TBC
: VT100
: Tab clear. _Ps_=0 -> clear current column. _Ps_=3 -> clear all.
| \\E[ _Ps_ ; _Ps_ f
: HVP
: VT100
: Horizontal and vertical position - move cursor to _row_ ; _column_.
| \\E[ _Ps_ ; _Ps_ H
: CUP
: VT100
: Cursor position - move cursor to _row_ ; _column_.
| \\E[ _Ps_ J
: ED
: VT100
: Erase in display. _Ps_=0 -> below cursor. _Ps_=1 -> above
| \\E[ _Ps_ K
: EL
: VT100
: Erase in line. _Ps_=0 -> right of cursor. _Ps_=1 -> left of
cursor. _Ps_=2 -> all.
| \\E[ _Pm_ h
: SM
: VT100
: Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All
other values of _Pm_ are unsupported.
| \\E[ _Pm_ l
: RM
: VT100
: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All
other values of _Pm_ are unsupported.
| \\E[ _Ps_ n
: DSR
: VT100
: Device status report. _Ps_=5 -> device status. _Ps_=6 -> cursor
position.
| \\E[ _Ps_ L
: IL
: VT220
: Insert _Ps_ lines.
| \\E[ _Ps_ M
: DL
: VT220
: Delete _Ps_ lines.
| \\E[ _Ps_ P
: DCH
: VT220
: Delete _Ps_ characters.
| \\E[ _Ps_ @
: ICH
: VT220
: Insert _Ps_ blank characters.
| \\E[ _Ps_ X
: ECH
: VT220
: Erase _Ps_ characters.
| \\E[ > c
: DA2
: VT220
: Send secondary device attributes. Foot responds with "I'm a VT220
and here's my version number".
| \\E[ ! p
: DECSTR
: VT220
: Soft terminal reset.
| \\E[ ? _Ps_ $ p
: DECRQM
: VT320
: Request status of DEC private mode. The _Ps_ parameter corresponds
to one of the values mentioned in the "Private Modes" section above
(as set with DECSET/DECRST).
| \\E[ _Ps_ $ p
: DECRQM
: VT320
: Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM
above for recognized _Ps_ values.
| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r
: DECCARA
: VT400
: Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_
denotes the rectangle, _Pm_ denotes the SGR attributes.
| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t
: DECRARA
: VT400
: Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_
denotes the rectangle, _Pm_ denotes the SGR attributes.
| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v
: DECCRA
: VT400
: Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the
rectangle, _Pt_ and _Pl_ denotes the target location.
| \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x
: DECFRA
: VT420
: Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_,
_Pb_ and _Pr_ denotes the rectangle.
| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z
: DECERA
: VT400
: Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the
rectangle.
| \\E[ _Ps_ T
: SD
: VT420
: Scroll down _Ps_ lines.
| \\E[ s
: SCOSC
: SCO, VT510
: Save cursor position.
| \\E[ u
: SCORC
: SCO, VT510
: Restore cursor position.
| \\E[ _Ps_ SP q
: DECSCUSR
: VT510
: Set cursor style. In foot, _Ps_=0 means "use style from foot.ini".
| \\E[ = _Ps_ c
: DA3
: VT510
: Send tertiary device attributes. Foot responds with "FOOT", in
hexadecimal.
| \\E[ _Pm_ d
: VPA
: ECMA-48
: Line position absolute - move cursor to line _Pm_.
| \\E[ _Pm_ e
: VPR
: ECMA-48
: Line position relative - move cursor down _Pm_ lines.
| \\E[ _Pm_ a
: HPR
: ECMA-48
: Character position relative - move cursor to the right _Pm_ times.
| \\E[ _Ps_ E
: CNL
: ECMA-48
: Cursor next line - move the cursor down _Ps_ times.
| \\E[ _Ps_ F
: CPL
: ECMA-48
: Cursor preceding line - move the cursor up _Ps_ times.
| \\E[ _Pm_ `
: HPA
: ECMA-48
: Character position absolute - move cursor to column _Pm_.
| \\E[ _Ps_ G
: CHA
: ECMA-48
: Cursor character absolute - move cursor to column _Ps_.
cursor. _Ps_=2 -> all. _Ps_=3 -> saved lines.
| \\E[ _Ps_ S
: SU
: ECMA-48
: Scroll up _Ps_ lines.
| \\E[ _Ps_ I
: CHT
: ECMA-48
: Cursor forward tabulation _Ps_ tab stops.
| \\E[ _Ps_ Z
: CBT
: ECMA-48
: Cursor backward tabulation _Ps_ tab stops.
| \\E[ _Ps_ b
: REP
: ECMA-48
: Repeat the preceding printable character _Ps_ times.
| \\E[ ? _Pi_ ; _Pa_ ; _Pv_ S
: XTSMGRAPHICS
: xterm
: Set or request sixel attributes.
| \\E[ > _Ps_ q
: XTVERSION
: xterm
: _Ps_=0 -> report terminal name and version, in the form
*\\EP>|foot(version)\\E\\*.
| \\E[ > 4 ; _Pv_ m
: XTMODKEYS
: xterm
: Set level of the _modifyOtherKeys_ property to _Pv_. Note that foot
only supports level 1 and 2, where level 1 is the default setting.
| \\E[ ? _Pp_ m
: XTQMODKEYS
: xterm
: Query key modifier options
| \\E[ > 4 n
: <unnamed>
: xterm
: Resets the _modifyOtherKeys_ property to level 1. Note that in foot,
this sequence does not completely disable _modifyOtherKeys_, since
foot only supports level 1 and level 2 (and not level 0).
| \\E[ ? u
: <unnamed>
: kitty
: Query current values of the Kitty keyboard flags.
| \\E[ > _flags_ u
: <unnamed>
: kitty
: Push a new entry, _flags_, to the Kitty keyboard stack.
| \\E[ < _number_ u
: <unnamed>
: kitty
: Pop _number_ of entries from the Kitty keyboard stack.
| \\E[ = _flags_ ; _mode_ u
: <unnamed>
: kitty
: Update current Kitty keyboard flags, according to _mode_.
| \\E[ # P
: XTPUSHCOLORS
: xterm
: Push current color palette onto stack
| \\E[ # Q
: XTPOPCOLORS
: xterm
: Pop color palette from stack
| \\E[ # R
: XTREPORTCOLORS
: xterm
: Report the current entry on the palette stack, and the number of
palettes stored on the stack.
| \\E[ ? 996 n
: Query the current (color) theme mode
: contour
: The current color theme mode (light or dark) is reported as *CSI ?
997 ; 1|2 n*, where *1* means dark and *2* light. By convention, the
primary theme in foot is considered dark, and the alternative theme
light.
# OSC
All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_.
[[ *Sequence*
:[ *Origin*
:< *Description*
| \\E] 0 ; _Pt_ \\E\\
: xterm
: Set window icon and title to _Pt_.
| \\E] 1 ; _Pt_ \\E\\
: xterm
: Set window icon to _Pt_.
| \\E] 2 ; _Pt_ \\E\\
: xterm
: Set window title to _Pt_
| \\E] 4 ; _c_ ; _spec_ \\E\\
: xterm
: Change color number _c_ to _spec_, where _spec_ is a color in
XParseColor format. foot only supports RGB colors; either
*rgb:<red>/<green>/<blue>*, or the legacy format (*#rgb*).
| \\E] 7 ; _Uri_ \\E\\
: iTerm2
: Update the terminal's current working directory. Newly spawned
terminals will launch in this directory. _Uri_ must be in the format
*file://<hostname>/<path>*. *hostname* must refer to your local host.
| \\E] 8 ; id=_ID_ ; _Uri_ \\E\\
: VTE+iTerm2
: Hyperlink (a.k.a HTML-like anchors). id=_ID_ is optional; if assigned,
all URIs with the same _ID_ will be treated as a single
hyperlink. An empty URI closes the hyperlink.
| \\E] 9 ; _msg_ \\E\\
: iTerm2
: Desktop notification, uses *notify* in *foot.ini*(5).
| \\E] 10 ; _spec_ \\E\\
: xterm
: Change the default foreground color to _spec_, a color in
XParseColor format.
| \\E] 11 ; _spec_ \\E\\
: xterm
: Change the default background color to _spec_, a color in
XParseColor format. Foot implements URxvt's transparency extension;
e.g. _spec_=*[75]#ff00ff* or _spec_=*rgba:ff/00/ff/bf* (pink with
75% alpha).
| \\E] 12 ; _spec_ \\E\\
: xterm
: Change cursor color to _spec_, a color in XParseColor format.
| \\E] 17 ; _spec_ \\E\\
: xterm
: Change selection background color to _spec_, a color in
XParseColor format.
| \\E] 19 ; _spec_ \\E\\
: xterm
: Change selection foreground color to _spec_, a color in XParseColor
format.
| \\E] 22 ; _xcursor-pointer-name_ \\E\\
: xterm
: Sets the xcursor pointer. An empty name, or an invalid name resets
it.
| \\E] 52 ; _Pc_ ; ? \\E\\
: xterm
: Send clipboard data. _Pc_ can be either *c*, *s* or *p*. *c* uses
the clipboard as source, and *s* and *p* uses the primary
selection. The response is *\\E] 52 ; Pc ; <base64-encoded data>
\E\\*, where _Pc_ denotes the source used.
| \\E] 52 ; _Pc_ ; _Pd_ \\E\\
: xterm
: Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the
target: *c* targets the clipboard and *s* and *p* the primary
selection.
| \\E] 66 ; _params_ ; text \\E\\
: kitty
: Text sizing protocol (only 'w', width, supported)
| \\E] 99 ; _params_ ; _payload_ \\E\\
: kitty
: Desktop notification; uses *desktop-notifications.command* in
*foot.ini*(5).
| \\E] 104 ; _c_ \\E\\
: xterm
: Reset color number _c_ (multiple semicolon separated _c_ values may
be provided), or all colors (excluding the default
foreground/background colors) if _c_ is omitted.
| \\E] 110 \\E\\
: xterm
: Reset default foreground color
| \\E] 111 \\E\\
: xterm
: Reset default background color
| \\E] 112 \\E\\
: xterm
: Reset cursor color
| \\E] 117 \\E\\
: xterm
: Reset selection background color
| \\E] 119 \\E\\
: xterm
: Reset selection foreground color
| \\E] 133 ; A \\E\\
: FinalTerm
: Mark start of shell prompt
| \\E] 133 ; C \\E\\
: FinalTerm
: Mark start of command output
| \\E] 133 ; D \\E\\
: FinalTerm
: Mark end of command output
| \\E] 176 ; _app-id_ \\E\\
: foot
: Set app ID. _app-id_ is optional; if assigned,
the terminal window App ID will be set to the value.
An empty App ID resets the value to the default.
| \\E] 555 \\E\\
: foot
: Flash the entire terminal (foot extension)
| \\E] 777;notify;_title_;_msg_ \\E\\
: urxvt
: Desktop notification, uses *desktop-notifications.command* in
*foot.ini*(5).
# DCS
All _DCS_ sequences begin with *\\EP* (sometimes abbreviated _DCS_),
and are terminated by *\\E\\* (ST).
[[ *Sequence*
:< *Description*
| \\EP q <sixel data> \\E\\
: Emit a sixel image at the current cursor position
| \\EP $ q <query> \\E\\
: Request selection or setting (DECRQSS). Implemented queries:
DECSTBM, SGR and DECSCUSR.
| \\EP = _C_ s \\E\\
: Begin (_C_=*1*) or end (_C_=*2*) application synchronized updates.
This sequence is supported for compatibility reasons, but it's
recommended to use private mode 2026 (see above) instead.
| \\EP + q <hex encoded capability name> \\E\\
: Query builtin terminfo database (XTGETTCAP)
# FOOTNOTE
Foot does not support 8-bit control characters ("C1").

735
doc/foot.1.scd Normal file
View file

@ -0,0 +1,735 @@
foot(1)
# NAME
foot - Wayland terminal emulator
# SYNOPSIS
*foot* [_OPTIONS_]++
*foot* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_]
All trailing (non-option) arguments are treated as a command, and its
arguments, to execute (instead of the default shell).
# DESCRIPTION
*foot* is a Wayland terminal emulator. Running it without arguments
will start a new terminal window with your default shell.
You can override the default shell by appending a custom command to
the foot command line
*foot htop*
# OPTIONS
*-c*,*--config*=_PATH_
Path to configuration file, see *foot.ini*(5) for details.
The configuration file is automatically passed to new terminals
spawned via *spawn-terminal* (see *foot.ini*(5)).
*-C*,*--check-config*
Verify configuration and then exit with 0 if ok, otherwise exit
with 230 (see *EXIT STATUS*).
*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_
Override an option set in the configuration file. If _SECTION_ is not
given, defaults to _main_.
*-f*,*--font*=_FONT_
Comma separated list of fonts to use, in fontconfig format (see
*FONT FORMAT*).
The first font is the primary font. The remaining fonts are
fallback fonts that will be used whenever a glyph cannot be found
in the primary font.
The fallback fonts are searched in the order they appear. If a
glyph cannot be found in any of the fallback fonts, the dynamic
fallback list from fontconfig (for the primary font) is
searched.
Default: _monospace_.
*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_
Set initial window width and height, in pixels. Default: _700x500_.
*-W*,*--window-size-chars*=_WIDTHxHEIGHT_
Set initial window width and height, in characters. Default: _not set_.
*-t*,*--term*=_TERM_
Value to set the environment variable *TERM* to (see *TERMINFO*
and *ENVIRONMENT*). Default: _@default_terminfo@_.
*-T*,*--title*=_TITLE_
Initial window title. Default: _foot_.
*-a*,*--app-id*=_ID_
Value to set the *app-id* property on the Wayland window
to. Default: _foot_ (normal mode), or _footclient_ (server mode).
*toplevel-tag*=_TAG_
Value to set the *toplevel-tag* property on the Wayland window
to. The compositor can use this value for session management,
window rules etc. Default: _not set_
*-m*,*--maximized*
Start in maximized mode. If both *--maximized* and *--fullscreen*
are specified, the _last_ one takes precedence.
*-F*,*--fullscreen*
Start in fullscreen mode. If both *--maximized* and *--fullscreen*
are specified, the _last_ one takes precedence.
*-L*,*--login-shell*
Start a login shell, by prepending a '-' to argv[0].
*--pty*
Display an existing pty instead of creating one. This is useful
for interacting with VM consoles.
This option is not currently supported in combination with
*-s*,*--server*.
*-D*,*--working-directory*=_DIR_
Initial working directory for the client application. Default:
_CWD of foot_.
*-s*,*--server*[=_PATH_|_FD_]
Run as a server. In this mode, a single foot instance hosts
multiple terminals (windows). Use *footclient*(1) to launch new
terminals.
This saves some memory since for example fonts and glyph caches
can be shared between the terminals.
It also saves upstart time since the config has already been
loaded and parsed, and most importantly, fonts have already been
loaded (and their glyph caches are likely to already have been
populated).
Each terminal will have its own rendering threads, but all Wayland
communication, as well as input/output to the shell, is
multiplexed in the main thread. Thus, this mode might result in
slightly worse performance when multiple terminals are under heavy
load.
Also be aware that should one terminal crash, it will take all the
others with it.
The default path is
*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*.
If *$XDG\_RUNTIME\_DIR* is not set, the default path is instead
*/tmp/foot.sock*.
If *$XDG\_RUNTIME\_DIR* is set, but *$WAYLAND\_DISPLAY* is not,
the default path is *$XDG\_RUNTIME\_DIR/foot.sock*.
Note that if you change the default, you will also need to use the
*--server-socket* option in *footclient*(1) and point it to your
custom socket path.
If the argument is a number, foot will interpret it as the file descriptor
of a socket provided by a supervision daemon (such as systemd or s6), and
use that socket as it's own.
Two systemd units (foot-server.{service,socket}) are provided to use that
feature with systemd. To use socket activation, only enable the
socket unit.
Note that starting *foot --server* as a systemd service will use
the environment of the systemd user instance; thus, you'll need
to import *$WAYLAND_DISPLAY* in it using *systemctl --user
import-environment WAYLAND_DISPLAY*.
*-H*,*--hold*
Remain open after child process exits.
*-p*,*--print-pid*=_FILE_|_FD_
Print PID to this file, or FD, when successfully started. The file
(or FD) is closed immediately after writing the PID. When a _FILE_
as been specified, the file is unlinked at exit.
This option can only be used in combination with *-s*,*--server*.
*-d*,*--log-level*={*info*,*warning*,*error*,*none*}
Log level, used both for log output on stderr as well as
syslog. Default: _warning_.
*-l*,*--log-colorize*=[{*never*,*always*,*auto*}]
Enables or disables colorization of log output on stderr. Default:
_auto_.
*-S*,*--log-no-syslog*
Disables syslog logging. Logging is only done on stderr. This
option can only be used in combination with *-s*,*--server*.
*-v*,*--version*
Show the version number and quit.
*-e*
Ignored; for compatibility with *xterm -e*.
This option was added in response to several program launchers
passing *-e* to arbitrary terminals, under the assumption that
they all implement the same semantics for it as *xterm*(1).
Ignoring it allows foot to be invoked as e.g. *foot -e man foot*
with the same results as with xterm, instead of producing an
"invalid option" error.
# KEYBOARD SHORTCUTS
The following keyboard shortcuts are available by default. They can be
changed in *foot.ini*(5). There are also more actions (disabled by
default) available; see *foot.ini*(5).
## NORMAL MODE
*shift*+*page up*/*page down*
Scroll up/down in history
*ctrl*+*shift*+*c*, *XF86Copy*
Copy selected text to the _clipboard_
*ctrl*+*shift*+*v*, *XF86Paste*
Paste from _clipboard_
*shift*+*insert*
Paste from the _primary selection_
*ctrl*+*shift*+*r*
Start a scrollback search
*ctrl*+*+*, *ctrl*+*=*
Increase font size
*ctrl*+*-*
Decrease font size
*ctrl*+*0*
Reset font size
*ctrl*+*shift*+*n*
Spawn a new terminal. If the shell has been configured to emit the
_OSC 7_ escape sequence, the new terminal will start in the
current working directory.
*ctrl*+*shift*+*o*
Activate URL mode, allowing you to "launch" URLs.
*ctrl*+*shift*+*u*
Activate Unicode input.
*ctrl*+*shift*+*z*
Jump to the previous, currently not visible, prompt. Requires
shell integration.
*ctrl*+*shift*+*x*
Jump to the next prompt. Requires shell integration.
## SCROLLBACK SEARCH
These keyboard shortcuts affect the search selection:
*ctrl*+*r*
Search _backward_ for the next match. If the search string is
empty, the last searched-for string is used.
*ctrl*+*s*
Search _forward_ for the next match. If the search string is
empty, the last searched-for string is used.
*shift*+*right*
Extend current selection to the right by one character.
*shift*+*left*
Extend current selection to the left by one character.
*ctrl*+*w*, *ctrl*+*shift*+*right*
Extend current selection (and thus the search criteria) to the end
of the word, or the next word if currently at a word separating
character.
*ctrl*+*shift*+*w*
Same as *ctrl*+*w*, except that the only word separating
characters are whitespace characters.
*ctrl*+*shift*+*left*
Extend current selection to the left to the last word boundary.
*shift*+*down*
Extend current selection down one line
*shift*+*up*
Extend current selection up one line.
*ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste*
Paste from clipboard into the search buffer.
*shift*+*insert*
Paste from primary selection into the search buffer.
*escape*, *ctrl*+*g*, *ctrl*+*c*
Cancel the search
*return*
Finish the search and copy the current match to the primary
selection. The terminal selection is kept, allowing you to press
*ctrl*+*shift*+*c* to copy it to the clipboard.
These shortcuts affect the search box in scrollback-search mode:
*ctrl*+*b*
Moves the cursor in the search box one **character** to the left.
*ctrl*+*left*, *alt*+*b*
Moves the cursor in the search box one **word** to the left.
*ctrl*+*f*
Moves the cursor in the search box one **character** to the right.
*ctrl*+*right*, *alt*+*f*
Moves the cursor in the search box one **word** to the right.
*Home*, *ctrl*+*a*
Moves the cursor in the search box to the beginning of the input.
*End*, *ctrl*+*e*
Moves the cursor in the search box to the end of the input.
*alt*+*backspace*, *ctrl*+*backspace*
Deletes the **word before** the cursor.
*alt*+*delete*, *ctrl*+*delete*
Deletes the **word after** the cursor.
*ctrl*+*u*
Deletes from the cursor to the start of the input
*ctrl*+*k*
Deletes from the cursor to the end of the input
These shortcuts affect scrolling in scrollback-search mode:
*shift*+*page-up*
Scrolls up/back one page in history.
*shift*+*page-down*
Scroll down/forward one page in history.
## URL MODE
*t*
Toggle URL visibility in jump label.
*escape*, *ctrl*+*g*, *ctrl*+*c*, *ctrl*+*d*
Exit URL mode without launching a URL.
## MOUSE SHORTCUTS
*left*, single-click
Drag to select; when released, the selected text is copied to the
_primary_ selection. This feature is normally *disabled* whenever
the client has enabled _mouse tracking_, but can be forced by
holding *shift*.
Holding *ctrl* will create a block selection.
*left*, double-click
Selects the _word_ (separated by spaces, period, comma,
parenthesis etc) under the pointer. Hold *ctrl* to select
everything under the pointer up to, and until, the next space
characters.
*left*, triple-click
Selects the everything between enclosing quotes, or the entire row
if not inside a quote.
*left*, quad-click
Selects the entire row
*middle*
Paste from the _primary_ selection
*right*
Extend current selection. Clicking immediately extends the
selection, while hold-and-drag allows you to interactively resize
the selection.
*ctrl*+*right*
Extend the current selection, but force it to be character wise,
rather than depending on the original selection mode.
*wheel*
Scroll up/down in history
*ctrl*+*wheel*
Increase/decrease font size
## TOUCHSCREEN
*tap*
Emulates mouse left button click.
*drag*
Scrolls up/down in history.
Holding for a while before dragging (time delay can be configured)
emulates mouse dragging with left button held.
# FONT FORMAT
The font is specified in FontConfig syntax. That is, a colon-separated
list of font name and font options.
_Examples_:
- Dina:weight=bold:slant=italic
- Courier New:size=12
# URLs
Foot supports URL detection. But, unlike many other terminal
emulators, where URLs are highlighted when they are hovered and opened
by clicking on them, foot uses a keyboard driven approach.
Pressing *ctrl*+*shift*+*o* enters _"Open URL mode"_, where all currently
visible URLs are underlined, and is associated with a
_"jump-label"_. The jump-label indicates the _key sequence_
(e.g. *"AF"*) to use to activate the URL.
The key binding can, of course, be customized, like all other key
bindings in foot. See *show-urls-launch* and *show-urls-copy* in
*foot.ini*(5).
*show-urls-launch* by default opens the URL with *xdg-open*. This can
be changed with the *url-launch* option.
*show-urls-copy* is an alternative to *show-urls-launch*, that changes
what activating a URL _does_; instead of opening it, it copies it to
the clipboard. It is unbound by default.
Jump label colors, the URL underline color, and the letters used in
the jump label key sequences can be configured.
# ALT/META CHARACTERS
By default, foot prefixes meta characters with *ESC*. This corresponds
to XTerm's *metaSendsEscape* option set to *true*.
This can be disabled programmatically with *\E[?1036l* (and enabled
again with *\E[?1036h*).
When disabled, foot will instead set the 8:th bit of meta character
and then UTF-8 encode it. This corresponds to XTerm's *eightBitMeta*
option set to *true*.
This can also be disabled programmatically with *rmm* (Reset Meta Mode,
*\E[?1034l*), and enabled again with *smm* (Set Meta Mode,
*\E[?1034h*).
# BACKSPACE
Foot transmits DEL (*^?*) on backspace. This corresponds to XTerm's
*backarrowKey* option set to *false*, and to DECBKM being _reset_.
To instead transmit BS (*^H*), press *ctrl*+*backspace*.
Note that foot does *not* implement DECBKM, and that the behavior
described above *cannot* be changed.
Finally, pressing *alt* will prefix the transmitted byte with ESC.
# KEYPAD
By default, *Num Lock* overrides the run-time configuration keypad
mode; when active, the keypad is always considered to be in
_numerical_ mode. This corresponds to XTerm's *numLock* option set to
*true*.
In this mode, the keypad keys always sends either numbers (Num Lock is
active) or cursor movement keys (up, down, left, right, page up, page
down etc).
This can be disabled programmatically with *\E[?1035l* (and enabled
again with *\E[?1035h*).
When disabled, the keypad sends custom escape sequences instead of
numbers, when in _application_ mode.
# CONFIGURATION
foot will search for a configuration file in the following locations,
in this order:
- *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to
*$HOME/.config/foot/foot.ini* if unset)
- *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to
*/etc/xdg/foot/foot.ini* if unset)
An example configuration file containing all options with their default value
commented out will usually be installed to */etc/xdg/foot/foot.ini*.
For more information, see *foot.ini*(5).
# SHELL INTEGRATION
## Current working directory
New foot terminal instances (bound to *ctrl*+*shift*+*n* by default)
will open in the current working directory, if the shell in the
"parent" terminal reports directory changes.
This is done with the OSC-7 escape sequence. Most shells can be
scripted to do this, if they do not support it natively. See the wiki
(https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory)
for details.
## Jumping between prompts
Foot can move the current viewport to focus prompts of already
executed commands (bound to *ctrl*+*shift*+*z*/*x* by default).
For this to work, the shell needs to emit an OSC-133;A
(*\\E]133;A\\E\\\\*) sequence before each prompt.
In zsh, one way to do this is to add a _precmd_ hook:
*precmd() {
print -Pn "\\e]133;A\\e\\\\"
}*
See the wiki
(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts)
for details, and examples for other shells.
## Piping last command's output
The key binding *pipe-command-output* can pipe the last command's
output to an application of your choice (similar to the other
*pipe-\** key bindings):
*\[key-bindings\]++
pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g*
When pressing *ctrl*+*shift*+*g*, the last command's output is written
to a temporary file, then an emacsclient is started in a new
footclient instance. The temporary file is removed after the
footclient instance has closed.
For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*)
sequence before command output starts, and an OSC-133;D
(*\\E]133;D\\E\\\\*) when the command output ends.
In fish, one way to do this is to add _preexec_ and _postexec_ hooks:
*function foot_cmd_start --on-event fish_preexec
echo -en "\\e]133;C\\e\\\\"
end*
*function foot_cmd_end --on-event fish_postexec
echo -en "\\e]133;D\\e\\\\"
end*
See the wiki
(https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output)
for details, and examples for other shells
# TERMINFO
Client applications use the terminfo identifier specified by the
environment variable *TERM* (set by foot) to determine terminal
capabilities.
Foot has two terminfo definitions: *foot* and *foot-direct*, with
*foot* being the default.
The difference between the two is in the number of colors they
describe; *foot* describes 256 colors and *foot-direct* 16.7 million
colors (24-bit truecolor).
Note that using the *foot* terminfo does not limit the number of
usable colors to 256; applications can still use 24-bit RGB colors. In
fact, most applications work best with *foot* (including 24-bit
colors). Using *\*-direct* terminfo entries has been known to crash
some ncurses applications even.
There are however applications that need a *\*-direct* terminfo entry
for 24-bit support. Emacs is one such example.
While using either *foot* or *foot-direct* is strongly recommended, it
is possible to use e.g. *xterm-256color* as well. This can be useful
when remoting to a system where foot's terminfo entries cannot easily
be installed.
Note that terminfo entries can be installed in the user's home
directory. I.e. if you do not have root access, or if there is no
distro package for foot's terminfo entries, you can install foot's
terminfo entries manually, by copying *foot* and *foot-direct* to
*~/.terminfo/f/*.
# XTGETTCAP
*XTGETTCAP* is an escape sequence initially introduced by XTerm, and
also implemented (and extended, to some degree) by Kitty.
It allows querying the terminal for terminfo classic, file-based,
terminfo definition. For example, if all applications used this
feature, you would no longer have to install foot's terminfo on remote
hosts you SSH into.
XTerm's implementation (as of XTerm-370) only supports querying key
(as in keyboard keys) capabilities, and three custom capabilities:
- TN - terminal name
- Co - number of colors (alias for the colors capability)
- RGB - number of bits per color channel (different semantics from
the RGB capability in file-based terminfo definitions!).
Kitty has extended this, and also supports querying all integer and
string capabilities.
Foot supports this, and extends it even further, to also include
boolean capabilities. This means foot's entire terminfo can be queried
via *XTGETTCAP*.
Note that both Kitty and foot handles responses to multi-capability
queries slightly differently, compared to XTerm.
XTerm will send a single DCS reply, with ;-separated
capability/value pairs. There are a couple of issues with this:
- The success/fail flag in the beginning of the response is always 1
(success), unless the very first queried capability is invalid.
- XTerm will not respond at all to an invalid capability, unless it's
the first one in the XTGETTCAP query.
- XTerm will end the response at the first invalid capability.
In other words, if you send a large multi-capability query, you will
only get responses up to, but not including, the first invalid
capability. All subsequent capabilities will be dropped.
Kitty and foot on the other hand, send one DCS response for each
capability in the multi query. This allows us to send a proper
success/fail flag for each queried capability. Responses for all
queried capabilities are always sent. No queries are ever dropped.
# EXIT STATUS
Foot will exit with code 230 if there is a failure in foot itself.
In all other cases, the exit code is that of the client application
(i.e. the shell).
# ENVIRONMENT
## Variables used by foot
*SHELL*
The default child process to run, when no _command_ argument is
specified and the *shell* option in *foot.ini*(5) is not set.
*HOME*
Used to determine the location of the configuration file, see
*foot.ini*(5) for details.
*XDG\_CONFIG\_HOME*
Used to determine the location of the configuration file, see
*foot.ini*(5) for details.
*XDG\_CONFIG\_DIRS*
Used to determine the location of the configuration file, see
*foot.ini*(5) for details.
*XDG\_RUNTIME\_DIR*
Used to construct the default _PATH_ for the *--server*
option, when no explicit argument is given (see above).
*WAYLAND\_DISPLAY*
Used to construct the default _PATH_ for the *--server*
option, when no explicit argument is given (see above).
*XCURSOR\_THEME*
The name of the *Xcursor*(3) theme to use for pointers (typically
set by the Wayland compositor).
*XCURSOR\_SIZE*
The size to use for *Xcursor*(3) pointers (typically set by the
Wayland compositor).
## Variables set in the child process
*TERM*
terminfo/termcap identifier. This is used by client applications
to determine which capabilities a terminal supports. The value is
set according to either the *--term* command-line option or the
*term* config option in *foot.ini*(5).
*COLORTERM*
This variable is set to *truecolor*, to indicate to client
applications that 24-bit RGB colors are supported.
*PWD*
Current working directory (at the time of launching foot)
*SHELL*
Set to the launched shell, if the shell is valid (it is listed in
*/etc/shells*).
In addition to the variables listed above, custom environment
variables may be defined in *foot.ini*(5).
## Variables *unset* in the child process
*TERM_PROGRAM*
*TERM_PROGRAM_VERSION*
These environment variables are set by certain other terminal
emulators. We unset them, to prevent applications from
misdetecting foot.
In addition to the variables listed above, custom environment
variables to unset may be defined in *foot.ini*(5).
# Signals
The following signals have special meaning in foot:
- SIGUSR1: switch to the dark color theme (*[colors-dark]*).
- SIGUSR2: switch to the light color theme (*[colors-light]*).
Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too,
in which case all client instances will switch theme. Furthermore, all
future client instances will also use the selected theme.
You can also send SIGUSR1/SIGUSR2 to a footclient instance, see
*footclient*(1) for details.
# BUGS
Please report bugs to https://codeberg.org/dnkl/foot/issues
Before you open a new issue, please search existing bug reports, both
open and closed ones. Chances are someone else has already reported
the same issue.
The report should contain the following:
- Foot version (*foot --version*).
- Log output from foot (run *foot -d info* from another terminal).
- Which Wayland compositor (and version) you are running.
- If reporting a crash, please try to provide a *bt full* backtrace
with symbols.
- Steps to reproduce. The more details the better.
# IRC
\#foot on irc.libera.chat
# SEE ALSO
*foot.ini*(5), *footclient*(1)

2166
doc/foot.ini.5.scd Normal file

File diff suppressed because it is too large Load diff

214
doc/footclient.1.scd Normal file
View file

@ -0,0 +1,214 @@
footclient(1)
# NAME
footclient - start new terminals in a foot server
# SYNOPSIS
*footclient* [_OPTIONS_]++
*footclient* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_]
All trailing (non-option) arguments are treated as a command, and its
arguments, to execute (instead of the default shell).
# DESCRIPTION
*footclient* is used together with *foot*(1) in *--server*
mode.
Running it without arguments will open a new terminal window (hosted
in the foot server), with your default shell. The exit code will be
that of the terminal. I.e *footclient* does not exit until the
terminal has terminated.
# OPTIONS
*-t*,*--term*=_TERM_
Value to set the environment variable *TERM* to (see *TERMINFO*
and *ENVIRONMENT*). Default: _@default_terminfo@_.
*-T*,*--title*=_TITLE_
Initial window title. Default: _foot_.
*-a*,*--app-id*=_ID_
Value to set the *app-id* property on the Wayland window
to. Default: _foot_ (normal mode), or _footclient_ (server mode).
*toplevel-tag*=_TAG_
Value to set the *toplevel-tag* property on the Wayland window
to. The compositor can use this value for session management,
window rules etc. Default: _not set_
*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_
Set initial window width and height, in pixels. Default: _700x500_.
*-W*,*--window-size-chars*=_WIDTHxHEIGHT_
Set initial window width and height, in characters. Default: _not set_.
*-m*,*--maximized*
Start in maximized mode. If both *--maximized* and *--fullscreen*
are specified, the _last_ one takes precedence.
*-F*,*--fullscreen*
Start in fullscreen mode. If both *--maximized* and *--fullscreen*
are specified, the _last_ one takes precedence.
*-L*,*--login-shell*
Start a login shell, by prepending a '-' to argv[0].
*-D*,*--working-directory*=_DIR_
Initial working directory for the client application. Default:
_CWD of footclient_.
*-s*,*--server-socket*=_PATH_
Connect to _PATH_ instead of
*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*.
*-H*,*--hold*
Remain open after child process exits.
*-N*,*--no-wait*
Detach the client process from the running terminal, exiting
immediately.
*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_
Override an option set in the configuration file. If _SECTION_ is not
given, defaults to _main_.
*-E*,*--client-environment*
The child process in the new terminal instance will use
footclient's environment, instead of the server's.
Environment variables listed in the *Variables set in the child
process* section will be overwritten by the foot server. For
example, the new terminal will use *TERM* from the configuration,
not footclient's environment.
*-d*,*--log-level*={*info*,*warning*,*error*,*none*}
Log level, used both for log output on stderr as well as
syslog. Default: _warning_.
*-l*,*--log-colorize*=[{*never*,*always*,*auto*}]
Enables or disables colorization of log output on stderr.
*-v*,*--version*
Show the version number and quit
*-e*
Ignored; for compatibility with *xterm -e*. See *foot*(1) for more
details.
# EXIT STATUS
Footclient will exit with code 220 if there is a failure in footclient
itself (for example, the server socket does not exist).
If *-N*,*--no-wait* is used, footclient exits with code 0 as soon as
the foot server has been instructed to open a new window.
If not, footclient may also exit with code 230. This indicates a
failure in the foot server.
In all other cases the exit code is that of the client application
(i.e. the shell).
# TERMINFO
Client applications use the terminfo identifier specified by the
environment variable *TERM* (set by foot) to determine terminal
capabilities.
Foot has two terminfo definitions: *foot* and *foot-direct*, with
*foot* being the default.
The difference between the two is in the number of colors they
describe; *foot* describes 256 colors and *foot-direct* 16.7 million
colors (24-bit truecolor).
Note that using the *foot* terminfo does not limit the number of
usable colors to 256; applications can still use 24-bit RGB colors. In
fact, most applications work best with *foot* (including 24-bit
colors)). Using *\*-direct* terminfo entries has been known to crash
some ncurses applications even.
There are however applications that need a *\*-direct* terminfo entry
for 24-bit support. Emacs is one such example.
While using either *foot* or *foot-direct* is strongly recommended, it
is possible to use e.g. *xterm-256color* as well. This can be useful
when remoting to a system where foot's terminfo entries cannot easily
be installed.
Note that terminfo entries can be installed in the user's home
directory. I.e. if you do not have root access, or if there is no
distro package for foot's terminfo entries, you can install foot's
terminfo entries manually, by copying *foot* and *foot-direct* to
*~/.terminfo/f/*.
# ENVIRONMENT
## Variables used by footclient
*XDG\_RUNTIME\_DIR*
Used to construct the default _PATH_ for the *--server-socket*
option, when no explicit argument is given (see above).
*WAYLAND\_DISPLAY*
Used to construct the default _PATH_ for the *--server-socket*
option, when no explicit argument is given (see above).
If the socket at default _PATH_ does not exist, *footclient* will
fallback to the less specific path, with the following priority:
*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*,
*$XDG\_RUNTIME\_DIR/foot.sock*, */tmp/foot.sock*.
## Variables set in the child process
*TERM*
terminfo/termcap identifier. This is used by client applications
to determine which capabilities a terminal supports. The value is
set according to either the *--term* command-line option or the
*term* config option in *foot.ini*(5).
*COLORTERM*
This variable is set to *truecolor*, to indicate to client
applications that 24-bit RGB colors are supported.
*PWD*
Current working directory (at the time of launching foot)
*SHELL*
Set to the launched shell, if the shell is valid (it is listed in
*/etc/shells*).
In addition to the variables listed above, custom environment
variables may be defined in *foot.ini*(5).
## Variables *unset* in the child process
*TERM_PROGRAM*
*TERM_PROGRAM_VERSION*
These environment variables are set by certain other terminal
emulators. We unset them, to prevent applications from
misdetecting foot.
In addition to the variables listed above, custom environment
variables to unset may be defined in *foot.ini*(5).
# Signals
The following signals have special meaning in footclient:
- SIGUSR1: switch to the dark color theme (*[colors-dark]*).
- SIGUSR2: switch to the light color theme (*[colors-light]*).
When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is
changed in that instance only. This is different from when you send
SIGUSR1/SIGUSR2 to the server process, where all instances change the
theme.
Note: for obvious reasons, this is not supported when footclient is
started with *--no-wait*.
# SEE ALSO
*foot*(1)

49
doc/meson.build Normal file
View file

@ -0,0 +1,49 @@
scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true)
if utmp_backend != 'none'
utmp_add_args = '@0@ $WAYLAND_DISPLAY'.format(utmp_add)
utmp_del_args = (utmp_del_have_argument
? '@0@ $WAYLAND_DISPLAY'.format(utmp_del)
: '@0@'.format(utmp_del))
utmp_path = utmp_default_helper_path
else
utmp_add_args = '<no utmp support in foot>'
utmp_del_args = '<no utmp support in foot>'
utmp_path = 'none'
endif
conf_data = configuration_data(
{
'default_terminfo': get_option('default-terminfo'),
'utmp_backend': utmp_backend,
'utmp_add_args': utmp_add_args,
'utmp_del_args': utmp_del_args,
'utmp_helper_path': utmp_path,
}
)
foreach man_src : [{'name': 'foot', 'section' : 1},
{'name': 'foot.ini', 'section': 5},
{'name': 'footclient', 'section': 1},
{'name': 'foot-ctlseqs', 'section': 7}]
name = man_src['name']
section = man_src['section']
out = '@0@.@1@'.format(name, section)
preprocessed = configure_file(
input: '@0@.@1@.scd'.format(name, section),
output: '@0@.preprocessed'.format(out),
configuration: conf_data,
)
custom_target(
out,
output: out,
input: preprocessed,
command: scdoc_prog.full_path(),
capture: true,
feed: true,
install: true,
install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section)))
endforeach

BIN
doc/sixel-tux-foot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

BIN
doc/tux-foot-ok.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 KiB

271
extract.c Normal file
View file

@ -0,0 +1,271 @@
#include "extract.h"
#include <string.h>
#define LOG_MODULE "extract"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "char32.h"
struct extraction_context {
char32_t *buf;
size_t size;
size_t idx;
size_t tab_spaces_left;
size_t empty_count;
size_t newline_count;
bool strip_trailing_empty;
bool failed;
const struct row *last_row;
const struct cell *last_cell;
enum selection_kind selection_kind;
};
struct extraction_context *
extract_begin(enum selection_kind kind, bool strip_trailing_empty)
{
struct extraction_context *ctx = malloc(sizeof(*ctx));
if (unlikely(ctx == NULL)) {
LOG_ERRNO("malloc() failed");
return NULL;
}
*ctx = (struct extraction_context){
.selection_kind = kind,
.strip_trailing_empty = strip_trailing_empty,
};
return ctx;
}
static bool
ensure_size(struct extraction_context *ctx, size_t additional_chars)
{
while (ctx->size < ctx->idx + additional_chars) {
size_t new_size = ctx->size == 0 ? 512 : ctx->size * 2;
char32_t *new_buf = realloc(ctx->buf, new_size * sizeof(new_buf[0]));
if (new_buf == NULL)
return false;
ctx->buf = new_buf;
ctx->size = new_size;
}
xassert(ctx->size >= ctx->idx + additional_chars);
return true;
}
bool
extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len)
{
if (text == NULL)
return false;
*text = NULL;
if (len != NULL)
*len = 0;
if (ctx->failed)
goto err;
if (!ctx->strip_trailing_empty) {
/* Insert pending newlines, and replace empty cells with spaces */
if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->newline_count; i++)
ctx->buf[ctx->idx++] = U'\n';
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = U' ';
}
if (ctx->idx == 0) {
/* Selection of empty cells only */
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'\0';
} else {
xassert(ctx->idx > 0);
xassert(ctx->idx <= ctx->size);
switch (ctx->selection_kind) {
default:
if (ctx->buf[ctx->idx - 1] == U'\n')
ctx->buf[ctx->idx - 1] = U'\0';
break;
case SELECTION_LINE_WISE:
if (ctx->buf[ctx->idx - 1] != U'\n') {
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'\n';
}
break;
}
if (ctx->buf[ctx->idx - 1] != U'\0') {
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'\0';
}
}
*text = ctx->buf;
if (len != NULL)
*len = ctx->idx - 1;
free(ctx);
return true;
err:
free(ctx->buf);
free(ctx);
return false;
}
bool
extract_finish(struct extraction_context *ctx, char **text, size_t *len)
{
if (text == NULL)
return false;
if (len != NULL)
*len = 0;
char32_t *wtext;
if (!extract_finish_wide(ctx, &wtext, NULL))
return false;
bool ret = false;
*text = ac32tombs(wtext);
if (*text == NULL) {
LOG_ERR("failed to convert selection to UTF-8");
goto out;
}
if (len != NULL)
*len = strlen(*text);
ret = true;
out:
free(wtext);
return ret;
}
bool
extract_one(const struct terminal *term, const struct row *row,
const struct cell *cell, int col, void *context)
{
struct extraction_context *ctx = context;
if (cell->wc >= CELL_SPACER)
return true;
if (ctx->last_row != NULL && row != ctx->last_row) {
/* New row - determine if we should insert a newline or not */
if (ctx->selection_kind != SELECTION_BLOCK) {
if (ctx->last_row->linebreak ||
ctx->empty_count > 0 ||
cell->wc == 0)
{
/* Row has a hard linebreak, or either last cell or
* current cell is empty */
/* Don't emit newline just yet - only if there are
* non-empty cells following it */
ctx->newline_count++;
if (!ctx->strip_trailing_empty) {
if (!ensure_size(ctx, ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = U' ';
}
ctx->empty_count = 0;
}
} else {
/* Always insert a linebreak */
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = U'\n';
if (!ctx->strip_trailing_empty) {
if (!ensure_size(ctx, ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = U' ';
}
ctx->empty_count = 0;
}
ctx->tab_spaces_left = 0;
}
if (cell->wc == U' ' && ctx->tab_spaces_left > 0) {
ctx->tab_spaces_left--;
return true;
}
ctx->tab_spaces_left = 0;
if (cell->wc == 0) {
ctx->empty_count++;
ctx->last_row = row;
ctx->last_cell = cell;
return true;
}
/* Insert pending newlines, and replace empty cells with spaces */
if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count))
goto err;
for (size_t i = 0; i < ctx->newline_count; i++)
ctx->buf[ctx->idx++] = U'\n';
for (size_t i = 0; i < ctx->empty_count; i++)
ctx->buf[ctx->idx++] = U' ';
ctx->newline_count = 0;
ctx->empty_count = 0;
if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI)
{
const struct composed *composed = composed_lookup(
term->composed, cell->wc - CELL_COMB_CHARS_LO);
if (!ensure_size(ctx, composed->count))
goto err;
for (size_t i = 0; i < composed->count; i++)
ctx->buf[ctx->idx++] = composed->chars[i];
}
else {
if (!ensure_size(ctx, 1))
goto err;
ctx->buf[ctx->idx++] = cell->wc;
if (cell->wc == U'\t') {
int next_tab_stop = term->cols - 1;
tll_foreach(term->tab_stops, it) {
if (it->item > col) {
next_tab_stop = it->item;
break;
}
}
if (next_tab_stop > col)
ctx->tab_spaces_left = next_tab_stop - col - 1;
}
}
ctx->last_row = row;
ctx->last_cell = cell;
return true;
err:
ctx->failed = true;
return false;
}

21
extract.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include <stddef.h>
#include <stdbool.h>
#include <uchar.h>
#include "terminal.h"
struct extraction_context;
struct extraction_context *extract_begin(
enum selection_kind kind, bool strip_trailing_empty);
bool extract_one(
const struct terminal *term, const struct row *row, const struct cell *cell,
int col, void *context);
bool extract_finish(
struct extraction_context *context, char **text, size_t *len);
bool extract_finish_wide(
struct extraction_context *context, char32_t **text, size_t *len);

496
fdm.c Normal file
View file

@ -0,0 +1,496 @@
#include "fdm.h"
#include <stdlib.h>
#include <stdbool.h>
#include <inttypes.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/epoll.h>
#include <tllist.h>
#define LOG_MODULE "fdm"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "debug.h"
#include "xmalloc.h"
#if !defined(SIGABBREV_NP)
#include <stdio.h>
static const char *
sigabbrev_np(int sig)
{
static char buf[16];
snprintf(buf, sizeof(buf), "<%d>", sig);
return buf;
}
#endif
struct fd_handler {
int fd;
int events;
fdm_fd_handler_t callback;
void *callback_data;
bool deleted;
};
struct sig_handler {
fdm_signal_handler_t callback;
void *callback_data;
};
struct hook {
fdm_hook_t callback;
void *callback_data;
};
typedef tll(struct hook) hooks_t;
struct fdm {
int epoll_fd;
bool is_polling;
tll(struct fd_handler *) fds;
tll(struct fd_handler *) deferred_delete;
sigset_t sigmask;
struct sig_handler *signal_handlers;
hooks_t hooks_low;
hooks_t hooks_normal;
hooks_t hooks_high;
};
static volatile sig_atomic_t got_signal = false;
static volatile sig_atomic_t *received_signals = NULL;
struct fdm *
fdm_init(void)
{
sigset_t sigmask;
if (sigprocmask(0, NULL, &sigmask) < 0) {
LOG_ERRNO("failed to get process signal mask");
return NULL;
}
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
LOG_ERRNO("failed to create epoll FD");
return NULL;
}
xassert(received_signals == NULL); /* Only one FDM instance supported */
received_signals = xcalloc(SIGRTMAX, sizeof(received_signals[0]));
got_signal = false;
struct fdm *fdm = malloc(sizeof(*fdm));
if (unlikely(fdm == NULL)) {
LOG_ERRNO("malloc() failed");
return NULL;
}
struct sig_handler *sig_handlers = calloc(SIGRTMAX, sizeof(sig_handlers[0]));
if (sig_handlers == NULL) {
LOG_ERRNO("failed to allocate signal handler array");
free(fdm);
return NULL;
}
*fdm = (struct fdm){
.epoll_fd = epoll_fd,
.is_polling = false,
.fds = tll_init(),
.deferred_delete = tll_init(),
.sigmask = sigmask,
.signal_handlers = sig_handlers,
.hooks_low = tll_init(),
.hooks_normal = tll_init(),
.hooks_high = tll_init(),
};
return fdm;
}
void
fdm_destroy(struct fdm *fdm)
{
if (fdm == NULL)
return;
if (tll_length(fdm->fds) > 0)
LOG_WARN("FD list not empty");
for (int i = 0; i < SIGRTMAX; i++) {
if (fdm->signal_handlers[i].callback != NULL)
LOG_WARN("handler for signal %d (SIG%s) not removed",
i, sigabbrev_np(i));
}
if (tll_length(fdm->hooks_low) > 0 ||
tll_length(fdm->hooks_normal) > 0 ||
tll_length(fdm->hooks_high) > 0)
{
LOG_WARN("hook list not empty");
}
xassert(tll_length(fdm->fds) == 0);
xassert(tll_length(fdm->deferred_delete) == 0);
xassert(tll_length(fdm->hooks_low) == 0);
xassert(tll_length(fdm->hooks_normal) == 0);
xassert(tll_length(fdm->hooks_high) == 0);
sigprocmask(SIG_SETMASK, &fdm->sigmask, NULL);
free(fdm->signal_handlers);
tll_free(fdm->fds);
tll_free(fdm->deferred_delete);
tll_free(fdm->hooks_low);
tll_free(fdm->hooks_normal);
tll_free(fdm->hooks_high);
close(fdm->epoll_fd);
free(fdm);
free((void *)received_signals);
received_signals = NULL;
}
bool
fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t cb, void *data)
{
#if defined(_DEBUG)
tll_foreach(fdm->fds, it) {
if (it->item->fd == fd) {
BUG("FD=%d already registered", fd);
}
}
#endif
struct fd_handler *handler = malloc(sizeof(*handler));
if (unlikely(handler == NULL)) {
LOG_ERRNO("malloc() failed");
return false;
}
*handler = (struct fd_handler) {
.fd = fd,
.events = events,
.callback = cb,
.callback_data = data,
.deleted = false,
};
tll_push_back(fdm->fds, handler);
struct epoll_event ev = {
.events = events,
.data = {.ptr = handler},
};
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) {
LOG_ERRNO("failed to register FD=%d with epoll", fd);
free(handler);
tll_pop_back(fdm->fds);
return false;
}
return true;
}
static bool
fdm_del_internal(struct fdm *fdm, int fd, bool close_fd)
{
if (fd == -1)
return true;
tll_foreach(fdm->fds, it) {
if (it->item->fd != fd)
continue;
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0)
LOG_ERRNO("failed to unregister FD=%d from epoll", fd);
if (close_fd)
close(it->item->fd);
it->item->deleted = true;
if (fdm->is_polling)
tll_push_back(fdm->deferred_delete, it->item);
else
free(it->item);
tll_remove(fdm->fds, it);
return true;
}
LOG_ERR("no such FD: %d", fd);
close(fd);
return false;
}
bool
fdm_del(struct fdm *fdm, int fd)
{
return fdm_del_internal(fdm, fd, true);
}
bool
fdm_del_no_close(struct fdm *fdm, int fd)
{
return fdm_del_internal(fdm, fd, false);
}
static bool
event_modify(struct fdm *fdm, struct fd_handler *fd, int new_events)
{
if (new_events == fd->events)
return true;
struct epoll_event ev = {
.events = new_events,
.data = {.ptr = fd},
};
if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) {
LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)",
fd->fd, fd->events, new_events);
return false;
}
fd->events = new_events;
return true;
}
bool
fdm_event_add(struct fdm *fdm, int fd, int events)
{
tll_foreach(fdm->fds, it) {
if (it->item->fd != fd)
continue;
return event_modify(fdm, it->item, it->item->events | events);
}
LOG_ERR("FD=%d not registered with the FDM", fd);
return false;
}
bool
fdm_event_del(struct fdm *fdm, int fd, int events)
{
tll_foreach(fdm->fds, it) {
if (it->item->fd != fd)
continue;
return event_modify(fdm, it->item, it->item->events & ~events);
}
LOG_ERR("FD=%d not registered with the FDM", fd);
return false;
}
static hooks_t *
hook_priority_to_list(struct fdm *fdm, enum fdm_hook_priority priority)
{
switch (priority) {
case FDM_HOOK_PRIORITY_LOW: return &fdm->hooks_low;
case FDM_HOOK_PRIORITY_NORMAL: return &fdm->hooks_normal;
case FDM_HOOK_PRIORITY_HIGH: return &fdm->hooks_high;
}
BUG("unhandled priority type");
return NULL;
}
bool
fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data,
enum fdm_hook_priority priority)
{
hooks_t *hooks = hook_priority_to_list(fdm, priority);
#if defined(_DEBUG)
tll_foreach(*hooks, it) {
if (it->item.callback == hook) {
LOG_ERR("hook=0x%" PRIxPTR " already registered", (uintptr_t)hook);
return false;
}
}
#endif
tll_push_back(*hooks, ((struct hook){hook, data}));
return true;
}
bool
fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority)
{
hooks_t *hooks = hook_priority_to_list(fdm, priority);
tll_foreach(*hooks, it) {
if (it->item.callback != hook)
continue;
tll_remove(*hooks, it);
return true;
}
LOG_WARN("hook=0x%" PRIxPTR " not registered", (uintptr_t)hook);
return false;
}
static void
signal_handler(int signo)
{
got_signal = true;
received_signals[signo] = true;
}
bool
fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data)
{
if (fdm->signal_handlers[signo].callback != NULL) {
LOG_ERR("signal %d (SIG%s) already has a handler",
signo, sigabbrev_np(signo));
return false;
}
sigset_t mask, original;
sigemptyset(&mask);
sigaddset(&mask, signo);
if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) {
LOG_ERRNO("failed to block signal %d (SIG%s)",
signo, sigabbrev_np(signo));
return false;
}
struct sigaction action = {.sa_handler = &signal_handler};
sigemptyset(&action.sa_mask);
if (sigaction(signo, &action, NULL) < 0) {
LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)",
signo, sigabbrev_np(signo));
sigprocmask(SIG_SETMASK, &original, NULL);
return false;
}
received_signals[signo] = false;
fdm->signal_handlers[signo].callback = handler;
fdm->signal_handlers[signo].callback_data = data;
return true;
}
bool
fdm_signal_del(struct fdm *fdm, int signo)
{
if (fdm->signal_handlers[signo].callback == NULL)
return false;
struct sigaction action = {.sa_handler = SIG_DFL};
sigemptyset(&action.sa_mask);
if (sigaction(signo, &action, NULL) < 0) {
LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)",
signo, sigabbrev_np(signo));
return false;
}
received_signals[signo] = false;
fdm->signal_handlers[signo].callback = NULL;
fdm->signal_handlers[signo].callback_data = NULL;
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, signo);
if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) {
LOG_ERRNO("failed to unblock signal %d (SIG%s)",
signo, sigabbrev_np(signo));
return false;
}
return true;
}
bool
fdm_poll(struct fdm *fdm)
{
xassert(!fdm->is_polling && "nested calls to fdm_poll() not allowed");
if (fdm->is_polling) {
LOG_ERR("nested calls to fdm_poll() not allowed");
return false;
}
tll_foreach(fdm->hooks_high, it) {
LOG_DBG(
"executing high priority hook 0x%" PRIxPTR" (fdm=%p, data=%p)",
(uintptr_t)it->item.callback, (void *)fdm,
(void *)it->item.callback_data);
it->item.callback(fdm, it->item.callback_data);
}
tll_foreach(fdm->hooks_normal, it) {
LOG_DBG(
"executing normal priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)",
(uintptr_t)it->item.callback, (void *)fdm,
(void *)it->item.callback_data);
it->item.callback(fdm, it->item.callback_data);
}
tll_foreach(fdm->hooks_low, it) {
LOG_DBG(
"executing low priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)",
(uintptr_t)it->item.callback, (void *)fdm,
(void *)it->item.callback_data);
it->item.callback(fdm, it->item.callback_data);
}
struct epoll_event events[tll_length(fdm->fds)];
int r = epoll_pwait(
fdm->epoll_fd, events, tll_length(fdm->fds), -1, &fdm->sigmask);
int errno_copy = errno;
if (unlikely(got_signal)) {
got_signal = false;
for (int i = 0; i < SIGRTMAX; i++) {
if (received_signals[i]) {
received_signals[i] = false;
struct sig_handler *handler = &fdm->signal_handlers[i];
xassert(handler->callback != NULL);
if (!handler->callback(fdm, i, handler->callback_data))
return false;
}
}
}
if (unlikely(r < 0)) {
if (errno_copy == EINTR)
return true;
LOG_ERRNO_P(errno_copy, "failed to epoll");
return false;
}
bool ret = true;
fdm->is_polling = true;
for (int i = 0; i < r; i++) {
struct fd_handler *fd = events[i].data.ptr;
if (fd->deleted)
continue;
if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) {
ret = false;
break;
}
}
fdm->is_polling = false;
tll_foreach(fdm->deferred_delete, it) {
free(it->item);
tll_remove(fdm->deferred_delete, it);
}
return ret;
}

34
fdm.h Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include <stdbool.h>
struct fdm;
typedef bool (*fdm_fd_handler_t)(struct fdm *fdm, int fd, int events, void *data);
typedef bool (*fdm_signal_handler_t)(struct fdm *fdm, int signo, void *data);
typedef void (*fdm_hook_t)(struct fdm *fdm, void *data);
enum fdm_hook_priority {
FDM_HOOK_PRIORITY_LOW,
FDM_HOOK_PRIORITY_NORMAL,
FDM_HOOK_PRIORITY_HIGH
};
struct fdm *fdm_init(void);
void fdm_destroy(struct fdm *fdm);
bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data);
bool fdm_del(struct fdm *fdm, int fd);
bool fdm_del_no_close(struct fdm *fdm, int fd);
bool fdm_event_add(struct fdm *fdm, int fd, int events);
bool fdm_event_del(struct fdm *fdm, int fd, int events);
bool fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data,
enum fdm_hook_priority priority);
bool fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority);
bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data);
bool fdm_signal_del(struct fdm *fdm, int signo);
bool fdm_poll(struct fdm *fdm);

42
foot-features.c Normal file
View file

@ -0,0 +1,42 @@
#include "foot-features.h"
#include "version.h"
const char version_and_features[] =
"version: " FOOT_VERSION
#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED
" +pgo"
#else
" -pgo"
#endif
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
" +ime"
#else
" -ime"
#endif
#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING
" +graphemes"
#else
" -graphemes"
#endif
#if defined(HAVE_XDG_TOPLEVEL_TAG)
" +toplevel-tag"
#else
" -toplevel-tag"
#endif
#if defined(HAVE_EXT_BACKGROUND_EFFECT)
" +blur"
#else
" -blur"
#endif
#if !defined(NDEBUG)
" +assertions"
#else
" -assertions"
#endif
;

13
foot-features.h Normal file
View file

@ -0,0 +1,13 @@
#pragma once
#include <stdio.h>
extern const char version_and_features[];
static inline void
print_version_and_features(const char *prefix)
{
fputs(prefix, stdout);
fputs(version_and_features, stdout);
fputc('\n', stdout);
}

11
foot-server.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Exec=foot --server
Icon=foot
Terminal=false
Categories=System;TerminalEmulator;
Keywords=shell;prompt;command;commandline;
Name=Foot Server
GenericName=Terminal
Comment=A wayland native terminal emulator (server)

15
foot-server.service.in Normal file
View file

@ -0,0 +1,15 @@
[Service]
ExecStart=@bindir@/foot --server=3
UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES
NonBlocking=true
[Unit]
Requires=%N.socket
Description=Foot terminal server mode
Documentation=man:foot(1)
PartOf=graphical-session.target
After=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
[Install]
WantedBy=graphical-session.target

10
foot-server.socket Normal file
View file

@ -0,0 +1,10 @@
[Socket]
ListenStream=%t/foot.sock
[Unit]
PartOf=graphical-session.target
After=graphical-session.target
ConditionEnvironment=WAYLAND_DISPLAY
[Install]
WantedBy=graphical-session.target

11
foot.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Exec=foot
Icon=foot
Terminal=false
Categories=System;TerminalEmulator;
Keywords=shell;prompt;command;commandline;
Name=Foot
GenericName=Terminal
Comment=A wayland native terminal emulator

285
foot.info Normal file
View file

@ -0,0 +1,285 @@
@default_terminfo@|foot terminal emulator,
use=@default_terminfo@+base,
colors#256,
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,
@default_terminfo@-direct|foot with direct color indexing,
use=@default_terminfo@+base,
colors#16777216,
RGB,
setab=\E[%?%p1%{8}%<%t4%p1%d%e48\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
@default_terminfo@+base|foot base fragment,
AX,
Su,
Tc,
XF,
XT,
am,
bce,
bw,
ccc,
hs,
mir,
msgr,
npc,
xenl,
cols#80,
it#8,
lines#24,
pairs#0x10000,
BD=\E[?2004l,
BE=\E[?2004h,
Cr=\E]112\E\\,
Cs=\E]12;%p1%s\E\\,
E3=\E[3J,
Ms=\E]52;%p1%s;%p2%s\E\\,
PE=\E[201~,
PS=\E[200~,
RV=\E[>c,
Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x,
Se=\E[ q,
Setulc=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
Smulx=\E[4:%p1%dm,
Ss=\E[%p1%d q,
Sync=\E[?2026%?%p1%{1}%-%tl%eh%;,
TS=\E]2;,
XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
XR=\E[>0q,
acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G,
blink=\E[5m,
bold=\E[1m,
cbt=\E[Z,
civis=\E[?25l,
clear=\E[H\E[2J,
cnorm=\E[?12l\E[?25h,
cr=\r,
csr=\E[%i%p1%d;%p2%dr,
cub1=^H,
cub=\E[%p1%dD,
cud1=\n,
cud=\E[%p1%dB,
cuf1=\E[C,
cuf=\E[%p1%dC,
cup=\E[%i%p1%d;%p2%dH,
cuu1=\E[A,
cuu=\E[%p1%dA,
cvvis=\E[?12;25h,
dch1=\E[P,
dch=\E[%p1%dP,
dim=\E[2m,
dl1=\E[M,
dl=\E[%p1%dM,
dsl=\E]2;\E\\,
ech=\E[%p1%dX,
ed=\E[J,
el1=\E[1K,
el=\E[K,
fd=\E[?1004l,
fe=\E[?1004h,
flash=\E]555\E\\,
fsl=\E\\,
home=\E[H,
hpa=\E[%i%p1%dG,
ht=^I,
hts=\EH,
ich=\E[%p1%d@,
il1=\E[L,
il=\E[%p1%dL,
ind=\n,
indn=\E[%p1%dS,
initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\,
invis=\E[8m,
is2=\E[!p\E[4l\E>,
kDC3=\E[3;3~,
kDC4=\E[3;4~,
kDC5=\E[3;5~,
kDC6=\E[3;6~,
kDC7=\E[3;7~,
kDC=\E[3;2~,
kDN3=\E[1;3B,
kDN4=\E[1;4B,
kDN5=\E[1;5B,
kDN6=\E[1;6B,
kDN7=\E[1;7B,
kDN=\E[1;2B,
kEND3=\E[1;3F,
kEND4=\E[1;4F,
kEND5=\E[1;5F,
kEND6=\E[1;6F,
kEND7=\E[1;7F,
kEND=\E[1;2F,
kHOM3=\E[1;3H,
kHOM4=\E[1;4H,
kHOM5=\E[1;5H,
kHOM6=\E[1;6H,
kHOM7=\E[1;7H,
kHOM=\E[1;2H,
kIC3=\E[2;3~,
kIC4=\E[2;4~,
kIC5=\E[2;5~,
kIC6=\E[2;6~,
kIC7=\E[2;7~,
kIC=\E[2;2~,
kLFT3=\E[1;3D,
kLFT4=\E[1;4D,
kLFT5=\E[1;5D,
kLFT6=\E[1;6D,
kLFT7=\E[1;7D,
kLFT=\E[1;2D,
kNXT3=\E[6;3~,
kNXT4=\E[6;4~,
kNXT5=\E[6;5~,
kNXT6=\E[6;6~,
kNXT7=\E[6;7~,
kNXT=\E[6;2~,
kPRV3=\E[5;3~,
kPRV4=\E[5;4~,
kPRV5=\E[5;5~,
kPRV6=\E[5;6~,
kPRV7=\E[5;7~,
kPRV=\E[5;2~,
kRIT3=\E[1;3C,
kRIT4=\E[1;4C,
kRIT5=\E[1;5C,
kRIT6=\E[1;6C,
kRIT7=\E[1;7C,
kRIT=\E[1;2C,
kUP3=\E[1;3A,
kUP4=\E[1;4A,
kUP5=\E[1;5A,
kUP6=\E[1;6A,
kUP7=\E[1;7A,
kUP=\E[1;2A,
kbs=^?,
kcbt=\E[Z,
kcub1=\EOD,
kcud1=\EOB,
kcuf1=\EOC,
kcuu1=\EOA,
kdch1=\E[3~,
kend=\EOF,
kf10=\E[21~,
kf11=\E[23~,
kf12=\E[24~,
kf13=\E[1;2P,
kf14=\E[1;2Q,
kf15=\E[1;2R,
kf16=\E[1;2S,
kf17=\E[15;2~,
kf18=\E[17;2~,
kf19=\E[18;2~,
kf1=\EOP,
kf20=\E[19;2~,
kf21=\E[20;2~,
kf22=\E[21;2~,
kf23=\E[23;2~,
kf24=\E[24;2~,
kf25=\E[1;5P,
kf26=\E[1;5Q,
kf27=\E[1;5R,
kf28=\E[1;5S,
kf29=\E[15;5~,
kf2=\EOQ,
kf30=\E[17;5~,
kf31=\E[18;5~,
kf32=\E[19;5~,
kf33=\E[20;5~,
kf34=\E[21;5~,
kf35=\E[23;5~,
kf36=\E[24;5~,
kf37=\E[1;6P,
kf38=\E[1;6Q,
kf39=\E[1;6R,
kf3=\EOR,
kf40=\E[1;6S,
kf41=\E[15;6~,
kf42=\E[17;6~,
kf43=\E[18;6~,
kf44=\E[19;6~,
kf45=\E[20;6~,
kf46=\E[21;6~,
kf47=\E[23;6~,
kf48=\E[24;6~,
kf49=\E[1;3P,
kf4=\EOS,
kf50=\E[1;3Q,
kf51=\E[1;3R,
kf52=\E[1;3S,
kf53=\E[15;3~,
kf54=\E[17;3~,
kf55=\E[18;3~,
kf56=\E[19;3~,
kf57=\E[20;3~,
kf58=\E[21;3~,
kf59=\E[23;3~,
kf5=\E[15~,
kf60=\E[24;3~,
kf61=\E[1;4P,
kf62=\E[1;4Q,
kf63=\E[1;4R,
kf6=\E[17~,
kf7=\E[18~,
kf8=\E[19~,
kf9=\E[20~,
khome=\EOH,
kich1=\E[2~,
kind=\E[1;2B,
kmous=\E[<,
knp=\E[6~,
kpp=\E[5~,
kri=\E[1;2A,
kxIN=\E[I,
kxOUT=\E[O,
nel=\EE,
oc=\E]104\E\\,
op=\E[39;49m,
rc=\E8,
rep=%p1%c\E[%p2%{1}%-%db,
rev=\E[7m,
ri=\EM,
rin=\E[%p1%dT,
ritm=\E[23m,
rmacs=\E(B,
rmam=\E[?7l,
rmcup=\E[?1049l\E[23;0;0t,
rmir=\E[4l,
rmkx=\E[?1l\E>,
rmm=\E[?1036h\E[?1034l,
rmso=\E[27m,
rmul=\E[24m,
rmxx=\E[29m,
rs1=\Ec,
rs2=\E[!p\E[4l\E>,
rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c,
sc=\E7,
setal=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m,
setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm,
setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm,
sgr0=\E(B\E[m,
sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m,
sitm=\E[3m,
smacs=\E(0,
smam=\E[?7h,
smcup=\E[?1049h\E[22;0;0t,
smir=\E[4h,
smkx=\E[?1h\E=,
smm=\E[?1036l\E[?1034h,
smso=\E[7m,
smul=\E[4m,
smxx=\E[9m,
tbc=\E[3g,
tsl=\E]2;,
u6=\E[%i%d;%dR,
u7=\E[6n,
u8=\E[?%[;0123456789]c,
u9=\E[c,
vpa=\E[%i%p1%dd,
xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;,
xr=\EP>\\|foot\\([0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+-g[a-f[0-9]+)?\\)?\E\\\\,
# XT,
# AX,

319
foot.ini Normal file
View file

@ -0,0 +1,319 @@
# -*- conf -*-
# shell=$SHELL (if set, otherwise user's default shell from /etc/passwd)
# term=foot (or xterm-256color if built with -Dterminfo=disabled)
# login-shell=no
# app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode
# title=foot
# locked-title=no
# font=monospace:size=8
# font-bold=<bold variant of regular font>
# font-italic=<italic variant of regular font>
# font-bold-italic=<bold+italic variant of regular font>
# font-size-adjustment=0.5
# line-height=<font metrics>
# letter-spacing=0
# horizontal-letter-offset=0
# vertical-letter-offset=0
# underline-offset=<font metrics>
# underline-thickness=<font underline thickness>
# strikeout-thickness=<font strikeout thickness>
# box-drawings-uses-font-glyphs=no
# dpi-aware=no
# gamma-correct-blending=no
# initial-color-theme=dark
# initial-window-size-pixels=700x500 # Or,
# initial-window-size-chars=<COLSxROWS>
# initial-window-mode=windowed
# pad=0x0 center-when-maximized-and-fullscreen
# resize-by-cells=yes
# resize-keep-grid=yes
# resize-delay-ms=100
# bold-text-in-bright=no
# word-delimiters=,│`|:"'()[]{}<>
# selection-target=primary
# workers=<number of logical CPUs>
# utmp-helper=/usr/lib/utempter/utempter # When utmp backend is libutempter (Linux)
# utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ulog (FreeBSD)
# uppercase-regex-insert=yes
[environment]
# name=value
[security]
# osc52=enabled # disabled|copy-enabled|paste-enabled|enabled
[bell]
# system=yes
# urgent=no
# notify=no
# visual=no
# command=
# command-focused=no
[desktop-notifications]
# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}
# command-action-argument=--action ${action-name}=${action-label}
# close=""
# inhibit-when-focused=yes
[scrollback]
# lines=1000
# multiplier=3.0
# indicator-position=relative
# indicator-format=""
[url]
# launch=xdg-open ${url}
# label-letters=sadfjklewcmpgh
# style=dotted (none|single|double|curly|dotted|dashed)
# osc8-underline=url-mode
# regex=(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')+([0-9a-zA-Z/#@$&*+=~_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*'))
# You can define your own regex's, by adding a section called
# 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied
# to a key-binding. See foot.ini(5) for details
# [regex:your-fancy-name]
# regex=<a POSIX-Extended Regular Expression>
# launch=<path to script or application> ${match}
#
# [key-bindings]
# regex-launch=[your-fancy-name] Control+Shift+q
# regex-copy=[your-fancy-name] Control+Alt+Shift+q
[cursor]
# style=block
# blink=no
# blink-rate=500
# beam-thickness=1.5
# underline-thickness=<font underline thickness>
[mouse]
# hide-when-typing=no
# alternate-scroll-mode=yes
[touch]
# long-press-delay=400
[colors-dark]
# alpha=1.0
# alpha-mode=default # Can be `default`, `matching` or `all`
# background=242424
# foreground=ffffff
# flash=7f7f00
# flash-alpha=0.5
# cursor=<inverse foreground/background>
## Normal/regular colors (color palette 0-7)
# regular0=242424 # black
# regular1=f62b5a # red
# regular2=47b413 # green
# regular3=e3c401 # yellow
# regular4=24acd4 # blue
# regular5=f2affd # magenta
# regular6=13c299 # cyan
# regular7=e6e6e6 # white
## Bright colors (color palette 8-15)
# bright0=616161 # bright black
# bright1=ff4d51 # bright red
# bright2=35d450 # bright green
# bright3=e9e836 # bright yellow
# bright4=5dc5f8 # bright blue
# bright5=feabf2 # bright magenta
# bright6=24dfc4 # bright cyan
# bright7=ffffff # bright white
## dimmed colors (see foot.ini(5) man page)
# dim-blend-towards=black
# dim0=<not set>
# ...
# dim7=<not-set>
## The remaining 256-color palette
# 16 = <256-color palette #16>
# ...
# 255 = <256-color palette #255>
## Sixel colors
# sixel0 = 000000
# sixel1 = 3333cc
# sixel2 = cc2121
# sixel3 = 33cc33
# sixel4 = cc33cc
# sixel5 = 33cccc
# sixel6 = cccc33
# sixel7 = 878787
# sixel8 = 424242
# sixel9 = 545499
# sixel10 = 994242
# sixel11 = 549954
# sixel12 = 995499
# sixel13 = 549999
# sixel14 = 999954
# sixel15 = cccccc
## Misc colors
# selection-foreground=<inverse foreground/background>
# selection-background=<inverse foreground/background>
# jump-labels=<regular0> <regular3> # black-on-yellow
# scrollback-indicator=<regular0> <bright4> # black-on-bright-blue
# search-box-no-match=<regular0> <regular1> # black-on-red
# search-box-match=<regular0> <regular3> # black-on-yellow
# urls=<regular3>
[colors-light]
# Alternative color theme, see man page foot.ini(5)
# Same builtin defaults as [color], except for:
# dim-blend-towards=white
[tabs]
enabled=yes
position=bottom
style=rounded
layout=floating
height=26
# tab-width=200 (max width per tab in floating mode)
# tab-padding=8 (gap between tabs in floating mode)
# label-padding=8 (horizontal padding around the label inside each tab pill)
# margin=4 (edge gap; auto-added to bar height, does not squish pill)
# corner-radius=6 (corner rounding in pixels)
# background=1c1c1c
# foreground=b0b0b0
# active-background=3a3a3a
# active-foreground=ffffff
# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support)
# unread-indicator=● (string drawn before label when tab has unseen output; empty disables)
# unread-color=fabd2f (color of the unread-indicator)
[csd]
# preferred=server
# size=26
# font=<primary font>
# color=<foreground color>
# hide-when-maximized=no
# double-click-to-maximize=yes
# border-width=0
# border-color=<csd.color>
# button-width=26
# button-color=<background color>
# button-minimize-color=<regular4>
# button-maximize-color=<regular2>
# button-close-color=<regular1>
[key-bindings]
# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up
# scrollback-up-half-page=none
# scrollback-up-line=none
# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down
# scrollback-down-half-page=none
# scrollback-down-line=none
# scrollback-home=none
# scrollback-end=none
# clipboard-copy=Control+Shift+c XF86Copy
# clipboard-paste=Control+Shift+v XF86Paste
# primary-paste=Shift+Insert
# search-start=Control+Shift+r
# font-increase=Control+plus Control+equal Control+KP_Add
# font-decrease=Control+minus Control+KP_Subtract
# font-reset=Control+0 Control+KP_0
# spawn-terminal=Control+Shift+n
# minimize=none
# maximize=none
# fullscreen=none
# pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none
# pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none
# pipe-selected=[xargs -r firefox] none
# pipe-command-output=[wl-copy] none # Copy last command's output to the clipboard
# show-urls-launch=Control+Shift+o
# show-urls-copy=none
# show-urls-persistent=none
# prompt-prev=Control+Shift+z
# prompt-next=Control+Shift+x
# unicode-input=Control+Shift+u
# color-theme-switch-1=none
# color-theme-switch-2=none
# color-theme-toggle=none
# noop=none
# quit=none
# tab-new=Control+Shift+t
# tab-close=Control+Shift+w
# tab-next=Control+Tab
# tab-prev=Control+Shift+Tab
# tab-overview=Control+Shift+space
[search-bindings]
# cancel=Control+g Control+c Escape
# commit=Return KP_Enter
# commit-line=Control+Return
# find-prev=Control+r
# find-next=Control+s
# toggle-case=Mod1+c
# toggle-whole-word=Mod1+w
# toggle-regex=Mod1+r
# history-prev=Up
# history-next=Down
# cursor-left=Left Control+b
# cursor-left-word=Control+Left Mod1+b
# cursor-right=Right Control+f
# cursor-right-word=Control+Right Mod1+f
# cursor-home=Home Control+a
# cursor-end=End Control+e
# delete-prev=BackSpace
# delete-prev-word=Mod1+BackSpace Control+BackSpace
# delete-next=Delete
# delete-next-word=Mod1+d Control+Delete
# delete-to-start=Control+u
# delete-to-end=Control+k
# extend-char=Shift+Right
# extend-to-word-boundary=Control+w Control+Shift+Right
# extend-to-next-whitespace=Control+Shift+w
# extend-line-down=Shift+Down
# extend-backward-char=Shift+Left
# extend-backward-to-word-boundary=Control+Shift+Left
# extend-backward-to-next-whitespace=none
# extend-line-up=Shift+Up
# clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste
# primary-paste=Shift+Insert
# unicode-input=none
# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up
# scrollback-up-half-page=none
# scrollback-up-line=none
# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down
# scrollback-down-half-page=none
# scrollback-down-line=none
# scrollback-home=none
# scrollback-end=none
[url-bindings]
# cancel=Control+g Control+c Control+d Escape
# toggle-url-visible=t
[text-bindings]
# \x03=Mod4+c # Map Super+c -> Ctrl+c
[mouse-bindings]
# scrollback-up-mouse=BTN_WHEEL_BACK
# scrollback-down-mouse=BTN_WHEEL_FORWARD
# font-increase=Control+BTN_WHEEL_BACK
# font-decrease=Control+BTN_WHEEL_FORWARD
# selection-override-modifiers=Shift
# primary-paste=BTN_MIDDLE
# select-begin=BTN_LEFT
# select-begin-block=Control+BTN_LEFT
# select-extend=BTN_RIGHT
# select-extend-character-wise=Control+BTN_RIGHT
# select-word=BTN_LEFT-2
# select-word-whitespace=Control+BTN_LEFT-2
# select-quote = BTN_LEFT-3
# select-row=BTN_LEFT-4
# vim: ft=dosini

11
footclient.desktop Normal file
View file

@ -0,0 +1,11 @@
[Desktop Entry]
Type=Application
Exec=footclient
Icon=foot
Terminal=false
Categories=System;TerminalEmulator;
Keywords=shell;prompt;command;commandline;
Name=Foot Client
GenericName=Terminal
Comment=A wayland native terminal emulator (client)

60
generate-version.sh Executable file
View file

@ -0,0 +1,60 @@
#!/bin/sh
set -e
if [ ${#} -ne 3 ]; then
echo "Usage: ${0} <default_version> <src_dir> <out_file>"
exit 1
fi
default_version=${1}
src_dir=${2}
out_file=${3}
# echo "default version: ${default_version}"
# echo "source directory: ${src_dir}"
# echo "output file: ${out_file}"
if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then
workdir=$(pwd)
cd "${src_dir}"
if git describe --tags > /dev/null 2>&1; then
git_version=$(git describe --always --tags)
else
# No tags available, happens in e.g. CI builds
git_version="${default_version}"
fi
git_branch=$(git rev-parse --abbrev-ref HEAD)
cd "${workdir}"
new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')"
else
new_version="${default_version}"
extra=""
fi
major=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1/')
minor=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\2/')
patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/')
extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/')
new_version="#define FOOT_VERSION \"${new_version}\"
#define FOOT_MAJOR ${major}
#define FOOT_MINOR ${minor}
#define FOOT_PATCH ${patch}
#define FOOT_EXTRA \"${extra}\""
if [ -f "${out_file}" ]; then
old_version=$(cat "${out_file}")
else
old_version=""
fi
# echo "old version: ${old_version}"
# echo "new version: ${new_version}"
if [ "${old_version}" != "${new_version}" ]; then
echo "${new_version}" > "${out_file}"
fi

1676
grid.c Normal file

File diff suppressed because it is too large Load diff

138
grid.h Normal file
View file

@ -0,0 +1,138 @@
#pragma once
#include <stddef.h>
#include "debug.h"
#include "terminal.h"
struct grid *grid_snapshot(const struct grid *grid);
void grid_free(struct grid *grid);
void grid_swap_row(struct grid *grid, int row_a, int row_b);
struct row *grid_row_alloc(int cols, bool initialize);
void grid_row_free(struct row *row);
void grid_resize_without_reflow(
struct grid *grid, int new_rows, int new_cols,
int old_screen_rows, int new_screen_rows);
void grid_resize_and_reflow(
struct grid *grid, const struct terminal *term, int new_rows, int new_cols,
int old_screen_rows, int new_screen_rows,
size_t tracking_points_count,
struct coord *const _tracking_points[static tracking_points_count]);
/* Convert row numbers between scrollback-relative and absolute coordinates */
int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row);
int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row);
int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows);
int grid_row_abs_to_sb_precalc_sb_start(
const struct grid *grid, int sb_start, int abs_row);
int grid_row_sb_to_abs_precalc_sb_start(
const struct grid *grid, int sb_start, int sb_rel_row);
static inline int
grid_row_absolute(const struct grid *grid, int row_no)
{
return (grid->offset + row_no) & (grid->num_rows - 1);
}
static inline int
grid_row_absolute_in_view(const struct grid *grid, int row_no)
{
return (grid->view + row_no) & (grid->num_rows - 1);
}
static inline struct row *
_grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null)
{
xassert(grid->offset >= 0);
int real_row = grid_row_absolute(grid, row_no);
struct row *row = grid->rows[real_row];
if (row == NULL && alloc_if_null) {
row = grid_row_alloc(grid->num_cols, false);
grid->rows[real_row] = row;
}
xassert(row != NULL);
return row;
}
static inline struct row *
grid_row(struct grid *grid, int row_no)
{
return _grid_row_maybe_alloc(grid, row_no, false);
}
static inline struct row *
grid_row_and_alloc(struct grid *grid, int row_no)
{
return _grid_row_maybe_alloc(grid, row_no, true);
}
static inline struct row *
grid_row_in_view(struct grid *grid, int row_no)
{
xassert(grid->view >= 0);
int real_row = grid_row_absolute_in_view(grid, row_no);
struct row *row = grid->rows[real_row];
xassert(row != NULL);
return row;
}
void grid_row_uri_range_put(
struct row *row, int col, const char *uri, uint64_t id);
void grid_row_uri_range_erase(struct row *row, int start, int end);
void grid_row_underline_range_put(
struct row *row, int col, struct underline_range_data data);
void grid_row_underline_range_erase(struct row *row, int start, int end);
static inline void
grid_row_uri_range_destroy(struct row_range *range)
{
free(range->uri.uri);
}
static inline void
grid_row_underline_range_destroy(struct row_range *range)
{
}
static inline void
grid_row_range_destroy(struct row_range *range, enum row_range_type type)
{
switch (type) {
case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break;
case ROW_RANGE_UNDERLINE: grid_row_underline_range_destroy(range); break;
}
}
static inline void
grid_row_ranges_destroy(struct row_ranges *ranges, enum row_range_type type)
{
for (int i = 0; i < ranges->count; i++) {
grid_row_range_destroy(&ranges->v[i], type);
}
}
static inline void
grid_row_reset_extra(struct row *row)
{
struct row_data *extra = row->extra;
if (likely(extra == NULL))
return;
grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI);
grid_row_ranges_destroy(&extra->underline_ranges, ROW_RANGE_UNDERLINE);
free(extra->uri_ranges.v);
free(extra->underline_ranges.v);
free(extra);
row->extra = NULL;
}

54
hsl.c Normal file
View file

@ -0,0 +1,54 @@
#include "hsl.h"
#include <math.h>
uint32_t
hsl_to_rgb(int hue, int sat, int lum)
{
double L = lum / 100.0;
double S = sat / 100.0;
double C = (1. - fabs(2. * L - 1.)) * S;
double X = C * (1. - fabs(fmod((double)hue / 60., 2.) - 1.));
double m = L - C / 2.;
double r, g, b;
if (hue >= 0 && hue <= 60) {
r = C;
g = X;
b = 0.;
} else if (hue >= 60 && hue <= 120) {
r = X;
g = C;
b = 0.;
} else if (hue >= 120 && hue <= 180) {
r = 0.;
g = C;
b = X;
} else if (hue >= 180 && hue <= 240) {
r = 0.;
g = X;
b = C;
} else if (hue >= 240 && hue <= 300) {
r = X;
g = 0.;
b = C;
} else if (hue >= 300 && hue <= 360) {
r = C;
g = 0.;
b = X;
} else {
r = 0.;
g = 0.;
b = 0.;
}
r += m;
g += m;
b += m;
return (
(uint8_t)round(r * 255.) << 16 |
(uint8_t)round(g * 255.) << 8 |
(uint8_t)round(b * 255.) << 0);
}

5
hsl.h Normal file
View file

@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
uint32_t hsl_to_rgb(int hue, int sat, int lum);

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128" height="128"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
<title>foot logo</title>
<sodipodi:namedview showgrid="true">
<inkscape:grid empspacing="4" />
</sodipodi:namedview>
<metadata>
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Lennard Hofmann</dc:title>
</cc:Agent>
</dc:creator>
<dc:source>https://freesvg.org/human-footprints</dc:source>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
<dc:title>foot logo</dc:title>
<dc:subject>
<rdf:Bag>
<rdf:li>terminal emulator</rdf:li>
<rdf:li>footprint</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:date>2020-06-23</dc:date>
<dc:description>Black square representing a terminal showing a human footprint as a prompt symbol and an underscore as the cursor</dc:description>
</cc:Work>
</rdf:RDF>
</metadata>
<rect
id="terminal-border"
ry="7.9238095" rx="7.9238095"
y="12.495239" x="12.495239"
height="103.00952" width="103.00952"
style="fill:#c0bfbc" />
<rect
id="terminal-bg"
style="fill:#282828"
width="96" height="96"
x="16" y="16"
rx="7.3846154" ry="7.3846154" />
<g
transform="matrix(0.85714209,0,0,0.85714209,3.4285132,-145.99985)" id="foot">
<path
style="fill:#fbf1c7"
id="main"
d="m 37.0057,207.12866 c -0.9539,-0.24232 -1.8766,-0.41748 -2.7117,-0.49721 -4.1121,-0.39258 -5.433,-0.0326 -7.5067,2.04908 -2.8434,2.85385 -2.2667,6.90676 1.4644,10.38038 8.0896,7.53109 8.2202,12.581 0.4808,18.2752 -3.1863,2.34409 -5.492,7.85321 -4.4994,10.73911 1.2501,3.63469 6.0098,5.02447 10.1635,2.97341 3.8964,-1.92373 8.3104,-8.49971 11.3208,-16.36234 0.2509,-0.65526 0.4966,-1.31942 0.7268,-1.9912 0.6909,-2.01525 1.2796,-4.09527 1.7466,-6.17933 1.4109,-6.29466 1.0666,-11.19321 -0.9956,-14.13789 -1.287,-1.83788 -6.056,-4.19923 -10.1892,-5.24924 z" />
<g
transform="translate(-1890.6933,-335.56903)" id="toes" style="fill:#fbf1c7">
<ellipse
id="toe3"
style="fill:#fbf1c7"
cx="1934.6934" cy="540.29291"
rx="2" ry="2.2761035" />
<ellipse
id="toe4"
style="fill:#fbf1c7"
cx="1939.0691" cy="543.35553"
rx="1.6242591" ry="1.7864922" />
<ellipse
id="toe5"
style="fill:#fbf1c7"
cx="1941.3607" cy="547.06903"
rx="1.33263" ry="1.5" />
<ellipse
id="toe1"
style="fill:#fbf1c7"
cx="1921.8835" cy="536.06903"
rx="3.8097539" ry="4.5" />
<ellipse
id="toe2"
style="fill:#fbf1c7"
cx="1929.1934" cy="537.56903"
rx="2.5" ry="3" />
</g>
</g>
<rect
id="cursor"
y="64" x="48"
height="4" width="22"
style="fill:#fbf1c7" />
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
icons/meson.build Normal file
View file

@ -0,0 +1 @@
install_subdir('hicolor', install_dir : join_paths(get_option('datadir'), 'icons'))

525
ime.c Normal file
View file

@ -0,0 +1,525 @@
#include "ime.h"
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
#include <string.h>
#include "text-input-unstable-v3.h"
#define LOG_MODULE "ime"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "char32.h"
#include "render.h"
#include "search.h"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xmalloc.h"
static void
ime_reset_pending_preedit(struct seat *seat)
{
free(seat->ime.preedit.pending.text);
seat->ime.preedit.pending.text = NULL;
}
static void
ime_reset_pending_commit(struct seat *seat)
{
free(seat->ime.commit.pending.text);
seat->ime.commit.pending.text = NULL;
}
void
ime_reset_pending(struct seat *seat)
{
ime_reset_pending_preedit(seat);
ime_reset_pending_commit(seat);
}
void
ime_reset_preedit(struct seat *seat)
{
if (seat->ime.preedit.cells == NULL)
return;
free(seat->ime.preedit.text);
free(seat->ime.preedit.cells);
seat->ime.preedit.text = NULL;
seat->ime.preedit.cells = NULL;
seat->ime.preedit.count = 0;
}
static void
enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
struct wl_surface *surface)
{
struct seat *seat = data;
struct wl_window *win = wl_surface_get_user_data(surface);
struct terminal *term = win->term;
LOG_DBG("enter: seat=%s, term=%p", seat->name, (const void *)term);
if (seat->kbd_focus != term) {
LOG_WARN("compositor sent ime::enter() event before the "
"corresponding keyboard_enter() event");
}
/* The main grid is the *only* input-receiving surface we have */
seat->ime_focus = term;
const struct coord *cursor = &term->grid->cursor.point;
term_ime_set_cursor_rect(
term,
term->margins.left + cursor->col * term->cell_width,
term->margins.top + cursor->row * term->cell_height,
term->cell_width,
term->cell_height);
ime_enable(seat);
}
static void
leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
struct wl_surface *surface)
{
struct seat *seat = data;
LOG_DBG("leave: seat=%s", seat->name);
ime_disable(seat);
seat->ime_focus = NULL;
}
static void
preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
const char *text, int32_t cursor_begin, int32_t cursor_end)
{
LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end);
struct seat *seat = data;
ime_reset_pending_preedit(seat);
if (text != NULL) {
seat->ime.preedit.pending.text = xstrdup(text);
seat->ime.preedit.pending.cursor_begin = cursor_begin;
seat->ime.preedit.pending.cursor_end = cursor_end;
}
}
static void
commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
const char *text)
{
LOG_DBG("commit: text=%s", text);
struct seat *seat = data;
ime_reset_pending_commit(seat);
if (text != NULL)
seat->ime.commit.pending.text = xstrdup(text);
}
static void
delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
uint32_t before_length, uint32_t after_length)
{
LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length);
struct seat *seat = data;
seat->ime.surrounding.pending.before_length = before_length;
seat->ime.surrounding.pending.after_length = after_length;
}
static void
done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3,
uint32_t serial)
{
/*
* From text-input-unstable-v3.h:
*
* The application must proceed by evaluating the changes in the
* following order:
*
* 1. Replace existing preedit string with the cursor.
* 2. Delete requested surrounding text.
* 3. Insert commit string with the cursor at its end.
* 4. Calculate surrounding text to send.
* 5. Insert new preedit text in cursor position.
* 6. Place cursor inside preedit text.
*/
LOG_DBG("done: serial=%u", serial);
struct seat *seat = data;
struct terminal *term = seat->ime_focus;
if (seat->ime.serial != serial) {
LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x",
seat->ime.serial, serial);
return;
}
if (term == NULL) {
static bool have_warned = false;
if (!have_warned) {
LOG_WARN(
"%s: text-input::done() received on seat that isn't "
"focusing a terminal window", seat->name);
have_warned = true;
}
}
/* 1. Delete existing pre-edit text */
if (seat->ime.preedit.cells != NULL) {
ime_reset_preedit(seat);
if (term != NULL) {
if (term->is_searching)
render_refresh_search(term);
else
render_refresh(term);
}
}
/*
* 2. Delete requested surrounding text
*
* We don't support deleting surrounding text. But, we also never
* call set_surrounding_text() so hopefully we should never
* receive any requests to delete surrounding text.
*/
/* 3. Insert commit string */
if (seat->ime.commit.pending.text != NULL) {
const char *text = seat->ime.commit.pending.text;
size_t len = strlen(text);
if (term != NULL) {
if (term->is_searching) {
search_add_chars(term, text, len);
render_refresh_search(term);
} else
term_to_slave(term, text, len);
}
ime_reset_pending_commit(seat);
}
/* 4. Calculate surrounding text to send - not supported */
/* 5. Insert new pre-edit text */
char32_t *allocated_preedit_text = NULL;
if (seat->ime.preedit.pending.text == NULL ||
seat->ime.preedit.pending.text[0] == '\0' ||
(allocated_preedit_text = ambstoc32(seat->ime.preedit.pending.text)) == NULL)
{
ime_reset_pending_preedit(seat);
return;
}
xassert(seat->ime.preedit.pending.text != NULL);
xassert(allocated_preedit_text != NULL);
seat->ime.preedit.text = allocated_preedit_text;
size_t wchars = c32len(seat->ime.preedit.text);
/* Next, count number of cells needed */
size_t cell_count = 0;
size_t widths[wchars + 1];
for (size_t i = 0; i < wchars; i++) {
int width = max(c32width(seat->ime.preedit.text[i]), 1);
widths[i] = width;
cell_count += width;
}
/* Allocate cells */
seat->ime.preedit.cells = xmalloc(
cell_count * sizeof(seat->ime.preedit.cells[0]));
seat->ime.preedit.count = cell_count;
/* Populate cells */
for (size_t i = 0, cell_idx = 0; i < wchars; i++) {
struct cell *cell = &seat->ime.preedit.cells[cell_idx];
int width = widths[i];
cell->wc = seat->ime.preedit.text[i];
cell->attrs = (struct attributes){.clean = 0};
for (int j = 1; j < width; j++) {
cell = &seat->ime.preedit.cells[cell_idx + j];
cell->wc = CELL_SPACER + width - j;
cell->attrs = (struct attributes){.clean = 1};
}
cell_idx += width;
}
const size_t byte_len = strlen(seat->ime.preedit.pending.text);
/* Pre-edit cursor - hidden */
if (seat->ime.preedit.pending.cursor_begin == -1 ||
seat->ime.preedit.pending.cursor_end == -1)
{
/* Note: docs says *both* begin and end should be -1,
* but what else can we do if only one is -1? */
LOG_DBG("pre-edit cursor is hidden");
seat->ime.preedit.cursor.hidden = true;
seat->ime.preedit.cursor.start = -1;
seat->ime.preedit.cursor.end = -1;
}
else if (seat->ime.preedit.pending.cursor_begin == byte_len &&
seat->ime.preedit.pending.cursor_end == byte_len)
{
/* Cursor is *after* the entire pre-edit string */
seat->ime.preedit.cursor.hidden = false;
seat->ime.preedit.cursor.start = cell_count;
seat->ime.preedit.cursor.end = cell_count;
}
else {
/*
* Translate cursor position to cell indices
*
* The cursor_begin and cursor_end are counted in
* *bytes*. We want to map them to *cell* indices.
*
* To do this, we use mblen() to step though the utf-8
* pre-edit string, advancing a unicode character index as
* we go, *and* advancing a *cell* index using c32width()
* of the unicode character.
*
* When we find the matching *byte* index, we at the same
* time know both the unicode *and* cell index.
*/
int cell_begin = -1, cell_end = -1;
for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0;
byte_idx < byte_len &&
wc_idx < wchars &&
cell_idx < cell_count &&
(cell_begin < 0 || cell_end < 0);
cell_idx += widths[wc_idx], wc_idx++)
{
if (seat->ime.preedit.pending.cursor_begin == byte_idx)
cell_begin = cell_idx;
if (seat->ime.preedit.pending.cursor_end == byte_idx)
cell_end = cell_idx;
/* Number of bytes of *next* utf-8 character */
size_t left = byte_len - byte_idx;
int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left);
if (wc_bytes <= 0)
break;
byte_idx += wc_bytes;
}
if (seat->ime.preedit.pending.cursor_end >= byte_len)
cell_end = cell_count;
/* Bounded by number of screen columns */
cell_begin = min(max(cell_begin, 0), cell_count - 1);
cell_end = min(max(cell_end, 0), cell_count);
if (cell_end < cell_begin)
cell_end = cell_begin;
/* Expand cursor end to end of glyph */
while (cell_end > cell_begin && cell_end < cell_count &&
seat->ime.preedit.cells[cell_end].wc >= CELL_SPACER)
{
cell_end++;
}
LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end);
xassert(cell_begin >= 0);
xassert(cell_begin < cell_count);
xassert(cell_begin <= cell_end);
xassert(cell_end >= 0);
xassert(cell_end <= cell_count);
seat->ime.preedit.cursor.hidden = false;
seat->ime.preedit.cursor.start = cell_begin;
seat->ime.preedit.cursor.end = cell_end;
}
/* Underline pre-edit string that is *not* covered by the cursor */
bool hidden = seat->ime.preedit.cursor.hidden;
int start = seat->ime.preedit.cursor.start;
int end = seat->ime.preedit.cursor.end;
for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) {
if (hidden || start == end || cell_idx < start || cell_idx >= end) {
struct cell *cell = &seat->ime.preedit.cells[cell_idx];
cell->attrs.underline = true;
}
}
ime_reset_pending_preedit(seat);
if (term != NULL) {
if (term->is_searching)
render_refresh_search(term);
else
render_refresh(term);
}
}
static void
ime_send_cursor_rect(struct seat *seat)
{
if (unlikely(seat->wayl->text_input_manager == NULL))
return;
if (seat->ime_focus == NULL)
return;
struct terminal *term = seat->ime_focus;
if (!term->ime_enabled)
return;
if (seat->ime.cursor_rect.pending.x == seat->ime.cursor_rect.sent.x &&
seat->ime.cursor_rect.pending.y == seat->ime.cursor_rect.sent.y &&
seat->ime.cursor_rect.pending.width == seat->ime.cursor_rect.sent.width &&
seat->ime.cursor_rect.pending.height == seat->ime.cursor_rect.sent.height)
{
return;
}
zwp_text_input_v3_set_cursor_rectangle(
seat->wl_text_input,
seat->ime.cursor_rect.pending.x / term->scale,
seat->ime.cursor_rect.pending.y / term->scale,
seat->ime.cursor_rect.pending.width / term->scale,
seat->ime.cursor_rect.pending.height / term->scale);
zwp_text_input_v3_commit(seat->wl_text_input);
seat->ime.serial++;
seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending;
}
void
ime_enable(struct seat *seat)
{
if (unlikely(seat->wayl->text_input_manager == NULL))
return;
if (seat->ime_focus == NULL)
return;
struct terminal *term = seat->ime_focus;
if (term == NULL)
return;
if (!term->ime_enabled)
return;
ime_reset_pending(seat);
ime_reset_preedit(seat);
zwp_text_input_v3_enable(seat->wl_text_input);
zwp_text_input_v3_set_content_type(
seat->wl_text_input,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL);
zwp_text_input_v3_set_cursor_rectangle(
seat->wl_text_input,
seat->ime.cursor_rect.pending.x / term->scale,
seat->ime.cursor_rect.pending.y / term->scale,
seat->ime.cursor_rect.pending.width / term->scale,
seat->ime.cursor_rect.pending.height / term->scale);
seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending;
zwp_text_input_v3_commit(seat->wl_text_input);
seat->ime.serial++;
}
void
ime_disable(struct seat *seat)
{
if (unlikely(seat->wayl->text_input_manager == NULL))
return;
if (seat->ime_focus == NULL)
return;
ime_reset_pending(seat);
ime_reset_preedit(seat);
zwp_text_input_v3_disable(seat->wl_text_input);
zwp_text_input_v3_commit(seat->wl_text_input);
seat->ime.serial++;
}
void
ime_update_cursor_rect(struct seat *seat)
{
struct terminal *term = seat->ime_focus;
/* Set in render_ime_preedit() */
if (seat->ime.preedit.cells != NULL)
goto update;
/* Set in render_search_box() */
if (term->is_searching)
goto update;
int x, y, width, height;
int col = term->grid->cursor.point.col;
int row = term->grid->cursor.point.row;
row += term->grid->offset;
row -= term->grid->view;
row &= term->grid->num_rows - 1;
x = term->margins.left + col * term->cell_width;
y = term->margins.top + row * term->cell_height;
if (term->cursor_style == CURSOR_BEAM)
width = 1;
else
width = term->cell_width;
height = term->cell_height;
seat->ime.cursor_rect.pending.x = x;
seat->ime.cursor_rect.pending.y = y;
seat->ime.cursor_rect.pending.width = width;
seat->ime.cursor_rect.pending.height = height;
update:
ime_send_cursor_rect(seat);
}
const struct zwp_text_input_v3_listener text_input_listener = {
.enter = &enter,
.leave = &leave,
.preedit_string = &preedit_string,
.commit_string = &commit_string,
.delete_surrounding_text = &delete_surrounding_text,
.done = &done,
};
#else /* !FOOT_IME_ENABLED */
void ime_enable(struct seat *seat) {}
void ime_disable(struct seat *seat) {}
void ime_update_cursor_rect(struct seat *seat) {}
void ime_reset_pending_preedit(struct seat *seat) {}
void ime_reset_pending_commit(struct seat *seat) {}
void ime_reset_pending(struct seat *seat) {}
void ime_reset_preedit(struct seat *seat) {}
#endif

19
ime.h Normal file
View file

@ -0,0 +1,19 @@
#pragma once
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
#include "text-input-unstable-v3.h"
extern const struct zwp_text_input_v3_listener text_input_listener;
#endif /* FOOT_IME_ENABLED */
struct seat;
struct terminal;
void ime_enable(struct seat *seat);
void ime_disable(struct seat *seat);
void ime_update_cursor_rect(struct seat *seat);
void ime_reset_pending(struct seat *seat);
void ime_reset_preedit(struct seat *seat);

3871
input.c Normal file

File diff suppressed because it is too large Load diff

40
input.h Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include <stdint.h>
#include <wayland-client.h>
#include "cursor-shape.h"
#include "misc.h"
#include "wayland.h"
/*
* Custom defines for mouse wheel left/right buttons.
*
* Libinput does not define these. On Wayland, all scroll events (both
* vertical and horizontal) are reported not as buttons, as 'axis'
* events.
*
* Libinput _does_ define BTN_BACK and BTN_FORWARD, which is
* what we use for vertical scroll events. But for horizontal scroll
* events, there aren't any pre-defined mouse buttons.
*
* Mouse buttons are in the range 0x110 - 0x11f, with joystick defines
* starting at 0x120.
*/
#define BTN_WHEEL_BACK 0x11c
#define BTN_WHEEL_FORWARD 0x11d
#define BTN_WHEEL_LEFT 0x11e
#define BTN_WHEEL_RIGHT 0x11f
extern const struct wl_keyboard_listener keyboard_listener;
extern const struct wl_pointer_listener pointer_listener;
extern const struct wl_touch_listener touch_listener;
void input_repeat(struct seat *seat, uint32_t key);
void get_current_modifiers(const struct seat *seat,
xkb_mod_mask_t *effective,
xkb_mod_mask_t *consumed,
uint32_t key, bool filter_locked);
enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y);

661
key-binding.c Normal file
View file

@ -0,0 +1,661 @@
#include "key-binding.h"
#include <stdlib.h>
#define LOG_MODULE "key-binding"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "config.h"
#include "debug.h"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xkbcommon-vmod.h"
#include "xmalloc.h"
struct vmod_map {
const char *name;
xkb_mod_mask_t virtual_mask;
xkb_mod_mask_t real_mask;
};
struct key_set {
struct key_binding_set public;
const struct config *conf;
const struct seat *seat;
size_t conf_ref_count;
/* Virtual to real modifier mappings */
struct vmod_map vmods[8];
};
typedef tll(struct key_set) bind_set_list_t;
struct key_binding_manager {
struct key_set *last_used_set;
bind_set_list_t binding_sets;
};
static void load_keymap(struct key_set *set);
static void unload_keymap(struct key_set *set);
struct key_binding_manager *
key_binding_manager_new(void)
{
struct key_binding_manager *mgr = xcalloc(1, sizeof(*mgr));
return mgr;
}
void
key_binding_manager_destroy(struct key_binding_manager *mgr)
{
xassert(tll_length(mgr->binding_sets) == 0);
free(mgr);
}
static void
initialize_vmod_mappings(struct key_set *set)
{
if (set->seat == NULL || set->seat->kbd.xkb_keymap == NULL)
return;
set->vmods[0].name = XKB_VMOD_NAME_ALT;
set->vmods[1].name = XKB_VMOD_NAME_HYPER;
set->vmods[2].name = XKB_VMOD_NAME_LEVEL3;
set->vmods[3].name = XKB_VMOD_NAME_LEVEL5;
set->vmods[4].name = XKB_VMOD_NAME_META;
set->vmods[5].name = XKB_VMOD_NAME_NUM;
set->vmods[6].name = XKB_VMOD_NAME_SCROLL;
set->vmods[7].name = XKB_VMOD_NAME_SUPER;
struct xkb_state *scratch_state = xkb_state_new(set->seat->kbd.xkb_keymap);
xassert(scratch_state != NULL);
for (size_t i = 0; i < ALEN(set->vmods); i++) {
xkb_mod_index_t virt_idx = xkb_keymap_mod_get_index(
set->seat->kbd.xkb_keymap, set->vmods[i].name);
if (virt_idx != XKB_MOD_INVALID) {
xkb_mod_mask_t vmask = 1 << virt_idx;
xkb_state_update_mask(scratch_state, vmask, 0, 0, 0, 0, 0);
set->vmods[i].real_mask = xkb_state_serialize_mods(
scratch_state, XKB_STATE_MODS_DEPRESSED) & ~vmask;
set->vmods[i].virtual_mask = vmask;
LOG_DBG("%s: 0x%04x -> 0x%04x",
set->vmods[i].name,
set->vmods[i].virtual_mask,
set->vmods[i].real_mask);
} else {
set->vmods[i].virtual_mask = 0;
set->vmods[i].real_mask = 0;
LOG_DBG("%s: virtual modifier not available", set->vmods[i].name);
}
}
xkb_state_unref(scratch_state);
}
void
key_binding_new_for_seat(struct key_binding_manager *mgr,
const struct seat *seat)
{
#if defined(_DEBUG)
tll_foreach(mgr->binding_sets, it)
xassert(it->item.seat != seat);
#endif
tll_foreach(seat->wayl->terms, it) {
struct key_set set = {
.public = {
.key = tll_init(),
.search = tll_init(),
.url = tll_init(),
.mouse = tll_init(),
},
.conf = it->item->conf,
.seat = seat,
.conf_ref_count = 1,
};
tll_push_back(mgr->binding_sets, set);
initialize_vmod_mappings(&tll_back(mgr->binding_sets));
LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1",
(void *)&tll_back(mgr->binding_sets),
(void *)set.seat, (void *)set.conf);
load_keymap(&tll_back(mgr->binding_sets));
}
LOG_DBG("new (seat): total number of sets: %zu",
tll_length(mgr->binding_sets));
}
void
key_binding_new_for_conf(struct key_binding_manager *mgr,
const struct wayland *wayl, const struct config *conf)
{
tll_foreach(wayl->seats, it) {
struct seat *seat = &it->item;
struct key_set *existing =
(struct key_set *)key_binding_for(mgr, conf, seat);
if (existing != NULL) {
existing->conf_ref_count++;
continue;
}
struct key_set set = {
.public = {
.key = tll_init(),
.search = tll_init(),
.url = tll_init(),
.mouse = tll_init(),
},
.conf = conf,
.seat = seat,
.conf_ref_count = 1,
};
tll_push_back(mgr->binding_sets, set);
initialize_vmod_mappings(&tll_back(mgr->binding_sets));
load_keymap(&tll_back(mgr->binding_sets));
/* Chances are high this set will be requested next */
mgr->last_used_set = &tll_back(mgr->binding_sets);
LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1",
(void *)&tll_back(mgr->binding_sets),
(void *)set.seat, (void *)set.conf);
}
LOG_DBG("new (conf): total number of sets: %zu",
tll_length(mgr->binding_sets));
}
struct key_binding_set * NOINLINE
key_binding_for(struct key_binding_manager *mgr, const struct config *conf,
const struct seat *seat)
{
struct key_set *last_used = mgr->last_used_set;
if (last_used != NULL &&
last_used->conf == conf &&
last_used->seat == seat)
{
// LOG_DBG("lookup: last used");
return &last_used->public;
}
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->conf != conf)
continue;
if (set->seat != seat)
continue;
#if 0
LOG_DBG("lookup: set=%p, seat=%p, conf=%p, ref-count=%zu",
(void *)set, (void *)seat, (void *)conf, set->conf_ref_count);
#endif
mgr->last_used_set = set;
return &set->public;
}
return NULL;
}
static void
key_binding_set_destroy(struct key_binding_manager *mgr,
struct key_set *set)
{
unload_keymap(set);
if (mgr->last_used_set == set)
mgr->last_used_set = NULL;
/* Note: caller must remove from binding_sets */
}
void
key_binding_remove_seat(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat != seat)
continue;
key_binding_set_destroy(mgr, set);
tll_remove(mgr->binding_sets, it);
LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu",
(void *)set, (void *)seat, tll_length(mgr->binding_sets));
}
LOG_DBG("remove seat: total number of sets: %zu",
tll_length(mgr->binding_sets));
}
void
key_binding_unref(struct key_binding_manager *mgr, const struct config *conf)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->conf != conf)
continue;
xassert(set->conf_ref_count > 0);
if (--set->conf_ref_count == 0) {
LOG_DBG("unref conf: set=%p, seat=%p, conf=%p",
(void *)set, (void *)set->seat, (void *)conf);
key_binding_set_destroy(mgr, set);
tll_remove(mgr->binding_sets, it);
}
}
LOG_DBG("unref conf: total number of sets: %zu",
tll_length(mgr->binding_sets));
}
static xkb_keycode_list_t
key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym)
{
xkb_keycode_list_t key_codes = tll_init();
/*
* Find all key codes that map to this symbol.
*
* This allows us to match bindings in other layouts
* too.
*/
struct xkb_state *state = xkb_state_new(keymap);
for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap);
code <= xkb_keymap_max_keycode(keymap);
code++)
{
if (xkb_state_key_get_one_sym(state, code) == sym)
tll_push_back(key_codes, code);
}
xkb_state_unref(state);
return key_codes;
}
static xkb_keysym_t
maybe_repair_key_combo(const struct seat *seat,
xkb_keysym_t sym, xkb_mod_mask_t mods)
{
/*
* Detect combos containing a shifted symbol and the corresponding
* modifier, and replace the shifted symbol with its unshifted
* variant.
*
* For example, the combo is "Control+Shift+U". In this case,
* Shift is the modifier used to "shift" 'u' to 'U', after which
* 'Shift' will have been "consumed". Since we filter out consumed
* modifiers when matching key combos, this key combo will never
* trigger (we will never be able to match the 'Shift' modifier).
*
* There are two correct variants of the above key combo:
* - "Control+U" (upper case 'U')
* - "Control+Shift+u" (lower case 'u')
*
* What we do here is, for each key *code*, check if there are any
* (shifted) levels where it produces 'sym'. If there are, check
* *which* sets of modifiers are needed to produce it, and compare
* with 'mods'.
*
* If there is at least one common modifier, it means 'sym' is a
* "shifted" symbol, with the corresponding shifting modifier
* explicitly included in the key combo. I.e. the key combo will
* never trigger.
*
* We then proceed and "repair" the key combo by replacing 'sym'
* with the corresponding unshifted symbol.
*
* To reduce the noise, we ignore all key codes where the shifted
* symbol is the same as the unshifted symbol.
*/
for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap);
code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap);
code++)
{
xkb_layout_index_t layout_idx =
xkb_state_key_get_layout(seat->kbd.xkb_state, code);
/* Get all unshifted symbols for this key */
const xkb_keysym_t *base_syms = NULL;
size_t base_count = xkb_keymap_key_get_syms_by_level(
seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms);
if (base_count == 0 || sym == base_syms[0]) {
/* No unshifted symbols, or unshifted symbol is same as 'sym' */
continue;
}
/* Name of the unshifted symbol, for logging */
char base_name[100];
xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name));
/* Iterate all shift levels */
for (xkb_level_index_t level_idx = 1;
level_idx < xkb_keymap_num_levels_for_key(
seat->kbd.xkb_keymap, code, layout_idx);
level_idx++) {
/* Get all symbols for current shift level */
const xkb_keysym_t *shifted_syms = NULL;
size_t shifted_count = xkb_keymap_key_get_syms_by_level(
seat->kbd.xkb_keymap, code,
layout_idx, level_idx, &shifted_syms);
for (size_t i = 0; i < shifted_count; i++) {
if (shifted_syms[i] != sym)
continue;
/* Get modifier sets that produces the current shift level */
xkb_mod_mask_t mod_masks[16];
size_t mod_mask_count = xkb_keymap_key_get_mods_for_level(
seat->kbd.xkb_keymap, code, layout_idx, level_idx,
mod_masks, ALEN(mod_masks));
/* Check if key combo's modifier set intersects */
for (size_t j = 0; j < mod_mask_count; j++) {
if ((mod_masks[j] & mods) != mod_masks[j])
continue;
char combo[64] = {0};
for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) {
if (!(mods & (1u << k)))
continue;
const char *mod_name = xkb_keymap_mod_get_name(
seat->kbd.xkb_keymap, k);
strcat(combo, mod_name);
strcat(combo, "+");
}
size_t len = strlen(combo);
xkb_keysym_get_name(
sym, &combo[len], sizeof(combo) - len);
LOG_WARN(
"%s: combo with both explicit modifier and shifted symbol "
"(level=%d, mod-mask=0x%08x), "
"replacing with %s",
combo, level_idx, mod_masks[j], base_name);
/* Replace with unshifted symbol */
return base_syms[0];
}
}
}
}
return sym;
}
static int
key_cmp(struct key_binding a, struct key_binding b)
{
xassert(a.type == b.type);
/*
* Sort bindings such that bindings with the same symbol are
* sorted with the binding having the most modifiers comes first.
*
* This fixes an issue where the "wrong" key binding are triggered
* when used with "consumed" modifiers.
*
* For example: if Control+BackSpace is bound before
* Control+Shift+BackSpace, then the latter binding is never
* triggered.
*
* Why? Because Shift is a consumed modifier. This means
* Control+BackSpace is "the same" as Control+Shift+BackSpace.
*
* By sorting bindings with more modifiers first, we work around
* the problem. But note that it is *just* a workaround, and I'm
* not confident there aren't cases where it doesn't work.
*
* See https://codeberg.org/dnkl/foot/issues/1280
*/
const int a_mod_count = __builtin_popcount(a.mods);
const int b_mod_count = __builtin_popcount(b.mods);
switch (a.type) {
case KEY_BINDING:
if (a.k.sym != b.k.sym)
return b.k.sym - a.k.sym;
return b_mod_count - a_mod_count;
case MOUSE_BINDING: {
if (a.m.button != b.m.button)
return b.m.button - a.m.button;
if (a_mod_count != b_mod_count)
return b_mod_count - a_mod_count;
return b.m.count - a.m.count;
}
}
BUG("invalid key binding type");
return 0;
}
static void NOINLINE
sort_binding_list(key_binding_list_t *list)
{
tll_sort(*list, key_cmp);
}
static xkb_mod_mask_t
mods_to_mask(const struct seat *seat,
const struct vmod_map *vmods, size_t vmod_count,
const config_modifier_list_t *mods)
{
xkb_mod_mask_t mask = 0;
tll_foreach(*mods, it) {
const xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item);
if (idx == XKB_MOD_INVALID) {
LOG_ERR("%s: invalid modifier name", it->item);
continue;
}
xkb_mod_mask_t mod = 1 << idx;
/* Check if this is a virtual modifier, and if so, use the
real modifier it maps to instead */
for (size_t i = 0; i < vmod_count; i++) {
if (vmods[i].virtual_mask == mod) {
mask |= vmods[i].real_mask;
mod = 0;
LOG_DBG("%s: virtual modifier, mapped to 0x%04x",
it->item, vmods[i].real_mask);
break;
}
}
mask |= mod;
}
return mask;
}
static void NOINLINE
convert_key_binding(struct key_set *set,
const struct config_key_binding *conf_binding,
key_binding_list_t *bindings)
{
const struct seat *seat = set->seat;
xkb_mod_mask_t mods = mods_to_mask(
seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers);
xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods);
struct key_binding binding = {
.type = KEY_BINDING,
.action = conf_binding->action,
.aux = &conf_binding->aux,
.mods = mods,
.k = {
.sym = sym,
.key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym),
},
};
tll_push_back(*bindings, binding);
sort_binding_list(bindings);
}
static void
convert_key_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.key.count; i++) {
const struct config_key_binding *binding = &conf->bindings.key.arr[i];
convert_key_binding(set, binding, &set->public.key);
}
}
static void
convert_search_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.search.count; i++) {
const struct config_key_binding *binding = &conf->bindings.search.arr[i];
convert_key_binding(set, binding, &set->public.search);
}
}
static void
convert_url_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.url.count; i++) {
const struct config_key_binding *binding = &conf->bindings.url.arr[i];
convert_key_binding(set, binding, &set->public.url);
}
}
static void
convert_mouse_binding(struct key_set *set,
const struct config_key_binding *conf_binding)
{
struct key_binding binding = {
.type = MOUSE_BINDING,
.action = conf_binding->action,
.aux = &conf_binding->aux,
.mods = mods_to_mask(set->seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers),
.m = {
.button = conf_binding->m.button,
.count = conf_binding->m.count,
},
};
tll_push_back(set->public.mouse, binding);
sort_binding_list(&set->public.mouse);
}
static void
convert_mouse_bindings(struct key_set *set)
{
const struct config *conf = set->conf;
for (size_t i = 0; i < conf->bindings.mouse.count; i++) {
const struct config_key_binding *binding =
&conf->bindings.mouse.arr[i];
convert_mouse_binding(set, binding);
}
}
static void NOINLINE
load_keymap(struct key_set *set)
{
LOG_DBG("load keymap: set=%p, seat=%p, conf=%p",
(void *)set, (void *)set->seat, (void *)set->conf);
if (set->seat->kbd.xkb_state == NULL ||
set->seat->kbd.xkb_keymap == NULL)
{
LOG_DBG("no XKB keymap");
return;
}
convert_key_bindings(set);
convert_search_bindings(set);
convert_url_bindings(set);
convert_mouse_bindings(set);
set->public.selection_overrides = mods_to_mask(
set->seat, set->vmods, ALEN(set->vmods),
&set->conf->mouse.selection_override_modifiers);
}
void
key_binding_load_keymap(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat == seat) {
initialize_vmod_mappings(set);
load_keymap(set);
}
}
}
static void NOINLINE
key_bindings_destroy(key_binding_list_t *bindings)
{
tll_foreach(*bindings, it) {
struct key_binding *bind = &it->item;
switch (bind->type) {
case KEY_BINDING: tll_free(it->item.k.key_codes); break;
case MOUSE_BINDING: break;
}
tll_remove(*bindings, it);
}
}
static void NOINLINE
unload_keymap(struct key_set *set)
{
key_bindings_destroy(&set->public.key);
key_bindings_destroy(&set->public.search);
key_bindings_destroy(&set->public.url);
key_bindings_destroy(&set->public.mouse);
set->public.selection_overrides = 0;
}
void
key_binding_unload_keymap(struct key_binding_manager *mgr,
const struct seat *seat)
{
tll_foreach(mgr->binding_sets, it) {
struct key_set *set = &it->item;
if (set->seat != seat)
continue;
LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p",
(void *)set, (void *)seat, (void *)set->conf);
unload_keymap(set);
}
}

200
key-binding.h Normal file
View file

@ -0,0 +1,200 @@
#pragma once
#include <stdint.h>
#include <xkbcommon/xkbcommon.h>
#include <tllist.h>
#include "config.h"
enum bind_action_normal {
BIND_ACTION_NONE,
BIND_ACTION_NOOP,
BIND_ACTION_SCROLLBACK_UP_PAGE,
BIND_ACTION_SCROLLBACK_UP_HALF_PAGE,
BIND_ACTION_SCROLLBACK_UP_LINE,
BIND_ACTION_SCROLLBACK_DOWN_PAGE,
BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE,
BIND_ACTION_SCROLLBACK_DOWN_LINE,
BIND_ACTION_SCROLLBACK_HOME,
BIND_ACTION_SCROLLBACK_END,
BIND_ACTION_CLIPBOARD_COPY,
BIND_ACTION_CLIPBOARD_PASTE,
BIND_ACTION_PRIMARY_PASTE,
BIND_ACTION_SEARCH_START,
BIND_ACTION_FONT_SIZE_UP,
BIND_ACTION_FONT_SIZE_DOWN,
BIND_ACTION_FONT_SIZE_RESET,
BIND_ACTION_SPAWN_TERMINAL,
BIND_ACTION_MINIMIZE,
BIND_ACTION_MAXIMIZE,
BIND_ACTION_FULLSCREEN,
BIND_ACTION_PIPE_SCROLLBACK,
BIND_ACTION_PIPE_VIEW,
BIND_ACTION_PIPE_SELECTED,
BIND_ACTION_PIPE_COMMAND_OUTPUT,
BIND_ACTION_SHOW_URLS_COPY,
BIND_ACTION_SHOW_URLS_LAUNCH,
BIND_ACTION_SHOW_URLS_PERSISTENT,
BIND_ACTION_TEXT_BINDING,
BIND_ACTION_PROMPT_PREV,
BIND_ACTION_PROMPT_NEXT,
BIND_ACTION_UNICODE_INPUT,
BIND_ACTION_QUIT,
BIND_ACTION_REGEX_LAUNCH,
BIND_ACTION_REGEX_COPY,
BIND_ACTION_THEME_SWITCH_1,
BIND_ACTION_THEME_SWITCH_2,
BIND_ACTION_THEME_SWITCH_DARK,
BIND_ACTION_THEME_SWITCH_LIGHT,
BIND_ACTION_THEME_TOGGLE,
/* Tab actions */
BIND_ACTION_TAB_NEW,
BIND_ACTION_TAB_CLOSE,
BIND_ACTION_TAB_NEXT,
BIND_ACTION_TAB_PREV,
BIND_ACTION_TAB_1,
BIND_ACTION_TAB_2,
BIND_ACTION_TAB_3,
BIND_ACTION_TAB_4,
BIND_ACTION_TAB_5,
BIND_ACTION_TAB_6,
BIND_ACTION_TAB_7,
BIND_ACTION_TAB_8,
BIND_ACTION_TAB_9,
BIND_ACTION_TAB_OVERVIEW,
/* Mouse specific actions - i.e. they require a mouse coordinate */
BIND_ACTION_SCROLLBACK_UP_MOUSE,
BIND_ACTION_SCROLLBACK_DOWN_MOUSE,
BIND_ACTION_SELECT_BEGIN,
BIND_ACTION_SELECT_BEGIN_BLOCK,
BIND_ACTION_SELECT_EXTEND,
BIND_ACTION_SELECT_EXTEND_CHAR_WISE,
BIND_ACTION_SELECT_WORD,
BIND_ACTION_SELECT_WORD_WS,
BIND_ACTION_SELECT_QUOTE,
BIND_ACTION_SELECT_ROW,
BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1,
BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1,
};
enum bind_action_search {
BIND_ACTION_SEARCH_NONE,
BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE,
BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE,
BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE,
BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE,
BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE,
BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE,
BIND_ACTION_SEARCH_SCROLLBACK_HOME,
BIND_ACTION_SEARCH_SCROLLBACK_END,
BIND_ACTION_SEARCH_CANCEL,
BIND_ACTION_SEARCH_COMMIT,
BIND_ACTION_SEARCH_FIND_PREV,
BIND_ACTION_SEARCH_FIND_NEXT,
BIND_ACTION_SEARCH_EDIT_LEFT,
BIND_ACTION_SEARCH_EDIT_LEFT_WORD,
BIND_ACTION_SEARCH_EDIT_RIGHT,
BIND_ACTION_SEARCH_EDIT_RIGHT_WORD,
BIND_ACTION_SEARCH_EDIT_HOME,
BIND_ACTION_SEARCH_EDIT_END,
BIND_ACTION_SEARCH_DELETE_PREV,
BIND_ACTION_SEARCH_DELETE_PREV_WORD,
BIND_ACTION_SEARCH_DELETE_NEXT,
BIND_ACTION_SEARCH_DELETE_NEXT_WORD,
BIND_ACTION_SEARCH_DELETE_TO_START,
BIND_ACTION_SEARCH_DELETE_TO_END,
BIND_ACTION_SEARCH_EXTEND_CHAR,
BIND_ACTION_SEARCH_EXTEND_WORD,
BIND_ACTION_SEARCH_EXTEND_WORD_WS,
BIND_ACTION_SEARCH_EXTEND_LINE_DOWN,
BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR,
BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD,
BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS,
BIND_ACTION_SEARCH_EXTEND_LINE_UP,
BIND_ACTION_SEARCH_CLIPBOARD_PASTE,
BIND_ACTION_SEARCH_PRIMARY_PASTE,
BIND_ACTION_SEARCH_UNICODE_INPUT,
BIND_ACTION_SEARCH_TOGGLE_CASE,
BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD,
BIND_ACTION_SEARCH_TOGGLE_REGEX,
BIND_ACTION_SEARCH_HISTORY_PREV,
BIND_ACTION_SEARCH_HISTORY_NEXT,
BIND_ACTION_SEARCH_COMMIT_LINE,
BIND_ACTION_SEARCH_COUNT,
};
enum bind_action_url {
BIND_ACTION_URL_NONE,
BIND_ACTION_URL_CANCEL,
BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL,
BIND_ACTION_URL_COUNT,
};
typedef tll(xkb_keycode_t) xkb_keycode_list_t;
struct key_binding {
enum key_binding_type type;
int action; /* enum bind_action_* */
xkb_mod_mask_t mods;
union {
struct {
xkb_keysym_t sym;
xkb_keycode_list_t key_codes;
} k;
struct {
uint32_t button;
int count;
} m;
};
const struct binding_aux *aux;
};
typedef tll(struct key_binding) key_binding_list_t;
struct terminal;
struct seat;
struct wayland;
struct key_binding_set {
key_binding_list_t key;
key_binding_list_t search;
key_binding_list_t url;
key_binding_list_t mouse;
xkb_mod_mask_t selection_overrides;
};
struct key_binding_manager;
struct key_binding_manager *key_binding_manager_new(void);
void key_binding_manager_destroy(struct key_binding_manager *mgr);
void key_binding_new_for_seat(
struct key_binding_manager *mgr, const struct seat *seat);
void key_binding_new_for_conf(
struct key_binding_manager *mgr, const struct wayland *wayl,
const struct config *conf);
/* Returns the set of key bindings associated with this seat/conf pair */
struct key_binding_set *key_binding_for(
struct key_binding_manager *mgr, const struct config *conf,
const struct seat *seat);
/* Remove all key bindings tied to the specified seat */
void key_binding_remove_seat(
struct key_binding_manager *mgr, const struct seat *seat);
void key_binding_unref(
struct key_binding_manager *mgr, const struct config *conf);
void key_binding_load_keymap(
struct key_binding_manager *mgr, const struct seat *seat);
void key_binding_unload_keymap(
struct key_binding_manager *mgr, const struct seat *seat);

414
keymap.h Normal file
View file

@ -0,0 +1,414 @@
#pragma once
#include <xkbcommon/xkbcommon.h>
#include "terminal.h"
enum modifier {
MOD_NONE = 0x0,
MOD_ANY = 0x1,
MOD_SHIFT = 0x2,
MOD_ALT = 0x4,
MOD_CTRL = 0x8,
MOD_META = 0x10,
MOD_MODIFY_OTHER_KEYS_STATE1 = 0x20,
MOD_MODIFY_OTHER_KEYS_STATE2 = 0x40,
};
struct key_data {
enum modifier modifiers;
enum cursor_keys cursor_keys_mode;
enum keypad_keys keypad_keys_mode;
const char *seq;
};
static const struct key_data key_escape[] = {
{MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"},
{MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"},
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"},
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"},
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"},
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;27~"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;27~"},
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;27~"},
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;27~"},
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;27~"},
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;27~"},
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;27~"},
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;27~"},
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;27~"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;27~"},
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033"},
};
static const struct key_data key_return[] = {
{MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;13~"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\r"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;13~"},
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;13~"},
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;13~"},
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;13~"},
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;13~"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;13~"},
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;13~"},
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;13~"},
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;13~"},
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;13~"},
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;13~"},
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;13~"},
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;13~"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;13~"},
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\r"},
};
/* Tab isn't covered by the regular "modifyOtherKeys" handling */
static const struct key_data key_tab[] = {
{MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"},
{MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;9~"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\t"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;9~"},
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"},
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;9~"},
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"},
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;9~"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"},
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;9~"},
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"},
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;9~"},
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"},
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;9~"},
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"},
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;9~"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"},
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\t"},
};
/*
* Shift+Tab produces ISO_Left_Tab
*
* However, all combos (except Shift+Tab) acts as if we pressed
* mods+shift+tab.
*/
static const struct key_data key_iso_left_tab[] = {
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"},
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"},
{MOD_SHIFT | MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"},
{MOD_SHIFT | MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"},
{MOD_SHIFT | MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"},
{MOD_SHIFT | MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"},
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"},
};
static const struct key_data key_backspace[] = {
{MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"},
{MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"},
{MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"},
{MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"},
{MOD_META | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"},
{MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"},
{MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"},
{MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"},
{MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"},
{MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"},
{MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;127~"},
{MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;127~"},
{MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;127~"},
{MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;8~"},
{MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;8~"},
{MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;8~"},
{MOD_META | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;127~"},
{MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;127~"},
{MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;127~"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;127~"},
{MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;8~"},
{MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;8~"},
{MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;8~"},
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;8~"},
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"},
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"},
};
#define DEFAULT_MODS_FOR_SINGLE(sym) \
{MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2"#sym}, \
{MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;3"#sym}, \
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;4"#sym}, \
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5"#sym}, \
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;6"#sym}, \
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;7"#sym}, \
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;8"#sym}, \
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;9"#sym}, \
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;10"#sym}, \
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;11"#sym}, \
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;12"#sym}, \
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;13"#sym}, \
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;14"#sym}, \
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;15"#sym}, \
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;16"#sym}
#define DEFAULT_MODS_FOR_TILDE(sym) \
{MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";2~"}, \
{MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";3~"}, \
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";4~"}, \
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";5~"}, \
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";6~"}, \
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";7~"}, \
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";8~"}, \
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";9~"}, \
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";10~"}, \
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";11~"}, \
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";12~"}, \
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";13~"}, \
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";14~"}, \
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";15~"}, \
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";16~"}
static const struct key_data key_up[] = {
DEFAULT_MODS_FOR_SINGLE(A),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"},
};
static const struct key_data key_down[] = {
DEFAULT_MODS_FOR_SINGLE(B),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"},
};
static const struct key_data key_right[] = {
DEFAULT_MODS_FOR_SINGLE(C),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"},
};
static const struct key_data key_left[] = {
DEFAULT_MODS_FOR_SINGLE(D),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"},
};
static const struct key_data key_home[] = {
DEFAULT_MODS_FOR_SINGLE(H),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"},
};
static const struct key_data key_end[] = {
DEFAULT_MODS_FOR_SINGLE(F),
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"},
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"},
};
static const struct key_data key_insert[] = {
DEFAULT_MODS_FOR_TILDE(2),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"},
};
static const struct key_data key_delete[] = {
DEFAULT_MODS_FOR_TILDE(3),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"},
};
static const struct key_data key_pageup[] = {
DEFAULT_MODS_FOR_TILDE(5),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"},
};
static const struct key_data key_pagedown[] = {
DEFAULT_MODS_FOR_TILDE(6),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"},
};
static const struct key_data key_f1[] = {
DEFAULT_MODS_FOR_SINGLE(P),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OP"},
};
static const struct key_data key_f2[] = {
DEFAULT_MODS_FOR_SINGLE(Q),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OQ"},
};
static const struct key_data key_f3[] = {
DEFAULT_MODS_FOR_SINGLE(R),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OR"},
};
static const struct key_data key_f4[] = {
DEFAULT_MODS_FOR_SINGLE(S),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OS"},
};
static const struct key_data key_f5[] = {
DEFAULT_MODS_FOR_TILDE(15),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15~"},
};
static const struct key_data key_f6[] = {
DEFAULT_MODS_FOR_TILDE(17),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17~"},
};
static const struct key_data key_f7[] = {
DEFAULT_MODS_FOR_TILDE(18),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18~"},
};
static const struct key_data key_f8[] = {
DEFAULT_MODS_FOR_TILDE(19),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19~"},
};
static const struct key_data key_f9[] = {
DEFAULT_MODS_FOR_TILDE(20),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20~"},
};
static const struct key_data key_f10[] = {
DEFAULT_MODS_FOR_TILDE(21),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21~"},
};
static const struct key_data key_f11[] = {
DEFAULT_MODS_FOR_TILDE(23),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23~"},
};
static const struct key_data key_f12[] = {
DEFAULT_MODS_FOR_TILDE(24),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24~"},
};
static const struct key_data key_f13[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2P"}};
static const struct key_data key_f14[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2Q"}};
static const struct key_data key_f15[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2R"}};
static const struct key_data key_f16[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2S"}};
static const struct key_data key_f17[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;2~"}};
static const struct key_data key_f18[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;2~"}};
static const struct key_data key_f19[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;2~"}};
static const struct key_data key_f20[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;2~"}};
static const struct key_data key_f21[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;2~"}};
static const struct key_data key_f22[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;2~"}};
static const struct key_data key_f23[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;2~"}};
static const struct key_data key_f24[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24;2~"}};
static const struct key_data key_f25[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5P"}};
static const struct key_data key_f26[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5Q"}};
static const struct key_data key_f27[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5R"}};
static const struct key_data key_f28[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5S"}};
static const struct key_data key_f29[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;5~"}};
static const struct key_data key_f30[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;5~"}};
static const struct key_data key_f31[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;5~"}};
static const struct key_data key_f32[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;5~"}};
static const struct key_data key_f33[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;5~"}};
static const struct key_data key_f34[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;5~"}};
static const struct key_data key_f35[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;5~"}};
static const struct key_data key_kp_up[] = {
DEFAULT_MODS_FOR_SINGLE(A),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"},
};
static const struct key_data key_kp_down[] = {
DEFAULT_MODS_FOR_SINGLE(B),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"},
};
static const struct key_data key_kp_right[] = {
DEFAULT_MODS_FOR_SINGLE(C),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"},
};
static const struct key_data key_kp_left[] = {
DEFAULT_MODS_FOR_SINGLE(D),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"},
};
static const struct key_data key_kp_begin[] = {
DEFAULT_MODS_FOR_SINGLE(E),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[E"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OE"},
};
static const struct key_data key_kp_home[] = {
DEFAULT_MODS_FOR_SINGLE(H),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"},
};
static const struct key_data key_kp_end[] = {
DEFAULT_MODS_FOR_SINGLE(F),
{MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"},
{MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"},
};
static const struct key_data key_kp_insert[] = {
DEFAULT_MODS_FOR_TILDE(2),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"},
};
static const struct key_data key_kp_delete[] = {
DEFAULT_MODS_FOR_TILDE(3),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"},
};
static const struct key_data key_kp_pageup[] = {
DEFAULT_MODS_FOR_TILDE(5),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"},
};
static const struct key_data key_kp_pagedown[] = {
DEFAULT_MODS_FOR_TILDE(6),
{MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"},
};
#undef DEFAULT_MODS_FOR_SINGLE
#undef DEFAULT_MODS_FOR_TILDE
#define DEFAULT_MODS_FOR_KP(sym) \
{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O"#sym}, \
{MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O2"#sym}, \
{MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O3"#sym}, \
{MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O4"#sym}, \
{MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O5"#sym}, \
{MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O6"#sym}, \
{MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O7"#sym}, \
{MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O8"#sym}, \
{MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O9"#sym}, \
{MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O10"#sym}, \
{MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O11"#sym}, \
{MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O12"#sym}, \
{MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O13"#sym}, \
{MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O14"#sym}, \
{MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O15"#sym}, \
{MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O16"#sym}
static const struct key_data key_kp_enter[] = {DEFAULT_MODS_FOR_KP(M)};
static const struct key_data key_kp_divide[] = {DEFAULT_MODS_FOR_KP(o)};
static const struct key_data key_kp_multiply[] = {DEFAULT_MODS_FOR_KP(j)};
static const struct key_data key_kp_subtract[] = {DEFAULT_MODS_FOR_KP(m)};
static const struct key_data key_kp_add[] = {DEFAULT_MODS_FOR_KP(k)};
static const struct key_data key_kp_separator[] = {DEFAULT_MODS_FOR_KP(l)};
static const struct key_data key_kp_decimal[] = {DEFAULT_MODS_FOR_KP(n)};
static const struct key_data key_kp_0[] = {DEFAULT_MODS_FOR_KP(p)};
static const struct key_data key_kp_1[] = {DEFAULT_MODS_FOR_KP(q)};
static const struct key_data key_kp_2[] = {DEFAULT_MODS_FOR_KP(r)};
static const struct key_data key_kp_3[] = {DEFAULT_MODS_FOR_KP(s)};
static const struct key_data key_kp_4[] = {DEFAULT_MODS_FOR_KP(t)};
static const struct key_data key_kp_5[] = {DEFAULT_MODS_FOR_KP(u)};
static const struct key_data key_kp_6[] = {DEFAULT_MODS_FOR_KP(v)};
static const struct key_data key_kp_7[] = {DEFAULT_MODS_FOR_KP(w)};
static const struct key_data key_kp_8[] = {DEFAULT_MODS_FOR_KP(x)};
static const struct key_data key_kp_9[] = {DEFAULT_MODS_FOR_KP(y)};
#undef DEFAULT_MODS_FOR_KP

136
kitty-keymap.h Normal file
View file

@ -0,0 +1,136 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <xkbcommon/xkbcommon.h>
struct kitty_key_data {
xkb_keysym_t sym;
uint16_t key;
uint8_t final:7;
bool is_modifier:1;
} __attribute__((packed));
_Static_assert(sizeof(struct kitty_key_data) == 7, "bad size");
/* Note! *Must* Be kept sorted (on 'sym') */
static const struct kitty_key_data kitty_keymap[] = {
{XKB_KEY_ISO_Level3_Shift, 57453, 'u', true},
{XKB_KEY_ISO_Level5_Shift, 57454, 'u', true},
{XKB_KEY_ISO_Left_Tab, 9, 'u', false},
{XKB_KEY_BackSpace, 127, 'u', false},
{XKB_KEY_Tab, 9, 'u', false},
{XKB_KEY_Return, 13, 'u', false},
{XKB_KEY_Pause, 57362, 'u', false},
{XKB_KEY_Scroll_Lock, 57359, 'u', false},
{XKB_KEY_Escape, 27, 'u', false},
{XKB_KEY_Home, 1, 'H', false},
{XKB_KEY_Left, 1, 'D', false},
{XKB_KEY_Up, 1, 'A', false},
{XKB_KEY_Right, 1, 'C', false},
{XKB_KEY_Down, 1, 'B', false},
{XKB_KEY_Prior, 5, '~', false},
{XKB_KEY_Next, 6, '~', false},
{XKB_KEY_End, 1, 'F', false},
{XKB_KEY_Print, 57361, 'u', false},
{XKB_KEY_Insert, 2, '~', false},
{XKB_KEY_Menu, 57363, 'u', false},
{XKB_KEY_Num_Lock, 57360, 'u', true},
{XKB_KEY_KP_Enter, 57414, 'u', false},
{XKB_KEY_KP_Home, 57423, 'u', false},
{XKB_KEY_KP_Left, 57417, 'u', false},
{XKB_KEY_KP_Up, 57419, 'u', false},
{XKB_KEY_KP_Right, 57418, 'u', false},
{XKB_KEY_KP_Down, 57420, 'u', false},
{XKB_KEY_KP_Prior, 57421, 'u', false},
{XKB_KEY_KP_Next, 57422, 'u', false},
{XKB_KEY_KP_End, 57424, 'u', false},
{XKB_KEY_KP_Begin, 1, 'E', false},
{XKB_KEY_KP_Insert, 57425, 'u', false},
{XKB_KEY_KP_Delete, 57426, 'u', false},
{XKB_KEY_KP_Multiply, 57411, 'u', false},
{XKB_KEY_KP_Add, 57413, 'u', false},
{XKB_KEY_KP_Separator, 57416, 'u', false},
{XKB_KEY_KP_Subtract, 57412, 'u', false},
{XKB_KEY_KP_Decimal, 57409, 'u', false},
{XKB_KEY_KP_Divide, 57410, 'u', false},
{XKB_KEY_KP_0, 57399, 'u', false},
{XKB_KEY_KP_1, 57400, 'u', false},
{XKB_KEY_KP_2, 57401, 'u', false},
{XKB_KEY_KP_3, 57402, 'u', false},
{XKB_KEY_KP_4, 57403, 'u', false},
{XKB_KEY_KP_5, 57404, 'u', false},
{XKB_KEY_KP_6, 57405, 'u', false},
{XKB_KEY_KP_7, 57406, 'u', false},
{XKB_KEY_KP_8, 57407, 'u', false},
{XKB_KEY_KP_9, 57408, 'u', false},
{XKB_KEY_KP_Equal, 57415, 'u', false},
{XKB_KEY_F1, 1, 'P', false},
{XKB_KEY_F2, 1, 'Q', false},
{XKB_KEY_F3, 13, '~', false},
{XKB_KEY_F4, 1, 'S', false},
{XKB_KEY_F5, 15, '~', false},
{XKB_KEY_F6, 17, '~', false},
{XKB_KEY_F7, 18, '~', false},
{XKB_KEY_F8, 19, '~', false},
{XKB_KEY_F9, 20, '~', false},
{XKB_KEY_F10, 21, '~', false},
{XKB_KEY_F11, 23, '~', false},
{XKB_KEY_F12, 24, '~', false},
{XKB_KEY_F13, 57376, 'u', false},
{XKB_KEY_F14, 57377, 'u', false},
{XKB_KEY_F15, 57378, 'u', false},
{XKB_KEY_F16, 57379, 'u', false},
{XKB_KEY_F17, 57380, 'u', false},
{XKB_KEY_F18, 57381, 'u', false},
{XKB_KEY_F19, 57382, 'u', false},
{XKB_KEY_F20, 57383, 'u', false},
{XKB_KEY_F21, 57384, 'u', false},
{XKB_KEY_F22, 57385, 'u', false},
{XKB_KEY_F23, 57386, 'u', false},
{XKB_KEY_F24, 57387, 'u', false},
{XKB_KEY_F25, 57388, 'u', false},
{XKB_KEY_F26, 57389, 'u', false},
{XKB_KEY_F27, 57390, 'u', false},
{XKB_KEY_F28, 57391, 'u', false},
{XKB_KEY_F29, 57392, 'u', false},
{XKB_KEY_F30, 57393, 'u', false},
{XKB_KEY_F31, 57394, 'u', false},
{XKB_KEY_F32, 57395, 'u', false},
{XKB_KEY_F33, 57396, 'u', false},
{XKB_KEY_F34, 57397, 'u', false},
{XKB_KEY_F35, 57398, 'u', false},
{XKB_KEY_Shift_L, 57441, 'u', true},
{XKB_KEY_Shift_R, 57447, 'u', true},
{XKB_KEY_Control_L, 57442, 'u', true},
{XKB_KEY_Control_R, 57448, 'u', true},
{XKB_KEY_Caps_Lock, 57358, 'u', true},
{XKB_KEY_Meta_L, 57446, 'u', true},
{XKB_KEY_Meta_R, 57452, 'u', true},
{XKB_KEY_Alt_L, 57443, 'u', true},
{XKB_KEY_Alt_R, 57449, 'u', true},
{XKB_KEY_Super_L, 57444, 'u', true},
{XKB_KEY_Super_R, 57450, 'u', true},
{XKB_KEY_Hyper_L, 57445, 'u', true},
{XKB_KEY_Hyper_R, 57451, 'u', true},
{XKB_KEY_Delete, 3, '~', false},
{XKB_KEY_XF86AudioLowerVolume, 57438, 'u', false},
{XKB_KEY_XF86AudioMute, 57440, 'u', false},
{XKB_KEY_XF86AudioRaiseVolume, 57439, 'u', false},
{XKB_KEY_XF86AudioPlay, 57428, 'u', false},
{XKB_KEY_XF86AudioStop, 57432, 'u', false},
{XKB_KEY_XF86AudioPrev, 57436, 'u', false},
{XKB_KEY_XF86AudioNext, 57435, 'u', false},
{XKB_KEY_XF86AudioRecord, 57437, 'u', false},
{XKB_KEY_XF86AudioPause, 57429, 'u', false},
{XKB_KEY_XF86AudioRewind, 57434, 'u', false},
{XKB_KEY_XF86AudioForward, 57433, 'u', false},
//{XKB_KEY_XF86AudioPlayPause, 57430, 'u', false},
//{XKB_KEY_XF86AudioReverse, 57431, 'u', false},
};

231
log.c Normal file
View file

@ -0,0 +1,231 @@
#include "log.h"
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include "debug.h"
#include "util.h"
#include "xsnprintf.h"
static bool colorize = false;
static bool do_syslog = false;
static enum log_class log_level = LOG_CLASS_NONE;
static const struct {
const char name[8];
const char log_prefix[7];
uint8_t color;
int syslog_equivalent;
} log_level_map[] = {
[LOG_CLASS_NONE] = {"none", "none", 5, -1},
[LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR},
[LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING},
[LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO},
[LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG},
};
void
log_init(enum log_colorize _colorize, bool _do_syslog,
enum log_facility syslog_facility, enum log_class _log_level)
{
static const int facility_map[] = {
[LOG_FACILITY_USER] = LOG_USER,
[LOG_FACILITY_DAEMON] = LOG_DAEMON,
};
/* Don't use colors if NO_COLOR is defined and not empty */
const char *no_color_str = getenv("NO_COLOR");
const bool no_color = no_color_str != NULL && no_color_str[0] != '\0';
colorize = _colorize == LOG_COLORIZE_ALWAYS
|| (_colorize == LOG_COLORIZE_AUTO
&& !no_color && isatty(STDERR_FILENO));
do_syslog = _do_syslog;
log_level = _log_level;
int slvl = log_level_map[_log_level].syslog_equivalent;
if (slvl < 0)
do_syslog = false;
if (do_syslog) {
openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]);
xassert(slvl >= 0);
setlogmask(LOG_UPTO(slvl));
}
}
void
log_deinit(void)
{
if (do_syslog)
closelog();
}
static void
_log(enum log_class log_class, const char *module, const char *file, int lineno,
const char *fmt, int sys_errno, va_list va)
{
xassert(log_class > LOG_CLASS_NONE);
xassert(log_class < ALEN(log_level_map));
if (log_class > log_level)
return;
const char *prefix = log_level_map[log_class].log_prefix;
unsigned int class_clr = log_level_map[log_class].color;
char clr[16];
xsnprintf(clr, sizeof(clr), "\033[%um", class_clr);
fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : "");
if (colorize)
fputs("\033[2m", stderr);
fprintf(stderr, "%s:%d: ", file, lineno);
if (colorize)
fputs("\033[0m", stderr);
vfprintf(stderr, fmt, va);
if (sys_errno != 0)
fprintf(stderr, ": %s", strerror(sys_errno));
fputc('\n', stderr);
}
static void
_sys_log(enum log_class log_class, const char *module,
const char UNUSED *file, int UNUSED lineno,
const char *fmt, int sys_errno, va_list va)
{
xassert(log_class > LOG_CLASS_NONE);
xassert(log_class < ALEN(log_level_map));
if (!do_syslog)
return;
if (log_class > log_level)
return;
/* Map our log level to syslog's level */
int level = log_level_map[log_class].syslog_equivalent;
char msg[4096];
int n = vsnprintf(msg, sizeof(msg), fmt, va);
xassert(n >= 0);
if (sys_errno != 0 && (size_t)n < sizeof(msg))
snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno));
syslog(level, "%s: %s", module, msg);
}
void
log_msg_va(enum log_class log_class, const char *module,
const char *file, int lineno, const char *fmt, va_list va)
{
va_list va2;
va_copy(va2, va);
_log(log_class, module, file, lineno, fmt, 0, va);
_sys_log(log_class, module, file, lineno, fmt, 0, va2);
va_end(va2);
}
void
log_msg(enum log_class log_class, const char *module,
const char *file, int lineno, const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
log_msg_va(log_class, module, file, lineno, fmt, va);
va_end(va);
}
void
log_errno_va(enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, va_list va)
{
log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va);
}
void
log_errno(enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
log_errno_va(log_class, module, file, lineno, fmt, va);
va_end(va);
}
void
log_errno_provided_va(enum log_class log_class, const char *module,
const char *file, int lineno, int errno_copy,
const char *fmt, va_list va)
{
va_list va2;
va_copy(va2, va);
_log(log_class, module, file, lineno, fmt, errno_copy, va);
_sys_log(log_class, module, file, lineno, fmt, errno_copy, va2);
va_end(va2);
}
void
log_errno_provided(enum log_class log_class, const char *module,
const char *file, int lineno, int errno_copy,
const char *fmt, ...)
{
va_list va;
va_start(va, fmt);
log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va);
va_end(va);
}
static size_t
map_len(void)
{
size_t len = ALEN(log_level_map);
#ifndef _DEBUG
/* Exclude "debug" entry for non-debug builds */
len--;
#endif
return len;
}
int
log_level_from_string(const char *str)
{
if (unlikely(str[0] == '\0'))
return -1;
for (int i = 0, n = map_len(); i < n; i++)
if (streq(str, log_level_map[i].name))
return i;
return -1;
}
const char *
log_level_string_hint(void)
{
static char buf[64];
if (buf[0] != '\0')
return buf;
for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) {
const char *entry = log_level_map[i].name;
const char *delim = (i + 1 < n) ? ", " : "";
pos += xsnprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim);
}
return buf;
}

70
log.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
#include <stdbool.h>
#include <stdarg.h>
#include "macros.h"
enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO };
enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON };
enum log_class {
LOG_CLASS_NONE,
LOG_CLASS_ERROR,
LOG_CLASS_WARNING,
LOG_CLASS_INFO,
LOG_CLASS_DEBUG,
LOG_CLASS_COUNT,
};
void log_init(enum log_colorize colorize, bool do_syslog,
enum log_facility syslog_facility, enum log_class log_level);
void log_deinit(void);
void log_msg(
enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...) PRINTF(5);
void log_errno(
enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, ...) PRINTF(5);
void log_errno_provided(
enum log_class log_class, const char *module,
const char *file, int lineno, int _errno,
const char *fmt, ...) PRINTF(6);
void log_msg_va(
enum log_class log_class, const char *module,
const char *file, int lineno, const char *fmt, va_list va) VPRINTF(5);
void log_errno_va(
enum log_class log_class, const char *module,
const char *file, int lineno,
const char *fmt, va_list va) VPRINTF(5);
void log_errno_provided_va(
enum log_class log_class, const char *module,
const char *file, int lineno, int _errno,
const char *fmt, va_list va) VPRINTF(6);
int log_level_from_string(const char *str);
const char *log_level_string_hint(void);
#define LOG_ERR(...) \
log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERRNO(...) \
log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_ERRNO_P(_errno, ...) \
log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \
_errno, __VA_ARGS__)
#define LOG_WARN(...) \
log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#define LOG_INFO(...) \
log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG
#define LOG_DBG(...) \
log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__)
#else
#define LOG_DBG(...)
#endif

211
macros.h Normal file
View file

@ -0,0 +1,211 @@
#pragma once
#define PASTE(a, b) a##b
#define XPASTE(a, b) PASTE(a, b)
#define STRLEN(str) (sizeof("" str "") - 1)
#define DO_PRAGMA(x) _Pragma(#x)
#define VERCMP(x, y, cx, cy) ((cx > x) || ((cx == x) && (cy >= y)))
#if defined(__GNUC__) && defined(__GNUC_MINOR__)
#define GNUC_AT_LEAST(x, y) VERCMP(x, y, __GNUC__, __GNUC_MINOR__)
#else
#define GNUC_AT_LEAST(x, y) 0
#endif
#if defined(__clang_major__) && defined(__clang_minor__)
#define CLANG_AT_LEAST(x, y) VERCMP(x, y, __clang_major__, __clang_minor__)
#else
#define CLANG_AT_LEAST(x, y) 0
#endif
#ifdef __has_attribute
#define HAS_ATTRIBUTE(x) __has_attribute(x)
#else
#define HAS_ATTRIBUTE(x) 0
#endif
#ifdef __has_builtin
#define HAS_BUILTIN(x) __has_builtin(x)
#else
#define HAS_BUILTIN(x) 0
#endif
#ifdef __has_include
#define HAS_INCLUDE(x) __has_include(x)
#else
#define HAS_INCLUDE(x) 0
#endif
#ifdef __has_feature
#define HAS_FEATURE(x) __has_feature(x)
#else
#define HAS_FEATURE(x) 0
#endif
// __has_extension() is a Clang macro used to determine if a feature is
// available even if not standardized in the current "-std" mode.
#ifdef __has_extension
#define HAS_EXTENSION(x) __has_extension(x)
#else
// Clang versions prior to 3.0 only supported __has_feature()
#define HAS_EXTENSION(x) HAS_FEATURE(x)
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(unused) || defined(__TINYC__)
#define UNUSED __attribute__((__unused__))
#else
#define UNUSED
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(const)
#define CONST __attribute__((__const__))
#else
#define CONST
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(malloc)
#define MALLOC __attribute__((__malloc__))
#else
#define MALLOC
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(constructor)
#define CONSTRUCTOR __attribute__((__constructor__))
#define HAVE_ATTR_CONSTRUCTOR 1
#else
#define CONSTRUCTOR
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(destructor)
#define DESTRUCTOR __attribute__((__destructor__))
#else
#define DESTRUCTOR
#endif
#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(format)
#define PRINTF(x) __attribute__((__format__(__printf__, (x), (x + 1))))
#define VPRINTF(x) __attribute__((__format__(__printf__, (x), 0)))
#else
#define PRINTF(x)
#define VPRINTF(x)
#endif
#if (GNUC_AT_LEAST(3, 0) || HAS_BUILTIN(__builtin_expect)) && defined(__OPTIMIZE__)
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
#else
#define likely(x) (x)
#define unlikely(x) (x)
#endif
#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(noinline)
#define NOINLINE __attribute__((__noinline__))
#else
#define NOINLINE
#endif
#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(always_inline)
#define ALWAYS_INLINE __attribute__((__always_inline__))
#else
#define ALWAYS_INLINE
#endif
#if GNUC_AT_LEAST(3, 3) || HAS_ATTRIBUTE(nonnull)
#define NONNULL_ARGS __attribute__((__nonnull__))
#define NONNULL_ARG(...) __attribute__((__nonnull__(__VA_ARGS__)))
#else
#define NONNULL_ARGS
#define NONNULL_ARG(...)
#endif
#if GNUC_AT_LEAST(3, 4) || HAS_ATTRIBUTE(warn_unused_result)
#define WARN_UNUSED_RESULT __attribute__((__warn_unused_result__))
#else
#define WARN_UNUSED_RESULT
#endif
#if GNUC_AT_LEAST(4, 1) || HAS_ATTRIBUTE(flatten)
#define FLATTEN __attribute__((__flatten__))
#else
#define FLATTEN
#endif
#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(hot)
#define HOT __attribute__((__hot__))
#else
#define HOT
#endif
#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(cold)
#define COLD __attribute__((__cold__))
#else
#define COLD
#endif
#if GNUC_AT_LEAST(4, 5) || HAS_BUILTIN(__builtin_unreachable)
#define UNREACHABLE() __builtin_unreachable()
#else
#define UNREACHABLE()
#endif
#if GNUC_AT_LEAST(5, 0) || HAS_ATTRIBUTE(returns_nonnull)
#define RETURNS_NONNULL __attribute__((__returns_nonnull__))
#else
#define RETURNS_NONNULL
#endif
#if HAS_ATTRIBUTE(diagnose_if)
#define DIAGNOSE_IF(x) __attribute__((diagnose_if((x), (#x), "error")))
#else
#define DIAGNOSE_IF(x)
#endif
#define XMALLOC MALLOC RETURNS_NONNULL WARN_UNUSED_RESULT
#define XSTRDUP XMALLOC NONNULL_ARGS
#if __STDC_VERSION__ >= 201112L
#define noreturn _Noreturn
#elif GNUC_AT_LEAST(3, 0)
#define noreturn __attribute__((__noreturn__))
#else
#define noreturn
#endif
#if CLANG_AT_LEAST(3, 6)
#define UNROLL_LOOP(n) DO_PRAGMA(clang loop unroll_count(n))
#elif GNUC_AT_LEAST(8, 0)
#define UNROLL_LOOP(n) DO_PRAGMA(GCC unroll (n))
#else
#define UNROLL_LOOP(n)
#endif
#ifdef __COUNTER__
// Supported by GCC 4.3+ and Clang
#define COUNTER_ __COUNTER__
#else
#define COUNTER_ __LINE__
#endif
#if defined(_DEBUG) && defined(HAVE_ATTR_CONSTRUCTOR)
#define UNITTEST static void CONSTRUCTOR XPASTE(unittest_, COUNTER_)(void)
#else
#define UNITTEST static void UNUSED XPASTE(unittest_, COUNTER_)(void)
#endif
#ifdef __clang__
#define IGNORE_WARNING(wflag) \
DO_PRAGMA(clang diagnostic push) \
DO_PRAGMA(clang diagnostic ignored "-Wunknown-pragmas") \
DO_PRAGMA(clang diagnostic ignored "-Wunknown-warning-option") \
DO_PRAGMA(clang diagnostic ignored wflag)
#define UNIGNORE_WARNINGS DO_PRAGMA(clang diagnostic pop)
#elif GNUC_AT_LEAST(4, 6)
#define IGNORE_WARNING(wflag) \
DO_PRAGMA(GCC diagnostic push) \
DO_PRAGMA(GCC diagnostic ignored "-Wpragmas") \
DO_PRAGMA(GCC diagnostic ignored wflag)
#define UNIGNORE_WARNINGS DO_PRAGMA(GCC diagnostic pop)
#else
#define IGNORE_WARNING(wflag)
#define UNIGNORE_WARNINGS
#endif

726
main.c Normal file
View file

@ -0,0 +1,726 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <limits.h>
#include <locale.h>
#include <getopt.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <fcntl.h>
#include <fcft/fcft.h>
#define LOG_MODULE "main"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "config.h"
#include "fdm.h"
#include "foot-features.h"
#include "key-binding.h"
#include "macros.h"
#include "reaper.h"
#include "render.h"
#include "server.h"
#include "shm.h"
#include "terminal.h"
#include "util.h"
#include "xmalloc.h"
#include "xsnprintf.h"
#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__
#error "char32_t does not use UTF-32"
#endif
static bool
fdm_sigint(struct fdm *fdm, int signo, void *data)
{
*(volatile sig_atomic_t *)data = true;
return true;
}
struct sigusr_context {
struct terminal *term;
struct server *server;
};
static bool
fdm_sigusr(struct fdm *fdm, int signo, void *data)
{
xassert(signo == SIGUSR1 || signo == SIGUSR2);
struct sigusr_context *ctx = data;
if (ctx->server != NULL) {
if (signo == SIGUSR1)
server_global_theme_switch_to_dark(ctx->server);
else
server_global_theme_switch_to_light(ctx->server);
} else {
if (signo == SIGUSR1)
term_theme_switch_to_dark(ctx->term);
else
term_theme_switch_to_light(ctx->term);
}
return true;
}
static void
print_usage(const char *prog_name)
{
static const char options[] =
"\nOptions:\n"
" -c,--config=PATH load configuration from PATH ($XDG_CONFIG_HOME/foot/foot.ini)\n"
" -C,--check-config verify configuration, exit with 0 if ok, otherwise exit with 1\n"
" -o,--override=[section.]key=value override configuration option\n"
" -f,--font=FONT comma separated list of fonts in fontconfig format (monospace)\n"
" -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n"
" -T,--title=TITLE initial window title (foot)\n"
" -a,--app-id=ID window application ID (foot)\n"
" --toplevel-tag=TAG set a custom toplevel tag\n"
" -m,--maximized start in maximized mode\n"
" -F,--fullscreen start in fullscreen mode\n"
" -L,--login-shell start shell as a login shell\n"
" --pty=PATH display an existing PTY instead of creating one\n"
" -D,--working-directory=DIR directory to start in (CWD)\n"
" -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n"
" -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n"
" -s,--server[=PATH] run as a server (use 'footclient' to start terminals).\n"
" Without PATH, $XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock will be used.\n"
" -H,--hold remain open after child process exits\n"
" -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n"
" -d,--log-level={info|warning|error|none} log level (warning)\n"
" -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n"
" -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n"
" -v,--version show the version number and quit\n"
" -e ignored (for compatibility with xterm -e)\n";
printf("Usage: %s [OPTIONS...]\n", prog_name);
printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name);
puts(options);
}
bool
locale_is_utf8(void)
{
static const char u8[] = u8"ö";
xassert(strlen(u8) == 2);
char32_t w;
if (mbrtoc32(&w, u8, 2, &(mbstate_t){0}) != 2)
return false;
return w == U'ö';
}
struct shutdown_context {
struct terminal **term;
int exit_code;
};
static void
term_shutdown_cb(void *data, int exit_code)
{
struct shutdown_context *ctx = data;
*ctx->term = NULL;
ctx->exit_code = exit_code;
}
static bool
print_pid(const char *pid_file, bool *unlink_at_exit)
{
LOG_DBG("printing PID to %s", pid_file);
errno = 0;
char *end;
int pid_fd = strtoul(pid_file, &end, 10);
if (errno != 0 || *end != '\0') {
if ((pid_fd = open(pid_file,
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC,
S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) {
LOG_ERRNO("%s: failed to open", pid_file);
return false;
} else
*unlink_at_exit = true;
}
if (pid_fd >= 0) {
char pid[32];
size_t n = xsnprintf(pid, sizeof(pid), "%u\n", getpid());
ssize_t bytes = write(pid_fd, pid, n);
close(pid_fd);
if (bytes < 0) {
LOG_ERRNO("failed to write PID to FD=%u", pid_fd);
return false;
}
LOG_DBG("wrote %zd bytes to FD=%d", bytes, pid_fd);
return true;
} else
return false;
}
static void
sanitize_signals(void)
{
sigset_t mask;
sigemptyset(&mask);
sigprocmask(SIG_SETMASK, &mask, NULL);
struct sigaction dfl = {.sa_handler = SIG_DFL};
sigemptyset(&dfl.sa_mask);
for (int i = 1; i < SIGRTMAX; i++)
sigaction(i, &dfl, NULL);
}
enum {
PTY_OPTION = CHAR_MAX + 1,
TOPLEVEL_TAG_OPTION = CHAR_MAX + 2,
};
int
main(int argc, char *const *argv)
{
/* Custom exit code, to enable users to differentiate between foot
* itself failing, and the client application failing */
static const int foot_exit_failure = -26;
int ret = foot_exit_failure;
sanitize_signals();
/* XDG startup notifications */
const char *token = getenv("XDG_ACTIVATION_TOKEN");
unsetenv("XDG_ACTIVATION_TOKEN");
/* Startup notifications; we don't support it, but must ensure we
* don't pass this on to programs launched by us */
unsetenv("DESKTOP_STARTUP_ID");
const char *const prog_name = argc > 0 ? argv[0] : "<nullptr>";
static const struct option longopts[] = {
{"config", required_argument, NULL, 'c'},
{"check-config", no_argument, NULL, 'C'},
{"override", required_argument, NULL, 'o'},
{"term", required_argument, NULL, 't'},
{"title", required_argument, NULL, 'T'},
{"app-id", required_argument, NULL, 'a'},
{"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION},
{"login-shell", no_argument, NULL, 'L'},
{"working-directory", required_argument, NULL, 'D'},
{"font", required_argument, NULL, 'f'},
{"window-size-pixels", required_argument, NULL, 'w'},
{"window-size-chars", required_argument, NULL, 'W'},
{"server", optional_argument, NULL, 's'},
{"hold", no_argument, NULL, 'H'},
{"maximized", no_argument, NULL, 'm'},
{"fullscreen", no_argument, NULL, 'F'},
{"presentation-timings", no_argument, NULL, 'P'}, /* Undocumented */
{"pty", required_argument, NULL, PTY_OPTION},
{"print-pid", required_argument, NULL, 'p'},
{"log-level", required_argument, NULL, 'd'},
{"log-colorize", optional_argument, NULL, 'l'},
{"log-no-syslog", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"help", no_argument, NULL, 'h'},
{NULL, no_argument, NULL, 0},
};
bool check_config = false;
const char *conf_path = NULL;
const char *custom_cwd = NULL;
const char *pty_path = NULL;
bool as_server = false;
const char *conf_server_socket_path = NULL;
bool presentation_timings = false;
bool hold = false;
bool unlink_pid_file = false;
const char *pid_file = NULL;
enum log_class log_level = LOG_CLASS_WARNING;
enum log_colorize log_colorize = LOG_COLORIZE_AUTO;
bool log_syslog = true;
user_notifications_t user_notifications = tll_init();
config_override_t overrides = tll_init();
while (true) {
int c = getopt_long(argc, argv, "+c:Co:t:T:a:LD:f:w:W:s::HmFPp:d:l::Sveh", longopts, NULL);
if (c == -1)
break;
switch (c) {
case 'c':
conf_path = optarg;
break;
case 'C':
check_config = true;
break;
case 'o':
tll_push_back(overrides, xstrdup(optarg));
break;
case 't':
tll_push_back(overrides, xstrjoin("term=", optarg));
break;
case 'L':
tll_push_back(overrides, xstrdup("login-shell=yes"));
break;
case 'T':
tll_push_back(overrides, xstrjoin("title=", optarg));
break;
case 'a':
tll_push_back(overrides, xstrjoin("app-id=", optarg));
break;
case TOPLEVEL_TAG_OPTION:
tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg));
break;
case 'D': {
struct stat st;
if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) {
fprintf(stderr, "error: %s: not a directory\n", optarg);
return ret;
}
custom_cwd = optarg;
break;
}
case 'f': {
char *font_override = xstrjoin("font=", optarg);
tll_push_back(overrides, font_override);
break;
}
case 'w': {
unsigned width, height;
if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg);
return ret;
}
tll_push_back(
overrides, xasprintf("initial-window-size-pixels=%ux%u",
width, height));
break;
}
case 'W': {
unsigned width, height;
if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) {
fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg);
return ret;
}
tll_push_back(
overrides, xasprintf("initial-window-size-chars=%ux%u",
width, height));
break;
}
case 's':
as_server = true;
if (optarg != NULL)
conf_server_socket_path = optarg;
break;
case PTY_OPTION:
pty_path = optarg;
break;
case 'P':
presentation_timings = true;
break;
case 'H':
hold = true;
break;
case 'm':
tll_push_back(overrides, xstrdup("initial-window-mode=maximized"));
break;
case 'F':
tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen"));
break;
case 'p':
pid_file = optarg;
break;
case 'd': {
int lvl = log_level_from_string(optarg);
if (unlikely(lvl < 0)) {
fprintf(
stderr,
"-d,--log-level: %s: argument must be one of %s\n",
optarg,
log_level_string_hint());
return ret;
}
log_level = lvl;
break;
}
case 'l':
if (optarg == NULL || streq(optarg, "auto"))
log_colorize = LOG_COLORIZE_AUTO;
else if (streq(optarg, "never"))
log_colorize = LOG_COLORIZE_NEVER;
else if (streq(optarg, "always"))
log_colorize = LOG_COLORIZE_ALWAYS;
else {
fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg);
return ret;
}
break;
case 'S':
log_syslog = false;
break;
case 'v':
print_version_and_features("foot ");
return EXIT_SUCCESS;
case 'h':
print_usage(prog_name);
return EXIT_SUCCESS;
case 'e':
break;
case '?':
return ret;
}
}
if (as_server && pty_path) {
fputs("error: --pty is incompatible with server mode\n", stderr);
return ret;
}
log_init(log_colorize, as_server && log_syslog,
as_server ? LOG_FACILITY_DAEMON : LOG_FACILITY_USER, log_level);
if (argc > 0) {
argc -= optind;
argv += optind;
}
LOG_INFO("%s", version_and_features);
{
struct utsname name;
if (uname(&name) < 0)
LOG_ERRNO("uname() failed");
else
LOG_INFO("arch: %s %s/%zu-bit",
name.sysname, name.machine, sizeof(void *) * 8);
}
srand(time(NULL));
const char *locale = setlocale(LC_CTYPE, "");
if (locale == NULL) {
/*
* If the user has configured an invalid locale, or a name of a locale
* that does not exist on this system, then the above call may return
* NULL. We should just continue with the fallback method below.
*/
LOG_ERR("setlocale() failed. The most common cause is that the "
"configured locale is not available, or has been misspelled");
}
LOG_INFO("locale: %s", locale != NULL ? locale : "<invalid>");
bool bad_locale = locale == NULL || !locale_is_utf8();
if (bad_locale) {
static const char fallback_locales[][12] = {
"C.UTF-8",
"en_US.UTF-8",
};
char *saved_locale = locale != NULL ? xstrdup(locale) : NULL;
/*
* Try to force an UTF-8 locale. If we succeed, launch the
* user's shell as usual, but add a user-notification saying
* the locale has been changed.
*/
for (size_t i = 0; i < ALEN(fallback_locales); i++) {
const char *const fallback_locale = fallback_locales[i];
if (setlocale(LC_CTYPE, fallback_locale) != NULL) {
if (saved_locale != NULL) {
LOG_WARN(
"'%s' is not a UTF-8 locale, falling back to '%s'",
saved_locale, fallback_locale);
user_notification_add_fmt(
&user_notifications, USER_NOTIFICATION_WARNING,
"'%s' is not a UTF-8 locale, falling back to '%s'",
saved_locale, fallback_locale);
} else {
LOG_WARN(
"invalid locale, falling back to '%s'", fallback_locale);
user_notification_add_fmt(
&user_notifications, USER_NOTIFICATION_WARNING,
"invalid locale, falling back to '%s'", fallback_locale);
}
bad_locale = false;
break;
}
}
if (bad_locale) {
if (saved_locale != NULL) {
LOG_ERR(
"'%s' is not a UTF-8 locale, and failed to find a fallback",
saved_locale);
user_notification_add_fmt(
&user_notifications, USER_NOTIFICATION_ERROR,
"'%s' is not a UTF-8 locale, and failed to find a fallback",
saved_locale);
} else {
LOG_ERR("invalid locale, and failed to find a fallback");
user_notification_add_fmt(
&user_notifications, USER_NOTIFICATION_ERROR,
"invalid locale, and failed to find a fallback");
}
}
free(saved_locale);
}
struct config conf = {NULL};
bool conf_successful = config_load(
&conf, conf_path, &user_notifications, &overrides, check_config, as_server);
tll_free_and_free(overrides, free);
if (!conf_successful) {
config_free(&conf);
return ret;
}
if (check_config) {
config_free(&conf);
return EXIT_SUCCESS;
}
_Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR,
"fcft log level enum offset");
_Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS,
"fcft colorize enum mismatch");
fcft_init(
(enum fcft_log_colorize)log_colorize,
as_server && log_syslog,
(enum fcft_log_class)log_level);
if (conf_server_socket_path != NULL) {
free(conf.server_socket_path);
conf.server_socket_path = xstrdup(conf_server_socket_path);
}
conf.presentation_timings = presentation_timings;
conf.hold_at_exit = hold;
if (conf.tweak.font_monospace_warn && conf.fonts[0].count > 0) {
check_if_font_is_monospaced(
conf.fonts[0].arr[0].pattern, &conf.notifications);
}
if (bad_locale) {
static char *const bad_locale_fake_argv[] = {"/bin/sh", "-c", "", NULL};
argc = 1;
argv = bad_locale_fake_argv;
conf.hold_at_exit = true;
}
struct fdm *fdm = NULL;
struct reaper *reaper = NULL;
struct key_binding_manager *key_binding_manager = NULL;
struct wayland *wayl = NULL;
struct renderer *renderer = NULL;
struct terminal *term = NULL;
struct server *server = NULL;
struct shutdown_context shutdown_ctx = {.term = &term, .exit_code = foot_exit_failure};
const char *cwd = custom_cwd;
char *_cwd = NULL;
if (cwd == NULL) {
size_t buf_len = 1024;
do {
_cwd = xrealloc(_cwd, buf_len);
errno = 0;
if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) {
LOG_ERRNO("failed to get current working directory");
goto out;
}
buf_len *= 2;
} while (errno == ERANGE);
cwd = _cwd;
}
const char *pwd = getenv("PWD");
if (pwd != NULL) {
char *resolved_path_cwd = realpath(cwd, NULL);
char *resolved_path_pwd = realpath(pwd, NULL);
if (resolved_path_cwd != NULL &&
resolved_path_pwd != NULL &&
streq(resolved_path_cwd, resolved_path_pwd))
{
/*
* The resolved path of $PWD matches the resolved path of
* the *actual* working directory - use $PWD.
*
* This makes a difference when $PWD refers to a symlink.
*/
cwd = pwd;
}
free(resolved_path_cwd);
free(resolved_path_pwd);
}
shm_set_max_pool_size(conf.tweak.max_shm_pool_size);
shm_set_min_stride_alignment(conf.tweak.min_stride_alignment);
if ((fdm = fdm_init()) == NULL)
goto out;
if ((reaper = reaper_init(fdm)) == NULL)
goto out;
if ((key_binding_manager = key_binding_manager_new()) == NULL)
goto out;
if ((wayl = wayl_init(
fdm, key_binding_manager, conf.presentation_timings)) == NULL)
{
goto out;
}
if ((renderer = render_init(fdm, wayl)) == NULL)
goto out;
if (!as_server && (term = term_init(
&conf, fdm, reaper, wayl, "foot", cwd, token, pty_path,
argc, argv, NULL,
&term_shutdown_cb, &shutdown_ctx)) == NULL) {
goto out;
}
free(_cwd);
_cwd = NULL;
if (as_server && (server = server_init(&conf, fdm, reaper, wayl)) == NULL)
goto out;
volatile sig_atomic_t aborted = false;
if (!fdm_signal_add(fdm, SIGINT, &fdm_sigint, (void *)&aborted) ||
!fdm_signal_add(fdm, SIGTERM, &fdm_sigint, (void *)&aborted))
{
goto out;
}
struct sigusr_context sigusr_context = {
.term = term,
.server = server,
};
if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) ||
!fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context))
{
goto out;
}
struct sigaction sig_ign = {.sa_handler = SIG_IGN};
sigemptyset(&sig_ign.sa_mask);
if (sigaction(SIGHUP, &sig_ign, NULL) < 0 ||
sigaction(SIGPIPE, &sig_ign, NULL) < 0)
{
LOG_ERRNO("failed to ignore SIGHUP+SIGPIPE");
goto out;
}
if (as_server)
LOG_INFO("running as server; launch terminals by running footclient");
if (as_server && pid_file != NULL) {
if (!print_pid(pid_file, &unlink_pid_file))
goto out;
}
ret = EXIT_SUCCESS;
while (likely(!aborted && (as_server || tll_length(wayl->terms) > 0))) {
if (unlikely(!fdm_poll(fdm))) {
ret = foot_exit_failure;
break;
}
}
out:
free(_cwd);
server_destroy(server);
term_destroy(term);
shm_fini();
render_destroy(renderer);
wayl_destroy(wayl);
key_binding_manager_destroy(key_binding_manager);
reaper_destroy(reaper);
fdm_signal_del(fdm, SIGUSR1);
fdm_signal_del(fdm, SIGUSR2);
fdm_signal_del(fdm, SIGTERM);
fdm_signal_del(fdm, SIGINT);
fdm_destroy(fdm);
config_free(&conf);
if (unlink_pid_file)
unlink(pid_file);
LOG_INFO("goodbye");
fcft_fini();
log_deinit();
return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret;
}
UNITTEST
{
char *s = xstrjoin("foo", "bar");
xassert(streq(s, "foobar"));
free(s);
s = xstrjoin3("foo", " ", "bar");
xassert(streq(s, "foo bar"));
free(s);
s = xstrjoin3("foo", ",", "bar");
xassert(streq(s, "foo,bar"));
free(s);
s = xstrjoin3("foo", "bar", "baz");
xassert(streq(s, "foobarbaz"));
free(s);
}

454
meson.build Normal file
View file

@ -0,0 +1,454 @@
project('foot', 'c',
version: '1.26.1',
license: 'MIT',
meson_version: '>=0.59.0',
default_options: [
'c_std=c11',
'warning_level=1',
'werror=true',
'b_ndebug=if-release'])
is_debug_build = get_option('buildtype').startswith('debug')
cc = meson.get_compiler('c')
# Newer clang versions warns when using __COUNTER__ without -std=c2y
if cc.has_argument('-Wc2y-extensions')
add_project_arguments('-Wno-c2y-extensions', language: 'c')
endif
if cc.has_function('memfd_create',
args: ['-D_GNU_SOURCE'],
prefix: '#include <sys/mman.h>')
add_project_arguments('-DMEMFD_CREATE', language: 'c')
endif
# Missing on DragonFly, FreeBSD < 14.1
if cc.has_function('execvpe',
args: ['-D_GNU_SOURCE'],
prefix: '#include <unistd.h>')
add_project_arguments('-DEXECVPE', language: 'c')
endif
if cc.has_function('sigabbrev_np',
args: ['-D_GNU_SOURCE'],
prefix: '#include <string.h>')
add_project_arguments('-DSIGABBREV_NP', language: 'c')
endif
utmp_backend = get_option('utmp-backend')
if utmp_backend == 'auto'
host_os = host_machine.system()
if host_os == 'linux'
utmp_backend = 'libutempter'
elif host_os == 'freebsd'
utmp_backend = 'ulog'
else
utmp_backend = 'none'
endif
endif
utmp_default_helper_path = get_option('utmp-default-helper-path')
if utmp_backend == 'none'
utmp_add = ''
utmp_del = ''
utmp_del_have_argument = false
utmp_default_helper_path = ''
elif utmp_backend == 'libutempter'
utmp_add = 'add'
utmp_del = 'del'
utmp_del_have_argument = false
if utmp_default_helper_path == 'auto'
utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter')
endif
elif utmp_backend == 'ulog'
utmp_add = 'login'
utmp_del = 'logout'
utmp_del_have_argument = false
if utmp_default_helper_path == 'auto'
utmp_default_helper_path = join_paths('/usr', get_option('libexecdir'), 'ulog-helper')
endif
else
error('invalid utmp backend')
endif
add_project_arguments(
['-D_GNU_SOURCE=200809L',
'-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] +
(utmp_backend != 'none'
? ['-DUTMP_ADD="@0@"'.format(utmp_add),
'-DUTMP_DEL="@0@"'.format(utmp_del),
'-DUTMP_DEFAULT_HELPER_PATH="@0@"'.format(utmp_default_helper_path)]
: []) +
(utmp_del_have_argument
? ['-DUTMP_DEL_HAVE_ARGUMENT=1']
: []) +
(is_debug_build
? ['-D_DEBUG']
: [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) +
(get_option('ime')
? ['-DFOOT_IME_ENABLED=1']
: []) +
(get_option('b_pgo') == 'use'
? ['-DFOOT_PGO_ENABLED=1']
: []) +
cc.get_supported_arguments(
['-pedantic',
'-fstrict-aliasing',
'-Wstrict-aliasing']),
language: 'c',
)
terminfo_install_location = get_option('custom-terminfo-install-location')
if terminfo_install_location != ''
add_project_arguments(
['-DFOOT_TERMINFO_PATH="@0@"'.format(
join_paths(get_option('prefix'), terminfo_install_location))],
language: 'c')
else
terminfo_install_location = join_paths(get_option('datadir'), 'terminfo')
endif
# Compute the relative path used by compiler invocations.
source_root = meson.current_source_dir().split('/')
build_root = meson.global_build_root().split('/')
relative_dir_parts = []
i = 0
in_prefix = true
foreach p : build_root
if i >= source_root.length() or not in_prefix or p != source_root[i]
in_prefix = false
relative_dir_parts += '..'
endif
i += 1
endforeach
i = 0
in_prefix = true
foreach p : source_root
if i >= build_root.length() or not in_prefix or build_root[i] != p
in_prefix = false
relative_dir_parts += p
endif
i += 1
endforeach
relative_dir = join_paths(relative_dir_parts) + '/'
if cc.has_argument('-fmacro-prefix-map=/foo=')
add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c')
endif
math = cc.find_library('m')
threads = [dependency('threads'), cc.find_library('stdthreads', required: false)]
libepoll = dependency('epoll-shim', required: false)
pixman = dependency('pixman-1')
wayland_protocols = dependency('wayland-protocols', version: '>=1.41',
fallback: 'wayland-protocols',
default_options: ['tests=false'])
wayland_client = dependency('wayland-client')
wayland_cursor = dependency('wayland-cursor')
xkb = dependency('xkbcommon', version: '>=1.0.0')
fontconfig = dependency('fontconfig')
utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering'))
if utf8proc.found()
add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c')
endif
if pixman.version().version_compare('>=0.46.0')
add_project_arguments('-DHAVE_PIXMAN_RGBA_16', language: 'c')
endif
tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist')
fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft')
wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir')
wscanner = dependency('wayland-scanner', native: true)
wscanner_prog = find_program(
wscanner.get_variable('wayland_scanner'), native: true)
wl_proto_headers = []
wl_proto_src = []
wl_proto_xml = [
wayland_protocols_datadir / 'stable/xdg-shell/xdg-shell.xml',
wayland_protocols_datadir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml',
wayland_protocols_datadir / 'unstable/xdg-output/xdg-output-unstable-v1.xml',
wayland_protocols_datadir / 'unstable/primary-selection/primary-selection-unstable-v1.xml',
wayland_protocols_datadir / 'stable/presentation-time/presentation-time.xml',
wayland_protocols_datadir / 'unstable/text-input/text-input-unstable-v3.xml',
wayland_protocols_datadir / 'staging/xdg-activation/xdg-activation-v1.xml',
wayland_protocols_datadir / 'stable/viewporter/viewporter.xml',
wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml',
wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1
wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml',
wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml',
wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml',
wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml',
wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml',
]
if (wayland_protocols.version().version_compare('>=1.43'))
wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml']
add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c')
endif
if (wayland_protocols.version().version_compare('>=1.45'))
wl_proto_xml += [wayland_protocols_datadir / 'staging/ext-background-effect/ext-background-effect-v1.xml']
add_project_arguments('-DHAVE_EXT_BACKGROUND_EFFECT=1', language: 'c')
endif
foreach prot : wl_proto_xml
wl_proto_headers += custom_target(
prot.underscorify() + '-client-header',
output: '@BASENAME@.h',
input: prot,
command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@'])
wl_proto_src += custom_target(
prot.underscorify() + '-private-code',
output: '@BASENAME@.c',
input: prot,
command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@'])
endforeach
env = find_program('env', native: true)
generate_version_sh = files('generate-version.sh')
version = custom_target(
'generate_version',
build_always_stale: true,
output: 'version.h',
command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
python = find_program('python3', native: true)
generate_builtin_terminfo_py = files('scripts/generate-builtin-terminfo.py')
foot_terminfo = files('foot.info')
builtin_terminfo = custom_target(
'generate_builtin_terminfo',
output: 'foot-terminfo.h',
command: [python, generate_builtin_terminfo_py,
'@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@']
)
generate_emoji_variation_sequences = files('scripts/generate-emoji-variation-sequences.py')
emoji_variation_sequences = custom_target(
'generate_emoji_variation_sequences',
input: 'unicode/emoji-variation-sequences.txt',
output: 'emoji-variation-sequences.h',
command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@']
)
generate_srgb_funcs = files('scripts/srgb.py')
srgb_funcs = custom_target(
'generate_srgb_funcs',
output: ['srgb.c', 'srgb.h'],
command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@']
)
common = static_library(
'common',
'log.c', 'log.h',
'char32.c', 'char32.h',
'debug.c', 'debug.h',
'macros.h',
'xmalloc.c', 'xmalloc.h',
'xsnprintf.c', 'xsnprintf.h',
dependencies: [utf8proc]
)
misc = static_library(
'misc',
'hsl.c', 'hsl.h',
'macros.h',
'misc.c', 'misc.h',
'uri.c', 'uri.h',
dependencies: [utf8proc],
link_with: [common]
)
vtlib = static_library(
'vtlib',
'base64.c', 'base64.h',
'composed.c', 'composed.h',
'cursor-shape.c', 'cursor-shape.h',
'csi.c', 'csi.h',
'dcs.c', 'dcs.h',
'macros.h',
'osc.c', 'osc.h',
'sixel.c', 'sixel.h',
'vt.c', 'vt.h',
builtin_terminfo, srgb_funcs,
wl_proto_src + wl_proto_headers,
version,
dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc],
link_with: [common, misc],
)
pgolib = static_library(
'pgolib',
'grid.c', 'grid.h',
'selection.c', 'selection.h',
'terminal.c', 'terminal.h',
emoji_variation_sequences,
wl_proto_src + wl_proto_headers,
dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc],
link_with: vtlib,
)
tokenize = static_library(
'tokenizelib',
'tokenize.c',
dependencies: [utf8proc],
link_with: [common],
)
if get_option('b_pgo') == 'generate'
executable(
'pgo',
'pgo/pgo.c',
wl_proto_src + wl_proto_headers,
dependencies: [math, threads, libepoll, pixman, wayland_client, xkb, utf8proc, fcft, tllist],
link_with: pgolib,
)
endif
executable(
'foot',
'async.c', 'async.h',
'box-drawing.c', 'box-drawing.h',
'config.c', 'config.h',
'commands.c', 'commands.h',
'extract.c', 'extract.h',
'fdm.c', 'fdm.h',
'foot-features.c', 'foot-features.h',
'ime.c', 'ime.h',
'input.c', 'input.h',
'key-binding.c', 'key-binding.h',
'main.c',
'notify.c', 'notify.h',
'quirks.c', 'quirks.h',
'reaper.c', 'reaper.h',
'render.c', 'render.h',
'search.c', 'search.h',
'server.c', 'server.h', 'client-protocol.h',
'shm.c', 'shm.h',
'slave.c', 'slave.h',
'spawn.c', 'spawn.h',
'tokenize.c', 'tokenize.h',
'unicode-mode.c', 'unicode-mode.h',
'url-mode.c', 'url-mode.h',
'user-notification.c', 'user-notification.h',
'wayland.c', 'wayland.h', 'shm-formats.h',
'xkbcommon-vmod.h',
srgb_funcs, wl_proto_src + wl_proto_headers, version,
dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc,
tllist, fcft],
link_with: pgolib,
install: true)
executable(
'footclient',
'client.c', 'client-protocol.h',
'foot-features.c', 'foot-features.h',
'macros.h',
'util.h',
version,
dependencies: [tllist, utf8proc],
link_with: common,
install: true)
install_data(
'foot.desktop', 'foot-server.desktop', 'footclient.desktop',
install_dir: join_paths(get_option('datadir'), 'applications'))
systemd = dependency('systemd', required: false)
custom_systemd_units_dir = get_option('systemd-units-dir')
if systemd.found() or custom_systemd_units_dir != ''
configuration = configuration_data()
configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir')))
if (custom_systemd_units_dir == '')
systemd_units_dir = systemd.get_variable('systemduserunitdir')
else
systemd_units_dir = custom_systemd_units_dir
endif
configure_file(
configuration: configuration,
input: 'foot-server.service.in',
output: '@BASENAME@',
install_dir: systemd_units_dir
)
install_data(
'foot-server.socket',
install_dir: systemd_units_dir)
endif
scdoc = dependency('scdoc', native: true, required: get_option('docs'))
install_data('foot.ini', install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'foot'))
if scdoc.found()
install_data(
'LICENSE', 'README.md', 'CHANGELOG.md',
install_dir: join_paths(get_option('datadir'), 'doc', 'foot'))
subdir('doc')
endif
if get_option('themes')
install_subdir('themes', install_dir: join_paths(get_option('datadir'), 'foot'))
endif
terminfo_base_name = get_option('terminfo-base-name')
if terminfo_base_name == ''
terminfo_base_name = get_option('default-terminfo')
endif
tic = find_program('tic', native: true, required: get_option('terminfo'))
if tic.found()
conf_data = configuration_data(
{
'default_terminfo': terminfo_base_name
}
)
preprocessed = configure_file(
input: 'foot.info',
output: 'foot.info.preprocessed',
configuration: conf_data,
)
custom_target(
'terminfo',
output: terminfo_base_name[0],
input: preprocessed,
command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'],
install: true,
install_dir: terminfo_install_location
)
endif
subdir('completions')
subdir('icons')
subdir('utils')
if (get_option('tests'))
subdir('tests')
endif
summary(
{
'Documentation': scdoc.found(),
'Themes': get_option('themes'),
'IME': get_option('ime'),
'Grapheme clustering': utf8proc.found(),
'utmp backend': utmp_backend,
'utmp helper default path': utmp_default_helper_path,
'Build terminfo': tic.found(),
'Terminfo base name': terminfo_base_name,
'Terminfo install location': terminfo_install_location,
'Default TERM': get_option('default-terminfo'),
'Set TERMINFO': get_option('custom-terminfo-install-location') != '',
'Build tests': get_option('tests'),
},
bool_yn: true
)

29
meson_options.txt Normal file
View file

@ -0,0 +1,29 @@
option('docs', type: 'feature',
description: 'Build and install documentation (man pages, example foot.ini, readme, changelog, license etc).')
option('themes', type: 'boolean', value: true,
description: 'Install themes (predefined color schemes)')
option('ime', type: 'boolean', value: true,
description: 'IME (Input Method Editor) support')
option('grapheme-clustering', type: 'feature',
description: 'Enables grapheme clustering using libutf8proc. Requires fcft with harfbuzz support to be useful.')
option('tests', type: 'boolean', value: true, description: 'Build tests')
option('terminfo', type: 'feature', value: 'enabled', description: 'Build and install foot\'s terminfo files.')
option('default-terminfo', type: 'string', value: 'foot',
description: 'Default value of the "term" option in foot.ini.')
option('terminfo-base-name', type: 'string',
description: 'Base name of the generated terminfo files. Defaults to the value of the \'default-terminfo\' meson option')
option('custom-terminfo-install-location', type: 'string', value: '',
description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.')
option('systemd-units-dir', type: 'string', value: '',
description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}')
option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'],
description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \'utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)')
option('utmp-default-helper-path', type: 'string', value: 'auto',
description: 'Default path to the utmp helper binary. Default: auto-detect')

63
misc.c Normal file
View file

@ -0,0 +1,63 @@
#include "misc.h"
#include "char32.h"
#include <stdlib.h>
bool
isword(char32_t wc, bool spaces_only, const char32_t *delimiters)
{
if (spaces_only)
return isc32graph(wc);
if (c32chr(delimiters, wc) != NULL)
return false;
return isc32graph(wc);
}
void
timespec_add(const struct timespec *a, const struct timespec *b,
struct timespec *res)
{
const long one_sec_in_ns = 1000000000;
res->tv_sec = a->tv_sec + b->tv_sec;
res->tv_nsec = a->tv_nsec + b->tv_nsec;
/* tv_nsec may be negative */
if (res->tv_nsec >= one_sec_in_ns) {
res->tv_sec++;
res->tv_nsec -= one_sec_in_ns;
}
}
void
timespec_sub(const struct timespec *a, const struct timespec *b,
struct timespec *res)
{
const long one_sec_in_ns = 1000000000;
res->tv_sec = a->tv_sec - b->tv_sec;
res->tv_nsec = a->tv_nsec - b->tv_nsec;
/* tv_nsec may be negative */
if (res->tv_nsec < 0) {
res->tv_sec--;
res->tv_nsec += one_sec_in_ns;
}
}
bool
is_valid_utf8_and_printable(const char *value)
{
char32_t *wide = ambstoc32(value);
if (wide == NULL)
return false;
for (const char32_t *c = wide; *c != U'\0'; c++) {
if (!isc32print(*c)) {
free(wide);
return false;
}
}
free(wide);
return true;
}

12
misc.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <stdbool.h>
#include <uchar.h>
#include <time.h>
bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters);
void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res);
void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res);
bool is_valid_utf8_and_printable(const char *value);

6
notes.txt Normal file
View file

@ -0,0 +1,6 @@
1. uses wrong cursor when switching to another tab, only first tab shows
the correct cursor.
2. if i resize a tab the next tab will be the same
size it was previously until i resize the window, but then i will have
to do that for each consecutive window. if i dont resize the window at
all the tabs look fine.

765
notify.c Normal file
View file

@ -0,0 +1,765 @@
#include "notify.h"
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/stat.h>
#include <fcntl.h>
#define LOG_MODULE "notify"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "config.h"
#include "spawn.h"
#include "terminal.h"
#include "util.h"
#include "wayland.h"
#include "xmalloc.h"
#include "xsnprintf.h"
void
notify_free(struct terminal *term, struct notification *notif)
{
if (notif->pid > 0)
fdm_del(term->fdm, notif->stdout_fd);
free(notif->id);
free(notif->title);
free(notif->body);
free(notif->category);
free(notif->app_id);
free(notif->icon_cache_id);
free(notif->icon_symbolic_name);
free(notif->icon_data);
free(notif->sound_name);
free(notif->xdg_token);
free(notif->stdout_data);
tll_free_and_free(notif->actions, free);
if (notif->icon_path != NULL) {
unlink(notif->icon_path);
free(notif->icon_path);
if (notif->icon_fd >= 0)
close(notif->icon_fd);
}
memset(notif, 0, sizeof(*notif));
}
static bool
write_icon_file(const void *data, size_t data_sz, int *fd, char **filename,
char **symbolic_name)
{
xassert(*filename == NULL);
xassert(*symbolic_name == NULL);
char name[64] = "/tmp/foot-notification-icon-XXXXXX";
*filename = NULL;
*symbolic_name = NULL;
*fd = mkostemp(name, O_CLOEXEC);
if (*fd < 0) {
LOG_ERRNO("failed to create temporary file for icon cache");
return false;
}
if (write(*fd, data, data_sz) != (ssize_t)data_sz) {
LOG_ERRNO("failed to write icon data to temporary file");
close(*fd);
*fd = -1;
return false;
}
LOG_DBG("wrote icon data to %s", name);
*filename = xstrdup(name);
*symbolic_name = xstrjoin("file://", *filename);
return true;
}
static bool
to_integer(const char *line, size_t len, uint32_t *res)
{
bool is_id = true;
uint32_t maybe_id = 0;
for (size_t i = 0; i < len; i++) {
char digit = line[i];
if (digit < '0' || digit > '9') {
is_id = false;
break;
}
maybe_id *= 10;
maybe_id += digit - '0';
}
*res = maybe_id;
return is_id;
}
static void
consume_stdout(struct notification *notif, bool eof)
{
char *data = notif->stdout_data;
const char *line = data;
size_t left = notif->stdout_sz;
/* Process stdout, line-by-line */
while (left > 0) {
line = data;
size_t len = left;
char *eol = (char *)memchr(line, '\n', left);
if (eol != NULL) {
*eol = '\0';
len = strlen(line);
data = eol + 1;
} else if (!eof)
break;
uint32_t maybe_id = 0;
uint32_t maybe_button_nr = 0;
/* Check for daemon assigned ID, either '123', or 'id=123' */
if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) ||
(len > 3 && memcmp(line, "id=", 3) == 0 &&
to_integer(&line[3], len - 3, &maybe_id)))
{
notif->external_id = maybe_id;
LOG_DBG("external ID: %u", notif->external_id);
}
/* Check for triggered action, either 'default' or 'action=default' */
else if ((len == 7 && memcmp(line, "default", 7) == 0) ||
(len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0))
{
notif->activated = true;
LOG_DBG("notification's default action was triggered");
}
else if (len > 7 && memcmp(line, "action=", 7) == 0) {
notif->activated = true;
if (to_integer(&line[7], len - 7, &maybe_button_nr)) {
notif->activated_button = maybe_button_nr;
LOG_DBG("custom action %u triggered", notif->activated_button);
} else {
LOG_DBG("unrecognized action triggered: %.*s",
(int)(len - 7), &line[7]);
}
}
else if (notif->external_id > 0 &&
to_integer(line, len, &maybe_button_nr) &&
maybe_button_nr > 0 &&
maybe_button_nr <= notif->button_count)
{
/* Single integer, appearing *after* the ID, and is within
the custom button/action range */
notif->activated = true;
notif->activated_button = maybe_button_nr;
LOG_DBG("custom action %u triggered", notif->activated_button);
}
/* Check for XDG activation token, 'xdgtoken=xyz' */
else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) {
notif->xdg_token = xstrndup(&line[9], len - 9);
LOG_DBG("XDG token: \"%s\"", notif->xdg_token);
}
left -= len + (eol != NULL ? 1 : 0);
}
if (left > 0)
memmove(notif->stdout_data, data, left);
notif->stdout_sz = left;
}
static bool
fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data)
{
const struct terminal *term = data;
struct notification *notif = NULL;
/* Find notification */
tll_foreach(term->active_notifications, it) {
if (it->item.stdout_fd == fd) {
notif = &it->item;
break;
}
}
if (events & EPOLLIN) {
char buf[512];
ssize_t count = read(fd, buf, sizeof(buf) - 1);
if (count < 0) {
if (errno == EINTR)
return true;
LOG_ERRNO("failed to read notification activation token");
return false;
}
if (count > 0 && notif != NULL) {
if (notif->stdout_data == NULL) {
xassert(notif->stdout_sz == 0);
notif->stdout_data = xmemdup(buf, count);
} else {
notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count);
memcpy(&notif->stdout_data[notif->stdout_sz], buf, count);
}
notif->stdout_sz += count;
consume_stdout(notif, false);
}
}
if (events & EPOLLHUP) {
fdm_del(fdm, fd);
if (notif != NULL) {
notif->stdout_fd = -1;
consume_stdout(notif, true);
}
}
return true;
}
static void
notif_done(struct reaper *reaper, pid_t pid, int status, void *data)
{
struct terminal *term = data;
tll_foreach(term->active_notifications, it) {
struct notification *notif = &it->item;
if (notif->pid != pid)
continue;
LOG_DBG("notification %s closed",
notif->id != NULL ? notif->id : "<unset>");
if (notif->activated && notif->focus) {
LOG_DBG("focus window on notification activation: \"%s\"",
notif->xdg_token);
if (notif->xdg_token == NULL)
LOG_WARN("cannot focus window: no activation token available");
else
wayl_activate(term->wl, term->window, notif->xdg_token);
}
if (notif->activated && notif->report_activated) {
LOG_DBG("sending notification activation event to client");
const char *id = notif->id != NULL ? notif->id : "0";
char button_nr[16] = {0};
if (notif->activated_button > 0) {
xsnprintf(
button_nr, sizeof(button_nr), "%u", notif->activated_button);
}
char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1];
size_t n = xsnprintf(
reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr);
term_to_slave(term, reply, n);
}
if (notif->report_closed) {
LOG_DBG("sending notification close event to client");
const char *id = notif->id != NULL ? notif->id : "0";
char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1];
size_t n = xsnprintf(
reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id);
term_to_slave(term, reply, n);
}
notify_free(term, notif);
tll_remove(term->active_notifications, it);
return;
}
}
static bool
expand_action_to_argv(struct terminal *term, const char *name, const char *label,
size_t *argc, char ***argv)
{
char **expanded = NULL;
size_t count = 0;
if (!spawn_expand_template(
&term->conf->desktop_notifications.command_action_arg, 2,
(const char *[]){"action-name", "action-label"},
(const char *[]){name, label},
&count, &expanded))
{
return false;
}
/* Append to the "global" actions argv */
*argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0]));
memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0]));
*argc += count;
free(expanded);
return true;
}
bool
notify_notify(struct terminal *term, struct notification *notif)
{
xassert(notif->xdg_token == NULL);
xassert(notif->external_id == 0);
xassert(notif->pid == 0);
xassert(notif->stdout_fd <= 0);
xassert(notif->stdout_data == NULL);
xassert(notif->icon_path == NULL);
xassert(notif->icon_fd <= 0);
notif->pid = -1;
notif->stdout_fd = -1;
notif->icon_fd = -1;
if (term->conf->desktop_notifications.command.argv.args == NULL)
return false;
if ((term->conf->desktop_notifications.inhibit_when_focused ||
notif->when != NOTIFY_ALWAYS)
&& term->kbd_focus)
{
/* No notifications while we're focused */
return false;
}
const char *app_id = notif->app_id != NULL
? notif->app_id
: term->app_id != NULL
? term->app_id
: term->conf->app_id;
const char *title = notif->title != NULL ? notif->title : notif->body;
const char *body = notif->title != NULL && notif->body != NULL ? notif->body : "";
/* Icon: symbolic name if present, otherwise a filename */
const char *icon_name_or_path = "";
if (notif->icon_cache_id != NULL) {
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
const struct notification_icon *icon = &term->notification_icons[i];
if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) {
/* For now, we set the symbolic name to 'file:///path'
* when using a file based icon. */
xassert(icon->symbolic_name != NULL);
icon_name_or_path = icon->symbolic_name;
LOG_DBG("using icon from cache (cache ID: %s): %s",
icon->id, icon_name_or_path);
break;
}
}
} else if (notif->icon_symbolic_name != NULL) {
icon_name_or_path = notif->icon_symbolic_name;
LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path);
} else if (notif->icon_data_sz > 0) {
xassert(notif->icon_data != NULL);
if (write_icon_file(
notif->icon_data, notif->icon_data_sz,
&notif->icon_fd,
&notif->icon_path,
&notif->icon_symbolic_name))
icon_name_or_path = notif->icon_symbolic_name;
LOG_DBG("using icon data from notification: %s", icon_name_or_path);
}
bool track_notification = notif->focus ||
notif->report_activated ||
notif->may_be_programatically_closed;
uint32_t replaces_id = 0;
if (notif->id != NULL) {
tll_foreach(term->active_notifications, it) {
struct notification *existing = &it->item;
if (existing->id == NULL)
continue;
/*
* When replacing/updating a notification, we may have
* *multiple* notification helpers running for the "same"
* notification. Make sure only the *last* notification's
* report closed/activated are honored, to avoid sending
* multiple reports.
*
* This also means we cannot 'break' out of the loop - we
* must check *all* notifications.
*/
if (existing->external_id != 0 && streq(existing->id, notif->id)) {
replaces_id = existing->external_id;
existing->report_activated = false;
existing->report_closed = false;
}
}
}
char replaces_id_str[16];
xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id);
const char *urgency_str =
notif->urgency == NOTIFY_URGENCY_LOW
? "low"
: notif->urgency == NOTIFY_URGENCY_NORMAL
? "normal" : "critical";
LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", "
"urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s, "
"sound-name=%s (tracking: %s)",
title, body, app_id, notif->category, urgency_str, icon_name_or_path,
notif->expire_time, replaces_id,
notif->muted ? "yes" : "no", notif->sound_name,
track_notification ? "yes" : "no");
xassert(title != NULL);
if (title == NULL)
return false;
char **argv = NULL;
size_t argc = 0;
char **action_argv = NULL;
size_t action_argc = 0;
char expire_time[16];
xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time);
if (term->conf->desktop_notifications.command_action_arg.argv.args) {
if (!expand_action_to_argv(
term, "default", "Activate", &action_argc, &action_argv))
{
return false;
}
size_t action_idx = 1;
tll_foreach(notif->actions, it) {
/* Custom actions use a numerical name, starting at 1 */
char name[16];
xsnprintf(name, sizeof(name), "%zu", action_idx++);
if (!expand_action_to_argv(
term, name, it->item, &action_argc, &action_argv))
{
for (size_t i = 0; i < action_argc; i++)
free(action_argv[i]);
free(action_argv);
return false;
}
}
}
if (!spawn_expand_template(
&term->conf->desktop_notifications.command, 12,
(const char *[]){
"app-id", "window-title", "icon", "title", "body", "category",
"urgency", "muted", "sound-name", "expire-time", "replace-id",
"action-argument"},
(const char *[]){
app_id, term->window_title, icon_name_or_path, title,
body != NULL ? body : "",
notif->category != NULL ? notif->category : "", urgency_str,
notif->muted ? "true" : "false",
notif->sound_name != NULL ? notif->sound_name : "",
expire_time, replaces_id_str,
/* Custom expansion below, since we need to expand to multiple arguments */
"${action-argument}"},
&argc, &argv))
{
return false;
}
/* Post-process the expanded argv, and patch in all the --action
arguments we expanded earlier */
for (size_t i = 0; i < argc; i++) {
if (!streq(argv[i], "${action-argument}"))
continue;
if (action_argc == 0) {
free(argv[i]);
/* Remove ${command-argument}, but include terminating NULL */
memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0]));
argc--;
break;
}
/* Remove the "${action-argument}" entry, add all actions argument
from earlier, but include terminating NULL */
argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0]));
/* Move remaining arguments to after the action arguments */
memmove(&argv[i + action_argc],
&argv[i + 1],
(argc - i) * sizeof(argv[0])); /* Include terminating NULL */
free(argv[i]); /* Free xstrdup("${action-argument}"); */
/* Insert the action arguments */
for (size_t j = 0; j < action_argc; j++) {
argv[i + j] = action_argv[j];
action_argv[j] = NULL;
}
argc += action_argc;
argc--; /* The ${action-argument} option has been removed */
break;
}
LOG_DBG("notify command:");
for (size_t i = 0; i < argc; i++)
LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]);
xassert(argv[argc] == NULL);
int stdout_fds[2] = {-1, -1};
if (track_notification) {
if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) {
LOG_WARN("failed to create stdout pipe");
track_notification = false;
/* Non-fatal */
} else {
tll_push_back(term->active_notifications, *notif);
/* We've taken over ownership of all data; clear, so that
notify_free() doesn't double free */
notif->id = NULL;
notif->title = NULL;
notif->body = NULL;
notif->category = NULL;
notif->app_id = NULL;
notif->icon_cache_id = NULL;
notif->icon_symbolic_name = NULL;
notif->icon_data = NULL;
notif->icon_data_sz = 0;
notif->icon_path = NULL;
notif->sound_name = NULL;
notif->icon_fd = -1;
notif->stdout_fd = -1;
struct notification *new_notif = &tll_back(term->active_notifications);
/* We don't need these anymore. They'll be free:d by the caller */
new_notif->button_count = tll_length(notif->actions);
memset(&new_notif->actions, 0, sizeof(new_notif->actions));
notif = new_notif;
}
}
if (stdout_fds[0] >= 0) {
fdm_add(term->fdm, stdout_fds[0], EPOLLIN,
&fdm_notify_stdout, (void *)term);
}
/* Redirect stdin to /dev/null, but ignore failure to open */
int devnull = open("/dev/null", O_RDONLY);
pid_t pid = spawn(
term->reaper, NULL, argv, devnull, stdout_fds[1], -1,
track_notification ? &notif_done : NULL, (void *)term, NULL);
if (stdout_fds[1] >= 0) {
/* Close write-end of stdout pipe */
close(stdout_fds[1]);
}
if (pid < 0 && stdout_fds[0] >= 0) {
/* Remove FDM callback if we failed to spawn */
fdm_del(term->fdm, stdout_fds[0]);
}
if (devnull >= 0)
close(devnull);
for (size_t i = 0; i < argc; i++)
free(argv[i]);
free(argv);
for (size_t i = 0; i < action_argc; i++)
free(action_argv[i]);
free(action_argv);
notif->pid = pid;
notif->stdout_fd = stdout_fds[0];
return true;
}
void
notify_close(struct terminal *term, const char *id)
{
xassert(id != NULL);
LOG_DBG("close notification %s", id);
tll_foreach(term->active_notifications, it) {
const struct notification *notif = &it->item;
if (notif->id == NULL || !streq(notif->id, id))
continue;
if (term->conf->desktop_notifications.close.argv.args == NULL) {
LOG_DBG(
"trying to close notification \"%s\" by sending SIGINT to %u",
id, notif->pid);
if (notif->pid == 0) {
LOG_WARN(
"cannot close notification \"%s\": no helper process running",
id);
} else {
/* Best-effort... */
kill(notif->pid, SIGINT);
}
} else {
LOG_DBG(
"trying to close notification \"%s\" "
"by running user defined command", id);
if (notif->external_id == 0) {
LOG_WARN("cannot close notification \"%s\": "
"no daemon assigned notification ID available", id);
return;
}
char **argv = NULL;
size_t argc = 0;
char external_id[16];
xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id);
if (!spawn_expand_template(
&term->conf->desktop_notifications.close, 1,
(const char *[]){"id"},
(const char *[]){external_id},
&argc, &argv))
{
return;
}
int devnull = open("/dev/null", O_RDONLY);
spawn(
term->reaper, NULL, argv, devnull, -1, -1,
NULL, (void *)term, NULL);
if (devnull >= 0)
close(devnull);
for (size_t i = 0; i < argc; i++)
free(argv[i]);
free(argv);
}
return;
}
LOG_WARN("cannot close notification \"%s\": no such notification", id);
}
static void
add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name,
const uint8_t *data, size_t data_sz)
{
icon->id = xstrdup(id);
icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL;
icon->tmp_file_name = NULL;
icon->tmp_file_fd = -1;
/*
* Dump in-line data to a temporary file. This allows us to pass
* the filename as a parameter to notification helpers
* (i.e. notify-send -i <path>).
*
* Optimization: since we always prefer (i.e. use) the symbolic
* name if present, there's no need to create a file on disk if we
* have a symbolic name.
*/
if (symbolic_name == NULL && data_sz > 0) {
write_icon_file(
data, data_sz,
&icon->tmp_file_fd,
&icon->tmp_file_name,
&icon->symbolic_name);
}
LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s",
icon->id, icon->symbolic_name, icon->tmp_file_name);
}
void
notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data, size_t data_sz)
{
#if defined(_DEBUG)
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id != NULL && streq(icon->id, id)) {
BUG("notification icon cache already contains \"%s\"", id);
}
}
#endif
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id == NULL) {
add_icon(icon, id, symbolic_name, data, data_sz);
return;
}
}
/* Cache full - throw out first entry, add new entry last */
notify_icon_free(&term->notification_icons[0]);
memmove(&term->notification_icons[0],
&term->notification_icons[1],
((ALEN(term->notification_icons) - 1) *
sizeof(term->notification_icons[0])));
add_icon(
&term->notification_icons[ALEN(term->notification_icons) - 1],
id, symbolic_name, data, data_sz);
}
void
notify_icon_del(struct terminal *term, const char *id)
{
for (size_t i = 0; i < ALEN(term->notification_icons); i++) {
struct notification_icon *icon = &term->notification_icons[i];
if (icon->id == NULL || !streq(icon->id, id))
continue;
LOG_DBG("expelled %s from the notification icon cache", icon->id);
notify_icon_free(icon);
return;
}
}
void
notify_icon_free(struct notification_icon *icon)
{
if (icon->tmp_file_name != NULL) {
unlink(icon->tmp_file_name);
if (icon->tmp_file_fd >= 0)
close(icon->tmp_file_fd);
}
free(icon->id);
free(icon->symbolic_name);
free(icon->tmp_file_name);
icon->id = NULL;
icon->symbolic_name = NULL;
icon->tmp_file_name = NULL;
icon->tmp_file_fd = -1;
}

95
notify.h Normal file
View file

@ -0,0 +1,95 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <unistd.h>
#include <tllist.h>
struct terminal;
enum notify_when {
/* First, so that it can be left out of initializer and still be
the default */
NOTIFY_ALWAYS,
NOTIFY_UNFOCUSED,
NOTIFY_INVISIBLE
};
enum notify_urgency {
/* First, so that it can be left out of initializer and still be
the default */
NOTIFY_URGENCY_NORMAL,
NOTIFY_URGENCY_LOW,
NOTIFY_URGENCY_CRITICAL,
};
struct notification {
/*
* Set by caller of notify_notify()
*/
char *id; /* Internal notification ID */
char *app_id; /* Custom app-id, overrides the terminal's app-id if set */
char *title; /* Required */
char *body;
char *category;
enum notify_when when;
enum notify_urgency urgency;
int32_t expire_time;
tll(char *) actions;
char *icon_cache_id;
char *icon_symbolic_name;
uint8_t *icon_data;
size_t icon_data_sz;
bool focus; /* Focus the foot window when notification is activated */
bool may_be_programatically_closed; /* OSC-99: notification may be programmatically closed by the client */
bool report_activated; /* OSC-99: report notification activation to client */
bool report_closed; /* OSC-99: report notification closed to client */
bool muted; /* Explicitly mute the notification */
char *sound_name; /* Should be set to NULL if muted == true */
/*
* Used internally by notify
*/
uint32_t external_id; /* Daemon assigned notification ID */
bool activated; /* User 'activated' the notification */
uint32_t button_count; /* Number of buttons (custom actions) in notification */
uint32_t activated_button; /* User activated one of the custom actions */
char *xdg_token; /* XDG activation token, from daemon */
pid_t pid; /* Notifier command PID */
int stdout_fd; /* Notifier command's stdout */
char *stdout_data; /* Data we've reado from command's stdout */
size_t stdout_sz;
/* Used when notification provides raw icon data, and it's
bypassing the icon cache */
char *icon_path;
int icon_fd;
};
struct notification_icon {
char *id;
char *symbolic_name;
char *tmp_file_name;
int tmp_file_fd;
};
bool notify_notify(struct terminal *term, struct notification *notif);
void notify_close(struct terminal *term, const char *id);
void notify_free(struct terminal *term, struct notification *notif);
void notify_icon_add(struct terminal *term, const char *id,
const char *symbolic_name, const uint8_t *data,
size_t data_sz);
void notify_icon_del(struct terminal *term, const char *id);
void notify_icon_free(struct notification_icon *icon);

1735
osc.c Normal file

File diff suppressed because it is too large Load diff

7
osc.h Normal file
View file

@ -0,0 +1,7 @@
#pragma once
#include <stdbool.h>
#include "terminal.h"
bool osc_ensure_size(struct terminal *term, size_t required_size);
void osc_dispatch(struct terminal *term);

8
pgo/full-current-session.sh Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -eux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}"

14
pgo/full-headless-cage.sh Executable file
View file

@ -0,0 +1,14 @@
#!/bin/sh
set -eux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
runtime_dir=$(mktemp -d)
trap "rm -rf '${runtime_dir}'" EXIT INT HUP TERM
XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}"
# Cage's exit code doesn't reflect our script's exit code
[ -f "${blddir}"/pgo-ok ] || exit 1

View file

@ -0,0 +1,9 @@
#!/bin/sh
set -ux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}"
swaymsg exit

24
pgo/full-headless-sway.sh Executable file
View file

@ -0,0 +1,24 @@
#!/bin/sh
set -eux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
runtime_dir=$(mktemp -d)
sway_conf=$(mktemp)
cleanup() {
rm -f "${sway_conf}"
rm -rf "${runtime_dir}"
}
trap cleanup EXIT INT HUP TERM
# Generate a custom config that executes our generate-pgo-data script
> "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'"
# Run Sway. full-headless-sway-inner.sh ends with a 'swaymsg exit'
XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" --unsupported-gpu
# Sway's exit code doesn't reflect our script's exit code
[ -f "${blddir}"/pgo-ok ] || exit 1

32
pgo/full-inner.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/sh
set -eux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
. "${srcdir}"/pgo/options
pgo_data=$(mktemp)
trap "rm -f '${pgo_data}'" EXIT INT HUP TERM
rm -f "${blddir}"/pgo-ok
# To ensure profiling data is generated in the build directory
cd "${blddir}"
"${blddir}"/utils/xtgettcap
"${blddir}"/footclient --version
"${blddir}"/foot \
--config=/dev/null \
--override tweak.grapheme-shaping=no \
--term=xterm \
sh -c "
set -eux
'${srcdir}/scripts/generate-alt-random-writes.py' \
${script_options} \"${pgo_data}\"
cat \"${pgo_data}\"
"
touch "${blddir}"/pgo-ok

1
pgo/options Normal file
View file

@ -0,0 +1 @@
script_options="--scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel"

29
pgo/partial.sh Executable file
View file

@ -0,0 +1,29 @@
#!/bin/sh
set -eux
srcdir=$(realpath "${1}")
blddir=$(realpath "${2}")
. "${srcdir}"/pgo/options
pgo_data=$(mktemp)
trap "rm -f ${pgo_data}" EXIT INT HUP TERM
rm -f "${blddir}"/pgo-ok
"${srcdir}"/scripts/generate-alt-random-writes.py \
--rows=67 \
--cols=135 \
${script_options} \
"${pgo_data}"
# To ensure profiling data is generated in the build directory
cd "${blddir}"
"${blddir}"/utils/xtgettcap
"${blddir}"/footclient --version
"${blddir}"/foot --version
"${blddir}"/pgo "${pgo_data}"
touch "${blddir}"/pgo-ok

Some files were not shown because too many files have changed in this diff Show more