diff options
146 files changed, 6393 insertions, 3876 deletions
diff --git a/.fmsbw/10-freebsd14-msys-sciteco b/.fmsbw/10-freebsd14-msys-sciteco new file mode 100755 index 0000000..037ac66 --- /dev/null +++ b/.fmsbw/10-freebsd14-msys-sciteco @@ -0,0 +1,189 @@ +#!/usr/local/bin/bash +set -ex +export ASSUME_ALWAYS_YES=yes + +# Already in freebsd14-sciteco +# TODO: Build this with buildah. +# Start with --network=host +#pkg update +#pkg install FreeBSD-clang FreeBSD-clibs-dev \ +# gmake pkgconf autoconf automake libtool \ +# glib gtk3 groff doxygen lowdown valgrind +# +#pkg install llvm21 gnugrep gmake coreutils gsed gawk git wget gnupg bash groff zip autoconf automake libtool python3 +#pkg remove FreeBSD-clang +#git clone https://github.com/HolyBlackCat/quasi-msys2.git /opt/quasi-msys2 +#cd /opt/quasi-msys2 +#ln -s /usr/local/bin/gpgv2 /usr/local/bin/gpgv +#ln -s /usr/local/bin/bash /bin/bash +#mkdir -p gnu-overrides +#ln -s /usr/local/bin/ggrep gnu-overrides/grep +#ln -s /usr/local/bin/gmake gnu-overrides/make +#ln -s /usr/local/bin/gsed gnu-overrides/sed +#ln -s /usr/local/bin/greadlink gnu-overrides/readlink +#ln -s /usr/local/bin/wine64 gnu-overrides/wine +#echo MINGW64 >msystem.txt +#cat >activate << EOF +#cd /opt/quasi-msys2 +#export PATH=`pwd`/gnu-overrides:$PATH +#export PKG_CONFIG=pkg-config +#set +ex +#. env/all.src +#set -ex +#EOF +#gmake install _autotools _gcc _libc++ _glib2 _pdcurses _gtk3 _librsvg +#ln -nfs "/opt/quasi-msys2/root/mingw64" /mingw64 +#pkg clean -a + +autoreconf -i +mkdir build-freebsd +cd build-freebsd +../configure --with-interface=ncurses --enable-debug --enable-html-docs +gmake + +# NOTE: The test suite must be run in verbose mode because if it fails +# we won't be able to analyze testsuite.log. +gmake check TESTSUITEFLAGS="--verbose --color=never --valgrind" +# Includes a second test suite run, but without Valgrind. +# This is good since we had to exclude several test cases when running +# under CI with --valgrind. +gmake distcheck + +# Test building Doxygen documentation +gmake -C doc devdoc + +gmake install + +# TODO: Also automatically rebuild the cheat sheet. + +# Build and deploy website +cd ../www +sciteco -m build.tes ../build-freebsd +cp *.html /opt/htdocs/ +cd .. +cp ico/sciteco.ico /opt/htdocs/graphics +cp ico/sciteco-48.png /opt/htdocs/graphics + +# TODO: Should we also distribute FreeBSD binaries? + +# FIXME: This should be a separate job but currently we need the +# boostrapping with the FreeBSD version of SciTECO. +# We can run with bootstrapping using --with-wine=wine64, +# but it doesn't even generate all files correctly. +# The test suite also doesn't fully work under Wine yet. +# Activate MSYS environment +. /opt/quasi-msys2/activate +cd /opt/build + +export CURSES_CFLAGS=-I/mingw64/include/pdcurses/ +# FIXME: glib on MinGW supports static linking but the gspawn +# helper binaries are still linked dynamically, forcing us to ship +# all DLLs anyway. Therefore it makes little sense to link SciTECO +# itself statically - it only wastes a few MB. +# You would also have to add --enable-static-executables. +# FIXME: Once there is an --enable-lto, we should use that. +# NOTE: LTO is broken with libstdc++. +# https://github.com/HolyBlackCat/quasi-msys2/issues/44 +export CFLAGS="-O3 -flto=thin" +export CXXFLAGS="-O3 -flto=thin -stdlib=libc++" +export LDFLAGS="-flto=thin -stdlib=libc++" + +# We cannot run Windows binaries automatically through Wine, +# so we must still force cross-compilation with --host. + +#autoreconf -i +mkdir build-wingui build-wincon +cd build-wingui +../configure --host=x86_64-w64-mingw32 \ + --with-interface=pdcurses-gui --enable-html-docs --program-prefix=g \ + --with-scitecodatadir=. \ + --disable-bootstrap \ + CURSES_LIBS="-lpdcurses_wingui -lgdi32 -lcomdlg32 -lwinmm" +make +make install-strip +#make check TESTSUITEFLAGS="--verbose --color=never" + +cd ../build-wincon +../configure --host=x86_64-w64-mingw32 \ + --with-interface=pdcurses --enable-html-docs \ + --with-scitecodatadir=. \ + --disable-bootstrap \ + CURSES_LIBS="-lpdcurses_wincon -lgdi32 -lwinmm" +make +make install-strip +#make check TESTSUITEFLAGS="--verbose --color=never" + +export MINGW_BUNDLEDLLS_SEARCH_PATH=/mingw64/bin + +cd .. +mkdir -p temp-bin-pdcurses/ +cd temp-bin-pdcurses/ +cp -r /mingw64/bin/{gsciteco.exe,sciteco.exe,grosciteco.tes,tedoc.tes} ./ +# datadir is relative to bindir +cp -r /mingw64/bin/{lib,*.tmac} ./ +cp /mingw64/bin/fallback.teco_ini .teco_ini +cp -r /mingw64/share/doc/sciteco/* ./ +cp ../COPYING ../ChangeLog ./ +cp /mingw64/bin/gspawn-win64-helper*.exe ./ +# Collect DLLs for all included binaries +for f in *.exe; do ../contrib/mingw-bundledlls --copy $f; done +zip -9 -r ../sciteco-pdcurses_nightly_win64.zip . +cd .. + +mkdir -p /opt/htdocs/downloads/nightly/ +cp sciteco-pdcurses_nightly_win64.zip /opt/htdocs/downloads/nightly/ + +# FIXME: If we had a working bootstrapping build (where SciTECO +# is run under wine64), this should also be in a separate job +# so we don't have to install into the same root as for the PDCurses versions. +mkdir build-gtk +cd build-gtk +../configure --host=x86_64-w64-mingw32 \ + --with-interface=gtk --enable-html-docs \ + --with-scitecodatadir=. \ + --disable-bootstrap +make +make install-strip +#make check TESTSUITEFLAGS="--verbose --color=never" + +export MINGW_BUNDLEDLLS_SEARCH_PATH=/mingw64/bin + +cd .. +mkdir -p temp-bin-gtk/ +cd temp-bin-gtk/ +cp /mingw64/bin/{sciteco.exe,grosciteco.tes,tedoc.tes} ./ +# datadir is relative to bindir +cp -r /mingw64/bin/{lib,*.tmac} ./ +cp /mingw64/bin/fallback.teco_ini .teco_ini +cp /mingw64/bin/fallback.css ../win32/.teco_css . +cp -r /mingw64/share/doc/sciteco/* ./ +cp ../COPYING ../ChangeLog ./ +cp /mingw64/bin/gspawn-win64-helper*.exe ./ +# Collect DLLs for all included binaries +for f in *.exe; do ../contrib/mingw-bundledlls --copy $f; done +#mkdir share +#cp /mingw64/share/loader.cache share/ +#glib-compile-schemas /mingw64/share/glib-2.0/schemas +#mkdir -p share/glib-2.0 +#cp /mingw64/share/glib-2.0/gschemas.compiled share/glib-2.0/ +mkdir -p share/icons/Adwaita +# FIXME: It should be sufficient to package the SVG icons, +# but I cannot get it to work. Perhaps index.theme would have to be tweaked. +# We could also try to include a pure scalable icon theme. +#cp -r /mingw64/share/icons/Adwaita/{scalable*,index.theme} share/icons/Adwaita/ +cp -r /mingw64/share/icons/Adwaita/* share/icons/Adwaita/ +wine64 /mingw64/bin/gtk-update-icon-cache-3.0.exe share/icons/Adwaita/ +# FIXME: It's possible to change the location of loaders.cache via $GDK_PIXBUF_MODULE_FILE. +# If we did that, we could avoid "reusing" the lib/ directory. +# This is important when somebody changes $SCITECOPATH. +cp /mingw64/lib/gdk-pixbuf-2.0/2.10.0/loaders/{pixbufloader_svg.dll,libpixbufloader-png.dll} . +# Collect DLLs for all pixbuf loaders into the root directory +for f in *pixbufloader*.dll; do ../contrib/mingw-bundledlls --copy $f; done +mkdir -p lib/gdk-pixbuf-2.0/2.10.0/loaders/ +mv *pixbufloader*.dll lib/gdk-pixbuf-2.0/2.10.0/loaders/ +cp ../win32/loaders.cache lib/gdk-pixbuf-2.0/2.10.0/ +zip -9 -r ../sciteco-gtk3_nightly_win64.zip . +cd .. + +mkdir -p /opt/htdocs/downloads/nightly/ +cp sciteco-gtk3_nightly_win64.zip /opt/htdocs/downloads/nightly/ diff --git a/.fmsbw/20-freebsd14-osx-sciteco b/.fmsbw/20-freebsd14-osx-sciteco new file mode 100755 index 0000000..bef1cfb --- /dev/null +++ b/.fmsbw/20-freebsd14-osx-sciteco @@ -0,0 +1,73 @@ +#!/bin/sh +set -ex + +# FIXME: We have to build a native version first since the Mac OS +# version cannot be bootstrapped. +# This could be avoided if a separate job would provide FreeBSD nightly builds. +autoreconf -i +mkdir build-freebsd +cd build-freebsd +../configure --with-interface=ncurses CFLAGS="-O3" CXXFLAGS="-O3" +gmake install +cd .. + +# Image is based on freebsd14-sciteco. +#git clone -b 2.0-llvm-based https://github.com/tpoechtrager/osxcross.git /opt/osxcross +#pkg install libxml2 FreeBSD-liblzma-dev FreeBSD-runtime-dev python3 bash llvm21 gsed cmake FreeBSD-openssl-lib-dev openbsm FreeBSD-clibs-dev openssl +#pkg remove FreeBSD-clang +#cd /opt/osxcross +# The SDK has been extracted from Command_Line_Tools_for_Xcode_26.dmg. +# I had to use my bhyve-ubuntu24 VM for that. +#cp /opt/tmp/* tarballs/ +#export CPPFLAGS="-I/usr/local/include" +#export SDK_VERSION=26.0 +#export PATH=/usr/local/llvm21/bin:/opt/osxcross/target/bin:$PATH +#UNATTENDED=1 ./build.sh +#unset CPPFLAGS +#export MACOSX_DEPLOYMENT_TARGET=10.13 +# FIXME: This is not unattended. Perhaps echo https://nue.de.packages.macports.org/macports/packages >target/macports/MIRROR +# dylibbundler is available but can't be run naturally. +#osxcross-macports install --static glib2-devel gtk3-devel +# +#pkg install cmake +#git clone https://github.com/auriamg/macdylibbundler.git /opt/macdylibbundler +#cd /opt/macdylibbundler +#cmake . +#make +#cp dylibbundler /usr/local/bin/ + +export PATH=/usr/local/llvm21/bin:/opt/osxcross/target/bin:$PATH +export MACOSX_DEPLOYMENT_TARGET=10.13 + +export CFLAGS="-O3 -flto=thin" +export CXXFLAGS="-O3 -flto=thin" +export LDFLAGS="-flto=thin" + +mkdir build-osx +cd build-osx +# NOTE: Make sure we pick up the SDK's ncurses instead of the one pulled in +# via MacPorts. +../configure --host=x86_64-apple-darwin25 --disable-bootstrap --with-interface=ncurses \ + --enable-static-executables --with-scitecodatadir=../share/sciteco \ + CURSES_CFLAGS="-D_DARWIN_C_SOURCE -DNCURSES_WIDECHAR" CURSES_LIBS="/opt/osxcross/target/SDK/MacOSX26.0.sdk/usr/lib/libncurses.tbd" +gmake install-strip DESTDIR=`pwd`/temp-install +# There are libraries we cannot link against statically. +# We ship them in /usr/local/lib/sciteco so as not to cause collisions with system +# libraries or libraries installed via Homebrew. +# System libraries are considered to have stable ABIs and +# are not currently bundled. +# FIXME: Is this really true for libc++? +# Anyway, currently it's apparently fully statically linked. +dylibbundler -b -x temp-install/usr/local/bin/sciteco \ + -cd -d temp-install/usr/local/lib/sciteco -p @executable_path/../lib/sciteco \ + --no-codesign +# FIXME: Perhaps create pkg using fpm (https://github.com/jordansissel/fpm)? +(cd temp-install; tar czf ../../sciteco-curses_nightly_macos_x86_64.tar.gz *) +cd .. + +# FIXME: Also build -arch arm64 and package with x86_64-apple-darwin25-lipo into universal binary. +# x86_64-apple-darwin25-lipo -lipo -create -output sciteco x86_64/usr/local/bin/sciteco arm64/usr/local/bin/sciteco +# TODO: Build Gtk version as well. + +mkdir -p /opt/htdocs/downloads/nightly/ +cp sciteco-curses_nightly_macos_x86_64.tar.gz /opt/htdocs/downloads/nightly/ diff --git a/.fmsbw/50-ubuntu22-appimage b/.fmsbw/50-ubuntu22-appimage new file mode 100755 index 0000000..ade1100 --- /dev/null +++ b/.fmsbw/50-ubuntu22-appimage @@ -0,0 +1,26 @@ +#!/bin/sh +set -ex + +# FIXME: AppImages can theoretically also be built on OBS. +# Unfortunately it only works with packages from openSUSE 15.6. +# Also, it's not trivial to build multiple AppImages on OBS. +# FIXME: This still relies on the Debian repositories provided +# via OBS. But there is no guarantee it is ready by the time +# we run this CI job. +# We should be fine, though unless committing at 6:00 in the morning. + +#apt-get update -o APT::Cache-Start=100000000 +#apt-get install -o APT::Cache-Start=100000000 -y fuse libfuse2 imagemagick wget file binutils libglib2.0-bin +#mkdir -p ~/pkg2appimage +#cd ~/pkg2appimage +#wget -O pkg2appimage.AppImage https://github.com/AppImageCommunity/pkg2appimage/releases/download/continuous/pkg2appimage-1eceb30-x86_64.AppImage +#chmod +x pkg2appimage.AppImage +# FIXME: We could get automatic mounting to work with fusefs in the host and by exposesing /dev/fuse. +#./pkg2appimage.AppImage --appimage-extract + +cd AppImage +~/pkg2appimage/squashfs-root/AppRun curses.yml +mv out/*.AppImage /opt/htdocs/downloads/nightly/sciteco-curses_nightly_x86_64.AppImage +~/pkg2appimage/squashfs-root/AppRun gtk.yml +mv out/*.AppImage /opt/htdocs/downloads/nightly/sciteco-gtk_nightly_x86_64.AppImage +chmod a+x /opt/htdocs/downloads/nightly/*.AppImage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 2f2b8f0..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,147 +0,0 @@ -name: Continuous Integration - -on: [push, pull_request] - -jobs: - ubuntu: - strategy: - matrix: - os: [ubuntu-22.04, ubuntu-24.04] - compiler: ['CC=gcc CXX=g++', 'CC=clang CXX=clang++'] - interface: [ncurses, gtk] - - # NOTE: The virtual environments already contain both GCC and Clang - runs-on: ${{ matrix.os }} - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - - name: Update Repositories - run: sudo apt-get update - - name: Install Build Dependencies - run: > - sudo apt-get install -y - build-essential - autoconf automake libtool - libglib2.0-dev libncurses-dev libgtk-3-dev xvfb - groff doxygen - - - name: Configure Build - env: - # Enable Adress Sanitizer only on ncurses. - # Gtk produces a lot of false negatives. - # This will improve the test suite quality, even without Valgrind. - CFLAGS: ${{ matrix.interface == 'ncurses' && '-fsanitize=address -fno-omit-frame-pointer' || ' ' }} - CXXFLAGS: ${{ matrix.interface == 'ncurses' && '-fsanitize=address -fno-omit-frame-pointer' || ' ' }} - MALLOC_REPLACEMENT: ${{ matrix.interface == 'ncurses' && 'no' || 'check' }} - run: | - autoreconf -i - ./configure --with-interface=${{ matrix.interface }} --enable-debug --enable-html-docs \ - --enable-malloc-replacement=$MALLOC_REPLACEMENT ${{ matrix.compiler }} - - # NOTE: xvfb-run emulates an XServer and is required when building - # Gtk versions (since SciTECO calls itself during the build). - - name: make - run: xvfb-run -a make - - name: make install - run: sudo xvfb-run -a make install - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: xvfb-run -a make check TESTSUITEFLAGS="--verbose" - - name: Build Developer Documentation - run: cd doc && make devdoc - - name: make distcheck - run: xvfb-run -a make distcheck - - name: Build Source Tarball - run: make dist - - macos: - runs-on: macos-latest - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - # NOTE: macOS already ships with ncurses and - # XCode already comes with the autotools. - - name: Install Build Dependencies - run: brew install autoconf automake libtool glib groff doxygen - - name: Configure Build - env: - # Make sure we don't pick up GCC by accident. - CC: clang - CXX: clang++ - run: | - autoreconf -i - ./configure --with-interface=ncurses --enable-debug --enable-html-docs - - - run: make - - run: sudo make install - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: make check TESTSUITEFLAGS="--verbose" - - name: Build Developer Documentation - run: cd doc && make devdoc - - run: make distcheck - - name: Build Source Tarball - run: make dist - - win32: - runs-on: windows-latest - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: MINGW32 - CHERE_INVOKING: 1 - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - - name: Set Up Shell - run: echo C:\msys64\usr\bin\ | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - shell: pwsh - - - name: Install Build Dependencies - run: > - pacman -S --noconfirm --needed - base-devel mingw-w64-i686-autotools mingw-w64-i686-toolchain - mingw-w64-i686-glib2 mingw-w64-i686-pdcurses - groff mingw-w64-i686-doxygen - - - name: Configure Build - env: - PDCURSES_CFLAGS: -I/mingw32/include/pdcurses/ - run: | - autoreconf -i - ./configure --with-interface=pdcurses-gui --enable-debug --enable-html-docs - - - run: make - - run: make install - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: make check TESTSUITEFLAGS="--verbose" - - name: Build Developer Documentation - run: cd doc && make devdoc - - name: make distcheck - env: - DISTCHECK_CONFIGURE_FLAGS: --with-interface=pdcurses-gui - PDCURSES_CFLAGS: -I/mingw32/include/pdcurses/ - run: make distcheck - - name: Build Source Tarball - run: make dist diff --git a/.github/workflows/irc.yml b/.github/workflows/irc.yml deleted file mode 100644 index a44774c..0000000 --- a/.github/workflows/irc.yml +++ /dev/null @@ -1,33 +0,0 @@ -# After every push with successful CI, -# post the commits since the last successful CI into the IRC channel. -name: IRC Post - -on: - workflow_run: - workflows: ['Continuous Integration'] - types: [completed] - branches: master - -jobs: - irc: - runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} - steps: - - name: Git Clone - uses: actions/checkout@v4.1.6 - - name: Install Build Dependencies - run: sudo apt-get install -y ncat - - name: Get last successful commit - uses: nrwl/last-successful-commit-action@v1 - id: last_successful_commit - with: - branch: 'master' - workflow_id: 'ci.yml' - github_token: ${{ secrets.GITHUB_TOKEN }} - - name: IRC Connection - run: | - (echo "NICK git-bot" - echo "USER git-bot 8 * : git-bot" - echo "JOIN #sciteco" - git log --pretty="format:PRIVMSG #sciteco %h %s" --reverse ${{ steps.last_successful_commit.outputs.commit_hash }}..HEAD - echo "QUIT") | ncat --ssl irc.libera.chat 6697 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml deleted file mode 100644 index c136b54..0000000 --- a/.github/workflows/nightly.yml +++ /dev/null @@ -1,366 +0,0 @@ -name: Nightly Builds - -on: - workflow_dispatch: - schedule: - # Daily at 3:14 - - cron: '14 3 * * *' - -jobs: - debian-packages: - strategy: - matrix: - os: [ubuntu-22.04, ubuntu-24.04] - - runs-on: ${{ matrix.os }} - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - - name: Update Repositories - run: sudo apt-get update - - name: Install Build Dependencies - run: > - sudo apt-get install -y - devscripts build-essential lintian debhelper dh-exec - autoconf automake libtool - libglib2.0-dev libncurses-dev libgtk-3-dev xvfb - groff - - # NOTE: We need to configure the build directory only to generate distribute.mk. - - name: Configure Build - run: | - autoreconf -i - ./configure - - # NOTE: The debian package build rules already use xvfb-run to emulate an XServer - # when necessary since the PPA build servers might also be headless. - # NOTE: Packages are left in debian-temp/. - - name: Build Debian/Ubuntu Packages - run: | - ./distribute.mk debian-binary - cp debian-temp/sciteco-curses_*.deb sciteco-curses_nightly_${{matrix.os}}_amd64.deb - cp debian-temp/sciteco-gtk_*.deb sciteco-gtk_nightly_${{matrix.os}}_amd64.deb - cp debian-temp/sciteco-common_*.deb sciteco-common_nightly_${{matrix.os}}_all.deb - - - name: Build AppImages - # Should always be on the oldest supported Ubuntu - if: matrix.os == 'ubuntu-22.04' - env: - GH_TOKEN: ${{ github.token }} - run: | - sudo apt-get install -y libfuse2 - cd AppImage - gh release download -R AppImageCommunity/pkg2appimage -O pkg2appimage.AppImage \ - -p 'pkg2appimage-*-x86_64.AppImage' continuous - chmod +x pkg2appimage.AppImage - ./pkg2appimage.AppImage curses.yml - mv out/*.AppImage ../sciteco-curses_nightly_x86_64.AppImage - ./pkg2appimage.AppImage gtk.yml - mv out/*.AppImage ../sciteco-gtk_nightly_x86_64.AppImage - chmod a+x *.AppImage - - - name: Archive Debian/Ubuntu Packages and AppImages - uses: pyTooling/Actions/releaser@v1.0.5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: nightly - files: ./*.deb ./*.AppImage - - macos: - runs-on: macos-13 - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - # NOTE: macOS already ships with ncurses and groff. - # The system libncurses has turned out to be buggy, though (keypad() does not work). - # However, it does work on real Mac OS systems, I was told. - # Linking in our own ncurses should also be more portable in case - # the system libncurses ABI breaks. - # However, Homebrew installs ncurses as a keg and it will refer to a - # non-standard $TERMINFO. This could be worked around. - # The macOS Groff version appears to be outdated. - - name: Install Build Dependencies - run: brew install autoconf automake libtool glib groff dylibbundler - # Required by pyTooling/Actions/releaser - - name: Set up Python - uses: actions/setup-python@v4.3.0 - with: - python-version: '3.10' - # FIXME: It would be nice to build universal arm64/x86_64 binaries, - # this apparently requires two separate build runs and a following merge - # using `lipo -create`. In this case we could just as well build two - # separate packages. - - name: Configure Build - env: - # Make sure we don't pick up GCC by accident. - CC: clang - CXX: clang++ - # FIXME: Once there is an --enable-lto, we should use that. - CFLAGS: -O3 -flto - CXXFLAGS: -O3 -flto - LDFLAGS: -flto - # Uncomment if you want to build against the Homebrew-installed libncurses. - #PKG_CONFIG_PATH: /usr/local/opt/ncurses/lib/pkgconfig - # NOTE: This will not result in a fully statically-linked binary, - # but the more we get rid off, the better. - # NOTE: Making the binary relocatable means it can be installed into non-root directories - # with `installer -pkg -target`. - run: | - autoreconf -i - ./configure --with-interface=ncurses --enable-static-executables --enable-html-docs \ - --with-scitecodatadir=../share/sciteco - - - name: make - run: make -j 2 - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: make check TESTSUITEFLAGS="--verbose" - - - name: Package - run: | - make install-strip DESTDIR=`pwd`/temp-install - # There are libraries we cannot link against statically. - # We ship them in /usr/local/lib/sciteco so as not to cause collisions with system - # libraries or libraries installed via Homebrew. - # System libraries are considered to have stable ABIs and - # are not currently bundled. - # FIXME: Is this really true for libc++? - dylibbundler -b -x temp-install/usr/local/bin/sciteco \ - -cd -d temp-install/usr/local/lib/sciteco -p @executable_path/../lib/sciteco - # FIXME: Should we encode the Git commit into the package version? - # Unfortunately, I cannot find detailed information on how Mac OS - # interpretes these version strings. - VERSION=`sed -nE 's/#define PACKAGE_VERSION "(.*)"/\1/p' config.h` - pkgbuild --identifier net.sf.sciteco.pkg --version $VERSION \ - --root temp-install --install-location / \ - sciteco-curses_nightly_macos_x86_64.pkg - - name: Archive Mac OS Distribution (ncurses) - uses: pyTooling/Actions/releaser/composite@v1.0.5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: nightly - files: ./*.pkg - - # The website is published on Mac OS only because we cannot tweak the - # ./configure flags on Ubuntu where Debian packages are built. - # FIXME: This could be done without a gh-pages branch, see - # https://github.com/actions/starter-workflows/blob/main/pages/static.yml - # This however should be in its own workflow and we'd have to rebuild - # SciTECO and everything. - # FIXME: Also build cheat-sheet.pdf automatically? - - run: make install - - name: Install lowdown (Markdown processor) - run: brew install lowdown - - name: Generate website - run: cd www && sciteco -m build.tes - - name: Publish Website - run: | - cd www - touch .nojekyll - git init - cp ../.git/config ./.git/config - git add .nojekyll *.html - git config --local user.email "Website@GitHubActions" - git config --local user.name "GitHub Actions" - git commit -a -m "update ${{ github.sha }}" - git push -u origin +HEAD:gh-pages - - win64-curses: - runs-on: windows-2019 - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: MINGW64 - CHERE_INVOKING: 1 - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - - name: Set Up Shell - run: echo C:\msys64\usr\bin\ | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - shell: pwsh - - - name: Install Build Dependencies - run: > - pacman -S --noconfirm --needed - base-devel mingw-w64-x86_64-autotools mingw-w64-x86_64-toolchain - mingw-w64-x86_64-glib2 mingw-w64-x86_64-pdcurses - groff - - - name: Configure Build - env: - PDCURSES_CFLAGS: -I/mingw64/include/pdcurses/ - # FIXME: glib on MinGW supports static linking but the gspawn - # helper binaries are still linked dynamically, forcing us to ship - # all DLLs anyway. Therefore it makes little sense to link SciTECO - # itself statically - it only wastes a few MB. - # You will also have to add --enable-static-executables. - # The additional Windows libraries are for PDCursesMod/WinGUI: -# LIBGLIB_LIBS: -lglib-2.0 -lintl -liconv -lpcre -lole32 -lws2_32 -luuid - # FIXME: Once there is an --enable-lto, we should use that. -# CFLAGS: -O3 -flto -DGLIB_STATIC_COMPILATION - CFLAGS: -O3 -flto - CXXFLAGS: -O3 -flto - LDFLAGS: -flto - run: | - autoreconf -i - mkdir build-wingui build-wincon - (cd build-wingui - ../configure --with-interface=pdcurses-gui --enable-html-docs --program-prefix=g \ - --with-scitecodatadir=. \ - PDCURSES_LIBS="-lpdcurses_wingui -lgdi32 -lcomdlg32 -lwinmm") - (cd build-wincon - ../configure --with-interface=pdcurses --enable-html-docs \ - --with-scitecodatadir=. \ - PDCURSES_LIBS="-lpdcurses_wincon -lgdi32 -lwinmm") - - - name: make - run: | - make -C build-wingui -j 2 - make -C build-wincon -j 2 - - name: make install - run: | - make -C build-wingui install-strip - make -C build-wincon install-strip - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: | - make -C build-wingui check TESTSUITEFLAGS="--verbose" - make -C build-wincon check TESTSUITEFLAGS="--verbose" - - - name: Prepare Distribution Directory - env: - MINGW_BUNDLEDLLS_SEARCH_PATH: /mingw64/bin - run: | - mkdir temp-bin/ - cd temp-bin/ - cp -r /mingw64/bin/{gsciteco.exe,sciteco.exe,grosciteco.tes,tedoc.tes} ./ - # datadir is relative to bindir - cp -r /mingw64/bin/{lib,*.tmac} ./ - cp /mingw64/bin/fallback.teco_ini .teco_ini - cp -r /mingw64/share/doc/sciteco/* ./ - cp ../COPYING ../ChangeLog ./ - cp /mingw64/bin/gspawn-win64-helper*.exe ./ - # Collect DLLs for all included binaries - for f in *.exe; do ../contrib/mingw-bundledlls --copy $f; done - zip -9 -r ../sciteco-pdcurses_nightly_win64.zip . - - name: Archive Windows Distribution (PDCurses) - uses: pyTooling/Actions/releaser/composite@v1.0.5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: nightly - files: ./*.zip - - # NOTE: There is a lot of redundancy with win32-curses. - # However the Curses version may be linked statically, while Gtk+3 cannot be - # linked statically on Windows (at least MSYS does not provide - # static libraries) and would draw in libglib, libintl, libiconv etc. anyway. - win64-gtk: - runs-on: windows-2019 - - defaults: - run: - shell: bash.exe --login -eo pipefail "{0}" - env: - MSYSTEM: MINGW64 - CHERE_INVOKING: 1 - - steps: - - - name: Git Clone - uses: actions/checkout@v4.1.6 - with: - submodules: true - - - name: Set Up Shell - run: echo C:\msys64\usr\bin\ | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - shell: pwsh - - - name: Install Build Dependencies - run: > - pacman -S --noconfirm --needed - base-devel mingw-w64-x86_64-autotools mingw-w64-x86_64-toolchain - mingw-w64-x86_64-glib2 mingw-w64-x86_64-gtk3 mingw-w64-x86_64-librsvg - groff - - - name: Configure Build - env: - # FIXME: Once there is an --enable-lto, we should use that. - CFLAGS: -O3 -flto - CXXFLAGS: -O3 -flto - LDFLAGS: -flto - run: | - autoreconf -i - ./configure --with-interface=gtk --enable-html-docs \ - --with-scitecodatadir=. - - - name: make - run: make -j 2 - - run: make install-strip - # NOTE: The test suite must be run in verbose mode because if it fails - # we won't be able to analyze testsuite.log. - - name: Run Test Suite - run: make check TESTSUITEFLAGS="--verbose" - - - name: Prepare Distribution Directory - env: - MINGW_BUNDLEDLLS_SEARCH_PATH: /mingw64/bin - run: | - mkdir temp-bin - cd temp-bin - cp /mingw64/bin/{sciteco.exe,grosciteco.tes,tedoc.tes} ./ - # datadir is relative to bindir - cp -r /mingw64/bin/{lib,*.tmac} ./ - cp /mingw64/bin/fallback.teco_ini .teco_ini - cp /mingw64/bin/fallback.css ../win32/.teco_css . - cp -r /mingw64/share/doc/sciteco/* ./ - cp ../COPYING ../ChangeLog ./ - cp /mingw64/bin/gspawn-win64-helper*.exe ./ - # Collect DLLs for all included binaries - for f in *.exe; do ../contrib/mingw-bundledlls --copy $f; done - #mkdir share - #cp /mingw64/share/loader.cache share/ - #glib-compile-schemas /mingw64/share/glib-2.0/schemas - #mkdir -p share/glib-2.0 - #cp /mingw64/share/glib-2.0/gschemas.compiled share/glib-2.0/ - mkdir -p share/icons/Adwaita - # FIXME: It should be sufficient to package the SVG icons, - # but I cannot get it to work. Perhaps index.theme would have to be tweaked. - # We could also try to include a pure scalable icon theme. - #cp -r /mingw64/share/icons/Adwaita/{scalable*,index.theme} share/icons/Adwaita/ - cp -r /mingw64/share/icons/Adwaita/* share/icons/Adwaita/ - gtk-update-icon-cache-3.0 share/icons/Adwaita/ - # FIXME: It's possible to change the location of loaders.cache via $GDK_PIXBUF_MODULE_FILE. - # If we did that, we could avoid "reusing" the lib/ directory. - # This is important when somebody changes $SCITECOPATH. - cp /mingw64/lib/gdk-pixbuf-2.0/2.10.0/loaders/{pixbufloader_svg.dll,libpixbufloader-png.dll} . - # Collect DLLs for all pixbuf loaders into the root directory - for f in *pixbufloader*.dll; do ../contrib/mingw-bundledlls --copy $f; done - mkdir -p lib/gdk-pixbuf-2.0/2.10.0/loaders/ - mv *pixbufloader*.dll lib/gdk-pixbuf-2.0/2.10.0/loaders/ - cp ../win32/loaders.cache lib/gdk-pixbuf-2.0/2.10.0/ - zip -9 -r ../sciteco-gtk3_nightly_win64.zip . - - name: Archive Windows Distribution (GTK+ 3) - uses: pyTooling/Actions/releaser/composite@v1.0.5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - tag: nightly - files: ./*.zip diff --git a/.gitmodules b/.gitmodules index ed91108..841ce77 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "scintilla"] path = contrib/scintilla - url = https://github.com/rhaberkorn/scintilla-mirror.git + url = git://git.fmsbw.de/scintilla-mirror ignore = untracked [submodule "scinterm"] path = contrib/scinterm @@ -1 +1 @@ -Robin Haberkorn <robin.haberkorn@googlemail.com> +Robin Haberkorn <rhaberkorn@fmsbw.de> diff --git a/AppImage/curses.yml b/AppImage/curses.yml index 675b66c..254947a 100755 --- a/AppImage/curses.yml +++ b/AppImage/curses.yml @@ -1,28 +1,25 @@ app: sciteco-curses ingredients: - packages: - - sciteco-curses dist: focal sources: - - deb http://archive.ubuntu.com/ubuntu/ jammy main universe -# ppas: -# - robin-haberkorn/sciteco - script: - - wget -c "https://github.com/rhaberkorn/sciteco/releases/download/nightly/sciteco-common_nightly_ubuntu-22.04_all.deb" - - wget -c "https://github.com/rhaberkorn/sciteco/releases/download/nightly/sciteco-curses_nightly_ubuntu-22.04_amd64.deb" + # should always build on the oldest supported version + - deb http://archive.ubuntu.com/ubuntu/ focal main universe + - deb http://download.opensuse.org/repositories/home:/rhaberkorn:/sciteco:/UNSTABLE/xUbuntu_20.04/ / + packages: + - sciteco-curses post_script: - - dpkg -I sciteco-curses*.deb | grep "Version:" | cut -d':' -f2 | cut -d'+' -f1 | sed 's/^[ ]*//g' >VERSION + - dpkg -I sciteco-curses*.deb | sed -En 's/ *Version: *(.*)/\1/p' >VERSION script: # This is currently not installed by sciteco-curses. # FIXME: There should perhaps be a unique name in the desktop file, so it does not conflict with the Gtk version. - - wget -O sciteco-curses.desktop -c "https://raw.githubusercontent.com/rhaberkorn/sciteco/master/src/sciteco.desktop" + - wget -O sciteco-curses.desktop -c "https://git.fmsbw.de/sciteco/plain/src/sciteco.desktop" - sed -i -e 's@gsciteco@sciteco@g' sciteco-curses.desktop - echo 'Terminal=true' >>sciteco-curses.desktop - - wget -O sciteco.png -c "https://raw.githubusercontent.com/rhaberkorn/sciteco/master/ico/sciteco-256.png" + - wget -O sciteco.png -c "https://git.fmsbw.de/sciteco/plain/ico/sciteco-256.png" - mkdir -p ./usr/share/metainfo/ - - wget -O ./usr/share/metainfo/sciteco-curses.appdata.xml -c "https://raw.githubusercontent.com/rhaberkorn/sciteco/master/AppImage/sciteco-curses.appdata.xml" + - wget -O ./usr/share/metainfo/sciteco-curses.appdata.xml -c "https://git.fmsbw.de/sciteco/plain/AppImage/sciteco-curses.appdata.xml" # Thinning: These documentation files are pointless. # SciTECO comes with its own online help system. - rm -rf ./usr/share/doc ./usr/share/man diff --git a/AppImage/gtk.yml b/AppImage/gtk.yml index 0ead771..99e834e 100755 --- a/AppImage/gtk.yml +++ b/AppImage/gtk.yml @@ -1,22 +1,19 @@ app: sciteco-gtk ingredients: + dist: focal + sources: + # should always build on the oldest supported version + - deb http://archive.ubuntu.com/ubuntu/ focal main universe + - deb http://download.opensuse.org/repositories/home:/rhaberkorn:/sciteco:/UNSTABLE/xUbuntu_20.04/ / packages: - sciteco-gtk exclude: # pkg2appimage blacklists Gtk, Pango and other libs from the GNOME stack, # so excluding glib as well should actually improve portability. - libglib2.0-0 - dist: focal - sources: - - deb http://archive.ubuntu.com/ubuntu/ jammy main universe -# ppas: -# - robin-haberkorn/sciteco - script: - - wget -c "https://github.com/rhaberkorn/sciteco/releases/download/nightly/sciteco-common_nightly_ubuntu-22.04_all.deb" - - wget -c "https://github.com/rhaberkorn/sciteco/releases/download/nightly/sciteco-gtk_nightly_ubuntu-22.04_amd64.deb" post_script: - - dpkg -I sciteco-gtk*.deb | grep "Version:" | cut -d':' -f2 | cut -d'+' -f1 | sed 's/^[ ]*//g' >VERSION + - dpkg -I sciteco-gtk*.deb | sed -En 's/ *Version: *(.*)/\1/p' >VERSION script: # FIXME: There should perhaps be a unique name in the desktop file, so it does not conflict with the Curses version. @@ -24,7 +21,7 @@ script: - cp ./usr/share/icons/hicolor/256x256/apps/sciteco.png ./sciteco.png - rm -rf ./usr/share/icons - mkdir -p ./usr/share/metainfo/ - - wget -O ./usr/share/metainfo/sciteco-gtk.appdata.xml -c "https://raw.githubusercontent.com/rhaberkorn/sciteco/master/AppImage/sciteco-gtk.appdata.xml" + - wget -O ./usr/share/metainfo/sciteco-gtk.appdata.xml -c "https://git.fmsbw.de/sciteco/plain/AppImage/sciteco-gtk.appdata.xml" # Thinning: These documentation files are pointless. # SciTECO comes with its own online help system. - rm -rf ./usr/share/doc ./usr/share/man diff --git a/AppImage/sciteco-curses.appdata.xml b/AppImage/sciteco-curses.appdata.xml index fb114fe..07e956d 100644 --- a/AppImage/sciteco-curses.appdata.xml +++ b/AppImage/sciteco-curses.appdata.xml @@ -15,10 +15,10 @@ This app contains the curses (terminal) version of SciTECO. </p></description> <launchable type="desktop-id">sciteco-curses.desktop</launchable> - <url type="homepage">https://rhaberkorn.github.io/sciteco/</url> + <url type="homepage">https://sciteco.fmsbw.de/</url> <screenshots> <screenshot type="default"> - <image>https://sciteco.sf.net/screenshots/v2.1.0-freebsd-ncurses.png</image> + <image>https://sciteco.fmsbw.de/screenshots/v2.1.0-freebsd-ncurses.png</image> </screenshot> </screenshots> <provides> diff --git a/AppImage/sciteco-gtk.appdata.xml b/AppImage/sciteco-gtk.appdata.xml index 59e17d7..d65cccf 100644 --- a/AppImage/sciteco-gtk.appdata.xml +++ b/AppImage/sciteco-gtk.appdata.xml @@ -15,10 +15,10 @@ This app contains the GTK+ 3 (graphical) version of SciTECO. </p></description> <launchable type="desktop-id">sciteco-gtk.desktop</launchable> - <url type="homepage">https://rhaberkorn.github.io/sciteco/</url> + <url type="homepage">https://sciteco.fmsbw.de/</url> <screenshots> <screenshot type="default"> - <image>https://sciteco.sf.net/screenshots/v2.3.0-freebsd-gtk.png</image> + <image>https://sciteco.fmsbw.de/screenshots/v2.3.0-freebsd-gtk.png</image> </screenshot> </screenshots> <provides> @@ -6,6 +6,143 @@ using a prebuilt binary) are included. Entries marked with "(!)" might break macro portability compared to the preceding release. +Version 2.5.0 (2026-01-01) +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* `make install` no longer touches already installed files, + which may appease some packaging systems (FreeBSD ports). + However `make install bindir=...` and the like are no longer supported. +* fixed clicking the "(Unnamed)" buffer in 0EB popups +* Implemented recovery file mechanism: + Unsaved changes are dumped to #filename#. + Use n,6EJ to configure the recovery file dumping interval. +* Fixed building on platforms with very large FILENAME_MAX (GNU/Hurd) +* fixed rub out of file writes to non-existing symlinks +* allow configuring the command line height using h,5EJ +* Added ED flag 2048 to redirect Scintilla messages to the command line view: + enables syntax highlighting on the command line. +* The command line macro is now managed by an ordinary Scintilla view + both for GTK and Curses. +* render tabs as "TAB" (without indention) in the command-line and in + SciTECO macros (SciTECO lexer) +* throw an error immediately after `nEB` if n != 0 +* ./configure --enable-static-executables now automatically pulls in static + libraries of libraries via pkg-config. +* ./configure supports $CURSES_CFLAGS and $CURSES_LIBS now +* ./configure --with-launcher=LAUNCHER can be used to run SciTECO with a launcher + command (e.g. wine or wine64) +* added high-contrast color scheme (contrast.tes) +* mention both mailing list and personal mail in `sciteco --help` +* Work around ncurses mouse handling bugs with GNOME Terminal and Xterm. + This has been fixed in ncurses since version 20250913. +* fixed parallel builds: womanpages were sometimes broken +* Support <:O>: if a label is not found, continue execution after the go-to statement. + Allows to use computed gotos as select-case-like constructs. +* bumped minimum GCC version to v8.1, Gtk to 3.24 +* bumped Scintilla to v5.5.7 +* fully support NetBSD with its native libcurses (netbsd-curses) +* support Groff v1.19.2 as still used by default on NetBSD 10 +* fixed building on openSUSE 15.5 and 15.6 +* fix up hash-bang lines only of the scripts really installed by the current + `make install` invocation +* allow messages to be of arbitrary length: fixes crashes +* added tecat.tes to standard library: can be installed as the Git textconv filter +* some internal refactoring and simplifications +* Fixed serious bug with certain alternative string termination chars in commands with + multiple string arguments. This affected curly braces, ^A and Escape ($). +* (!) Whitespace is now ignored in front of alternative string terminators (as in TECO-64). + E.g. `@I /Hello world/` is valid now. +* implemented the ^W command for refreshing the screen in loops, for sleeping and also the + CTRL+L immediate editing command for forcing a complete screen redraw +* repl.tes: added script that reproduces the classic TECO REPL command-line. + This is not ready, though, as it terminates on the first error. +* `ED&2` can be used to access the program termination flag now. + This can be useful for checking whether a macro has run <EX> or to cancel + the effect of EX. +* Fixed using the command-line replacement register (Escape, $) in batch mode: + was causing assertions when entering interactive mode +* fixed a,b,c^Uq...$: The arguments where written in the wrong (reverse) order +* ncurses: support setting the window/tab title on XTerm-like emulators +* if <EX> fails because of a dirty buffer, the buffer's id is now included + in the error message +* FreeBSD: enable dlmalloc by default (--enable-malloc-replacement) which + gives a 20-25% speedup +* Special Q-registers `$` (working directory) and the clipboard registers now + support the append operation (:Xq, :^Uq...), i.e. you can append (or cut-append) + to the clipboard +* added topics for all colon-modified commands to the online help (`?` command) +* (!) <nEL> (set EOL mode) now sets the buffer's dirty flag, forcing you to + save or discard changes +* fixed minor memory leaks during SciTECO syntax highlighting and in case + of end-of-macro errors +* The primary clipboard (`~` register) is now chosen by the 10th bit in the ED flags. + This allows you to use the "selection" X11 clipboard as the default backend of `~`. +* Implemented <ER> command for reading a file into the current buffer. + This is a Video TECO extension. +* <EW> now accepts a numeric argument to specify the buffer to save +* <EF> supports a numeric buffer id now +* sciteco(7): clarified SciTECO's policy with regards to TECO-11 and Video TECO compatibility +* sciteco(7): minor manpage fixes +* sciteco(7): added a help topic for booleans +* PDCurses/Wincon: disable hardware cursor after window resize +* Improved mouse support in PDCurses v4.5.1. + This is in the official Windows builds. +* GTK: implemented --detach|-d option for detaching from controlling terminal +* GTK: fixed scrolling on systems that only support smooth scrolling +* GTK: monospaced sections in womanpages now respect lexer.font and variable-width + font is configurable via lexer.woman.font (refs #34) +* fixed ^S/^Y for search-replacement commands +* ^S/^Y fixed for <Gq> and <EN> +* ^S/^Y calculates the glyph offsets earlier, so that deletions after an insert or search + no longer affect the results. + This also fixes querying ranges after <FD>. +* added <FN> as a search-and-replace variant of <N> +* Refactored some lexer configurations to make them more pleasurable to look at. + Most text should always be in the default colors. +* SciTECO lexer: style comma, braces and two-character operators as operators +* SciTECO lexer: now tries to avoid unnecessary restylings by styling + from the current line as well +* YAML lexer: default to 2 character soft tabs +* added LaTeX lexer config +* added --quiet, --stdin and --stdout for easier integration into UNIX pipelines +* Improved DEC TECO compatibility - makes SciTECO much more usable as a scripting language. + * (!) <nA> and <nQq> now return -1 in case the index n is out of range + * (!) <EI> has been repurposed and is the macro file inclusion (indirect file) command now. + <EM> is deprecated. + Pre-v2.5.0 <EI> can be replaced with `I^P`. + * (!) <^C> is a plain "return" command now, while <^C^C> exits from the program + * implemented <^B> for returning the current date + * implemented ^E<code> string building constructs for embedding bytes and codepoints + in a strtoul()-like manner + * support <:]q> (pop Q-Register) for getting a success/failure boolean + * support <==> and <===> for printing octal and hexadecimal numbers + * support :=/:==/:=== commands: print number without trailing linefeed + * Implemented the <^A> command for printing arbitrary strings. + You can use :^A to force raw ANSI output. + * implemented <:Gq> for printing the Q-Register string as a message instead of inserting it + * implemented the <T> (typeout) command for printing to the terminal from the current buffer + * implemented ^T command: allows typing by code and getting characters from stdin or the user + * implemented ^H command for returning the current time since midnight. + :^H returns the seconds since the UNIX epoch and ::^H the monotonic time in microseconds + (useful for benchmarking). + * added -v/--version and <EO> command to query the program version + * (!) the computed go-to command (O) is now 0-indexed and all invalid indexes and + empty labels are ignored +* (!) Command-line arguments are no longer passed via the unnamed buffer, + but via special Q-registers ^Ax. + This introduces one point of incompatibility with DEC TECO. +* new string building construct ^P disables all further string building magic +* allow process exit status to be determined by macros +* python lexer: fixed block comment styling +* disallow command-line termination ($$) while editing the command-line replacement + register (after `{`) +* fixed rubbing out stack operations in macro calls (was causing memory violations) +* opener.tes: Fixed +line,column syntax +* fnkeys.tes: support folding via F1 and clicks in the folding margin +* implemented email and "git" lexer folding, as well as folding in womanpages and for + the SciTECO language lexer +* There is an Alpine Linux package in the "community" repository now. + Version 2.4.0 (2025-04-19) ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -16,7 +16,7 @@ SciTECO Build and Runtime Dependencies * Autotools and an UNIX-like environment * GNU Make * A GCC-compatible C11 and C++17 compiler, e.g. GNU C/C++ - (v5.0 or later) or LLVM/gcc or LLVM/Clang. + (v8.1 or later) or LLVM/gcc or LLVM/Clang. SciTECO itself does not require C++, but Scintilla does. * Glib 2 as a cross-platform runtime library (v2.44 or later): https://developer.gnome.org/glib/ @@ -26,7 +26,7 @@ SciTECO Build and Runtime Dependencies I recommend ncurses 6.0 or later. * NetBSD Curses (https://github.com/sabotage-linux/netbsd-curses). This is the default on NetBSD. - * PDCursesMod v4.3.4 or later (https://github.com/Bill-Gray/PDCursesMod.git). + * PDCursesMod v4.5.1 or later (https://github.com/Bill-Gray/PDCursesMod.git). This is the recommended flavor of PDCurses to use. * PDCurses/EMCurses (https://github.com/rhaberkorn/emcurses). * PDCurses/XCurses (http://pdcurses.sourceforge.net/). @@ -34,8 +34,8 @@ SciTECO Build and Runtime Dependencies build from PDCurses Git instead. * other curses implementations might work as well but are untested * When choosing the GTK interface: - * GTK+ v3.12 or later: http://www.gtk.org/ - * GNU roff (groff): https://www.gnu.org/software/groff/ + * GTK+ v3.24 or later: http://www.gtk.org/ + * GNU roff (groff) v1.19.2 or later: https://www.gnu.org/software/groff/ Required at build-time, but it is already shipped on most UNIX-like systems to format man pages. * Doxygen (only when generating developer documentation) @@ -67,9 +67,14 @@ The same on Fedora: And on FreeBSD: - $ sudo pkg install git gmake pkgconfig autoconf automake libtool \ + $ sudo pkg install git gmake pkgconf autoconf automake libtool \ glib gtk3 groff doxygen +On NetBSD: + + $ sudo pkgin install git gmake pkg-config autoconf automake libtool-base \ + glib2 gtk3+ doxygen + Building from Source Tar Ball or Repository =========================================== @@ -91,7 +96,7 @@ the submodules, so that SciTECO can build a verified and prepared version of Scintilla/Scinterm autmatically. Just make sure you have cloned your repository as follows: - $ git clone https://github.com/rhaberkorn/sciteco.git + $ git clone git://git.fmsbw.de/sciteco $ cd sciteco/ $ git submodule update --init @@ -99,6 +104,7 @@ If you already have a Git clone of the SciTECO repository and want to update it, you should issue the following commands: $ git pull + $ git submodule sync $ git submodule update When building from Git, you must first generate the ./configure @@ -134,6 +140,9 @@ To install SciTECO, type something like: $ sudo make install +Overwriting installation directories at install-time (make install bindir=...) +is not supported. + You are recommended to use the included "fallback.teco_ini" as a starting point for your profile. On UNIX, you can copy it to your $HOME directory at "~/.teco_ini" while on Windows, it should be in the same directory diff --git a/Makefile.am b/Makefile.am index 0a9f23b..8284bc1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -22,6 +22,7 @@ EXTRA_DIST += ico/sciteco-256.png ico/sciteco.ico # Distribute entire Scintilla/Scinterm/Lexilla directory and # do some manual cleanup. +love:;@echo 'Abg jne?'|rot13 dist-hook: cp -r $(srcdir)/contrib/scintilla $(distdir)/contrib cp -r $(srcdir)/contrib/scinterm $(distdir)/contrib @@ -2,11 +2,8 @@ News ==== <span class="nf nf-md-new_box"></span> -There is now an [Alpine Linux package](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/sciteco) -thanks to user Celeste. - -<span class="nf nf-md-new_box"></span> -FreeBSD is the first operating system that adds an official [port/package for SciTECO](https://www.freshports.org/editors/sciteco/). - -<span class="nf nf-md-new_box"></span> -Join our [IRC chatroom](https://web.libera.chat/#sciteco): #sciteco at irc.libera.chat +SciTECO [v2.5.0](https://sciteco.fmsbw.de/downloads/v2.5.0/) has been released. +This release brings many new features, but most importantly +makes the language much more usable as a non-interactive scripting language. +Also, the command-line is syntax highlighted now and recovery files are written +to help you recover from crashes and other unexpected terminations. @@ -1,6 +1,3 @@ -[](https://github.com/rhaberkorn/sciteco/actions/workflows/ci.yml) -[](https://github.com/rhaberkorn/sciteco/releases/tag/nightly) - Overview ======== @@ -16,7 +13,7 @@ as far as possible. For instance, moving the cursor to the right can be done imm When you delete a character from the end of the command line macro (called rubout), the side-effects of that character which may be a command or part of a command, are undone. - + SciTECO uses the [Scintilla](https://www.scintilla.org/) editor component and supports GTK+ 3 as well as Curses frontends (using [Scinterm](https://foicica.com/scinterm/)). @@ -29,7 +26,7 @@ The Curses frontend is verified to work with [ncurses](https://www.gnu.org/softw All X/Open-compatible libraries should be supported. SVr4 curses without enhanced definitions is **not** supported. -Linux, FreeBSD, [Mac OS X](https://github.com/rhaberkorn/sciteco/wiki/Mac-OS-Support), +Linux, FreeBSD, NetBSD, [Mac OS X](https://sciteco.fmsbw.de/knowledge/Mac%20OS%20Support), Windows (MinGW 32/64) ~~and [Haiku](https://www.haiku-os.org/) (gcc4)~~ are tested and supported. SciTECO compiles with both GCC and Clang. SciTECO should compile just fine on other UNIX-compatible platforms. @@ -76,6 +73,9 @@ Features This makes it even harder to destroy work by accident than in most other editors. Rubbed out commands can be re-inserted (redo). +* Timing-based recovery mechanism: + Modified buffers are regularily dumped into **#**_files_**#** to protect against + crashes and unexpected restarts etc. * Munging: Macros may be munged, that is executed in batch mode. In other words, SciTECO can be used for scripting. By default, a profile is munged. @@ -100,13 +100,15 @@ Features to be used interactively on system terminals, can be integrated into UNIX pipelines and can be extended with external command-line tools (see `EC` command). + It can easily replace tools like *sed* and *awk*. * Themeability and consistency: Color settings (or schemes) are applied consistenly across all supported platforms. Gtk+ builds allow further customization using CSS. The user interface is kept minimalistic and is consistent in spirit across the different platforms. -* Syntax highlighting, styles, line numbers, etc. thanks to Scintilla, Lexilla and Scintillua. +* Syntax highlighting, styles, line numbers, folding, etc. thanks to Scintilla, Lexilla and Scintillua. Low-level Scintilla commands can also be accessed to extend SciTECO. SciTECO even syntax highlights code, written in the SciTECO language itself. +* Configurable command line with syntax highlighting. * A growing standard library of macros with frameworks for color schemes, syntax highlighting and buffer sessions. Optimized for hack-ability rather than completeness. @@ -116,59 +118,79 @@ Download There are prebuilt binary packages and source bundles for your convenience: -* [Github Releases](https://github.com/rhaberkorn/sciteco/releases) +* [Main download archive](https://sciteco.fmsbw.de/downloads) * [Download Archive at Sourceforge](https://sourceforge.net/projects/sciteco/files/) * [FreeBSD port](https://www.freshports.org/editors/sciteco/) [](https://repology.org/project/sciteco-curses/versions) -* [Ubuntu PPA](https://launchpad.net/~robin-haberkorn/+archive/sciteco) +* OBS repositories and binary downloads for RPM-based (Fedora, openSUSE, etc.) and + Debian-based (Debian, Raspbian, Ubuntu) distributions: + [](https://build.opensuse.org/package/show/home:rhaberkorn:sciteco:STABLE/sciteco) + * [Common packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:STABLE&package=sciteco-common) + * [Gtk packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:STABLE&package=sciteco-gtk) + * [Curses packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:STABLE&package=sciteco-curses) * [Arch User Repository](https://aur.archlinux.org/packages/sciteco-git) [](https://repology.org/project/sciteco/versions) -* [Alpine Linux package](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/sciteco) +* [Alpine Linux package](https://pkgs.alpinelinux.org/package/edge/community/x86_64/sciteco) [](https://repology.org/project/sciteco/versions) * [Chocolatey package](https://community.chocolatey.org/packages/SciTECO) for Windows users [](https://repology.org/project/sciteco/versions) * Yocto/OpenEmbedded users should try the - [`sciteco` package from this layer](https://github.com/rhaberkorn/meta-rhaberkorn). + [`sciteco` package from this layer](https://git.fmsbw.de/meta-rhaberkorn/). * Users of OpenWrt may try to install the - [`sciteco` package of this feed](https://github.com/rhaberkorn/nanonote-ports). + [`sciteco` package of this feed](https://git.fmsbw.de/nanonote-ports/). +* ~~[Ubuntu PPA repository](https://launchpad.net/~robin-haberkorn/+archive/sciteco)~~ (deprecated) These releases may be quite outdated and not all of them are provided or tested by the author. -So you may also try out the [nightly builds](https://github.com/rhaberkorn/sciteco/releases/tag/nightly) - -they represent the repository's HEAD commit but may well be unstable. -Both ncurses and Gtk+ packages are provided for Ubuntu, generic Linux -(in the form of [AppImages](https://appimage.org/)) and Windows. -For [Mac OS X](https://github.com/rhaberkorn/sciteco/wiki/Mac-OS-Support), +So you may also try out the following nightly builds: + +* OBS repositories and binary downloads for RPM-based (Fedora, openSUSE, etc.) and + Debian-based (Debian, Raspbian, Ubuntu) distributions: + [](https://build.opensuse.org/package/show/home:rhaberkorn:sciteco:UNSTABLE/sciteco) + * [Common packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:UNSTABLE&package=sciteco-common) + * [Gtk packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:UNSTABLE&package=sciteco-gtk) + * [Curses packages](https://software.opensuse.org/download.html?project=home:rhaberkorn:sciteco:UNSTABLE&package=sciteco-curses) +* [Nightly builds](https://sciteco.fmsbw.de/downloads/nightly) + for Mac OS, Windows and Linux (AppImages). + +Nightly builds represent the repository's HEAD commit but may well be unstable. +For [Mac OS X](https://sciteco.fmsbw.de/knowledge/Mac%20OS%20Support/), we currently only provide *experimental* ncurses builds. If everything fails, you can try building from source. -See [`INSTALL`](https://github.com/rhaberkorn/sciteco/blob/master/INSTALL) for more details. +See [`INSTALL`](https://git.fmsbw.de/sciteco/tree/INSTALL) for more details. Community ========= -* Join our new [IRC chatroom](https://web.libera.chat/#sciteco): #sciteco at irc.libera.chat -* Report [bugs via Github](https://github.com/rhaberkorn/sciteco/issues) - if you can (or write an E-Mail to the author). -* You can also use [Github Discussions](https://github.com/rhaberkorn/sciteco/discussions) - for asking questions. -* We are also present in the [alt.lang.teco](https://newsgrouper.org.uk/alt.lang.teco) Usenet group, +* Report bugs or submit patches via the [hackers@fmsbw.de mailing list](https://git.fmsbw.de/?p=about). + You can also send an E-Mail to the author. + Bugs and planned features are managed in the [TODO](https://git.fmsbw.de/sciteco/tree/TODO) file, + so you might want to check it before sending your email. + Make sure to include `[sciteco]` in the mail subject. +* Use the [dings@fmsbw.de mailing list](https://git.fmsbw.de/?p=about) + for discussions and asking questions. + Make sure to include `[sciteco]` in the mail subject. +* Join our [IRC chatroom](https://web.libera.chat/#sciteco): #sciteco at irc.libera.chat +* We are also present in the [alt.lang.teco](https://newsgrouper.org/alt.lang.teco) Usenet group, but it is not restricted to SciTECO. Additional Documentation ======================== * Online manpages: - [__sciteco__(1)](https://rhaberkorn.github.io/sciteco/sciteco.1.html), - [__sciteco__(7)](https://rhaberkorn.github.io/sciteco/sciteco.7.html), - [__grosciteco.tes__(1)](https://rhaberkorn.github.io/sciteco/grosciteco.tes.1.html), - [__tedoc.tes__(1)](https://rhaberkorn.github.io/sciteco/tedoc.tes.1.html) -* [Tutorial](https://rhaberkorn.github.io/sciteco/tutorial.html): + [__sciteco__(1)](https://sciteco.fmsbw.de/sciteco.1.html), + [__sciteco__(7)](https://sciteco.fmsbw.de/sciteco.7.html), + [__grosciteco.tes__(1)](https://sciteco.fmsbw.de/grosciteco.tes.1.html), + [__tedoc.tes__(1)](https://sciteco.fmsbw.de/tedoc.tes.1.html) +* [Tutorial](https://sciteco.fmsbw.de/tutorial.html): This is what you see when you launch SciTECO for the first time. -* [Cheat Sheet and Language Overview](https://sciteco.sf.net/manuals/cheat-sheet.pdf). +* [Cheat Sheet and Language Overview](https://sciteco.fmsbw.de/manuals/cheat-sheet.pdf). This can be printed on an A4 sheet of paper. -* [Wiki at Github](https://github.com/rhaberkorn/sciteco/wiki) -* A [short presentation](https://sciteco.sf.net/manuals/presentation.pdf) +* [Knowledge Base](https://sciteco.fmsbw.de/knowledge/): + Contains [useful macros](https://sciteco.fmsbw.de/knowledge/Useful%20Macros), + the [FAQ](https://sciteco.fmsbw.de/knowledge/Frequently%20Asked%20Questions%20(FAQ)) etc. +* A [short presentation](https://sciteco.fmsbw.de/manuals/presentation.pdf) (in German!) hold at [Netz39](https://www.netz39.de/). -<p align="center"><img alt="SciTECO icon" src="https://github.com/rhaberkorn/sciteco/raw/master/ico/sciteco-48.png"/></p> +<p align="center"><img alt="SciTECO icon" src="https://sciteco.fmsbw.de/graphics/sciteco-48.png"/></p> @@ -1,7 +1,37 @@ Tasks: * Have a look at TECO-86. + * VEDIT and PMATE for MS-DOS + * Scintilla: upstream 2 patches Known Bugs: + * OBS GTK builds sometimes fail: "cannot open display" + Obviously, xvfb-run isn't reliable or has race conditions. + * In multiline command lines, the asterisk can scroll out of + view. Perhaps it has to be drawn independently of Scinterm. + * FreeBSD's `crontab -e` is not compatible with SciTECO's savepoint mechanism. + * ncurses: GNOME Terminal and Xterm produces BUTTON3_PRESSED (without BUTTON3_RELEASED) + events when scrolling horizontally. + This is fixed upstream in ncurses and there is a workaround for + older ncurses versions which limits the effects of this bug. + * ncurses: st and Xterm produce BUTTON2_RELEASED, followed by BUTTON2_PRESSED + when clicking the middle button. + We also *sometimes* get it in the correct order. + This bug has been fixed upstream and there is a workaround for + older ncurses versions. You may loose the distinction between + pressed and release events, though. + * Upgrade to Scintilla 5.5.7 requires charconv header which bumps + the minimum GCC version to 8.1 (officially 9). + This breaks OpenSUSE 15.5-15.6 builds. + * Build problems on Fedora 41: See mail from Blake McBride. + * @^Um{-$$} Mm= should probably return -1. + * {@I/$$23=/} doesn't insert anything after $$. + This would be necessary for an interactive screen editing script, + that leaves you in <I> always. + This would require some refactoring. + * Gtk: The control characters in tutorial.woman are still styled with + the variable-width font since its rendered in STYLE_CONTROLCHAR (36), + which is reset in woman.tes. + Perhaps it should always be in lexer.font. * The current horizontal position (set by 4EJ via SCI_GETCOLUMN) is often wrong, i.e. pressing the up-cursor key can get you into the wrong column. @@ -16,6 +46,7 @@ Known Bugs: We try to work around this with click detection, but it still behaves a bit oddly. See https://github.com/Bill-Gray/PDCursesMod/issues/330 + Waiting for PDCurses in MSYS to be updated. * PDCurses/WinGUI: There is still some flickering, but it got better since key macros update the command line only once. Could already be fixed upstream, see: @@ -24,7 +55,25 @@ Known Bugs: Affects both PDCurses/WinGUI and Gtk. This no longer happens with ECbash -c 'while true; do true; done'$. However ECping -t 8.8.8.8$ still cannot be interrupted. + * Win32 builds cannot work with the /mingw64 path as used in MSYS/MinGW. + However the UNIX path translation appears to be a Cygwin feature. + If SciTECO would do that, it might break other things (e.g. you might + want to refer directory C:\mingw64 instead). + * Win32/Wincon: cat ... | sciteco -i + "Redirection is not supported." + PR: https://github.com/Bill-Gray/PDCursesMod/pull/344 + Waiting for an MSYS package releae. + * PDCurses/Win32: Both Wincon and WinGUI crash when you press *any* + non-ANSI character. This is fixed since PDCurses v4.5.2: + https://github.com/Bill-Gray/PDCursesMod/issues/335 + We're waiting for an MSYS package upgrade. + It could also be worked around by using wget_wch() instead of wgetch(). * PDCurses/Win32: Crashes sometimes without any error message. + * NetBSD Curses: scrolling apparently uses hardware idl capabilities + resulting in graphical glitches on slow terminals. + idlok(FALSE) is apparently ignored. + * NetBSD: Very slow, even the redrawing. + This does not happen with ncurses on NetBSD. * dlmalloc's malloc_trim() does not seem to free any resident memory after hitting the OOM limit, eg. after <%a>. Apparently an effect of HAVE_MORECORE (sbrk()) - some allocation is @@ -36,7 +85,7 @@ Known Bugs: when using Solarized. Affects e.g. the message line which uses the reverse of STYLE_DEFAULT. Perhaps we must call init_color() before initializing color pairs - (currently done by Scinterm). + (currently done first by Scinterm). * Saving another user's file will only preserve the user when run as root. Generally, it is hard to ensure that a) save point files can be created and b) the file mode and ownership of re-created files can be preserved. @@ -83,10 +132,6 @@ Known Bugs: There is also MoveFileEx(file, NULL, MOVEFILE_DELAY_UNTIL_REBOOT). * Windows has file system forks, but they can be orphaned just like ordinary files but are harder to locate and clean up manually. - * Setting window title is broken on ncurses/XTerm. - The necessary capabilities are usually not in the Terminfo database. - Perhaps do some XTerm magic here. We can also restore - window titles on exit using XTerm. * The XTerm OSC-52 clipboard feature appears to garble Unicode characters. This is apparently an XTerm bug, probably due to 8-bit-uncleanliness. It was verified by `printf "\e]52;c;?\a"` on the command line. @@ -121,10 +166,6 @@ Known Bugs: be set/disabled. This doesn't even work with SCI_SETPROPERTY, probably since we do lexing "in the container". - * Mac OS: The colors are screwed up with the terminal.tes color scheme - (and with --no-profile) under Mac OS terminal emulators. - This does not happen under Linux with Darling. - See https://github.com/rhaberkorn/sciteco/issues/12 * File name autocompletion should take glob patterns into account. The simple reason is that if a filename really contains glob characters and you are trying to open it with EB, you might end up not being @@ -132,9 +173,9 @@ Known Bugs: escaped glob patterns. Unfortunately, this would be very tricky to do right. * The git.blame macro is broken, at least on Git v2.45.2 and v2.25.1. Compare - cat sample.teco_ini | git blame --incremental --contents - -- sample.teco_ini | grep -E '^[a-f0-9]{40}' + cat fallback.teco_ini | git blame --incremental --contents - -- fallback.teco_ini | grep -E '^[a-f0-9]{40}' (which is wrong and does not even contain all commits) with - git blame --incremental --contents sample.teco_ini -- sample.teco_ini | grep -E '^[a-f0-9]{40}' + git blame --incremental --contents fallback.teco_ini -- fallback.teco_ini | grep -E '^[a-f0-9]{40}' which is correct. Without --incremental even the formatting is broken. This could well be a Git bug. * Margins, identions and the like are not configured on the unnamed @@ -155,16 +196,43 @@ Known Bugs: also have unwanted side effects. * Solaris/OmniOS: There are groff build errors. * session.vcs does not properly work in MSYS2 environments. - * The Windows GTK version no longer works under Wine: + * Wine: The Windows GTK version no longer works: "Failed to translate keypress (keycode: 88) for group 0 (00000409) because we could not load the layout." Also, all Windows builds have problems executing ECdir$ (under Wine!). See also https://github.com/fontforge/fontforge/issues/5031#issuecomment-1143098230 + * Wine: --stdin is broken: + "g_io_channel_read_chars: assertion 'channel->is_readable' failed" + This among other things prevents bootstrapping and test suite runs under Wine when cross-compiling + from Linux or FreeBSD. + Therefore Windows cross compilations are currently included in .fmsbw/10-freebsd14-msys-sciteco. + https://bugs.winehq.org/show_bug.cgi?id=58745 + https://gitlab.gnome.org/GNOME/glib/-/issues/3793 * At least the GTK version with --xembed is prone to unexpected crashes. Interestingly, while this does leave orphaned savepoint files around, it does not produce a core dump. + * repl.tes terminates when encountering the first error. + This requires error catching to be fixed. + Also, the command-line redrawing is still broken in GNOME Terminal. + * When config.status is run as a consequence of touching configure.ac, + config.h will be wrong, forcing us to rerun ./configure. Features: + * Should we support *.sgml files with the HTML lexer? + Old Docbook documents are sometimes SGML based. + * Folding support: Perhaps there should also be builtin commands + [:]F+ and [:]F- + The mouse events in fnkeys.tes would have to edit the + commandline, though. * Gtk: special key macros for drag-and-drop interactions? + This could be used to send the tabs of one SciTECO process into another + instance and close the sender instance. + * The opposite is also useful and could be done now already: + A macro that takes the current buffer (or a buffer range?) + and opens a new SciTECO instance with it. If successful, the + given buffer is removed from the ring. + This would work with the Curses version as well. + Could we also send tabs into an existing SciTECO instance? + Would probably require some kind of server... * opener.tes should try to center the opened line (SCI_SETFIRSTVISIBLELINE). * Rubout of SCI_GOTOPOS could also restore the vertical @@ -180,8 +248,6 @@ Features: insert commands with default escape chars. Then this could be part of fnkeys.tes. * :$ and :$$ to pop/return only single values - * allow top-level macros to influence the proces return code. - This can be used in macros to call $$ or ^C akin to exit(1). * Special macro assignment command. It could use the SciTECO parser for finding the end of the macro definition which is more reliable than @^Uq{}. @@ -203,9 +269,6 @@ Features: be no more real need for command variants with disabled string building (as string building will naturally always be disabled in parser-terminator-mode). - Instead, a special string building character for disabling - string building character processing can be introduced, - and all the command variants like EI and EU can be repurposed. Q-Reg specs should support alternative balanced escapes as well for symmetry. * Numbers could be separate states instead of stack operating @@ -242,6 +305,7 @@ Features: and it's good to keep true integers around. * Having a separate number parser state will slightly simplify number syntax highlighting (see teco_lexer_getstyle()). + * The first application of floats could be for U[lexer.font]. * ^H as shortcut for 16^R. ^H is backspace, but it won't be necessary to type very often. * Key macro masking flag for the beginning of the command @@ -259,6 +323,10 @@ Features: insertion commands by automatically terminating the command. Even more simple, the function key flag could be effective only when the termination character is $. + Instead of reserving a character, we could also reserverve + a bit in the function key maks. + It's however impossible to reliably return to the start state + from arbitrary parser states. * Support more function keys. We can define more function keys via define_key(3NCURSES). Unfortunately they are not really standardized - st and urxvt for instance @@ -271,24 +339,6 @@ Features: keys and Alt and Ctrl modifiers. See also https://stackoverflow.com/questions/31379824/how-to-get-control-characters-for-ctrlleft-from-terminfo-in-zsh https://gist.github.com/rkumar/1237091 - * Support loading from stdin (--stdin) and writing to - the current buffer to stdout on exit (--stdout). - This will make it easy to write command line filters, - We will need flags like and --quiet with - single-letter forms to make it possible to write hash-bang - lines like #!...sciteco -q8iom - Command line arguments should then also be handled - differently, passing them in an array or single string - register, so they no longer affect the unnamed buffer. - * Once we've got --stdout, it makes sense to ship a version of - tecat written in SciTECO. - This is useful as a git diff textconv filter. - See https://gist.github.com/rhaberkorn/6534ecf1b05de6216d0a9c33f31ab5f8 - * For third-party macro authors, it is useful to know - the standard library path (e.g. to install new lexers). - There could be a --print-path option, or with the --quiet - and --stdout options, one could write: - sciteco -qoe 'G[$SCITECOPATH]' * Now that we have redo/reinsertion: When ^G modifier is active, normal inserts could insert between effective and rubbed out command line - without @@ -302,25 +352,24 @@ Features: rubbed out command line). * some missing useful VideoTECO/TECO-11 commands and unnecessary incompatibilities: - * EF with buffer id - * ER command: read file into current buffer at dot - * nEW to save a buffer by id - * EI is used to open an indirect macro file in classic TECO. - EM should thus be renamed EI. - EM (position magtape in classic TECO) would be free again, - e.g. for execute macro with string argument or as a special version - of EI that considers $SCITECOPATH. - Current use of EI (insert without string building) will have - to move, but it might vanish anyway once we can disable string building - with a special character, eg. you could write I^C instead. + * EI$ would "resume" command input from the terminal in TECO-11. + But how is that different from ^C/$$ in SciTECO? + * In TECOC, <EI> searches the TEC_LIBRARY and ignores the file extension. + This is generally very useful to have, how to force a file name verbatim? + You can always add a leading `./` of course. + * <^C> actually returns all the way back to the command-line in + TECO-11, ie. it aborts the current command string. + This cannot be fully reproduced in SciTECO, but we could + return from all the stack frames up to the toplevel macro. * <I> doesn't have string building enabled in classic TECO. Changing this would perhaps be a change too radical. Also, we would then need a string-building variant like <:I>. - * <FN> as a search-replace variant of <N>. * FB for bounded search and FC for bounded search-replace. One advantage in comparison to ::S (which also supports arguments in SciTECO), would be the ability to bound comparisons by line with n:FB. + * FRtext$ is equivalent to ^SDItext$ in DEC TECO. + If we adopt these semantics we should of course prefer ^YDItext$. * Searches can extend beyond the given bounds in DEC TECO as long as they start within the range. That's why ::S is equivalent to .,.:FB in DEC TECO. @@ -328,29 +377,38 @@ Features: just like in Video TECO. The DEC behavior could be achieved by always searching till the end of the buffer, but excluding all matches beyond the target range. - * ^A, :Gq, T and stdio in general - * nA returned -1 in case of invalid positions (similar to SciTECO's ^E) - instead of failing in DEC TECO. - The failing <A> command is inherited from Video TECO. - * ^W was an immediate action command to repaint the screen. - This could be a regular command to allow refreshing in long loops. - Video TECO had ET for the same purpose. - TECO 10 had a ^W regular command for case folding all strings, - but I don't think it's worth supporting. * MSUTECO for MS-DOS had ^P for giving the number of characters until the next matching ( ) { } [ ] < > " '. However there is no single way to get brace skipping right, so this might be prime candidate for a macro. - * There should be a string building character for including - a character by code. Currently, there is only ^EUq where - q must be set earlier. - This would be useful when searching in binary files or - to include Unicode characters by code point. - Unfortunately its syntax cannot depend on the string argument's - encoding, as that could confuse parse-only mode. - TECO-11 has ^E<nnn> for octal-only, which is extended by Video TECO - to support the typical strtoul() semantics. - This is probably what we want as well. + * Video TECO's <EC> updates the display when reading from + long running processes. This might be useful as well, at least + when detecting interactive invocation. + But perhaps there should rather be a global configurable delay/refresh + that could be considered by all loops etc. + * Video TECO: EQq...$ and ER...$ accepts glob patterns and reads + all files into the register. But we probably won't want to have that. + * :EB -> Bool on Video TECO. + * Video TECO: EV as an alias for EB, but create read-only buffer. + * EN behaves differently on TECO-11. + We could only emulate it by immediately traversing the entire + directory. EN however is also heavily overloaded on SciTECO + and we probably don't want to loose these features. + * ET for controlling stdio. Currently you must execute stty. + This would require binding the UNIX-specific tcsetattr(), + as Curses APIs won't be available in Gtk. + Does it also make sense to perform echoing of control codes + in ^A as TECO-11 does by default? + * Add flag to allow redirecting output to stderr. + * ^A currently doesn't flush the output. + Perhaps flushing should be controlled by the language. + Perhaps via the ET flags. + * Mini game where you can drive a tank around your source code. + * `@]q` to pop keeping the numeric part of q intact. + Perhaps `@@]q` to overwrite __only__ the numeric part, + but keeping the string intact. + * <F?> Return a random value between 1 and n + Could automatically work with floats. * n:"x to leave <n> on the stack (i.e. only peek). This simplifies expressions like Qa"N Qa ... * Perhaps there should be a pattern match construct for word characters @@ -394,24 +452,16 @@ Features: * Command to free Q-Register (remove from table). e.g. FQ (free Q). :FQ could free by QRegister prefix name for the common use case of Q-Register subtables and lists. - * multiline commandline - * Perhaps use Scintilla view as mini buffer. - This means patching Scintilla, so it does not break lines - on new line characters and we can use character representations - (extend SCI_SETLINEENDTYPESALLOWED?). - Also, we cannot currently force ^I to be rendered with representations. - cmdline.c can then directly operate on the Scintilla document. - * A Scintilla view will allow syntax highlighting - * These Scintilla enhancements will also improve hex mode (M#hx). + * EF is currently disallowed when editing a Q-Reg unless a numeric + argument is specified. + On Video TECO it appears to free the current Q-Reg, which probably + makes more sense than the current semantics. + Should be changed along with implementing FQq. * command line could highlight dead branches (e.g. gray them out) * Perhaps add a ^E register analogous to ":", but working with byte offsets. This would mainly be useful in ^E\^E. * EL command could also be used to convert all EOLs in the current buffer. - * nEL should perhaps dirtify the buffer, at least when automatic - EOL translation is active, or always (see above). - The dirty flag exists to remind users to save their buffers and - nEL changes the result of a Save. * exclusive access to all opened files/buffers (locking): SciTECO will never be able to notice when a file has been changed externally. Also reversing a file write will overwrite @@ -439,13 +489,14 @@ Features: * Touch restored save point files - should perhaps be configurable. This is important when working with Makefiles, as make looks at the modification times of files. - * There should really be a backup mechanism. It would be relatively - easy to implement portably, by using timeout() on Curses. - The Gtk version can simply use a glib timer. - Backup files should NOT be hidden and the timeout should be - configurable (EJ?). + * Could we somehow offer to open #recovery# files? + This would require a hook on interactive startup and/or after + command-line termination. + Also, we'd need a command to fetch the modification timestamp + as well. + * Recovery files should probably be hidden during auto completions. * Error handling in SciTECO macros: Allow throwing errors with - e.g. [n]ET<description>$ where n is an error code, defaulting + e.g. [n]^F<description>^F where n is an error code, defaulting to 0 and description is the error string - there could be code-specific defaults. All internal error classes use defined codes. Errors could be catched using a structured try-catch-like construct @@ -454,11 +505,12 @@ Features: We will have to go through all the command implementations as well since must no longer rely on undo token execution after throwing errors. + * Error codes should probably be logged to the console + as well. Perhaps adopt a style similar to DEC TECO: Exxxx * Backtracking execution semantics, bringing the power of SNOBOL (and more!). This can be a variant of a structured error handling construct. This will also require managing our own function call stack. - See https://github.com/rhaberkorn/sciteco/issues/26#issuecomment-2449983076 * Once we have our own function call stack, it will be possible, although not trivial, to add support for user-definable macros that accept string arguments, eg. @@ -514,11 +566,6 @@ Features: special. Even if we always increased the nest_level, that variable does not discern Ifs and Whiles. * Possible Nightly Build improvements (and therefore releases): - * Push nightly builds into the Ubuntu PPA. - We should probably create a new PPA sciteco-nightly. - A new private key has already been registered on Launchpad and - Github. We just need to integrate with CI. - See also https://github.com/marketplace/actions/import-gpg * Mac OS Arm64 builds either separately or via universal binary. See https://codetinkering.com/switch-homebrew-arm-x86/ Target flag: `-target arm64-apple-macos11` @@ -528,6 +575,12 @@ Features: * Get into mentors.debian.net. First step to being adopted into the Debian repositories. * Get meta-rhaberkorn into https://layers.openembedded.org + * AppImage via OBS. + Apparently the AppImage repo is based on openSUSE 15.6. + I have to fix openSUSE 15.6 builds. + * CircleCI could be used for Windows and Mac OS builds. + Supposedly it works from arbitrary (self-hosted) Git repositories. + https://circleci.com/docs/guides/plans-pricing/plan-free/ * Bash completions. * FreeBSD: rctl(8) theoretically allows setting up per-process actions when exceeding the memory limit. @@ -550,10 +603,7 @@ Features: to XOR, allowing for the idiom `Qa~Qb"=` when checking for equality that is faster than using `-` and works for floats as well. * ^& could be a NAND operator. - * Perhaps ignore whitespace after @ as does TECO-64. - There is little benefit in using spaces or tabs as string delimiters, - but ignoring whitespace may increase readability. Eg. - @^Ua {...} + * ^< left shift, ^> right shift * It should be possible to disable auto-completions of one-character register names, so that we can map the idention macro to M<TAB>. * Add a configure-switch for LTO (--enable-lto). @@ -676,11 +726,81 @@ Features: i.e. typically shell-like languages with "$var" constructs. * Install autocompletion scripts. They will have to be preprocessed, though. See also - https://github.com/rhaberkorn/sciteco/wiki/Shell-auto-completions + https://sciteco.fmsbw.de/knowledge/Shell%20auto%20completions/ * There should perhaps we a --revision siteconfig option, to pass in a Git revision, that's printed by `sciteco --help`. But it would have to work with tarballs as well, so it has to be written into a separate header, that can be distributed. + * Perhaps undefined operations like -(X) should check X for MININT. + * m,nQq to get a list of codepoints from char m to n. + This would ease subscripting, e.g. to remove the last character: + 0,:Qq-1Qq^Uq$ + There should also be m,nA for consistency. + * Sort order in Q-register autocompletions should be numeric, + so ^A11 comes after ^A1. + * FFI interface: There could be a command nFXlib$func$str$ + to call C function func() from lib with a string argument and numeric + arguments. Would allow extending SciTECO with external libraries + as well. + * Solaris libxcurses (/usr/include/xpg4/curses.h) + is X/Open compatible and could be supported. + * Go-to labels cannot practically contain commas since when used with the <O> + command it would be interpreted as a label list. + This could be worked around by expecting label lists only if an integer + is given (ie. no longer imply 0). + However, it would be nice if we could include commas in labels of + the <nO> variant as well. + Also, labels cannot contain `!`. + The "," and "!" restrictions are mainly relevant when using computed + gotos in O^EQ[label]$ as you cannot match arbitrary strings. + * pad,n\ to format number with padding. + Allowing `\` to format multiple numbers when called with a list + of numbers makes little sense. + I find myself wanting to pad numbers, especially hexadecimal numbers, + often. If <pad> is negative it might perform a right padding with + spaces. + To format a hex byte, you would write 16^R 2,Qa\ ^D. + The same extension might not be desirable for =/==/===, unless + they are colon-modified?. + * Curses: allow freely using RGB colors instead of only the constants + corresponding with the 16 default curses colors. + Scinterm v5.5 supports that. This means we also need a hash map for + mapping RGB color values to curses color numbers, analoguous to what + Scinterm does. + The problem of Scinterm reusing the same color namespace both for + arbitrary RGB values and for predefined special constants however + hasn't been solved yet. + * Scinterm: INDIC_PLAIN and INDIC_STRIKE could theoretically be supported. + PDCurses has A_STRIKETHROUGH. This would require Scintilla API + changes, though, as it currently calls SurfaceImpl::FillRectangle(), + which cannot be reliably identified as small horizontal lines. + We'd have to patch Scintilla APIs as well. + * Auto-completion popup should perhaps be above the line with the cursor + in multi-line command lines. + This would be tricky to do in Gtk, though since we're currently using + an overlay. + * <EB> could create directories on demand and clean them up on rubout. + Unless additional files appeared in the meantime, in which case we should + output a warning. + * Savepoint and recovery files could fall back to ~/ if writing in the same + directory is not possible. + Or savepoint files should perhaps fall back to /tmp. + * ^E@q could be a generic escape mechanism instead of having separate + constructs like ^E@q and ^ENq (for globbing). + You rarely need both in the same command and we would need potentially + many more variants, especially for escaping search patterns. + So ^E@q could just do the right thing in the right context via a + teco_state_expectstring_t callback (defaulting to ^EQq). + On Windows without POSIX-Shell emulation, it could also properly + escape for cmd.exe. + On the downside, ^E@q could no longer be used to construct shell + scripts in the buffer or Q-registers. + * session.tes: Allow sessions to be opened after startup and + navigating into the correct directory with FG. + This would help with non-terminal-based workflows. + * There could be a key macro script for my Russian-phonetic + keyboard layout. This would probably require some new + key macro states as well. Optimizations: * Use SC_DOCUMENTOPTION_STYLES_NONE in batch mode. @@ -721,6 +841,9 @@ Optimizations: using __VA_OPT__(). * A few macros like TECO_CTL_KEY() could be turned into constexpr functions. + * teco_state_t should be a constexpr, so that static + assertions are guaranteed to work on them. + This means you can get rid of the TECO_ASSERT_SAFE() hack. * Compound literals could be abused for default values in the Scintilla SSM functions. All the wrapper functions would have to be turned into macros, though. @@ -761,6 +884,7 @@ Optimizations: are defined. But how to even test for glibc's ptmalloc? Linux could use musl as well for instance. + * Evaluate TLSF allocator. * Resolve Coverity Scan issues. If this turns out to be useful, perhaps we can automatically upload builds via CI? @@ -781,6 +905,25 @@ Optimizations: priorities, so we can call the interface cleanup last. Unfortunately, destructor-priorities are not supported on all targets. atexit() callbacks are apparently called before the destructors. + * Are there any numerical warnings we could activate? + There have been quite some bugs due to numeric issues. + -Wsign-compare -Wconversion + Even -Werror? + * Add build system support for some static code analyzer. + scan-build, clang-tidy or CodeChecker? + Or run scan-build in CI with a selection of fatal warnings. + * The unix.Malloc check unfortunately produces countless bogus warnings + due to g_auto. + * Refactor control flow commands into flow-commands.c. + * fnkeys.tes: Don't insert (0C) or (0R). + * tecat.tes is too slow even though it doesn't even use Q-reg strings. + An ideal test case to study. + * the process_edit_cmd() callbacks from cmdline.c should probably + be in a separate compilation unit. + * Perhaps EOL normalization can be avoided by letting teco_view_glyphs2bytes() & + co take care of it. + However insertion commands would also have to take care of expanding + LF to the buffers EOL sequence. Documentation: * Doxygen docs could be deployed on Github pages @@ -816,3 +959,16 @@ Documentation: installed along with other documents. Then however we'd have to tweak it, so the CI-generated file always looks good. + * We probably do not declare FOSS licenses properly. + Except for the Debian package. + Should all the licenses be mentioned in COPYING as well? + We definitely have to ship licensing information in the binary + distributions, so adding everything to COPYING would be easiest. + On the other side, in binary distros we have to include licenses + for all the shipped DLLs as well. + * sciteco(7) should document the difference between state + controlled by the SciTECO language and other state (scroll + position, window size, folding) and what that all means + for writing robust macros. + * Knowledge Base: Document how to open SciTECO from file managers, + so that sessions are initialized correctly. diff --git a/bootstrap.am b/bootstrap.am index 8bce720..872ed70 100644 --- a/bootstrap.am +++ b/bootstrap.am @@ -11,11 +11,11 @@ export SCITECOPATH=@top_srcdir@/lib # available after the binary has been built in src/ # (ie. in SUBDIRS after src/). if BOOTSTRAP -SCITECO_MINIMAL = @top_builddir@/src/sciteco-minimal$(EXEEXT) -SCITECO_FULL = @top_builddir@/src/sciteco$(EXEEXT) +SCITECO_MINIMAL = @LAUNCHER@ @top_builddir@/src/sciteco-minimal$(EXEEXT) +SCITECO_FULL = @LAUNCHER@ @top_builddir@/src/sciteco$(EXEEXT) else -SCITECO_MINIMAL = @SCITECO@ -SCITECO_FULL = @SCITECO@ +SCITECO_MINIMAL = @LAUNCHER@ @SCITECO@ +SCITECO_FULL = @LAUNCHER@ @SCITECO@ endif # Path of installed `sciteco` binary, @@ -24,8 +24,7 @@ endif SCITECO_INSTALLED = \ $(bindir)/`echo sciteco | @SED@ '$(transform)'`$(EXEEXT) -SUBST_MACRO = EB$<\e \ - <FS@PACKAGE^Q@\e@PACKAGE@\e;>J \ +SUBST_MACRO = <FS@PACKAGE^Q@\e@PACKAGE@\e;>J \ <FS@PACKAGE_NAME^Q@\e@PACKAGE_NAME@\e;>J \ <FS@PACKAGE_VERSION^Q@\e@PACKAGE_VERSION@\e;>J \ <FS@PACKAGE_URL^Q@\e@PACKAGE_URL@\e;>J \ @@ -34,12 +33,11 @@ SUBST_MACRO = EB$<\e \ <FS@scitecodatadir^Q@\e$(scitecodatadir)\e;>J \ <FS@scitecolibdir^Q@\e$(scitecolibdir)\e;>J \ <FS@TECO_INTEGER^Q@\e@TECO_INTEGER@\e;>J \ - <FS@DATE^Q@\e$(shell LC_ALL=C @DATE@ "+%d %b %Y")\e;>J \ - EW$@\e + <FS@DATE^Q@\e$(shell LC_ALL=C @DATE@ "+%d %b %Y")\e;>J # The SciTECO-based substitutor must not process config.h.in. @top_srcdir@/config.h: ; SUFFIXES = .in .in: - $(SCITECO_MINIMAL) -e $$'$(SUBST_MACRO)' + $(SCITECO_MINIMAL) -qioe $$'$(SUBST_MACRO)' <$< >$@ diff --git a/configure.ac b/configure.ac index 8620050..5ac7aad 100644 --- a/configure.ac +++ b/configure.ac @@ -2,10 +2,10 @@ # Process this file with autoconf to produce a configure script. AC_PREREQ([2.65]) -AC_INIT([SciTECO], [2.4.0], - [robin.haberkorn@googlemail.com], +AC_INIT([SciTECO], [2.5.0], + [hackers@fmsbw.de], [sciteco], - [https://github.com/rhaberkorn/sciteco]) + [https://sciteco.fmsbw.de/]) AC_CONFIG_MACRO_DIR(m4) AC_CONFIG_AUX_DIR(config) AM_INIT_AUTOMAKE @@ -38,6 +38,30 @@ AC_SUBST(SCINTILLA_CXXFLAGS) # Necessary so we can change their default values here AC_SUBST(AM_CPPFLAGS) +# This cannot be done with --enable-static as it only controls +# which kind of libraries libtool builds. +# Also, it cannot be controlled reliably by setting LDFLAGS for +# ./configure, as this would be used for linking the test cases +# without libtool and libtool would ignore it. +# It is only possible to call `make LDFLAGS="-all-static"` but +# this is inconvenient... +AC_ARG_ENABLE(static-executables, + AS_HELP_STRING([--enable-static-executables], + [Link in as many runtime dependencies as possible + statically [default=no]]), + [static_executables=$enableval], [static_executables=no]) +AM_CONDITIONAL(STATIC_EXECUTABLES, [test x$static_executables = xyes]) + +# Wrapper around PKG_CHECK_MODULES(), but takes --enable-static-executables +# into account. +AC_DEFUN([TE_CHECK_MODULES], [ + AS_IF([test x$static_executables = xyes], [ + PKG_CHECK_MODULES_STATIC([$1], [$2], [$3], [$4]) + ], [ + PKG_CHECK_MODULES([$1], [$2], [$3], [$4]) + ]) +]) + # Auxiliary functions # expand $1 and print its absolute path @@ -77,6 +101,13 @@ AC_PROG_CXX([c++ g++ clang++]) AX_CXX_COMPILE_STDCXX(17, noext, mandatory) AC_CHECK_TOOL(AR, ar) +# If ptrdiff_t does not alias int, we need a workaround +# in Scintilla. +AX_PTRDIFF_ALIASES_INT +if [[ "x$ax_cv_ptrdiff_aliases_int" = "xno" ]]; then + SCINTILLA_CXXFLAGS="$SCINTILLA_CXXFLAGS -DPTRDIFF_DOESNT_ALIAS_INT" +fi + # Whether $CC is Clang AM_CONDITIONAL(CLANG, [$CC --version | $GREP -i clang >/dev/null]) @@ -115,6 +146,14 @@ if [[ x$GROFF = x ]]; then AC_MSG_ERROR([GNU roff required!]) fi +# preconv was added only in Groff v1.20, which is still missig +# in NetBSD. +AC_CHECK_PROG(PRECONV, preconv, preconv) +if [[ x$PRECONV != x ]]; then + GROFF_FLAGS="-Kutf-8" +fi +AC_SUBST(GROFF_FLAGS) + # Doxygen is not necessarily required as long as # you do not run `make devdoc`. AC_CHECK_PROG(DOXYGEN, doxygen, doxygen) @@ -127,7 +166,7 @@ AC_SUBST(DOXYGEN_HAVE_DOT) AC_CHECK_PROG(SCITECO, sciteco, sciteco) # Checks for libraries. -PKG_CHECK_MODULES(LIBGLIB, [glib-2.0 >= 2.44 gmodule-2.0], [ +TE_CHECK_MODULES(LIBGLIB, [glib-2.0 >= 2.44 gmodule-2.0], [ CFLAGS="$CFLAGS $LIBGLIB_CFLAGS" CXXFLAGS="$CXXFLAGS $LIBGLIB_CFLAGS" LIBS="$LIBS $LIBGLIB_LIBS" @@ -174,7 +213,9 @@ AC_CHECK_FUNCS([memset setlocale strchr strrchr fstat sscanf], , [ # glib defines G_OS_UNIX instead... case $host in *-*-linux* | *-*-*bsd* | *-*-darwin* | *-*-cygwin* | *-*-haiku*) - AC_CHECK_FUNCS([realpath readlink pathconf fchown dup dup2 getpid open read kill mmap popen pclose], , [ + # NOTE: Keep this on a single line for compatibility + # with ancient versions of Autoconf. + AC_CHECK_FUNCS([realpath readlink pathconf fchown dup dup2 getpid open read kill mmap popen pclose isatty fork setsid], , [ AC_MSG_ERROR([Missing libc function]) ]) AC_SEARCH_LIBS(dladdr, [dl], , [ @@ -246,55 +287,68 @@ AC_ARG_WITH(interface, case $INTERFACE in *curses*) case $INTERFACE in - ncurses | netbsd-curses) + ncurses) # This gives precendence to the widechar version of ncurses, # which is necessary for Unicode support even when not using widechar APIs. - AX_WITH_CURSES + # However we also accept libncurses.so if it also contains the + # enhanced definitions. + # NOTE: This also defines CURSES_CFLAGS and CURSES_LIBS arguments, + # which are used by all the other curses variants as well. + AX_WITH_NCURSES if [[ x$ax_cv_curses_enhanced != xyes -o x$ax_cv_curses_color != xyes ]]; then - AC_MSG_ERROR([X/Open curses compatible library not found!]) + AC_MSG_ERROR([X/Open-compatible libncurses not found! Perhaps you must point CURSES_CFLAGS to the correct curses.h.]) fi + CFLAGS="$CFLAGS $CURSES_CFLAGS" CXXFLAGS="$CXXFLAGS $CURSES_CFLAGS" LIBS="$LIBS $CURSES_LIBS" AC_CHECK_FUNCS([tigetstr]) + ;; + + netbsd-curses) + # NetBSD's curses can theoretically be detected by checking for + # ti_puts(), but since both netbsd-curses and ncurses can be + # installed, we want this to be an explicit setting. + AC_DEFINE(NETBSD_CURSES, 1, [Building against netbsd-curses]) + + CFLAGS="$CFLAGS $CURSES_CFLAGS" + CXXFLAGS="$CXXFLAGS $CURSES_CFLAGS" + LIBS="$LIBS $CURSES_LIBS" - if [[ x$INTERFACE = xnetbsd-curses ]]; then - # NetBSD's curses can act as a ncurses - # drop-in replacement and ships with a ncurses - # pkg-config file. Still we define a symbol since - # it's hard to detect at build-time. - AC_DEFINE(NETBSD_CURSES, 1, [Building against netbsd-curses]) + if [[ "x$CURSES_LIBS" = "x" ]]; then + # libncurses.pc is only shipped by Void Linux' fork, + # not in NetBSD itself. + AC_CHECK_LIB(curses, ti_puts, , [ + AC_MSG_ERROR([NetBSD's libcurses missing!]) + ]) + else + AC_MSG_CHECKING([checking for netbsd-curses (CURSES_LIBS)]) + AC_MSG_RESULT([$CURSES_LIBS]) + LIBS="$LIBS $CURSES_LIBS" fi + + AC_CHECK_FUNCS([tigetstr]) ;; xcurses) AC_CHECK_PROG(XCURSES_CONFIG, xcurses-config, xcurses-config) - AC_ARG_VAR(XCURSES_CFLAGS, [ - C compiler flags for XCurses, - overriding the autoconf check - ]) - if [[ "x$XCURSES_CFLAGS" = "x" -a "x$XCURSES_CONFIG" != "x" ]]; then - XCURSES_CFLAGS=`$XCURSES_CONFIG --cflags` + if [[ "x$CURSES_CFLAGS" = "x" -a "x$XCURSES_CONFIG" != "x" ]]; then + CURSES_CFLAGS=`$XCURSES_CONFIG --cflags` fi - CFLAGS="$CFLAGS $XCURSES_CFLAGS" - CXXFLAGS="$CXXFLAGS $XCURSES_CFLAGS" + CFLAGS="$CFLAGS $CURSES_CFLAGS" + CXXFLAGS="$CXXFLAGS $CURSES_CFLAGS" - AC_ARG_VAR(XCURSES_LIBS, [ - linker flags for XCurses, - overriding the autoconf check - ]) AC_MSG_CHECKING([checking for XCurses]) - if [[ "x$XCURSES_LIBS" = "x" -a "x$XCURSES_CONFIG" != "x" ]]; then - XCURSES_LIBS=`$XCURSES_CONFIG --libs` + if [[ "x$CURSES_LIBS" = "x" -a "x$XCURSES_CONFIG" != "x" ]]; then + CURSES_LIBS=`$XCURSES_CONFIG --libs` fi - if [[ "x$XCURSES_LIBS" = "x" ]]; then - AC_MSG_ERROR([libXCurses not configured correctly! - xcurses-config must be present or XCURSES_LIBS must be specified.]) + if [[ "x$CURSES_LIBS" = "x" ]]; then + AC_MSG_ERROR([libXCurses not configured correctly! xcurses-config must be present or CURSES_LIBS must be specified.]) fi - AC_MSG_RESULT([$XCURSES_LIBS]) - LIBS="$LIBS $XCURSES_LIBS" + AC_MSG_RESULT([$CURSES_LIBS]) + LIBS="$LIBS $CURSES_LIBS" # It is crucial to define XCURSES before including curses.h. AC_DEFINE(XCURSES, 1, [Enable PDCurses/XCurses extensions]) @@ -303,26 +357,17 @@ case $INTERFACE in ;; pdcurses*) - AC_ARG_VAR(PDCURSES_CFLAGS, [ - C compiler flags for PDCurses, - overriding the autoconf check - ]) - CFLAGS="$CFLAGS $PDCURSES_CFLAGS" - CXXFLAGS="$CXXFLAGS $PDCURSES_CFLAGS" + CFLAGS="$CFLAGS $CURSES_CFLAGS" + CXXFLAGS="$CXXFLAGS $CURSES_CFLAGS" - AC_ARG_VAR(PDCURSES_LIBS, [ - linker flags for PDCurses, - overriding the autoconf check - ]) - if [[ "x$PDCURSES_LIBS" = "x" ]]; then + if [[ "x$CURSES_LIBS" = "x" ]]; then AC_CHECK_LIB(pdcurses, PDC_get_version, , [ - AC_MSG_ERROR([libpdcurses missing! - Perhaps it is not named correctly or has wrong permissions.]) + AC_MSG_ERROR([libpdcurses missing!]) ]) else - AC_MSG_CHECKING([checking for PDCurses (PDCURSES_LIBS)]) - AC_MSG_RESULT([$PDCURSES_LIBS]) - LIBS="$LIBS $PDCURSES_LIBS" + AC_MSG_CHECKING([checking for PDCurses (CURSES_LIBS)]) + AC_MSG_RESULT([$CURSES_LIBS]) + LIBS="$LIBS $CURSES_LIBS" fi # It is crucial to define PDC_WIDE before including curses.h. @@ -349,8 +394,6 @@ case $INTERFACE in ;; esac - # AX_WITH_CURSES defines per-header symbols, but we currently - # demand that CPPFLAGS are set up such, that we can find curses.h anyway. AC_CHECK_HEADERS([curses.h], , [ AC_MSG_ERROR([Curses header missing!]) ]) @@ -361,7 +404,7 @@ case $INTERFACE in ;; gtk) - PKG_CHECK_MODULES(LIBGTK, [gtk+-3.0 >= 3.12], [ + TE_CHECK_MODULES(LIBGTK, [gtk+-3.0 >= 3.24], [ CFLAGS="$CFLAGS $LIBGTK_CFLAGS" CXXFLAGS="$CXXFLAGS $LIBGTK_CFLAGS" LIBS="$LIBS $LIBGTK_LIBS" @@ -410,10 +453,8 @@ AC_ARG_ENABLE(malloc-replacement, [malloc_replacement=$enableval], [malloc_replacement=check]) if [[ $malloc_replacement = check ]]; then # We currently do not support dlmalloc on Windows and Mac OS. - # It works on FreeBSD, but is slower than the polling fallback, - # it has been disabled by default. case $host in - *-*-*bsd* | *-*-darwin* | *-mingw*) malloc_replacement=no;; + *-*-darwin* | *-mingw*) malloc_replacement=no;; *) malloc_replacement=yes;; esac fi @@ -467,19 +508,11 @@ else esac fi -# This cannot be done with --enable-static as it only controls -# which kind of libraries libtool builds. -# Also, it cannot be controlled reliably by setting LDFLAGS for -# ./configure, as this would be used for linking the test cases -# without libtool and libtool would ignore it. -# It is only possible to call `make LDFLAGS="-all-static"` but -# this is inconvenient... -AC_ARG_ENABLE(static-executables, - AS_HELP_STRING([--enable-static-executables], - [Link in as many runtime dependencies as possible - statically [default=no]]), - [static_executables=$enableval], [static_executables=no]) -AM_CONDITIONAL(STATIC_EXECUTABLES, [test x$static_executables = xyes]) +AC_ARG_WITH(launcher, + AS_HELP_STRING([--with-launcher=LAUNCHER], + [Use the given launcher when executing SciTECO (e.g. wine)]), + [LAUNCHER=$withval], [LAUNCHER=]) +AC_SUBST(LAUNCHER) AC_CONFIG_FILES([GNUmakefile:Makefile.in src/GNUmakefile:src/Makefile.in] [src/interface-gtk/GNUmakefile:src/interface-gtk/Makefile.in] diff --git a/contrib/scinterm b/contrib/scinterm -Subproject c2186fb728bb207b74a6dc1e502adc8e68c08f6 +Subproject 8a0e888a87e760eec05224b93fdd8ea3c3be40f diff --git a/contrib/scintilla b/contrib/scintilla -Subproject 26df8def5e3ad506c514a42262d98584011d544 +Subproject b64652b857d3a5922c72ccc801ac77aa9cff27c diff --git a/contrib/scintilla.am b/contrib/scintilla.am index 46159c1..8b6a531 100644 --- a/contrib/scintilla.am +++ b/contrib/scintilla.am @@ -25,8 +25,9 @@ MAKE_SCINTILLA = $(MAKE) -C @top_builddir@/contrib/scintilla/bin \ -f @SCINTERM_PATH@/Makefile \ srcdir=@SCINTERM_PATH@ basedir=@SCINTILLA_PATH@ \ scintilla=$(LIBSCINTILLA) \ + CFLAGS='@SCINTILLA_CXXFLAGS@' \ CXXFLAGS='@SCINTILLA_CXXFLAGS@' \ - CURSES_FLAGS='@PDCURSES_CFLAGS@ @XCURSES_CFLAGS@ @CURSES_CFLAGS@' + CURSES_FLAGS='@CURSES_CFLAGS@' endif # Pass toolchain configuration to Scintilla. diff --git a/debian/changelog b/debian/changelog index fc8c229..0923f61 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,8 +1,14 @@ +sciteco (2.5.0-0) unstable; urgency=low + + * new upstream version v2.5.0 + + -- Robin Haberkorn <rhaberkorn@fmsbw.de> Wed, 31 Dec 2025 19:49:12 +0000 + sciteco (2.4.0-0) unstable; urgency=low * new upstream version v2.4.0 - -- Robin Haberkorn <robin.haberkorn@googlemail.com> Tue, 01 Apr 2025 01:02:03 +0000 + -- Robin Haberkorn <robin.haberkorn@googlemail.com> Sat, 19 Apr 2025 22:11:34 +0000 sciteco (2.3.0-0) unstable; urgency=low diff --git a/debian/control b/debian/control index a5cdc76..8acd8b3 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,16 @@ Source: sciteco Section: editors Priority: optional -Maintainer: Robin Haberkorn <robin.haberkorn@googlemail.com> -Build-Depends: debhelper (>= 10), dh-exec, g++ (>= 4:5.0), libglib2.0-dev (>= 2.44), +Maintainer: Robin Haberkorn <rhaberkorn@fmsbw.de> +Build-Depends: debhelper (>= 10), dh-exec, gcc (>= 4:8.1), g++ (>= 4:8.1), + libglib2.0-dev (>= 2.44), ncurses-term, libncurses-dev, - libgtk-3-dev (>= 3.12), xvfb, - groff + libgtk-3-dev (>= 3.24), xvfb, + groff (>= 1.19.2) Standards-Version: 4.5.0 -Homepage: https://rhaberkorn.github.io/sciteco/ -Vcs-Browser: https://github.com/rhaberkorn/sciteco -Vcs-Git: git://github.com/rhaberkorn/sciteco.git +Homepage: https://sciteco.fmsbw.de/ +Vcs-Browser: https://git.fmsbw.de/sciteco +Vcs-Git: git://git.fmsbw.de/sciteco Package: sciteco-curses Architecture: any diff --git a/debian/copyright b/debian/copyright index 83c46ac..b8908ae 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,10 +1,10 @@ Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: SciTECO -Upstream-Contact: robin.haberkorn@googlemail.com -Source: https://github.com/rhaberkorn/sciteco +Upstream-Contact: rhaberkorn@fmsbw.de +Source: https://git.fmsbw.de/sciteco Files: * -Copyright: Copyright 2012-2025 Robin Haberkorn <robin.haberkorn@googlemail.com> +Copyright: Copyright 2012-2026 Robin Haberkorn <rhaberkorn@fmsbw.de> License: GPL-3 /usr/share/common-licenses/GPL-3 @@ -81,28 +81,3 @@ License: MIT 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. - -Files: contrib/mingw-bundledlls -Copyright: Copyright 2015 Martin Preisler -License: MIT - The MIT License - . - Copyright (c) 2015 Martin Preisler - . - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - . - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - . - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. diff --git a/debian/rules b/debian/rules index 1808b15..5ce058d 100755 --- a/debian/rules +++ b/debian/rules @@ -26,6 +26,9 @@ NUMJOBS = $(patsubst parallel=%,%,$(filter parallel=%,$(DEB_BUILD_OPTIONS))) MAKEFLAGS += -j$(NUMJOBS) endif +# In case `make check` fails, there will be a complete log. +export TESTSUITEFLAGS="--verbose --color=never" + %: dh $@ diff --git a/debian/sciteco-gtk.install b/debian/sciteco-gtk.install index bafa0d8..7381dd7 100755 --- a/debian/sciteco-gtk.install +++ b/debian/sciteco-gtk.install @@ -1,4 +1,6 @@ #!/usr/bin/dh-exec +# NOTE: DO NOT REMOVE THE EXECUTABLE FLAG FROM THIS FILE + usr/bin/gsciteco # Theoretically, these scripts could be in sciteco-common, # but they need an interpreter and the name depends on the version installed. diff --git a/distribute.mk.in b/distribute.mk.in index cab74ae..c3fb0e0 100644 --- a/distribute.mk.in +++ b/distribute.mk.in @@ -94,10 +94,11 @@ ppa : debian-source # We do nothing to sync the ports tree with our mirror of # the FreeBSD port's Makefile (freebsd/Makefile) or to supply Poudriere # with a recent tarball of SciTECO. -POUDRIERE_JAIL ?= 141amd64 +POUDRIERE_JAIL ?= 142amd64 +POUDRIERE_BRANCH ?= quarterly poudriere: - poudriere testport -j $(POUDRIERE_JAIL) -o editors/sciteco@curses - poudriere testport -j $(POUDRIERE_JAIL) -o editors/sciteco@gtk + poudriere testport -j $(POUDRIERE_JAIL) -b $(POUDRIERE_BRANCH) -o editors/sciteco@curses + poudriere testport -j $(POUDRIERE_JAIL) -b $(POUDRIERE_BRANCH) -o editors/sciteco@gtk # Create Windows release. # Assumes a correctly installed glib (with pkgconfig script) @@ -111,7 +112,7 @@ poudriere: # we should include gspawn-win32-helper.exe instead. # # When linking in PDCursesMod/WinGUI statically, the -# environment variable PDCURSES_LIBS should be set to +# environment variable CURSES_LIBS should be set to # "-lpdcurses -lgdi32 -lcomdlg32 -lwinmm" # Also, don't forget to set MINGW_UI=pdcurses-gui. MINGW_UI=pdcurses diff --git a/doc/Makefile.am b/doc/Makefile.am index f69d95f..f5b3d03 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -7,16 +7,20 @@ include $(top_srcdir)/bootstrap.am # code. # It generates Troff manpage markup and acts as a Troff # preprocessor to manpage templates. -dist_bin_SCRIPTS = tedoc.tes +bin_SCRIPTS = tedoc.tes +CLEANFILES = tedoc.tes +dist_noinst_SCRIPTS = tedoc.tes.in %.in : %.template tedoc.tes @top_srcdir@/src/*.c - $(SCITECO_FULL) -m -- @srcdir@/tedoc.tes \ + $(SCITECO_FULL) -m -- tedoc.tes \ -C $@ $< @top_srcdir@/src/*.c # grosciteco is a troff postprocessor similar to grotty # which can be used to produce SciTECO-friendly output # (woman pages). -dist_bin_SCRIPTS += grosciteco.tes +bin_SCRIPTS += grosciteco.tes +CLEANFILES += grosciteco.tes +dist_noinst_SCRIPTS += grosciteco.tes.in # grosciteco, like many other postprocessors, works # best with its own macro package. # Unfortunately, there is no way to query the built-in @@ -27,56 +31,59 @@ dist_bin_SCRIPTS += grosciteco.tes # the directory via `groff -M`. dist_scitecodata_DATA = sciteco.tmac -# Fix up the hash-bang line of installed SciTECO scripts upon -# installation to refer to the installed sciteco binary. -# This takes --program-prefix into account. -# -# FIXME: This will patch the hash-bang line repeatedly. -install-exec-hook: - $(SCITECO_FULL) -e "@EB'$(DESTDIR)$(bindir)/*.tes' 1U* \ - EJ-1<%*^[ 0,^Q::@FR'#!^EM^Xsciteco'#!$(SCITECO_INSTALLED)'> \ - :EX" +# Fix up the hash-bang line of installed SciTECO scripts. +# This takes --prefix and --program-prefix into account. +# On the other hand, since this is run at build time, +# it breaks bindir-overwriting at `make install` time, +# which could only be made to work with an install-exec-hook. +# Patching installed scripts however broke the FreeBSD port +# builds with Poudriere where BINMODE=555 is set. +# It was therefore decided to sacrifice the rarely used +# `make install bindir=...` and appease the FreeBSD port +# committers instead. +%.tes : %.tes.in + $(SCITECO_FULL) -qioe '0,^Q::@FR/#!^EM^Xsciteco/#!^EQ[^A1]/^[' \ + $(SCITECO_INSTALLED) <$< >$@ womendir = $(scitecolibdir)/women women_DATA = grosciteco.tes.1.woman grosciteco.tes.1.woman.tec -CLEANFILES = grosciteco.tes.1 grosciteco.tes.1.intermediate +CLEANFILES += grosciteco.tes.1 EXTRA_DIST = grosciteco.tes.1.in women_DATA += tedoc.tes.1.woman tedoc.tes.1.woman.tec -CLEANFILES += tedoc.tes.1 tedoc.tes.1.intermediate +CLEANFILES += tedoc.tes.1 EXTRA_DIST += tedoc.tes.1.in women_DATA += sciteco.1.woman sciteco.1.woman.tec -CLEANFILES += sciteco.1 sciteco.1.intermediate +CLEANFILES += sciteco.1 EXTRA_DIST += sciteco.1.in women_DATA += sciteco.7.woman sciteco.7.woman.tec -CLEANFILES += sciteco.7 sciteco.7.intermediate sciteco.7.in sciteco.7.htbl +CLEANFILES += sciteco.7 sciteco.7.in EXTRA_DIST += sciteco.7.template women_DATA += tutorial.woman tutorial.woman.tec -CLEANFILES += tutorial.ms tutorial.intermediate +CLEANFILES += tutorial.ms EXTRA_DIST += tutorial.ms.in CLEANFILES += $(women_DATA) -# NOTE: *.intermediate files are only generated since SciTECO scripts -# cannot currently read stdin, so the grosciteco postprocessor -# has to be run on a separate file. -%.woman %.woman.tec : %.intermediate grosciteco.tes - $(SCITECO_FULL) -m -- @srcdir@/grosciteco.tes \ - $@ $< +# NOTE: grosciteco.tes generates two artifacts, but two targets in one rule would be independent. +# Grouped targets (&:) on the other hand are unreliable/buggy. +%.woman.tec : %.woman; -%.intermediate : % sciteco.tmac - @GROFF@ -wall -Z -Kutf-8 -Tutf8 -t -man -M@srcdir@ -msciteco $< >$@ +%.woman : % sciteco.tmac grosciteco.tes + @GROFF@ @GROFF_FLAGS@ -wall -Z -Tutf8 -t -man -M@srcdir@ -msciteco $< | \ + $(SCITECO_FULL) -im -- grosciteco.tes $@ -tutorial.intermediate : tutorial.ms sciteco.tmac - @GROFF@ -wall -Z -Kutf-8 -Tutf8 -t -ms -M@srcdir@ -msciteco $< >$@ +tutorial.woman : tutorial.ms sciteco.tmac grosciteco.tes + @GROFF@ @GROFF_FLAGS@ -wall -Z -Tutf8 -t -ms -M@srcdir@ -msciteco $< | \ + $(SCITECO_FULL) -im -- grosciteco.tes $@ man_MANS = grosciteco.tes.1 tedoc.tes.1 sciteco.1 sciteco.7 -dist_noinst_SCRIPTS = htbl.tes +dist_noinst_SCRIPTS += htbl.tes if BUILD_HTMLDOCS html_DATA = grosciteco.tes.1.html tedoc.tes.1.html \ @@ -84,16 +91,12 @@ html_DATA = grosciteco.tes.1.html tedoc.tes.1.html \ CLEANFILES += $(html_DATA) endif -# NOTE: The *.htbl files are only generated since SciTECO -# scripts cannot be integrated into pipelines easily yet. -%.htbl : % htbl.tes - $(SCITECO_FULL) -m -- @srcdir@/htbl.tes $< $@ - -%.html : %.htbl - @GROFF@ -wall -Thtml -man $< >$@ +%.html : % htbl.tes + $(SCITECO_FULL) -qiom -- @srcdir@/htbl.tes <$< | \ + @GROFF@ @GROFF_FLAGS@ -wall -Thtml -man >$@ %.html : %.ms - @GROFF@ -wall -Thtml -ms $< >$@ + @GROFF@ @GROFF_FLAGS@ -wall -Thtml -ms $< >$@ # FIXME: We may want to build the cheat sheet automatically. # This would require a full Groff installation, though. diff --git a/doc/cheat-sheet.mm b/doc/cheat-sheet.mm index 60104ff..ba7247c 100644 --- a/doc/cheat-sheet.mm +++ b/doc/cheat-sheet.mm @@ -14,7 +14,7 @@ \#.SP Overview of \fBSciTECO\fP as an editor. A full language description can be found in -.pdfhref W -D https://rhaberkorn.github.io/sciteco/sciteco.7.html -A . \fBsciteco\fP(7) +.pdfhref W -D https://sciteco.fmsbw.de/sciteco.7.html -A . \fBsciteco\fP(7) .br . .\" subscripts @@ -278,7 +278,7 @@ Insert \*$ (ASCII 27). . TD Insert \fItext\fP with leading tab/indentation. See also -.pdfhref W -D https://github.com/rhaberkorn/sciteco/wiki/Useful-Macros#indent-code-block -A . \fIn\^\fCM#it\fP +.pdfhref W -D https://sciteco.fmsbw.de/knowledge/Useful%20Macros/#indent-code-block -A . \fIn\^\fCM#it\fP .ETB . .TBLX "Text Deletion" width='30% 70%' diff --git a/doc/grosciteco.tes.1.in b/doc/grosciteco.tes.1.in index 9056a60..4d3fd0b 100644 --- a/doc/grosciteco.tes.1.in +++ b/doc/grosciteco.tes.1.in @@ -18,7 +18,7 @@ GNU roff post-processor for \*(ST .OP "-t" tec_output .OP "--" .I text_output -.I input +.RI < input .YS . . @@ -79,8 +79,8 @@ This is also the default when omitted. .IP "\fItext_output\fP The plain-text output file, e.g. \(lqgreat-macro.woman\(rq. .IP "\fIinput\fP" -The \fIinput\fP file is in \fBtroff\fP's device-independant -output format. +\fBtroff\fP's device-independant output format +is read from \fIstdin\fP. . . .SH SPECIAL TROFF MACROS @@ -168,8 +168,8 @@ a \*(ST macro package with a man page called .SCITECO_TT .EX groff -Z -Kutf-8 -Tutf8 -man -M@scitecodatadir@ -msciteco \\ - great-macro.tes.7sciteco >great-macro.tes.7sciteco.intermediate -grosciteco.tes great-macro.tes.7sciteco.woman great-macro.tes.7sciteco.intermediate + great-macro.tes.7sciteco | \\ +grosciteco.tes great-macro.tes.7sciteco.woman .SCITECO_TT_END .EE .RE @@ -211,7 +211,7 @@ The \fBGNU roff\fP \(lqman\(rq macros for writing man pages: .SH AUTHOR . This manpage and the \*(ST program was written by -.MT robin.haberkorn@googlemail.com +.MT rhaberkorn@fmsbw.de Robin Haberkorn .ME . \# EOF
\ No newline at end of file diff --git a/doc/grosciteco.tes b/doc/grosciteco.tes.in index ac268b6..c4f16e1 100755 --- a/doc/grosciteco.tes +++ b/doc/grosciteco.tes.in @@ -1,16 +1,18 @@ -#!/usr/local/bin/sciteco -m -!* grosciteco.tes [-t <output-tec>] [--] <output-woman> <input> *! +#!/usr/local/bin/sciteco -mi +!* grosciteco.tes [-t <output-tec>] [--] <output> <input *! 0,2EJ !* FIXME: Memory limiting is too slow *! -:EMQ[$SCITECOPATH]/getopt.tes +:EIQ[$SCITECOPATH]/getopt.tes !* Process command-line options *! -[optstring]t: M[getopt]"F (0/0) ' -LR 0X[output-woman] 2LR 0X[input] HK +[optstring]t: +M[getopt]U[output-woman] Q[output-woman]"< Invalid command-line^J 1 ' +EU[output-woman]Q[\[output-woman]] :Q[getopt.t]"< EU[getopt.t]Q[output-woman].tec ' -EBN[input] +EBN[output-woman] HK 1EB + 0EE !* Groff intermediate code is always ASCII *! !* skip whitespace characters *! @@ -54,7 +56,7 @@ EBN[input] !* FIXME: Works only for straight lines *! @[line]{ U.[to.v] U.[to.h] Q.[to.h]-Q[pos.h]"= Q.[to.v]-Q[pos.v]"= ' ' - [* EB + [* EBN[output-woman] Q.[to.h]-Q[pos.h]"= !* vertical line *! Q.[to.v]-Q[pos.v]"< Q[pos.v]U.v Q.[to.v]U[pos.v] | Q.[to.v]U.v ' @@ -113,6 +115,7 @@ EBN[input] [glyphs.Fc]» [glyphs.rs]\ [glyphs.ti]~ +[glyphs.sl]/ [glyphs.+]+ [glyphs.->]→ [glyphs.tm]™ @@ -146,29 +149,35 @@ EBN[input] * a table). Only the line+column should no longer change. * Either store line+column or use markers. *! - [* EB 0:M[move] U.d ]* + [* EBN[output-woman] 0:M[move] U.d ]* :EU[topics]\.d: C :X[topics] L F< !cmd.xXsciteco_tt! - [* EB 0:M[move] U[ttstart] ]* + [* EBN[output-woman] 0:M[move] U[ttstart] ]* L F< !cmd.xXsciteco_tt_end! - [* EB 0:M[move] + [* EBN[output-woman] 0:M[move] -Q[ttstart]< Q[ttstart]ESSTARTSTYLING Q[ttstart]ESGETSTYLEAT+16,1ESSETSTYLING %[ttstart]> ]* L F< !cmd.xXsciteco_startstyling! - [* EB 0:M[move] U[stylestart] ]* + [* EBN[output-woman] 0:M[move] U[stylestart] ]* L F< !cmd.xXsciteco_setstyling! C :M#giU.s - [* EB 0:M[move] + [* EBN[output-woman] 0:M[move] Q[stylestart]ESSTARTSTYLING Q.s,(-Q[stylestart])ESSETSTYLING ]* L F< + !cmd.xXsciteco_foldlevel! + C :M#giU.[foldlevel] + [* EBN[output-woman] 0:M[move] + Q.[foldlevel],(ESLINEFROMPOSITION)ESSETLINESTATE + ]* + L F< !cmd.xXtty! !cmd.xXdevtag! L F< @@ -176,11 +185,11 @@ EBN[input] !cmd.xf! :M#sa :M#giU.n Q.n+16U.#nt .(:M#sa).X[font] :M#sc :Q[fonts.\.n]"F F< ' -U[fonts.\.n] - @:EU[styles]{\.#ntESSTYLESETFONTMonospace^J} + @:EU[styles]{\.#ntESSTYLESETFONTQ[lexer.font]^J} Ocmd.xfQ[font] !cmd.xfR! Q.nU[default-style] - @:EU[styles]{16ESSTYLESETFONTMonospace^J} + @:EU[styles]{16ESSTYLESETFONTQ[lexer.font]^J} F< !cmd.xfB! @:EU[styles]{1,\.nESSTYLESETBOLD 1,\.#ntESSTYLESETBOLD^J} @@ -210,7 +219,7 @@ EBN[input] :M#sw :M#gi :M#sc F< !cmd.V! - :M#sw :M#gi/Q[res.v]-1+Q[origin.v]U[pos.v] :M#sc F< + :M#sw :M#gi/Q[res.v]-1+Q[origin.v]U[pos.v] Q[pos.v]"<0U[pos.v]' :M#sc F< !cmd.v! :M#sw :M#gi/Q[res.v]%[pos.v] :M#sc F< @@ -267,7 +276,7 @@ EBN[input] !cmd.t! :M#sw .(:M#sa).X.w - [* EB :Q.w:M[move] + [* EBN[output-woman] :Q.w:M[move] G.w :Q.w:M[style] ]* :Q.w%[pos.h] :M#sc F< @@ -278,17 +287,17 @@ EBN[input] | .(:M#sa).X.w 0Q[glyphs.Q.w]U.w ' - [* EB 1:M[move] + [* EBN[output-woman] 1:M[move] Q.wI 1:M[style] ]* :M#sc F< !cmd.c! :M#sw 0AU.w C - [* EB 1:M[move] + [* EBN[output-woman] 1:M[move] G[glyphs.U.w] 1:M[style] ]* :M#sc F< !cmd.N! :M#sw :M#giU.w - [* EB 1:M[move] + [* EBN[output-woman] 1:M[move] Q.wI 1:M[style] ]* :M#sc F< !cmd.n! @@ -329,7 +338,7 @@ Q*U* * TODO: The size can still be improved by using SCI_SETSTYLINGEX * if appropriate. *! -EB 0EE !* operate in single-byte mode *! +EBN[output-woman] 0EE !* operate in single-byte mode *! J 0U#cs 0U#cd < .ESGETSTYLEATUs Qs"< Qs= ' @@ -342,9 +351,33 @@ J 0U#cs 0U#cd :C;> !* + * The fold level is stored in the line state since it is set + * while the document is not fully built yet. + *! +J 0U#li 0U[cur.fl] 0U[last.line] +ESGETLINECOUNT< + Q#liESGETLINESTATEU#fl + Q#fl"N + Q[cur.fl]"N + Q#li-Q[last.line]Ux + 1024%[cur.fl] + @:EU[styles]{\[last.line]U.l \x<\[cur.fl],Q.lESSETFOLDLEVEL %.l>^J} + ' + Q#li+1U[last.line] Q#flU[cur.fl] + (1024+Q#fl-1)#(2^*13)U#fl + @:EU[styles]{\#fl,\#liESSETFOLDLEVEL^J} + ' +%#li> +Q[cur.fl]"N + Q#li-Q[last.line]Ux + 1024%[cur.fl] + @:EU[styles]{\[last.line]U.l \x<\[cur.fl],Q.lESSETFOLDLEVEL %.l>^J} +' + +!* * Save the clear-text part of the document into <output-woman> *! -2EL EWQ[output-woman] +2EL EW EQ[styles] diff --git a/doc/htbl.tes b/doc/htbl.tes index 9cd5100..19f81a2 100755 --- a/doc/htbl.tes +++ b/doc/htbl.tes @@ -1,11 +1,9 @@ -#!/usr/local/bin/sciteco -m -!* htbl.tes <input> <output> *! +#!/usr/local/bin/sciteco -qiom +!* cat input | htbl.tes >output *! !* Troff tbl "drop-in" replacement *! 0,2EJ !* FIXME: Memory limiting is too slow *! -LR 0X#in 2LR 0X#ou EBN#in EB -EF - < !* * <table> will implicitly close <p>'s so we must recalculate the margin. @@ -112,5 +110,4 @@ q.[drows]< I.HTML </table>^J > -2EL EWQ#ou -EX
\ No newline at end of file +2EL -EX diff --git a/doc/sciteco.1.in b/doc/sciteco.1.in index f57415c..e47ca93 100644 --- a/doc/sciteco.1.in +++ b/doc/sciteco.1.in @@ -16,6 +16,10 @@ Scintilla-based \fBT\fPext \fBE\fPditor and \fBCO\fPrrector .SCITECO_TOPIC "sciteco" .SY @PACKAGE@ .OP "-h|--help" +.OP "-v|--version" +.OP "-q|--quiet" +.OP "-i|--stdin" +.OP "-o|--stdout" .OP "-e|--eval" macro .OP "-m|--mung" .OP "--no-profile" @@ -59,12 +63,13 @@ invoked as scripts by using a Hash-Bang line like .RS .SCITECO_TT .EX -#!@bindir@/sciteco -m +#!@bindir@/@PACKAGE@ -m .SCITECO_TT_END .EE .RE . .LP +.SCITECO_TOPIC argv arguments Note that UNIX Hash-Bang lines will only pass a \fBsingle\fP argument to the interpreter before the script's file name, so all required \*(ST options must be mangled into a single argument with their single-letter names. @@ -72,27 +77,29 @@ Passing option-like arguments (beginning with a dash) to scripts may cause problems because \*(ST might try to interpret these options. \*(ST thus stops parsing at the first non-option argument (which will always be the munged file name in a script invocation). -. -.LP -.SCITECO_TOPIC argv arguments -Upon startup \*(ST's buffer ring contains only one unnamed empty buffer. -All command line arguments after the \*(ST options are passed as -.I arguments -to the munged macro by placing each argument on its own line in -the buffer. -The \fIscript\fP file name expected when \(lq--mung\(rq is given -is currently \fBnot\fP considered a macro argument. -In any case the current buffer position (called -.IR dot ) -is left at the beginning of the buffer. Optionally \(lq\-\-\(rq might be used to explicitly separate \*(ST options and macro arguments, but is never passed down as a macro argument. Since it's sometimes useful to pass down \(rq\-\-\(rq to the profile macro, you can use \(lq\-S\(rq, which is equivalent to \(lq\-\- \-\-\(rq. +All remaining arguments after the built-in processing of options +are passed to the executed macro using an array of global Q-registers \fB^A\fIx\fR. +The \fIscript\fP file name expected when \(lq\-\-mung\(rq is given +is currently \fBnot\fP included in this array. +Q-register \fB^A0\fP is the name of the current process as passed to C +programs in \(lqargv[0]\(rq. +. +.LP +Upon startup \*(ST's buffer ring always contains only one unnamed buffer. +If \(lq\-\-stdin\(rq is given, \*(ST first reads from \fIstdin\fP until end-of-file +into the unnamed buffer. +This is includes automatic EOL translation just like when using the \fBEB\fP command +and can therefore be disabled by specifying \(lq\-\-8bit\(rq. +If reading from \fIstdin\fP results in any text additions, the buffer is left \fIdirty\fP. +The current position (called dot) is always left at the beginning of the buffer. . .LP If the munged macro does not request program termination using the -\fBEX\fP command or exits using \fB^C\fP, \*(ST will automatically +\fBEX\fP command or exits using \fB^C^C\fP, \*(ST will automatically switch into its graphical \fIinteractive\fP mode. \*(ST may be built with different graphical user interfaces, including Curses and GTK+ based ones. @@ -119,7 +126,7 @@ to these streams while in interactive mode, or messages are continued to be written to these streams (in addition to being displayed in the GUI). .IP \(bu Messages logged to \fIstdout\fP or \fIstderr\fP \(em except -for messages written explicitly via some \*(ST command \(em +for \(lquser\(rq-level messages (written explicitly via some \*(ST command) \(em are prefixed with a string signifying the message's severity. In interactive mode, messages are also shown in a GUI-dependant manner. @@ -141,6 +148,10 @@ Batch mode does not have these restrictions. .IP \(bu A few commands that modify the command line are only available in interactive mode. +.IP \(bu +A few commands like \fB^C\fP are disallowed in interactive mode +when run from the command-line macro or behave slightly +differently compared to batch mode (e.g. \fB$$\fP or \fB==\fP). .RE . .LP @@ -179,6 +190,29 @@ option. .IP "\fB-h\fR, \fB--help\fR" .SCITECO_TOPIC "-h" "--help" Display a short help text on the console. +.IP "\fB-v\fR, \fB--version\fR" +.SCITECO_TOPIC "-v" "--version" +Display the \*(ST version in an easy to parse way. +See also the \fBEO\fP command. +.IP "\fB-q\fR, \fB--quiet\fR" +.SCITECO_TOPIC "-q" "--quiet" +Do not print any messages to \fIstdout\fP except for \(lquser\(rq-level +messages, i.e. messages printed via \fBT\fP, \fB^T\fP and related +commands. +This is useful when piping the output of \*(ST into other programs. +.IP "\fB-i\fR, \fB--stdin\fR" +.SCITECO_TOPIC "-i" "--stdin" +Reads from \fIstdin\fP into the unnamed buffer before executing any macro. +Allows to easily integrate \*(ST into an UNIX pipeline, at least +if you don't need to process infinite streams. +This normalizes end-of-line (EOL) characters just like when opening +any other file. +.IP "\fB-o\fR, \fB--stdout\fR" +.SCITECO_TOPIC "-o" "--stdout" +\# FIXME: Perhaps this should imply --quiet? +Prints the current document's contents to \fIstdout\fP immediately before +terminating the program, restoring the original EOL characters. +This is also useful when piping the output of \*(ST into other programs. .IP "\fB-e\fR, \fB--eval\fR \fImacro" .SCITECO_TOPIC "-e" "--eval" Evaluate (execute) @@ -223,7 +257,12 @@ Execute \(lqsciteco --help\(rq for more details. .SCITECO_TOPIC status . \*(ST will return a non-null exit code if an error occurred during -batch mode processing. +batch mode processing \(em usually 1 on UNIX. +Otherwise the top value on the numeric stack will determine +the process' exit code as if passed to libc's +.BR exit (3) +function. +On UNIX systems only numbers between 0 and 255 may be meaningfull. . . .SH ENVIRONMENT @@ -411,12 +450,22 @@ and opening files specified on the command line. .B $SCITECOPATH/*.tes Standard library macros. .TP -.SCITECO_TOPIC savepoint +.BI # filename # +Recovery file: +After a configurable recovery interval \*(ST dumps up all modified buffers +(unsaved changes), that have not been dumped previously. +These files should be ignored by version control systems and may +be left around after crashes and unexpected restarts. +See \fB6EJ\fP for more details. +.TP +.SCITECO_TOPIC savepoint backup .BI .teco- n - filename ~ Save point files created by \*(ST when saving files during interactive execution have this format. On Windows, these files have the hidden attribute set. -Savepoint files are temporary and should be ignored by version +They are internally used when rubbing out file saves +and are conceptually similar to backup files in other editors. +However they are temporary and should be ignored by version control systems, etc. .TP .SCITECO_TOPIC ".teco_session" session @@ -492,6 +541,42 @@ In order to execute a stand-alone script or custom profile macro: .EE .RE . +.LP +The easiest way to integrate \*(ST into an UNIX pipeline is +by using the \(lq--quiet --stdin --stdout\(rq or \(lq-qio\(rq +parameters. +For instance in order to prefix all lines with a line number: +.RS +.SCITECO_TT +.EX +dmesg | @PACKAGE@ -qioe '<%a\\@I/ / :L;>' +.SCITECO_TT_END +.EE +.RE +. +.LP +This also works in interactive mode, so \(lq@PACKAGE@ -i\(rq +allows you to interactively edit text read from \fIstdin\fP. +Suppose you would want to print only those lines from \fIstdin\fP +matching \(lqiwm0:\(rq: +.RS +.SCITECO_TT +.EX +dmesg | @PACKAGE@ -qie '<@S/iwm0:/; :L; -T>' +.SCITECO_TT_END +.EE +.RE +. +.LP +In order to query the installation path of the standard library, +which is useful for authors of third-party macro packages: +.RS +.SCITECO_TT +.EX +@PACKAGE@ -qe ':G[$SCITECOPATH]' +.SCITECO_TT_END +.EE +.RE . .SH SEE ALSO . @@ -527,7 +612,7 @@ DEC Standard TECO-11 .SH AUTHOR . This manpage and the \*(ST program was written by -.MT robin.haberkorn@googlemail.com +.MT rhaberkorn@fmsbw.de Robin Haberkorn .ME . \# EOF
\ No newline at end of file diff --git a/doc/sciteco.7.template b/doc/sciteco.7.template index 4736be2..cb718e0 100644 --- a/doc/sciteco.7.template +++ b/doc/sciteco.7.template @@ -680,6 +680,16 @@ work as an immediate editing command in the GUI or as a signal dispatched from an associated console or from another process. T} T{ +.SCITECO_TOPIC ^L redraw +Redraw +T};12;^L;T{ +Everywhere +T};T{ +Enforces a complete redraw of the entire window. +This is useful when the display becomes corrupted, +especially when using the Curses UI. +T} +T{ .SCITECO_TOPIC interrupt Interrupt T};3;^C;T{ @@ -864,8 +874,9 @@ areas\fP. .IP \(bu The \fIcommand line area\fP, showing the currently effective and rubbed-out command line as it is edited. -This is currently a single line of text that is scrolled -automatically. +This is a single line by default, but can be configured with \(lq5EJ\(rq. +Since it is also a Scintilla view, it can be further customized by +running \(lq0,2048ED\(rq and invoking Scintilla messages using \fBES\fP. . .SS Colors and Theming .SCITECO_TOPIC colors theming @@ -982,6 +993,7 @@ The integer storage size may be changed at \*(ST build time however. In this specific build, integers are @TECO_INTEGER@-bit. .LP +.SCITECO_TOPIC bool boolean Some commands expect or return booleans, most often signifying success or failure. Booleans are also integers but unlike in ordinary (sane) languages @@ -1251,12 +1263,12 @@ stack are discarded. This way, \fPED\fP hooks should not interfere with the stack-semantics of commands triggering them. .LP -Possible arguments to the \(lqED\(rq macro begin with 1 and +Possible arguments to the \(lqED\(rq macro begin with 0 and are defined consecutively so the macro can branch to the operation using a computed goto. The different values are defined as follows: .TP -.B 1 +.B 0 A file has been \fBadded\fP to the buffer ring. It may or may not already exist in the file system. This file is \*(ST's current document when this hook @@ -1264,7 +1276,7 @@ executes. Scintilla lexing may be configured in this hook \(em it usually only has to be done once. .TP -.B 2 +.B 1 A buffer has been \fBedited\fP (made the current file). This hook is not executed when a file is freshly added to the buffer ring, since this can be simulated easily @@ -1272,13 +1284,13 @@ by branching within the \(lqED\(rq macro. In this hook you may want to define language-specific auxiliary macros, for instance. .TP -.B 3 +.B 2 A buffer is about to be \fBclosed\fP (removed from the buffer ring). The buffer that is about to be closed, is still the current document when this hook runs. .TP -.B 4 +.B 3 \*(ST is about to \fBquit\fP, i.e. exit normally. This is \*(ST's equivalent of .BR atexit (3) @@ -1383,13 +1395,16 @@ return 0 instead. .LP .SCITECO_TOPIC :: Two colons (\fB::\fP) can sometimes further modify a command's behavior \(em -currently it is used by the \fB::S\fP search comparison command -and a few related search-and-replace operations. +currently it is used by the timestamp command \fB::^H\fP, +the \fB::S\fP search comparison command and a few related search-and-replace operations. .LP .SCITECO_TOPIC @ at When put in front of a command with string arguments, the at (\fB@\fP) modifier always allows the string termination character to be changed for that particular command. +\# This particular bit of syntax is TECO-64 inspired. +Whitespace characters (as by \*(ST's understanding of no-op characters) +are ignored immediately after the command name if it was \fB@\fP-modified. The alternative termination character must be specified just before the first string argument. For instance: @@ -1398,13 +1413,13 @@ For instance: @FS/foo/bar/ .SCITECO_TT_END .EE -Any character may be used as an alternative termination character. +Any non-whitespace character may be used as an alternative termination character +and is matched case-insensitively. There is one special case, though. If specified as the opening curly brace (\fB{\fP), a string argument will continue until the closing curly brace (\fB}\fP). Curly braces must be balanced and after the closing curly brace -the termination character is reset to Escape and another one may -be chosen. +a new termination character may be chosen after optional whitespace. This feature is especially useful for embedding TECO code in string arguments, as in: .SCITECO_TT @@ -1414,6 +1429,16 @@ string arguments, as in: } .SCITECO_TT_END .EE +Since whitespace is ignored in front of the alternative escape character, +this could have also been written as: +.SCITECO_TT +.EX +@^Um +{ + @FS {foo} /bar/ +} +.SCITECO_TT_END +.EE The termination character can be \fIquoted\fP if you want to handle it like any regular character. For instance, you could write \(lqS^Q\fB$$\fP\(rq to search for the @@ -1548,7 +1573,6 @@ but querying \(lq$\(rq will still return an absolute path. The \(lq$\(rq register may also be edited but changing its string contents this way has no effect on the current working directory. -Appending to the \(lq$\(rq register is unsupported. The register is initialized automatically on startup. .TP .BR $ " (Escape)" @@ -1590,6 +1614,11 @@ The existence of a clipboard register can thus be checked in macros to determine whether getting and modifying that particular clipboard is supported natively. .br +The default clipboard \(lq~\(rq refers to the clipboard selection +if there are multiple clipboards. +You can set bit 10 of the \fBED\fP flags to change it to the +primary clipboard. +.br \*(ST supports two ways of driving the clipboard on ncurses. .SCITECO_TOPIC OSC-52 xterm First of all, there is built-in support for OSC-52 escape sequences, @@ -1637,8 +1666,28 @@ EOL normalization will take place (if enabled), so that pasting clipboards does not introduce unexpected EOL sequences. The Q-Register view's EOL mode will \fBnot\fP be guessed from the original clipboard contents, though. -The numeric parts of the clipboard registers are currently -not used by \*(ST. +The numeric parts of the clipboard registers are +currently not used by \*(ST. +.TP +.SCITECO_TOPIC ^Ax +.BI ^A x +An argument passed on the operating system's command line. +\fIx\fP can be an integer beginning with 0. +.B ^A0 +is the name of the program, as passed via \(lqargv[0]\(rq to +C programs, and is guaranteed to always exist. +All remaining registers for \fIx\fP larger than or equal to 1 +represent the command-line parameters after all the built-in +options as reported by \(lq@PACKAGE@ --help\(rq. +In other words all the options processed by the \*(ST main program +including the file names after \(lq--mung\(rq are automatically +excluded from the +.BI ^A x +array. +The numeric parts of the argument registers are +currently not used by \*(ST. +For more details, see +.BR sciteco (1). .TP .BI ^K key Key macro registers as documented in section @@ -1786,6 +1835,12 @@ character and their case is insignificant. In the following list of supported expressions, the caret-notation thus refers to the corresponding control code: .TP +.SCITECO_TOPIC ^P +.B ^P +Disable interpretation of all further string building characters, +so all following characters will be taken verbatim. +There is no way to escape the string terminator after \(lq^P\(rq. +.TP .SCITECO_TOPIC ^Qc ^Rc quote .BI ^Q c .TQ @@ -1839,6 +1894,19 @@ Operations on registers (\fBEU\fP) similarily consult the register's encoding. Everything else expects Unicode codepoints. .TP +.SCITECO_TOPIC ^E< ^E<> +.BI ^E< code > +Expands to the character, specified by \fIcode\fP. +\# This is Video-TECO-like. TECO-11 only accepts octal codes AFAIK. +It is read in decimal by default, but can be given in hexadecimal if lead by \(lq0x\(rq +or octal if given with a leading \(lq0\(rq. +The current radix as specified by \fB^R\fP does not influence parsing. +These semantics are compatible with libc's +.BR stroull (3). +The sequence \(lq^E<0x41>\(rq for instance expands to the character \(lqA\(rq. +The interpretation of this code as bytes or Unicode codepoints depends on the context +just like for \fB^EU\fP. +.TP .SCITECO_TOPIC ^EQ ^EQq .BI ^EQ q Expands to the string contents of the Q-Register specified by @@ -2118,13 +2186,15 @@ in classic TECOs. . .SS Gotos and Labels . -The most basic flow control command in \*(ST is the Go-to command. +An important flow control command in \*(ST is the Go-to command. Since it is really an ordinary command, exceptional only in setting the program counter and influencing parsing, it is described in this document's command reference. -\*(ST can perform simple unconditional and computed gotos. +\*(ST can perform simple unconditional and computed gotos +which are just as powerful as switch-case statements in other +languages. .LP -.SCITECO_TOPIC label +.SCITECO_TOPIC "!" label Labels are symbolic and are defined with the following syntax: .br .BI ! label ! @@ -2153,7 +2223,7 @@ In addition to labels and unlike most classic TECO dialects, \*(ST also supports true comments. True comments are parsed faster than labels and do not take up memory in goto tables. -.SCITECO_TOPIC "block comment" +.SCITECO_TOPIC "!*" "block comment" One form of comments is the block comment: .br .BI !* comment *! @@ -2165,7 +2235,8 @@ They are analoguous to C's .BI /* ... */ comments. .LP -.SCITECO_TOPIC "EOL comment" +\# This form of comment was originally in TECO-64. +.SCITECO_TOPIC "!!" "EOL comment" The second form of real comments are end-of-line comments, which are analogous to C++'s \fB//\fP comments: .br @@ -2444,12 +2515,14 @@ The same conventions are used elsewhere in this manual. . .SH COMPATIBILITY . -\*(ST is not compatible with any particular TECO dialect, -but is quite similar to -.BR "Video TECO" . -Most Video TECO and many Standard TECO programs should +\*(ST strives to be compatible with \(lqstandard\(rq DEC TECO-11 whereever this +does not hinder \*(ST's nature as an interactive modern editor. +In questions concerning multiple buffers and interactivity +\*(ST draws inspiration from Video TECO. +Most Video TECO and many \(lqstandard\(rq TECO macros should be portable with little or no changes. -This manual mentions differences on several occasions. +\*(ST however conciously deviates from both dialects to preserve +features considered intrinsic and defining to the project. . . .SH SEE ALSO @@ -2488,7 +2561,7 @@ Overview of CSS in GTK .SH AUTHOR . This manpage and the \*(ST program was written by -.MT robin.haberkorn@googlemail.com +.MT rhaberkorn@fmsbw.de Robin Haberkorn .ME . \# EOF
\ No newline at end of file diff --git a/doc/sciteco.tmac b/doc/sciteco.tmac index 026e489..8e53407 100644 --- a/doc/sciteco.tmac +++ b/doc/sciteco.tmac @@ -66,6 +66,29 @@ . device sciteco_setstyling:\\$1 .. . +.\" Set the fold level of the current line __and__ all following lines. +.\" This could be called at the beginning of chapters and sections. +.de SCITECO_FOLDLEVEL +. device sciteco_foldlevel:\\$1 +.. +. +.\" man-page specific extensions. +.\" FIXME: Is there a reliable way to detect the man-page macros? +.if dan \{\ +. rn SH SH-orig +. de SH +. SH-orig +. SCITECO_FOLDLEVEL 1 +. nop \\$* +. . +. rn SS SS-orig +. de SS +. SS-orig +. SCITECO_FOLDLEVEL 2 +. nop \\$* +. . +.\} +. .\" .\" Effectively disable paragraph filling in man pages. .\" Word wrapping will be performed by Scintilla. diff --git a/doc/tedoc.tes.1.in b/doc/tedoc.tes.1.in index 9f87b37..f942552 100644 --- a/doc/tedoc.tes.1.in +++ b/doc/tedoc.tes.1.in @@ -124,7 +124,7 @@ The \fBGNU roff\fP \(lqman\(rq macros for writing man pages: .SH AUTHOR . This manpage and the \*(ST program was written by -.MT robin.haberkorn@googlemail.com +.MT rhaberkorn@fmsbw.de Robin Haberkorn .ME . \# EOF
\ No newline at end of file diff --git a/doc/tedoc.tes b/doc/tedoc.tes.in index 1f13e54..cc4f726 100755 --- a/doc/tedoc.tes +++ b/doc/tedoc.tes.in @@ -3,12 +3,12 @@ 0,2EJ !* FIXME: Memory limiting is too slow *! -:EMQ[$SCITECOPATH]/getopt.tes +:EIQ[$SCITECOPATH]/getopt.tes @[format_header]{ FD--S .,(:L"S.|Z')@Xa I^J - EBQ#tm + EBN[\#tm] I\# GENERATED FROM Q.#sc (\.#sc):^J I.SS Ga -A-10"N I^J ' :Q.[topics]"> @@ -45,7 +45,7 @@ J <FR^J^J^J;> J 0A-10"=D' ZJ -A-10"=-D' - EBQ#tm + EBN[\#tm] G.[header] I^J.^J } @@ -73,35 +73,34 @@ J <S<MC>; -D I^J -S< -D I^J.I > J <FRS^J^J;> J <FR^JS^J;> - EBN#tm + EBN[\#tm] G.c I^J.^J.^J } !* process command-line options *! -[optstring]C M[getopt]"F (0/0) ' +[optstring]C +M[getopt]U#ou Q#ou"< Invalid command-line^J 1 ' :Q[getopt.C]"< [comment.start]!* [comment.end]*! | [comment.start]/* [comment.end]*/ ' -LR 0X#ou -2LR 0X#tm EBN#tm EB L -[.f - <:L;R 0X.f EBQ.f EB L> -].f --EF +Q#ou+1U#tm Q#tmU.i <:Q[\.i]:; EBN[\.i] %.i> + +!* switch to the main troff template (#tm) *! +EB EF I\# -\# AUTOGENERATED FROM Q#tm +\# AUTOGENERATED FROM Q[\#tm] \# DO NOT EDIT MANUALLY!!! \#^J !* find insertion point *! -FS^J.TEDOC^J^J +:FS^J.TEDOC^J^J"F Missing .TEDOC call^J 1 ' EJ-1< < - 2U* EU.#scQ* + 2U* [*].#sc !* extract comment *! SQ[comment.start]$; @@ -127,5 +126,5 @@ EJ-1< EF > -2EL EWQ#ou +2EL EWQ[\#ou] EX diff --git a/doc/tutorial.ms.in b/doc/tutorial.ms.in index acbf9ac..92da0af 100644 --- a/doc/tutorial.ms.in +++ b/doc/tutorial.ms.in @@ -58,6 +58,7 @@ This tutorial guides you through the bare essentials of the editor. Try the examples directly on this document. . .NH 2 +.SCITECO_FOLDLEVEL 1 Get me out of here! . .LP @@ -73,6 +74,7 @@ In the remainder of this document, command examples are therefore formatted as f .DE . .NH 2 +.SCITECO_FOLDLEVEL 1 Navigation . .LP @@ -94,6 +96,7 @@ Using cursor movement or mouse buttons updates the movement commands in the command line (the line at the bottom of the screen after \fB*\fP). . .NH 2 +.SCITECO_FOLDLEVEL 1 Insertion . .LP @@ -115,6 +118,7 @@ There is no \(lqinsert\(rq mode, you are merely providing a string argument to the interactively executed \fBI\fP command. . .NH 2 +.SCITECO_FOLDLEVEL 1 Deletion . .LP @@ -130,6 +134,7 @@ Use the following command instead to discard all changes: .DE . .NH 2 +.SCITECO_FOLDLEVEL 1 Saving . .LP @@ -155,6 +160,7 @@ Remember, you can type CTRL+W to undo even the file write and restore the previous state of the file. . .NH 2 +.SCITECO_FOLDLEVEL 1 Loading . .LP @@ -184,6 +190,7 @@ in a typical UNIX shell! Of course, you can press CTRL+W to revert opening the file. . .NH 2 +.SCITECO_FOLDLEVEL 1 Search and replace . .LP @@ -210,6 +217,7 @@ so \fBS\fP\*$ repeats the last search and \fBFR\fP\*($$ repeats the last search-replace operation. . .NH 2 +.SCITECO_FOLDLEVEL 1 Q-Registers . .LP @@ -232,6 +240,7 @@ This however might not work out of the box if you are running the ncurses version of \*(ST. . .NH 2 +.SCITECO_FOLDLEVEL 1 Programming . .LP @@ -247,6 +256,7 @@ of performing a search-replace operation on the entire buffer: .DE . .NH 2 +.SCITECO_FOLDLEVEL 1 Customize . .LP @@ -271,6 +281,7 @@ $ @PACKAGE@ --no-profile .DE . .NH 2 +.SCITECO_FOLDLEVEL 1 Further reading . .LP @@ -280,7 +291,7 @@ Consult or even print the official Cheat Sheet to extend your \(lqvocabulary\(rq of \*(ST commands: . .ID 0 -https://sciteco.sf.net/manuals/cheat-sheet.pdf +https://sciteco.fmsbw.de/manuals/cheat-sheet.pdf .DE . .LP @@ -302,16 +313,15 @@ To open the language reference, type: .LP If you want to read the tutorial at any later time, just type \fB?\fPtutorial\*$. -You may also want to have a look at the Wiki and FAQ: +You may also want to have a look at the Knowledge Base and FAQ: . .ID 0 -https://github.com/rhaberkorn/sciteco/wiki +https://sciteco.fmsbw.de/knowledge/ .DE . .LP If you cannot find a solution to your problem, -you can of course open an Issue or Discussion on \*(ST's -Github page. +you can of course write to the dings@fmsbw.de mailing list. We are also happy to help out on the official IRC channel: Join #sciteco at irc.libera.chat. diff --git a/fallback.teco_ini b/fallback.teco_ini Binary files differindex 0211ade..c0b3a49 100644 --- a/fallback.teco_ini +++ b/fallback.teco_ini diff --git a/freebsd/Makefile b/freebsd/Makefile index a8a46db..1dcd194 100644 --- a/freebsd/Makefile +++ b/freebsd/Makefile @@ -1,12 +1,12 @@ PORTNAME= sciteco -DISTVERSION= 2.3.0 +DISTVERSION= 2.5.0 CATEGORIES= editors textproc devel -MASTER_SITES= https://github.com/rhaberkorn/${PORTNAME}/releases/download/v${DISTVERSION}/ \ +MASTER_SITES= https://sciteco.fmsbw.de/downloads/v${DISTVERSION}/ \ SOURCEFORGE/${PORTNAME}/v${DISTVERSION}/ -MAINTAINER= robin.haberkorn@googlemail.com +MAINTAINER= rhaberkorn@fmsbw.de COMMENT= Scintilla-based Text Editor and Corrector -WWW= https://rhaberkorn.github.io/sciteco/ +WWW= https://sciteco.fmsbw.de/ LICENSE= GPLv3+ LICENSE_FILE= ${WRKSRC}/COPYING @@ -33,10 +33,6 @@ CONFIGURE_OUTSOURCE= yes MAKEFILE= GNUmakefile TEST_TARGET= check -# SciTECO uses an install-exec-hook to fix up hash-bang lines. -# This is broken by the default 0555 mode. -BINMODE= 755 - # NOTE: Unlike on Debian, we cannot build a sciteco-common package. # FreeBSD does not yet support subpackages. # Therefore both flavors will install totally independant @@ -65,19 +61,23 @@ PLIST_SUB+= GTK="" \ PROGRAM_PREFIX=g .endif -OPTIONS_DEFINE= LEXILLA MALLOC_REPLACEMENT TECO_INTEGER_32 -OPTIONS_DEFAULT= LEXILLA +OPTIONS_DEFINE= LEXILLA MALLOC_REPLACEMENT TECO_INTEGER_32 LTO +OPTIONS_DEFAULT= LEXILLA MALLOC_REPLACEMENT OPTIONS_SUB= yes LEXILLA_DESC= Build with Lexilla lexer support (larger) -MALLOC_REPLACEMENT_DESC= Force replacement of system malloc() +MALLOC_REPLACEMENT_DESC= Replace system malloc() for memory limiting TECO_INTEGER_32_DESC= Use 32-bit TECO integers +LTO_DESC= Apply Link-Time Optimizations (significantly faster) LEXILLA_CONFIGURE_OFF= --without-lexilla MALLOC_REPLACEMENT_CONFIGURE_ON= --enable-malloc-replacement TECO_INTEGER_32_CONFIGURE_ON= --with-teco-integer=32 -WITH_LTO= yes +# Once we support an --enable-lto site-config-option, we should rather use that. +LTO_CFLAGS= -flto=thin +LTO_CXXFLAGS= -flto=thin +LTO_LDFLAGS= -flto=thin .include <bsd.port.pre.mk> diff --git a/freebsd/distinfo b/freebsd/distinfo index 9da3f9c..49a7b48 100644 --- a/freebsd/distinfo +++ b/freebsd/distinfo @@ -1,3 +1,3 @@ -TIMESTAMP = 1735085085 -SHA256 (sciteco-2.3.0.tar.gz) = f131cd88164bd4ee271813a7c6f448b9987ddb54ca6f39328beb979fba3e56a2 -SIZE (sciteco-2.3.0.tar.gz) = 4005903 +TIMESTAMP = 1745085748 +SHA256 (sciteco-2.4.0.tar.gz) = 5b053644d8365eb0fddd9b268af9d6c6c44786dfa51dd4cbb962abac66670402 +SIZE (sciteco-2.4.0.tar.gz) = 4077220 diff --git a/freebsd/files/xvfb-run.sh b/freebsd/files/xvfb-run.sh index b4fd5a0..b4fd5a0 100644..100755 --- a/freebsd/files/xvfb-run.sh +++ b/freebsd/files/xvfb-run.sh diff --git a/freebsd/pkg-plist b/freebsd/pkg-plist index 0f06e0b..ab74145 100644 --- a/freebsd/pkg-plist +++ b/freebsd/pkg-plist @@ -6,11 +6,14 @@ share/man/man1/%%PROGRAM_PREFIX%%sciteco.1.gz share/man/man1/%%PROGRAM_PREFIX%%tedoc.tes.1.gz share/man/man7/%%PROGRAM_PREFIX%%sciteco.7.gz %%DATADIR%%/lib/color.tes +%%DATADIR%%/lib/colors/contrast.tes %%DATADIR%%/lib/colors/solarized.tes %%DATADIR%%/lib/colors/terminal.tes %%DATADIR%%/lib/fnkeys.tes %%DATADIR%%/lib/getopt.tes %%DATADIR%%/lib/lexer.tes +%%DATADIR%%/lib/repl.tes +%%DATADIR%%/lib/tecat.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/abaqus.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/ada.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/asciidoc.tes @@ -50,6 +53,7 @@ share/man/man7/%%PROGRAM_PREFIX%%sciteco.7.gz %%LEXILLA%%%%DATADIR%%/lib/lexers/java.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/js.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/kix.tes +%%LEXILLA%%%%DATADIR%%/lib/lexers/latex.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/lisp.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/lout.tes %%LEXILLA%%%%DATADIR%%/lib/lexers/lua.tes diff --git a/lib/Makefile.am b/lib/Makefile.am index b8832ab..0da616a 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,7 +1,12 @@ - dist_scitecolib_DATA = color.tes lexer.tes session.tes opener.tes \ fnkeys.tes string.tes getopt.tes +# Standalone scripts. +# These are not installed via _SCRIPTS as it would add the --program-prefix. +# Since they won't get the executable flag, it makes no sense to fix up their +# hash-bang lines as well. +dist_scitecolib_DATA += repl.tes tecat.tes + # Helper script for creating lexer definitions EXTRA_DIST = scite2co.lua @@ -9,6 +14,7 @@ EXTRA_DIST = scite2co.lua # a new color scheme: colorschemedir = $(scitecolibdir)/colors dist_colorscheme_DATA = colors/terminal.tes \ + colors/contrast.tes \ colors/solarized.tes lexerdir = $(scitecolibdir)/lexers @@ -105,6 +111,7 @@ dist_lexer_DATA += lexers/verilog.tes \ lexers/asciidoc.tes \ lexers/troff.tes \ lexers/sql.tes \ - lexers/css.tes + lexers/css.tes \ + lexers/latex.tes endif diff --git a/lib/color.tes b/lib/color.tes index f732848..a45c646 100644 --- a/lib/color.tes +++ b/lib/color.tes @@ -79,4 +79,14 @@ !* Set up brace lightning *! :M[color.bracelight],34M[color.set] :M[color.error],35M[color.set] + + !* Configure fold margin (except on the command line) *! + ED&2048"= + :M[color.linenumber]U.fU.b + Q.b,1ESSETFOLDMARGINCOLOUR Q.b,1ESSETFOLDMARGINHICOLOUR + 25U.x 7<Q.f,Q.xESMARKERSETFORE Q.b,Q.xESMARKERSETBACK %.x> + 10000++,25ESMARKERDEFINE 10000+-,26ESMARKERDEFINE + 10000++,30ESMARKERDEFINE 10000+-,31ESMARKERDEFINE + (2^*25 # 2^*26 # 2^*30 # 2^*31),2ESSETMARGINMASKN 0,2ESSETMARGINTYPEN + ' } diff --git a/lib/colors/contrast.tes b/lib/colors/contrast.tes new file mode 100644 index 0000000..256faad --- /dev/null +++ b/lib/colors/contrast.tes @@ -0,0 +1,39 @@ +!* + * High-contrast minimalist color scheme. + * This only highlights strings and comments (if supported by the terminal). + * This is also the recommended color scheme on monochrome displays. + *! +[color.default] 0,Q[color.black],Q[color.white] +[[color.default]][color.linenumber] +Q[color.black]U[color.caretline] +Q[color.lwhite]U[color.caretfore] +Q[color.black]U[color.selfore] +Q[color.white]U[color.selback] + +!* Also used for popups *! +[color.calltip] 0,Q[color.lwhite],Q[color.black] + +[color.comment] 2,Q[color.black],Q[color.white] +[[color.default]][color.number] +[[color.default]][color.keyword] +[color.string] 1,Q[color.black],Q[color.white] +[color.string2] 1,Q[color.black],Q[color.white] +[[color.default]][color.preproc] +[[color.default]][color.preproc2] +[[color.default]][color.operator] +[[color.default]][color.variable] +[[color.default]][color.error] + +!* Makes only sense for Makefiles *! +[[color.default]][color.target] + +!* Makes only sense for Patch/Diff files *! +[[color.default]][color.deletion] +[[color.default]][color.addition] +[[color.default]][color.change] + +!* For highlighting braces *! +[[color.default]][color.bracelight] + +!* Style the Q-Register view *! +[* EQ.b :M[color.init] ]* diff --git a/lib/colors/solarized.tes b/lib/colors/solarized.tes index fd0823e..11567d9 100644 --- a/lib/colors/solarized.tes +++ b/lib/colors/solarized.tes @@ -131,6 +131,8 @@ Q[solarized.light]"T :M[solarized.light] | :M[solarized.dark] ' [* EJ<%.bEB M[lexer.auto]> EQ.b :M[color.init] + !* FIXME: What if the user disabled it in .teco_ini? *! + :M[lexer.set.cmdline] ]* } diff --git a/lib/colors/terminal.tes b/lib/colors/terminal.tes index 2aa7354..ea048aa 100644 --- a/lib/colors/terminal.tes +++ b/lib/colors/terminal.tes @@ -6,6 +6,7 @@ Q[color.lwhite]U[color.caretfore] Q[color.black]U[color.selfore] Q[color.white]U[color.selback] +!* Also used for popups *! [color.calltip] 0,Q[color.lwhite],Q[color.black] [color.comment] 1,Q[color.black],Q[color.lblack] diff --git a/lib/fnkeys.tes b/lib/fnkeys.tes index 3fd0701..937caf9 100644 --- a/lib/fnkeys.tes +++ b/lib/fnkeys.tes @@ -117,6 +117,12 @@ 1U[CLOSE] !* + * F1 toggles __all__ folds. + *! +@[F1]{(2ESFOLDALL{-14D}} +1U[F1] + +!* * Zoom with F9/F10 if function keys are enabled. * This is automatically rubbed out. *! @@ -136,9 +142,26 @@ * Ctrl+right click: Insertion beginning of line * Scroll wheel: scrolls (faster with shift) * Ctrl+scroll wheel: zoom (GTK-only) + * + * Also, you can click on the folding margin to toggle folds. *! @[MOUSE]{ -2EJESCHARPOSITIONFROMPOINTU.p + + -2EJU.x ESGETMARGINLEFTU.r + ESGETMARGINS< + Q.rU.l Q.iESGETMARGINWIDTHN%.r + Q.x-Q.l+1"> Q.x-Q.r"< !* mouse within margin i *! + Q.iESGETMARGINMASKN&(-33554432)"N !* folding margin *! + -EJ-1"= !* mouse released *! + Q.pESLINEFROMPOSITIONESTOGGLEFOLD + {-9D} + ' + ' + 1; !* handle like click in text area *! + ' ' + %.i> + -4EJ&2"N Q.pESLINEFROMPOSITIONESPOSITIONFROMLINEU.p ' 1,Q.pESWORDSTARTPOSITION:U.#ws 1,Q.pESWORDENDPOSITION:U.#we diff --git a/lib/getopt.tes b/lib/getopt.tes index 46c813f..4ecfa3f 100644 --- a/lib/getopt.tes +++ b/lib/getopt.tes @@ -1,5 +1,5 @@ !*$ - * M[getopt] -> Success|Failure -- Parse options + * M[getopt] -> First non-flag argument -- Parse options * * Parses command line options according to the string * \(lqoptstring\(rq, similar to \fBgetopt\P(3). @@ -14,37 +14,47 @@ * If the option had an argument, it is stored in the register's * string part. * - * A condition boolean is returned to signify whether - * there was a parsing error. + * Returns the index of the first command-line argument that does not + * belong to a flag or -1 in case of parsing errors. *! [optstring] -@[getopt]{ [: - < - .-Z"= 1; ' 0A-^^-"N :L; F< ' +@[getopt]{ + <%.j + :Q[\.j]:; + 0Q[\.j]--"N 1; ' + !next! - 1A-^^-"= K 1; ' + 1Q[\.j]U.f + Q.f--"= !* -- *! Q.j+1 ' + !* j-th argument is -f *! 0U.i < - :Q[optstring]-Q.i"= ]: 0 ' - Q.iQ[optstring]U.c + Q.iQ[optstring]U.o Q.o"< -1 ' - 0U.#ar < - %.i-:Q[optstring]"= 1; ' - Q.iQ[optstring]-^^:"N 1; ' - %.#ar> + !* count number of : after o *! + 0U.#ar <%.iQ[optstring]-:"N 1; ' %.#ar> - 1A-Q.c"= - -U[getopt.U.c] + Q.f-Q.o"= + -U[getopt.U.o] Q.#ar"> - 2A-10"= - K Q.#ar-1"> 0A-^^-"= 0U.i F< ' ' + 2Q[\.j]"< + !* -f arg *! + %.j + Q.#ar-1"> !* optional arg *! + :Q[\.j]"< Q.j ' + 0Q[\.j]--"= Onext ' + | + :Q[\.j]"< -1 ' + ' + EU[getopt.U.o]Q[\.j] | - 2D + !* -farg *! + EU[getopt.U.o]Q[\.j] + [* EQ[getopt.U.o] 2D ]* ' - LR 0X[getopt.U.c] 0L ' - K 1; + 1; ' > > -]: -1} +Q.j} diff --git a/lib/lexer.tes b/lib/lexer.tes index 2676e1b..af6793a 100644 --- a/lib/lexer.tes +++ b/lib/lexer.tes @@ -5,12 +5,19 @@ [: 0,(1ESPOSITIONFROMLINE:)::S ]: } +!* + * The monospaced font for source code and + * figures in woman-pages. + *! +[lexer.font]Monospace +1200U[lexer.font] + @[lexer.auto]{ - - 0EJ-1"> :Q[lexer.font]"> + 0EJ-1"> 32ESSTYLESETFONTQ[lexer.font] Q[lexer.font],32ESSTYLESETSIZEFRACTIONAL - ' ' + ' :M[color.init] :Q*"= ' [_ @@ -22,7 +29,7 @@ [_ 1ENQ[$SCITECOPATH]/lexers/*.tes ]_ J <:L;R 0X.[filename] 4R .U.p <-A-^^/"= 1; ':R;> .,Q.pX.[name] - EMQ.[filename] + EIQ.[filename] :@EU[lexer.auto]{ :M[lexer.test.Q.[name]]"S :M[lexer.set.Q.[name]] ]_ ' } @@ -32,3 +39,12 @@ :@[lexer.auto]{ ]_ } + +@[lexer.set.cmdline]{ + 0,2048ED + :M[color.init] + !* indicator for rubbed out part of the command line *! + :M[color.default],8ESINDICSETFORE + :M[lexer.set.sciteco] + 2048,0ED +} diff --git a/lib/lexers/bash.tes b/lib/lexers/bash.tes index 862a0c1..893fc8d 100644 --- a/lib/lexers/bash.tes +++ b/lib/lexers/bash.tes @@ -35,10 +35,12 @@ :M[color.number],3M[color.set] :M[color.keyword],4M[color.set] :M[color.string],5M[color.set] - :M[color.string],6M[color.set] + :M[color.string2],6M[color.set] :M[color.operator],7M[color.set] - :M[color.target],8M[color.set] !* Identifiers, e.g. FOO=... *! + !!:M[color.target],8M[color.set] !* Identifiers, e.g. FOO=... *! :M[color.variable],9M[color.set] :M[color.variable],10M[color.set] - :M[color.string2],11M[color.set] !* Backticks *! + :M[color.preproc],11M[color.set] !* Backticks *! + :M[color.target],12M[color.set] !* Heredoc delimiter *! + :M[color.preproc2],13M[color.set] !* Heredoc *! } diff --git a/lib/lexers/batch.tes b/lib/lexers/batch.tes index dddd802..97717a1 100644 --- a/lib/lexers/batch.tes +++ b/lib/lexers/batch.tes @@ -17,7 +17,8 @@ :M[color.keyword],2M[color.set] :M[color.target],3M[color.set] !* Labels *! :M[color.preproc],4M[color.set] !* Hide Cmd @ *! - :M[color.preproc2],5M[color.set] !* External Cmd *! + !!:M[color.preproc2],5M[color.set] !* External Cmd *! :M[color.variable],6M[color.set] :M[color.operator],7M[color.set] + :M[color.preproc2],8M[color.set] !* After label *! } diff --git a/lib/lexers/email.tes b/lib/lexers/email.tes index 895aeea..11dbcf7 100644 --- a/lib/lexers/email.tes +++ b/lib/lexers/email.tes @@ -21,14 +21,25 @@ ZJ [_-:S^J-- ^J]_"S !* signatures *! 4R .U.z ESSTARTSTYLING 1,(:-)ESSETSTYLING + + !* the signature is foldable *! + ESLINEFROMPOSITIONU.l + 1024#(2^*13),Q.lESSETFOLDLEVEL + <:L; 1025,%.lESSETFOLDLEVEL> ' + 0U.l J< .-Q.z"= 1; ' - 0A->"= !* quotes *! - U.s - <:C; 0A- "N 1; '> .-Q.z"= 1; ' + U.s + 0U.q <0A->"N1;' %.q <:C; 0A- "N 1; '>> + + Q.q"> !* quotes *! Q.sESSTARTSTYLING - (0A->"=3|2'),(Q.lESLINELENGTH)ESSETSTYLING + (Q.q-1">3|2'),(Q.lESLINELENGTH)ESSETSTYLING + + !* first line with higher quote level: will be the header *! + (Q.l-1ESGETLINESTATE)-Q.q"< (1024+Q.q-1)#(2^*13) | (1024+Q.q) ',Q.lESSETFOLDLEVEL + Q.q,Q.lESSETLINESTATE ' :L; %.l> ]:} diff --git a/lib/lexers/git.tes b/lib/lexers/git.tes index 8495f12..491ec1e 100644 --- a/lib/lexers/git.tes +++ b/lib/lexers/git.tes @@ -18,6 +18,13 @@ :M[color.comment],1M[color.set] J< .-Z"= 1; ' - 0A-#"= ESSTARTSTYLING 1,(Q.lESLINELENGTH)ESSETSTYLING ' + 0A-#"= + ESSTARTSTYLING 1,(Q.lESLINELENGTH)ESSETSTYLING + + !* only the first line gets the fold level header *! + -2ESGETSTYLEINDEXAT-1"N 1024#(2^*13) | 1025 ',Q.lESSETFOLDLEVEL + ' + !* empty line *! + 0A-10"= 1024#(2^*12),Q.lESSETFOLDLEVEL ' :L; %.l> ]:} diff --git a/lib/lexers/latex.tes b/lib/lexers/latex.tes new file mode 100644 index 0000000..de3d1cf --- /dev/null +++ b/lib/lexers/latex.tes @@ -0,0 +1,32 @@ +!* LaTeX *! + +@[lexer.test.latex]{ + :EN*.texQ*"S -1 ' + :EN*.styQ* +} + +@[lexer.set.latex]{ + ESSETILEXERlatex + + !* command *! + :M[color.keyword],1M[color.set] + !* tag opening *! + :M[color.string],2M[color.set] + !* math inline *! + !!:M[color.preproc2],3M[color.set] + :M[color.comment],4M[color.set] + !* tag closing *! + :M[color.string],5M[color.set] + !* math block *! + !!:M[color.preproc2],6M[color.set] + :M[color.comment],7M[color.set] + !* verbatim segment *! + !!:M[color.string],8M[color.set] + !* short command *! + :M[color.preproc],9M[color.set] + !* special char *! + :M[color.preproc2],10M[color.set] + !* command optional argument *! + :M[color.string2],11M[color.set] + :M[color.error],12M[color.set] +} diff --git a/lib/lexers/make.tes b/lib/lexers/make.tes index 4ff519a..68acda1 100644 --- a/lib/lexers/make.tes +++ b/lib/lexers/make.tes @@ -3,6 +3,7 @@ @[lexer.test.make]{ :EN*/MakefileQ*"S -1 ' :EN*/makefileQ*"S -1 ' + :EN*/GNUmakefileQ*"S -1 ' :EN*.makQ* } @@ -13,5 +14,5 @@ :M[color.variable],3M[color.set] :M[color.operator],4M[color.set] :M[color.target],5M[color.set] - :M[color.error],6M[color.set] + :M[color.error],9M[color.set] } diff --git a/lib/lexers/python.tes b/lib/lexers/python.tes index f85f92b..6a46f1b 100644 --- a/lib/lexers/python.tes +++ b/lib/lexers/python.tes @@ -58,7 +58,9 @@ :M[color.string2],6M[color.set] :M[color.string],7M[color.set] :M[color.operator],10M[color.set] - :M[color.string],12M[color.set] + !!:M[color.variable],11M[color.set] + :M[color.comment],12M[color.set] + :M[color.error],13M[color.set] :M[color.string],16M[color.set] :M[color.string2],17M[color.set] :M[color.string2],18M[color.set] diff --git a/lib/lexers/sciteco.tes b/lib/lexers/sciteco.tes Binary files differindex 7ca9c70..0e7db64 100644 --- a/lib/lexers/sciteco.tes +++ b/lib/lexers/sciteco.tes diff --git a/lib/lexers/woman.tes b/lib/lexers/woman.tes index 86580f3..01b19d5 100644 --- a/lib/lexers/woman.tes +++ b/lib/lexers/woman.tes @@ -10,11 +10,17 @@ :EN*.womanQ* } +!* + * Font used for body text in woman pages. + * This can be a variable-width font. + *! +[lexer.woman.font]Serif + @[lexer.set.woman]{ 1ESSETWRAPMODE 1ESSETWRAPINDENTMODE 10,1#4ESSETYCARETPOLICY - 0EJ-1"> 32ESSTYLESETFONTSerif :M[color.init] ' + 0EJ-1"> 32ESSTYLESETFONTQ[lexer.woman.font] :M[color.init] ' - 1:EN*Q*.tec"S EMQ*.tec ' + 1:EN*Q*.tec"S EIQ*.tec ' } diff --git a/lib/lexers/yaml.tes b/lib/lexers/yaml.tes index 45104e7..2102afa 100644 --- a/lib/lexers/yaml.tes +++ b/lib/lexers/yaml.tes @@ -11,10 +11,13 @@ } @[lexer.set.yaml]{ + 2ESSETTABWIDTH + 0ESSETUSETABS + ESSETILEXERyaml 0ESSETKEYWORDStrue false yes no :M[color.comment],1M[color.set] - :M[color.target],2M[color.set] + :M[color.string],2M[color.set] :M[color.keyword],3M[color.set] :M[color.number],4M[color.set] :M[color.variable],5M[color.set] diff --git a/lib/opener.tes b/lib/opener.tes index 6de6237..c0183c8 100644 --- a/lib/opener.tes +++ b/lib/opener.tes @@ -1,38 +1,38 @@ !*$ - * M[opener] -- Open a number of files from the current buffer + * M[opener] -- Open files, specified on the command line * - * This is usually the unnamed buffer as initialized from the command line. * It supports both the +line[,column] and filename:line[:column] syntaxes. * Since this may make it hard to open files with certain file names, all * filenames after "--" are interpreted verbatim. + * It does not change the current buffer. *! -@[opener]{ - <.-Z; +@[opener]{ [* + <%.i :Q[\.i]:; EQ[\.i] !* --/-S stops processing of special arguments *! - 0A--"= 1A--"= 2A-10"= - L <:L;R 0X.f [* EBN.f ]* L> 1; + 0A--"= 1A--"= 2A"< + <%.i :Q[\.i]:; EBN[\.i]> 1; ''' 1U.l 1U.c !* +line[,column] *! - 0A-+"= - C 0A"D \U.l <0A"DC|1;'> 0A-,"= C \U.c ' 0A-10"=L' ' - ' + 0A-+"= C 0A"D + \U.l <0A"DC|1;'> 0A-,"= C \U.c <0A"DC|1;'> ' + 0A"< %.i Oopen ' + '' !* filename:line[:column][:] *! - LR -A-:"=R' + ZJ -A-:"=R' 0U.p <-%.pA"D|1;'> Q.pA-:"= Q.p+1C \U.a R 0U.p <-%.pA"D|1;'> Q.pA-:"= Q.p+1C \U.l Q.aU.c R | Q.aU.l 1U.c ' - | - LR + !* FIXME: modifies the i *! + .,ZD ' - 0X.f [* - EBN.f Q.c-1,Q.l-1ESFINDCOLUMN:J - ]* - L> -} + !open! + EBN[\.i] Q.c-1,Q.l-1ESFINDCOLUMN:J + > +]* } diff --git a/lib/repl.tes b/lib/repl.tes new file mode 100755 index 0000000..dbbf3ce --- /dev/null +++ b/lib/repl.tes @@ -0,0 +1,55 @@ +#!/usr/local/bin/sciteco -m +!* + * This is a stand-alone script that mimics + * classic TECO command lines. + * Requires an ANSI-compatible terminal. + * + * Currently, you must set the terminal characteristics on the outside: + * stty raw opost icrnl && sciteco -m /usr/local/share/sciteco/lib/repl.tes + * + * You can launch into interactive mode by typing -u#ex + * + * TODO: + * - Catch errors + * - Support *q + *! +0U#ex +< + * + < + U Q:; + !* erase current command line *! + [.c[.l + 0U.i 0U.l :Q<%.i-1Q-10"=%.l'> + Q.l"> [\.lF | 13 ' + [C[J + ].l].c + !* Handle rub out *! + Q-8"= 127U ' + Q-127"= + :Q"> + !* + * Remove last character from reg + * Doesn't require EQ which cannot be reliably undone. + *! + [.i 0U.i :Q-1<%.i-1Q:> ].i + ' + | + Q: + ' + !* Redraw command line *! + [.i[.c + 0U.i :Q< %.i-1QU.c + Q.c-10"= 10 F> ' + Q.c-"= $ F> ' + Q.c-32"< ^,(Q.c#64) | Q.c ' + > + ].c].i + !* FIXME: Catch errors *! + Q-"= + Q#">0U# 10 :M 1;' 1|0 + 'U# + > + ED&2"N1;' !* EX called *! + Q#ex:; +> diff --git a/lib/session.tes b/lib/session.tes Binary files differindex 3048e9e..845db25 100644 --- a/lib/session.tes +++ b/lib/session.tes diff --git a/lib/tecat.tes b/lib/tecat.tes new file mode 100755 index 0000000..5432997 --- /dev/null +++ b/lib/tecat.tes @@ -0,0 +1,22 @@ +#!/usr/local/bin/sciteco -8qm +!* + * Replace all control characters with printable representations as in SciTECO. + * These characters are printed in reverse using ANSI escape sequences. + * This is especially useful as a textconv filter for Git as in + * git config --global diff.teco.textconv "sciteco -8qm /usr/local/share/sciteco/lib/tecat.tes" + * + * You also have to add the following to ~/.gitattributes: + * *.teco_ini diff=teco + * .teco_session diff=teco + * *.tes diff=teco + * *.tec diff=teco + *! +0,2EJ !* FIXME: Memory limiting may be too slow *! +:Q#1"> EBN#1 ' +2 10011000000000Um + +<Q*-1"=|%i-1A'Uc Qc:; + Qc-31"> Qc F< ' Qm&(2^*Qc)"N Qc F< ' + [7m Qc-"=$|^,Qc#64' [27m +> +EX diff --git a/m4/ax_ptrdiff_aliases_int.m4 b/m4/ax_ptrdiff_aliases_int.m4 new file mode 100644 index 0000000..32e1712 --- /dev/null +++ b/m4/ax_ptrdiff_aliases_int.m4 @@ -0,0 +1,18 @@ + +AC_DEFUN([AX_PTRDIFF_ALIASES_INT], [ + AC_CACHE_CHECK([whether ptrdiff_t* aliases int*], [ax_cv_ptrdiff_aliases_int], [ + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[ + @%:@include <stddef.h> + @%:@include <assert.h> + ]], [[ + ptrdiff_t x = 23; + _Static_assert(_Generic(&x, int* : 1, default : 0), + "ptrdiff_t* does not alias int*"); + ]])], + [ax_cv_ptrdiff_aliases_int=yes], + [ax_cv_ptrdiff_aliases_int=no]) + ]) + AS_IF([test "x$ax_cv_ptrdiff_aliases_int" = "xyes"], [ + AC_DEFINE([PTRDIFF_ALIASES_INT], [1], [Whether ptrdiff_t* aliases int*]) + ]) +])dnl diff --git a/m4/ax_with_curses.m4 b/m4/ax_with_curses.m4 deleted file mode 100644 index dcdc129..0000000 --- a/m4/ax_with_curses.m4 +++ /dev/null @@ -1,582 +0,0 @@ -# =========================================================================== -# https://www.gnu.org/software/autoconf-archive/ax_with_curses.html -# =========================================================================== -# -# SYNOPSIS -# -# AX_WITH_CURSES -# -# DESCRIPTION -# -# This macro checks whether a SysV or X/Open-compatible Curses library is -# present, along with the associated header file. The NcursesW -# (wide-character) library is searched for first, followed by Ncurses, -# then the system-default plain Curses. The first library found is the -# one returned. Finding libraries will first be attempted by using -# pkg-config, and should the pkg-config files not be available, will -# fallback to combinations of known flags itself. -# -# The following options are understood: --with-ncursesw, --with-ncurses, -# --without-ncursesw, --without-ncurses. The "--with" options force the -# macro to use that particular library, terminating with an error if not -# found. The "--without" options simply skip the check for that library. -# The effect on the search pattern is: -# -# (no options) - NcursesW, Ncurses, Curses -# --with-ncurses --with-ncursesw - NcursesW only [*] -# --without-ncurses --with-ncursesw - NcursesW only [*] -# --with-ncursesw - NcursesW only [*] -# --with-ncurses --without-ncursesw - Ncurses only [*] -# --with-ncurses - NcursesW, Ncurses [**] -# --without-ncurses --without-ncursesw - Curses only -# --without-ncursesw - Ncurses, Curses -# --without-ncurses - NcursesW, Curses -# -# [*] If the library is not found, abort the configure script. -# -# [**] If the second library (Ncurses) is not found, abort configure. -# -# The following preprocessor symbols may be defined by this macro if the -# appropriate conditions are met: -# -# HAVE_CURSES - if any SysV or X/Open Curses library found -# HAVE_CURSES_ENHANCED - if library supports X/Open Enhanced functions -# HAVE_CURSES_COLOR - if library supports color (enhanced functions) -# HAVE_CURSES_OBSOLETE - if library supports certain obsolete features -# HAVE_NCURSESW - if NcursesW (wide char) library is to be used -# HAVE_NCURSES - if the Ncurses library is to be used -# -# HAVE_CURSES_H - if <curses.h> is present and should be used -# HAVE_NCURSESW_H - if <ncursesw.h> should be used -# HAVE_NCURSES_H - if <ncurses.h> should be used -# HAVE_NCURSESW_CURSES_H - if <ncursesw/curses.h> should be used -# HAVE_NCURSES_CURSES_H - if <ncurses/curses.h> should be used -# -# (These preprocessor symbols are discussed later in this document.) -# -# The following output variables are defined by this macro; they are -# precious and may be overridden on the ./configure command line: -# -# CURSES_LIBS - library to add to xxx_LDADD -# CURSES_CFLAGS - include paths to add to xxx_CPPFLAGS -# -# In previous versions of this macro, the flags CURSES_LIB and -# CURSES_CPPFLAGS were defined. These have been renamed, in keeping with -# AX_WITH_CURSES's close bigger brother, PKG_CHECK_MODULES, which should -# eventually supersede the use of AX_WITH_CURSES. Neither the library -# listed in CURSES_LIBS, nor the flags in CURSES_CFLAGS are added to LIBS, -# respectively CPPFLAGS, by default. You need to add both to the -# appropriate xxx_LDADD/xxx_CPPFLAGS line in your Makefile.am. For -# example: -# -# prog_LDADD = @CURSES_LIBS@ -# prog_CPPFLAGS = @CURSES_CFLAGS@ -# -# If CURSES_LIBS is set on the configure command line (such as by running -# "./configure CURSES_LIBS=-lmycurses"), then the only header searched for -# is <curses.h>. If the user needs to specify an alternative path for a -# library (such as for a non-standard NcurseW), the user should use the -# LDFLAGS variable. -# -# The following shell variables may be defined by this macro: -# -# ax_cv_curses - set to "yes" if any Curses library found -# ax_cv_curses_enhanced - set to "yes" if Enhanced functions present -# ax_cv_curses_color - set to "yes" if color functions present -# ax_cv_curses_obsolete - set to "yes" if obsolete features present -# -# ax_cv_ncursesw - set to "yes" if NcursesW library found -# ax_cv_ncurses - set to "yes" if Ncurses library found -# ax_cv_plaincurses - set to "yes" if plain Curses library found -# ax_cv_curses_which - set to "ncursesw", "ncurses", "plaincurses" or "no" -# -# These variables can be used in your configure.ac to determine the level -# of support you need from the Curses library. For example, if you must -# have either Ncurses or NcursesW, you could include: -# -# AX_WITH_CURSES -# if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then -# AC_MSG_ERROR([requires either NcursesW or Ncurses library]) -# fi -# -# If any Curses library will do (but one must be present and must support -# color), you could use: -# -# AX_WITH_CURSES -# if test "x$ax_cv_curses" != xyes || test "x$ax_cv_curses_color" != xyes; then -# AC_MSG_ERROR([requires an X/Open-compatible Curses library with color]) -# fi -# -# Certain preprocessor symbols and shell variables defined by this macro -# can be used to determine various features of the Curses library. In -# particular, HAVE_CURSES and ax_cv_curses are defined if the Curses -# library found conforms to the traditional SysV and/or X/Open Base Curses -# definition. Any working Curses library conforms to this level. -# -# HAVE_CURSES_ENHANCED and ax_cv_curses_enhanced are defined if the -# library supports the X/Open Enhanced Curses definition. In particular, -# the wide-character types attr_t, cchar_t and wint_t, the functions -# wattr_set() and wget_wch() and the macros WA_NORMAL and _XOPEN_CURSES -# are checked. The Ncurses library does NOT conform to this definition, -# although NcursesW does. -# -# HAVE_CURSES_COLOR and ax_cv_curses_color are defined if the library -# supports color functions and macros such as COLOR_PAIR, A_COLOR, -# COLOR_WHITE, COLOR_RED and init_pair(). These are NOT part of the -# X/Open Base Curses definition, but are part of the Enhanced set of -# functions. The Ncurses library DOES support these functions, as does -# NcursesW. -# -# HAVE_CURSES_OBSOLETE and ax_cv_curses_obsolete are defined if the -# library supports certain features present in SysV and BSD Curses but not -# defined in the X/Open definition. In particular, the functions -# getattrs(), getcurx() and getmaxx() are checked. -# -# To use the HAVE_xxx_H preprocessor symbols, insert the following into -# your system.h (or equivalent) header file: -# -# #if defined HAVE_NCURSESW_CURSES_H -# # include <ncursesw/curses.h> -# #elif defined HAVE_NCURSESW_H -# # include <ncursesw.h> -# #elif defined HAVE_NCURSES_CURSES_H -# # include <ncurses/curses.h> -# #elif defined HAVE_NCURSES_H -# # include <ncurses.h> -# #elif defined HAVE_CURSES_H -# # include <curses.h> -# #else -# # error "SysV or X/Open-compatible Curses header file required" -# #endif -# -# For previous users of this macro: you should not need to change anything -# in your configure.ac or Makefile.am, as the previous (serial 10) -# semantics are still valid. However, you should update your system.h (or -# equivalent) header file to the fragment shown above. You are encouraged -# also to make use of the extended functionality provided by this version -# of AX_WITH_CURSES, as well as in the additional macros -# AX_WITH_CURSES_PANEL, AX_WITH_CURSES_MENU and AX_WITH_CURSES_FORM. -# -# LICENSE -# -# Copyright (c) 2009 Mark Pulford <mark@kyne.com.au> -# Copyright (c) 2009 Damian Pietras <daper@daper.net> -# Copyright (c) 2012 Reuben Thomas <rrt@sc3d.org> -# Copyright (c) 2011 John Zaitseff <J.Zaitseff@zap.org.au> -# -# This program is free software: you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation, either version 3 of the License, or (at your -# option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General -# Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program. If not, see <https://www.gnu.org/licenses/>. -# -# As a special exception, the respective Autoconf Macro's copyright owner -# gives unlimited permission to copy, distribute and modify the configure -# scripts that are the output of Autoconf when processing the Macro. You -# need not follow the terms of the GNU General Public License when using -# or distributing such scripts, even though portions of the text of the -# Macro appear in them. The GNU General Public License (GPL) does govern -# all other use of the material that constitutes the Autoconf Macro. -# -# This special exception to the GPL applies to versions of the Autoconf -# Macro released by the Autoconf Archive. When you make and distribute a -# modified version of the Autoconf Macro, you may extend this special -# exception to the GPL to apply to your modified version as well. - -#serial 18 - -# internal function to factorize common code that is used by both ncurses -# and ncursesw -AC_DEFUN([_FIND_CURSES_FLAGS], [ - AC_MSG_CHECKING([for $1 via pkg-config]) - - AX_REQUIRE_DEFINED([PKG_CHECK_EXISTS]) - _PKG_CONFIG([_ax_cv_$1_libs], [libs], [$1]) - _PKG_CONFIG([_ax_cv_$1_cppflags], [cflags], [$1]) - - AS_IF([test "x$pkg_failed" = "xyes" || test "x$pkg_failed" = "xuntried"],[ - AC_MSG_RESULT([no]) - # No suitable .pc file found, have to find flags via fallback - AC_CACHE_CHECK([for $1 via fallback], [ax_cv_$1], [ - AS_ECHO() - pkg_cv__ax_cv_$1_libs="-l$1" - pkg_cv__ax_cv_$1_cppflags="-D_GNU_SOURCE $CURSES_CFLAGS" - LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" - CPPFLAGS="$ax_saved_CPPFLAGS $pkg_cv__ax_cv_$1_cppflags" - - AC_MSG_CHECKING([for initscr() with $pkg_cv__ax_cv_$1_libs]) - AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])], - [ - AC_MSG_RESULT([yes]) - AC_MSG_CHECKING([for nodelay() with $pkg_cv__ax_cv_$1_libs]) - AC_LINK_IFELSE([AC_LANG_CALL([], [nodelay])],[ - ax_cv_$1=yes - ],[ - AC_MSG_RESULT([no]) - m4_if( - [$1],[ncursesw],[pkg_cv__ax_cv_$1_libs="$pkg_cv__ax_cv_$1_libs -ltinfow"], - [$1],[ncurses],[pkg_cv__ax_cv_$1_libs="$pkg_cv__ax_cv_$1_libs -ltinfo"] - ) - LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" - - AC_MSG_CHECKING([for nodelay() with $pkg_cv__ax_cv_$1_libs]) - AC_LINK_IFELSE([AC_LANG_CALL([], [nodelay])],[ - ax_cv_$1=yes - ],[ - ax_cv_$1=no - ]) - ]) - ],[ - ax_cv_$1=no - ]) - ]) - ],[ - AC_MSG_RESULT([yes]) - # Found .pc file, using its information - LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" - CPPFLAGS="$ax_saved_CPPFLAGS $pkg_cv__ax_cv_$1_cppflags" - ax_cv_$1=yes - ]) -]) - -AU_ALIAS([MP_WITH_CURSES], [AX_WITH_CURSES]) -AC_DEFUN([AX_WITH_CURSES], [ - AC_ARG_VAR([CURSES_LIBS], [linker library for Curses, e.g. -lcurses]) - AC_ARG_VAR([CURSES_CFLAGS], [preprocessor flags for Curses, e.g. -I/usr/include/ncursesw]) - AC_ARG_WITH([ncurses], [AS_HELP_STRING([--with-ncurses], - [force the use of Ncurses or NcursesW])], - [], [with_ncurses=check]) - AC_ARG_WITH([ncursesw], [AS_HELP_STRING([--without-ncursesw], - [do not use NcursesW (wide character support)])], - [], [with_ncursesw=check]) - - ax_saved_LIBS=$LIBS - ax_saved_CPPFLAGS=$CPPFLAGS - - AS_IF([test "x$with_ncurses" = xyes || test "x$with_ncursesw" = xyes], - [ax_with_plaincurses=no], [ax_with_plaincurses=check]) - - ax_cv_curses_which=no - - # Test for NcursesW - AS_IF([test "x$CURSES_LIBS" = x && test "x$with_ncursesw" != xno], [ - _FIND_CURSES_FLAGS([ncursesw]) - - AS_IF([test "x$ax_cv_ncursesw" = xno && test "x$with_ncursesw" = xyes], [ - AC_MSG_ERROR([--with-ncursesw specified but could not find NcursesW library]) - ]) - - AS_IF([test "x$ax_cv_ncursesw" = xyes], [ - ax_cv_curses=yes - ax_cv_curses_which=ncursesw - CURSES_LIBS="$pkg_cv__ax_cv_ncursesw_libs" - CURSES_CFLAGS="$pkg_cv__ax_cv_ncursesw_cppflags" - AC_DEFINE([HAVE_NCURSESW], [1], [Define to 1 if the NcursesW library is present]) - AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present]) - - AC_CACHE_CHECK([for working ncursesw/curses.h], [ax_cv_header_ncursesw_curses_h], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@define _XOPEN_SOURCE_EXTENDED 1 - @%:@include <ncursesw/curses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - attr_t d = WA_NORMAL; - cchar_t e; - wint_t f; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - wattr_set(stdscr, d, 0, NULL); - wget_wch(stdscr, &f); - ]])], - [ax_cv_header_ncursesw_curses_h=yes], - [ax_cv_header_ncursesw_curses_h=no]) - ]) - AS_IF([test "x$ax_cv_header_ncursesw_curses_h" = xyes], [ - ax_cv_curses_enhanced=yes - ax_cv_curses_color=yes - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions]) - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - AC_DEFINE([HAVE_NCURSESW_CURSES_H], [1], [Define to 1 if <ncursesw/curses.h> is present]) - ]) - - AC_CACHE_CHECK([for working ncursesw.h], [ax_cv_header_ncursesw_h], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@define _XOPEN_SOURCE_EXTENDED 1 - @%:@include <ncursesw.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - attr_t d = WA_NORMAL; - cchar_t e; - wint_t f; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - wattr_set(stdscr, d, 0, NULL); - wget_wch(stdscr, &f); - ]])], - [ax_cv_header_ncursesw_h=yes], - [ax_cv_header_ncursesw_h=no]) - ]) - AS_IF([test "x$ax_cv_header_ncursesw_h" = xyes], [ - ax_cv_curses_enhanced=yes - ax_cv_curses_color=yes - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions]) - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - AC_DEFINE([HAVE_NCURSESW_H], [1], [Define to 1 if <ncursesw.h> is present]) - ]) - - AC_CACHE_CHECK([for working ncurses.h], [ax_cv_header_ncurses_h_with_ncursesw], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@define _XOPEN_SOURCE_EXTENDED 1 - @%:@include <ncurses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - attr_t d = WA_NORMAL; - cchar_t e; - wint_t f; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - wattr_set(stdscr, d, 0, NULL); - wget_wch(stdscr, &f); - ]])], - [ax_cv_header_ncurses_h_with_ncursesw=yes], - [ax_cv_header_ncurses_h_with_ncursesw=no]) - ]) - AS_IF([test "x$ax_cv_header_ncurses_h_with_ncursesw" = xyes], [ - ax_cv_curses_enhanced=yes - ax_cv_curses_color=yes - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions]) - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - AC_DEFINE([HAVE_NCURSES_H], [1], [Define to 1 if <ncurses.h> is present]) - ]) - - AS_IF([test "x$ax_cv_header_ncursesw_curses_h" = xno && test "x$ax_cv_header_ncursesw_h" = xno && test "x$ax_cv_header_ncurses_h_with_ncursesw" = xno], [ - AC_MSG_WARN([could not find a working ncursesw/curses.h, ncursesw.h or ncurses.h]) - ]) - ]) - ]) - unset pkg_cv__ax_cv_ncursesw_libs - unset pkg_cv__ax_cv_ncursesw_cppflags - - # Test for Ncurses - AS_IF([test "x$CURSES_LIBS" = x && test "x$with_ncurses" != xno && test "x$ax_cv_curses_which" = xno], [ - _FIND_CURSES_FLAGS([ncurses]) - - AS_IF([test "x$ax_cv_ncurses" = xno && test "x$with_ncurses" = xyes], [ - AC_MSG_ERROR([--with-ncurses specified but could not find Ncurses library]) - ]) - - AS_IF([test "x$ax_cv_ncurses" = xyes], [ - ax_cv_curses=yes - ax_cv_curses_which=ncurses - CURSES_LIBS="$pkg_cv__ax_cv_ncurses_libs" - CURSES_CFLAGS="$pkg_cv__ax_cv_ncurses_cppflags" - AC_DEFINE([HAVE_NCURSES], [1], [Define to 1 if the Ncurses library is present]) - AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present]) - - AC_CACHE_CHECK([for working ncurses/curses.h], [ax_cv_header_ncurses_curses_h], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@include <ncurses/curses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - ]])], - [ax_cv_header_ncurses_curses_h=yes], - [ax_cv_header_ncurses_curses_h=no]) - ]) - AS_IF([test "x$ax_cv_header_ncurses_curses_h" = xyes], [ - ax_cv_curses_color=yes - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - AC_DEFINE([HAVE_NCURSES_CURSES_H], [1], [Define to 1 if <ncurses/curses.h> is present]) - ]) - - AC_CACHE_CHECK([for working ncurses.h], [ax_cv_header_ncurses_h], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@include <ncurses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - ]])], - [ax_cv_header_ncurses_h=yes], - [ax_cv_header_ncurses_h=no]) - ]) - AS_IF([test "x$ax_cv_header_ncurses_h" = xyes], [ - ax_cv_curses_color=yes - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - AC_DEFINE([HAVE_NCURSES_H], [1], [Define to 1 if <ncurses.h> is present]) - ]) - - AS_IF([test "x$ax_cv_header_ncurses_curses_h" = xno && test "x$ax_cv_header_ncurses_h" = xno], [ - AC_MSG_WARN([could not find a working ncurses/curses.h or ncurses.h]) - ]) - ]) - ]) - unset pkg_cv__ax_cv_ncurses_libs - unset pkg_cv__ax_cv_ncurses_cppflags - - # Test for plain Curses (or if CURSES_LIBS was set by user) - AS_IF([test "x$with_plaincurses" != xno && test "x$ax_cv_curses_which" = xno], [ - AS_IF([test "x$CURSES_LIBS" != x], [ - LIBS="$ax_saved_LIBS $CURSES_LIBS" - ], [ - LIBS="$ax_saved_LIBS -lcurses" - ]) - - AC_CACHE_CHECK([for Curses library], [ax_cv_plaincurses], [ - AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])], - [ax_cv_plaincurses=yes], [ax_cv_plaincurses=no]) - ]) - - AS_IF([test "x$ax_cv_plaincurses" = xyes], [ - ax_cv_curses=yes - ax_cv_curses_which=plaincurses - AS_IF([test "x$CURSES_LIBS" = x], [ - CURSES_LIBS="-lcurses" - ]) - AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present]) - - # Check for base conformance (and header file) - - AC_CACHE_CHECK([for working curses.h], [ax_cv_header_curses_h], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@include <curses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - initscr(); - ]])], - [ax_cv_header_curses_h=yes], - [ax_cv_header_curses_h=no]) - ]) - AS_IF([test "x$ax_cv_header_curses_h" = xyes], [ - AC_DEFINE([HAVE_CURSES_H], [1], [Define to 1 if <curses.h> is present]) - - # Check for X/Open Enhanced conformance - - AC_CACHE_CHECK([for X/Open Enhanced Curses conformance], [ax_cv_plaincurses_enhanced], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@define _XOPEN_SOURCE_EXTENDED 1 - @%:@include <curses.h> - @%:@ifndef _XOPEN_CURSES - @%:@error "this Curses library is not enhanced" - "this Curses library is not enhanced" - @%:@endif - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - attr_t d = WA_NORMAL; - cchar_t e; - wint_t f; - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - wattr_set(stdscr, d, 0, NULL); - wget_wch(stdscr, &f); - ]])], - [ax_cv_plaincurses_enhanced=yes], - [ax_cv_plaincurses_enhanced=no]) - ]) - AS_IF([test "x$ax_cv_plaincurses_enhanced" = xyes], [ - ax_cv_curses_enhanced=yes - ax_cv_curses_color=yes - AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions]) - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - ]) - - # Check for color functions - - AC_CACHE_CHECK([for Curses color functions], [ax_cv_plaincurses_color], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@define _XOPEN_SOURCE_EXTENDED 1 - @%:@include <curses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - chtype c = COLOR_PAIR(1) & A_COLOR; - initscr(); - init_pair(1, COLOR_WHITE, COLOR_RED); - ]])], - [ax_cv_plaincurses_color=yes], - [ax_cv_plaincurses_color=no]) - ]) - AS_IF([test "x$ax_cv_plaincurses_color" = xyes], [ - ax_cv_curses_color=yes - AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) - ]) - - # Check for obsolete functions - - AC_CACHE_CHECK([for obsolete Curses functions], [ax_cv_plaincurses_obsolete], [ - AC_LINK_IFELSE([AC_LANG_PROGRAM([[ - @%:@include <curses.h> - ]], [[ - chtype a = A_BOLD; - int b = KEY_LEFT; - int g = getattrs(stdscr); - int h = getcurx(stdscr) + getmaxx(stdscr); - initscr(); - ]])], - [ax_cv_plaincurses_obsolete=yes], - [ax_cv_plaincurses_obsolete=no]) - ]) - AS_IF([test "x$ax_cv_plaincurses_obsolete" = xyes], [ - ax_cv_curses_obsolete=yes - AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) - ]) - ]) - - AS_IF([test "x$ax_cv_header_curses_h" = xno], [ - AC_MSG_WARN([could not find a working curses.h]) - ]) - ]) - ]) - - AS_IF([test "x$ax_cv_curses" != xyes], [ax_cv_curses=no]) - AS_IF([test "x$ax_cv_curses_enhanced" != xyes], [ax_cv_curses_enhanced=no]) - AS_IF([test "x$ax_cv_curses_color" != xyes], [ax_cv_curses_color=no]) - AS_IF([test "x$ax_cv_curses_obsolete" != xyes], [ax_cv_curses_obsolete=no]) - - LIBS=$ax_saved_LIBS - CPPFLAGS=$ax_saved_CPPFLAGS - - unset ax_saved_LIBS - unset ax_saved_CPPFLAGS -])dnl diff --git a/m4/ax_with_ncurses.m4 b/m4/ax_with_ncurses.m4 new file mode 100644 index 0000000..4dc2f33 --- /dev/null +++ b/m4/ax_with_ncurses.m4 @@ -0,0 +1,253 @@ +# SYNOPSIS +# +# AX_WITH_NCURSES +# +# DESCRIPTION +# +# This macro checks for an ncurses library with enhanced definitions +# providing a curses.h either in the default search path or as +# established by pkg-config. +# +# It is based on the AX_WITH_CURSES macro but does not attempt +# to find any non-standard header, which would require #ifdefing +# over all the possible headers or using a computed #include. +# This would complicate ncurses detection unnecessarily especially +# since Scinterm needs to include a curses header as well. +# ncurses packages, which do not ship a pkg-config script and +# do not install curses.h into a standard include path +# (e.g. NetBSD's pkgsrc ncurses package) will *not* be detected +# by this macro unless defining CURSES_CFLAGS. +# +# The following preprocessor symbols may be defined by this macro if the +# appropriate conditions are met: +# +# HAVE_CURSES - if any SysV or X/Open Curses library found +# HAVE_CURSES_ENHANCED - if library supports X/Open Enhanced functions +# HAVE_CURSES_COLOR - if library supports color (enhanced functions) +# HAVE_CURSES_OBSOLETE - if library supports certain obsolete features +# HAVE_NCURSESW - if NcursesW (wide char) library is to be used +# HAVE_NCURSES - if the Ncurses library is to be used +# +# HAVE_CURSES_H - if <curses.h> is present and should be used +# +# (These preprocessor symbols are discussed later in this document.) +# +# The following output variables are defined by this macro; they are +# precious and may be overridden on the ./configure command line: +# +# CURSES_LIBS - library to add to xxx_LDADD +# CURSES_CFLAGS - include paths to add to xxx_CPPFLAGS +# +# If CURSES_LIBS is set on the configure command line (such as by running +# "./configure CURSES_LIBS=-lmycurses"), then the only header searched for +# is <curses.h>. If the user needs to specify an alternative path for a +# library (such as for a non-standard NcurseW), the user should use the +# LDFLAGS variable. +# +# The following shell variables may be defined by this macro: +# +# ax_cv_curses - set to "yes" if any Curses library found +# ax_cv_curses_enhanced - set to "yes" if Enhanced functions present +# ax_cv_curses_color - set to "yes" if color functions present +# ax_cv_curses_obsolete - set to "yes" if obsolete features present +# +# ax_cv_ncursesw - set to "yes" if NcursesW library found +# ax_cv_ncurses - set to "yes" if Ncurses library found +# ax_cv_curses_which - set to "ncursesw", "ncurses", "plaincurses" or "no" +# +# These variables can be used in your configure.ac to determine the level +# of support you need from the Curses library. For example, if you must +# have either Ncurses or NcursesW, you could include: +# +# AX_WITH_NCURSES +# if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then +# AC_MSG_ERROR([requires either NcursesW or Ncurses library]) +# fi +# +# HAVE_CURSES_OBSOLETE and ax_cv_curses_obsolete are defined if the +# library supports certain features present in SysV and BSD Curses but not +# defined in the X/Open definition. In particular, the functions +# getattrs(), getcurx() and getmaxx() are checked. +# +# LICENSE +# +# Copyright (c) 2009 Mark Pulford <mark@kyne.com.au> +# Copyright (c) 2009 Damian Pietras <daper@daper.net> +# Copyright (c) 2012 Reuben Thomas <rrt@sc3d.org> +# Copyright (c) 2011 John Zaitseff <J.Zaitseff@zap.org.au> +# Copyright (c) 2025-2026 Robin Haberkorn <rhaberkorn@fmsbw.de> +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# +# As a special exception, the respective Autoconf Macro's copyright owner +# gives unlimited permission to copy, distribute and modify the configure +# scripts that are the output of Autoconf when processing the Macro. You +# need not follow the terms of the GNU General Public License when using +# or distributing such scripts, even though portions of the text of the +# Macro appear in them. The GNU General Public License (GPL) does govern +# all other use of the material that constitutes the Autoconf Macro. +# +# This special exception to the GPL applies to versions of the Autoconf +# Macro released by the Autoconf Archive. When you make and distribute a +# modified version of the Autoconf Macro, you may extend this special +# exception to the GPL to apply to your modified version as well. + +#serial 18 + +# internal function to factorize common code that is used by both ncurses +# and ncursesw +AC_DEFUN([_FIND_CURSES_FLAGS], [ + AC_MSG_CHECKING([for $1 via pkg-config]) + + AX_REQUIRE_DEFINED([PKG_CHECK_EXISTS]) + pkg_failed=no + _PKG_CONFIG([_ax_cv_$1_libs], [libs], [$1]) + _PKG_CONFIG([_ax_cv_$1_cppflags], [cflags], [$1]) + + AS_IF([test "x$pkg_failed" = "xyes" || test "x$pkg_failed" = "xuntried"],[ + AC_MSG_RESULT([no]) + # No suitable .pc file found, have to find flags via fallback + AC_CACHE_CHECK([for $1 via fallback], [ax_cv_$1], [ + AS_ECHO() + pkg_cv__ax_cv_$1_libs="-l$1" + pkg_cv__ax_cv_$1_cppflags="-D_GNU_SOURCE $CURSES_CFLAGS" + LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" + CPPFLAGS="$ax_saved_CPPFLAGS $pkg_cv__ax_cv_$1_cppflags" + + AC_MSG_CHECKING([for initscr() with $pkg_cv__ax_cv_$1_libs]) + AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])], + [ + AC_MSG_RESULT([yes]) + AC_MSG_CHECKING([for nodelay() with $pkg_cv__ax_cv_$1_libs]) + AC_LINK_IFELSE([AC_LANG_CALL([], [nodelay])],[ + ax_cv_$1=yes + ],[ + AC_MSG_RESULT([no]) + m4_if( + [$1],[ncursesw],[pkg_cv__ax_cv_$1_libs="$pkg_cv__ax_cv_$1_libs -ltinfow"], + [$1],[ncurses],[pkg_cv__ax_cv_$1_libs="$pkg_cv__ax_cv_$1_libs -ltinfo"] + ) + LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" + + AC_MSG_CHECKING([for nodelay() with $pkg_cv__ax_cv_$1_libs]) + AC_LINK_IFELSE([AC_LANG_CALL([], [nodelay])],[ + ax_cv_$1=yes + ],[ + ax_cv_$1=no + ]) + ]) + ],[ + ax_cv_$1=no + ]) + ]) + ],[ + AC_MSG_RESULT([yes]) + # Found .pc file, using its information + LIBS="$ax_saved_LIBS $pkg_cv__ax_cv_$1_libs" + CPPFLAGS="$ax_saved_CPPFLAGS $pkg_cv__ax_cv_$1_cppflags" + ax_cv_$1=yes + ]) +]) + +AU_ALIAS([MP_WITH_NCURSES], [AX_WITH_NCURSES]) +AC_DEFUN([AX_WITH_NCURSES], [ + AC_ARG_VAR([CURSES_LIBS], [linker library for Curses, e.g. -lcurses]) + AC_ARG_VAR([CURSES_CFLAGS], [preprocessor flags for Curses, e.g. -I/usr/include/ncursesw]) + + ax_saved_LIBS=$LIBS + ax_saved_CPPFLAGS=$CPPFLAGS + + # Test for NcursesW + AS_IF([test "x$CURSES_LIBS" = x], [ + _FIND_CURSES_FLAGS([ncursesw]) + AS_IF([test "x$ax_cv_ncursesw" = xyes], [ + ax_cv_curses=yes + ax_cv_curses_which=ncursesw + CURSES_LIBS="$pkg_cv__ax_cv_ncursesw_libs" + CURSES_CFLAGS="$pkg_cv__ax_cv_ncursesw_cppflags" + unset pkg_cv__ax_cv_ncursesw_libs + unset pkg_cv__ax_cv_ncursesw_cppflags + ], [ + _FIND_CURSES_FLAGS([ncurses]) + AS_IF([test "x$ax_cv_ncurses" = xyes], [ + ax_cv_curses=yes + ax_cv_curses_which=ncurses + CURSES_LIBS="$pkg_cv__ax_cv_ncurses_libs" + CURSES_CFLAGS="$pkg_cv__ax_cv_ncurses_cppflags" + unset pkg_cv__ax_cv_ncurses_libs + unset pkg_cv__ax_cv_ncurses_cppflags + ]) + ]) + ], [ + ax_cv_curses=yes + ax_cv_curses_which=ncurses + LIBS="$ax_saved_LIBS $CURSES_LIBS" + CPPFLAGS="$ax_saved_CPPFLAGS $CURSES_CFLAGS" + ]) + + AS_IF([test "x$ax_cv_curses" = xyes], [ + AC_DEFINE([HAVE_NCURSESW], [1], [Define to 1 if the NcursesW library is present]) + AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present]) + + AC_CACHE_CHECK([for working curses.h], [ax_cv_header_curses_h], [ + AC_LINK_IFELSE([AC_LANG_PROGRAM([[ + @%:@define _XOPEN_SOURCE_EXTENDED 1 + @%:@include <curses.h> + @%:@ifndef _XOPEN_CURSES + @%:@error "this Curses library is not enhanced" + @%:@endif + @%:@ifndef NCURSES_VERSION + @%:@error "the curses library is not an ncurses" + @%:@endif + ]], [[ + chtype a = A_BOLD; + int b = KEY_LEFT; + chtype c = COLOR_PAIR(1) & A_COLOR; + attr_t d = WA_NORMAL; + cchar_t e; + wint_t f; + int g = getattrs(stdscr); + int h = getcurx(stdscr) + getmaxx(stdscr); + initscr(); + init_pair(1, COLOR_WHITE, COLOR_RED); + wattr_set(stdscr, d, 0, NULL); + wget_wch(stdscr, &f); + ]])], + [ax_cv_header_curses_h=yes], + [ax_cv_header_curses_h=no]) + ]) + AS_IF([test "x$ax_cv_header_curses_h" = xyes], [ + ax_cv_curses_enhanced=yes + ax_cv_curses_color=yes + ax_cv_curses_obsolete=yes + AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions]) + AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)]) + AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features]) + AC_DEFINE([HAVE_CURSES_H], [1], [Define to 1 if <curses.h> is present]) + ], [ + AC_MSG_WARN([could not find a working curses.h]) + ]) + ]) + + AS_IF([test "x$ax_cv_curses" != xyes], [ax_cv_curses=no]) + AS_IF([test "x$ax_cv_curses_enhanced" != xyes], [ax_cv_curses_enhanced=no]) + AS_IF([test "x$ax_cv_curses_color" != xyes], [ax_cv_curses_color=no]) + AS_IF([test "x$ax_cv_curses_obsolete" != xyes], [ax_cv_curses_obsolete=no]) + + LIBS=$ax_saved_LIBS + CPPFLAGS=$ax_saved_CPPFLAGS + + unset ax_saved_LIBS + unset ax_saved_CPPFLAGS +])dnl diff --git a/src/Makefile.am b/src/Makefile.am index e7a8545..ff2e86b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -45,6 +45,7 @@ libsciteco_base_la_SOURCES = main.c sciteco.h list.h \ parser.c parser.h \ core-commands.c core-commands.h \ move-commands.c move-commands.h \ + stdio-commands.c stdio-commands.h \ search.c search.h \ spawn.c spawn.h \ glob.c glob.h \ diff --git a/src/cmdline.c b/src/cmdline.c index b03f72a..fa69d91 100644 --- a/src/cmdline.c +++ b/src/cmdline.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,17 +61,49 @@ int malloc_trim(size_t pad); #define TECO_DEFAULT_BREAK_CHARS " \t\v\r\n\f<>,;@" -teco_cmdline_t teco_cmdline = {}; +/** Style used for the asterisk at the beginning of the command line */ +#define STYLE_ASTERISK 64 -/* - * FIXME: Should this be here? - * Should perhaps rather be in teco_machine_main_t or teco_cmdline_t. - */ -gboolean teco_quit_requested = FALSE; +teco_cmdline_t teco_cmdline = { + .height = 1 +}; -/** Last terminated command line */ +/** + * Last terminated command line. + * This is not a teco_doc_scintilla_t since we have to return it as a string + * at the end of the day. + */ static teco_string_t teco_last_cmdline = {NULL, 0}; +void +teco_cmdline_init(void) +{ + teco_cmdline.view = teco_view_new(); + teco_view_setup(teco_cmdline.view); + + teco_cmdline_ssm(SCI_SETUNDOCOLLECTION, FALSE, 0); + teco_cmdline_ssm(SCI_SETVSCROLLBAR, FALSE, 0); + teco_cmdline_ssm(SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE); + teco_cmdline_ssm(SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT); + teco_cmdline_ssm(SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK); + teco_cmdline_ssm(SCI_SETMARGINWIDTHN, 1, + teco_cmdline_ssm(SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*")); + /* NOTE: might not work on all UIs */ + teco_cmdline_ssm(SCI_INDICSETSTYLE, INDICATOR_RUBBEDOUT, INDIC_STRIKE); + teco_cmdline_ssm(SCI_INDICSETFORE, INDICATOR_RUBBEDOUT, + teco_cmdline_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0)); + + /* single line mode - EOL characters won't break the line */ + teco_cmdline_ssm(SCI_SETLINEENDTYPESALLOWED, SC_LINE_END_TYPE_NONE, 0); + /* render tabs as "TAB" without indentation */ + teco_cmdline_ssm(SCI_SETTABDRAWMODE, SCTD_CONTROLCHAR, 0); + + /* + * FIXME: Something resets the margin text, so we have to set it last. + */ + teco_cmdline_ssm(SCI_MARGINSETTEXT, 0, (sptr_t)"*"); +} + /** * Insert string into command line and execute * it immediately. @@ -89,40 +121,49 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error) g_auto(teco_string_t) old_cmdline = {NULL, 0}; gsize repl_pc = 0; - teco_cmdline.machine.macro_pc = teco_cmdline.pc = teco_cmdline.effective_len; + gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_cmdline.machine.macro_pc = teco_cmdline.pc = effective_len; + + const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); - if (len <= teco_cmdline.str.len - teco_cmdline.effective_len && - !teco_string_cmp(&src, teco_cmdline.str.data + teco_cmdline.effective_len, len)) { - teco_cmdline.effective_len += len; + if (len <= macro_len - effective_len && + !teco_string_cmp(src, macro + effective_len, len)) { + /* extend effective command line from rubbed out part */ + teco_cmdline_ssm(SCI_GOTOPOS, effective_len+len, 0); } else { - if (teco_cmdline.effective_len < teco_cmdline.str.len) + /* discard rubbed out part of the command line */ + if (effective_len < macro_len) /* * Automatically disable immediate editing modifier. * FIXME: Should we show a message as when pressing ^G? */ teco_cmdline.modifier_enabled = FALSE; - teco_cmdline.str.len = teco_cmdline.effective_len; - teco_string_append(&teco_cmdline.str, data, len); - teco_cmdline.effective_len = teco_cmdline.str.len; + teco_cmdline_ssm(SCI_DELETERANGE, effective_len, macro_len - effective_len); + teco_cmdline_ssm(SCI_ADDTEXT, len, (sptr_t)data); + + /* the pointer shouldn't have changed... */ + macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); } + effective_len += len; /* * Parse/execute characters, one at a time so * undo tokens get emitted for the corresponding characters. */ - while (teco_cmdline.pc < teco_cmdline.effective_len) { + while (teco_cmdline.pc < effective_len) { g_autoptr(GError) tmp_error = NULL; - if (!teco_machine_main_step(&teco_cmdline.machine, teco_cmdline.str.data, - teco_cmdline.pc+1, &tmp_error)) { + if (!teco_machine_main_step(&teco_cmdline.machine, macro, teco_cmdline.pc+1, &tmp_error)) { if (g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_CMDLINE)) { /* * Result of command line replacement (}): - * Exchange command lines, avoiding deep copying + * Exchange command lines */ teco_qreg_t *cmdline_reg = teco_qreg_table_find(&teco_qreg_table_globals, "\e", 1); - teco_string_t new_cmdline; + g_auto(teco_string_t) new_cmdline = {NULL, 0}; if (!cmdline_reg->vtable->get_string(cmdline_reg, &new_cmdline.data, &new_cmdline.len, NULL, error)) @@ -133,16 +174,26 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error) * new command line. This avoids unnecessary rubouts * and insertions when the command line is updated. */ - teco_cmdline.pc = teco_string_diff(&teco_cmdline.str, new_cmdline.data, new_cmdline.len); + teco_cmdline.pc = teco_string_diff(new_cmdline, macro, effective_len); teco_undo_pop(teco_cmdline.pc); + /* + * We don't replace the command line's document, since that would + * reset the line end type and other configurable settings. + * Also, we don't clear the document to avoid unnecessary restylings + * if syntax highlighting is enabled on the command line. + */ g_assert(old_cmdline.len == 0); - old_cmdline = teco_cmdline.str; - teco_cmdline.str = new_cmdline; - teco_cmdline.effective_len = new_cmdline.len; + teco_string_init(&old_cmdline, macro, effective_len); + teco_cmdline_ssm(SCI_DELETERANGE, teco_cmdline.pc, + old_cmdline.len-teco_cmdline.pc); + teco_cmdline_ssm(SCI_ADDTEXT, new_cmdline.len-teco_cmdline.pc, + (sptr_t)new_cmdline.data+teco_cmdline.pc); + + macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + macro_len = effective_len = new_cmdline.len; teco_cmdline.machine.macro_pc = repl_pc = teco_cmdline.pc; - continue; } @@ -154,17 +205,26 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error) /* * Error during command-line replacement. * Replay previous command-line. - * This avoids deep copying. + * The commands leading up to the failed replacement + * will be left rubbed out. */ teco_undo_pop(repl_pc); - teco_string_clear(&teco_cmdline.str); - teco_cmdline.str = old_cmdline; + /* + * May cause restyling of the command lines, + * but that's probably okay - it's just a fallback. + */ + teco_cmdline_ssm(SCI_CLEARALL, 0, 0); + teco_cmdline_ssm(SCI_ADDTEXT, old_cmdline.len, (sptr_t)old_cmdline.data); + teco_string_clear(&old_cmdline); memset(&old_cmdline, 0, sizeof(old_cmdline)); teco_cmdline.machine.macro_pc = teco_cmdline.pc = repl_pc; - /* rubout cmdline replacement command */ - teco_cmdline.effective_len--; + /* rub out cmdline replacement command */ + teco_cmdline_ssm(SCI_GOTOPOS, --effective_len, 0); + + macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); continue; } } @@ -183,14 +243,18 @@ teco_cmdline_insert(const gchar *data, gsize len, GError **error) static gboolean teco_cmdline_rubin(GError **error) { - if (!teco_cmdline.str.len) - return TRUE; - - const gchar *start, *end, *next; - start = teco_cmdline.str.data+teco_cmdline.effective_len; - end = teco_cmdline.str.data+teco_cmdline.str.len; - next = g_utf8_find_next_char(start, end) ? : end; - return teco_cmdline_insert(start, next-start, error); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); + gsize pos = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); + gchar buf[4+1]; + struct Sci_TextRangeFull range = { + .chrg = {pos, MIN(macro_len, pos+sizeof(buf)-1)}, + .lpstrText = buf + }; + gsize len = teco_cmdline_ssm(SCI_GETTEXTRANGEFULL, 0, (sptr_t)&range); + + const gchar *end = buf+len; + const gchar *next = g_utf8_find_next_char(buf, end) ? : end; + return teco_cmdline_insert(buf, next-buf, error); } /** @@ -211,10 +275,9 @@ teco_cmdline_rubin(GError **error) gboolean teco_cmdline_keypress(const gchar *data, gsize len, GError **error) { - const teco_string_t str = {(gchar *)data, len}; teco_machine_t *machine = &teco_cmdline.machine.parent; - if (!teco_string_validate_utf8(&str)) { + if (!teco_string_validate_utf8((teco_string_t){(gchar *)data, len})) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT, "Invalid UTF-8 sequence"); return FALSE; @@ -225,7 +288,7 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error) */ teco_interface_msg_clear(); - gsize start_pc = teco_cmdline.effective_len; + gsize start_pc = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); for (guint i = 0; i < len; i = g_utf8_next_char(data+i) - data) { gunichar chr = g_utf8_get_char(data+i); @@ -252,9 +315,9 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error) * up until the insertion point. */ teco_undo_pop(start_pc); - teco_cmdline.effective_len = start_pc; + teco_cmdline_ssm(SCI_GOTOPOS, start_pc, 0); /* program counter could be messed up */ - teco_cmdline.machine.macro_pc = teco_cmdline.effective_len; + teco_cmdline.machine.macro_pc = start_pc; #ifdef HAVE_MALLOC_TRIM /* @@ -285,7 +348,7 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error) teco_interface_popup_clear(); - if (teco_quit_requested) { + if (teco_ed & TECO_ED_EXIT) { /* caught by user interface */ g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); return FALSE; @@ -303,9 +366,16 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error) g_array_remove_range(teco_loop_stack, 0, teco_loop_stack->len); teco_string_clear(&teco_last_cmdline); - teco_last_cmdline = teco_cmdline.str; - memset(&teco_cmdline.str, 0, sizeof(teco_cmdline.str)); - teco_cmdline.effective_len = 0; + teco_last_cmdline.len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_last_cmdline.data = g_malloc(teco_last_cmdline.len + 1); + teco_cmdline_ssm(SCI_GETTEXT, teco_last_cmdline.len, + (sptr_t)teco_last_cmdline.data); + /* + * FIXME: Preserve the command line after the $$. + * This would be useful for command line editing macros. + * Perhaps just call teco_cmdline_insert(). + */ + teco_cmdline_ssm(SCI_CLEARALL, 0, 0); #ifdef HAVE_MALLOC_TRIM /* see above */ @@ -320,10 +390,8 @@ teco_cmdline_keypress(const gchar *data, gsize len, GError **error) start_pc = 0; } - /* - * Echo command line - */ - teco_interface_cmdline_update(&teco_cmdline); + teco_cmdline_update(); + return TRUE; } @@ -380,20 +448,48 @@ teco_cmdline_keymacro(const gchar *name, gssize name_len, GError **error) static void teco_cmdline_rubout(void) { - const gchar *p; - p = g_utf8_find_prev_char(teco_cmdline.str.data, - teco_cmdline.str.data+teco_cmdline.effective_len); - if (p) { - teco_cmdline.effective_len = p - teco_cmdline.str.data; - teco_undo_pop(teco_cmdline.effective_len); + gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); + gssize p = teco_view_glyphs2bytes_relative(teco_cmdline.view, effective_len, -1); + if (p >= 0) { + teco_cmdline_ssm(SCI_GOTOPOS, p, 0); + teco_undo_pop(p); } } -static void TECO_DEBUG_CLEANUP +/** + * Update the command line, i.e. prepare it for displaying. + * + * This updates the indicators and scrolls the caret, which isn't done every time + * we touch the command line itself. + */ +void +teco_cmdline_update(void) +{ + /* + * FIXME: Perhaps this can be avoided completely by updating the + * indicators in teco_cmdline_insert(). + */ + gsize effective_len = teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); + teco_cmdline_ssm(SCI_SETINDICATORCURRENT, INDICATOR_RUBBEDOUT, 0); + teco_cmdline_ssm(SCI_INDICATORCLEARRANGE, 0, macro_len); + teco_cmdline_ssm(SCI_INDICATORFILLRANGE, effective_len, macro_len - effective_len); + + teco_cmdline_ssm(SCI_SCROLLCARET, 0, 0); + + /* + * FIXME: This gets reset repeatedly. + * Setting it once per keypress however means you can no longer customize + * the margin text. + */ + teco_cmdline_ssm(SCI_MARGINSETTEXT, 0, (sptr_t)"*"); +} + +void teco_cmdline_cleanup(void) { teco_machine_main_clear(&teco_cmdline.machine); - teco_string_clear(&teco_cmdline.str); + teco_view_free(teco_cmdline.view); teco_string_clear(&teco_last_cmdline); } @@ -451,11 +547,12 @@ teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gun if (teco_cmdline.modifier_enabled) { /* reinsert construct */ + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); do { if (!teco_cmdline_rubin(error)) return FALSE; } while (!ctx->current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len); + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len); } else { /* rubout construct */ do @@ -475,6 +572,11 @@ teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gun raise(SIGTSTP); return TRUE; #endif + + case TECO_CTL_KEY('L'): + /* causes a complete screen redraw */ + teco_interface_refresh(TRUE); + return TRUE; } teco_interface_popup_clear(); @@ -506,6 +608,9 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa case TECO_CTL_KEY('W'): /* rubout/reinsert command */ teco_interface_popup_clear(); + const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); + /* * This mimics the behavior of the `Y` command, * so it also rubs out no-op commands. @@ -515,9 +620,8 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa /* reinsert command */ /* @ and : are not separate states, but practically belong to the command */ while (ctx->parent.current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len && - (teco_cmdline.str.data[teco_cmdline.effective_len] == ':' || - teco_cmdline.str.data[teco_cmdline.effective_len] == '@')) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + strchr(":@", macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]) != NULL) if (!teco_cmdline_rubin(error)) return FALSE; @@ -525,11 +629,11 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa if (!teco_cmdline_rubin(error)) return FALSE; } while (!ctx->parent.current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len); + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len); while (ctx->parent.current->is_start && - teco_cmdline.effective_len < teco_cmdline.str.len && - strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len])) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + teco_is_noop(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)])) if (!teco_cmdline_rubin(error)) return FALSE; @@ -538,8 +642,8 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa /* rubout command */ while (ctx->parent.current->is_start && - teco_cmdline.effective_len > 0 && - strchr(TECO_NOOPS, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) > 0 && + teco_is_noop(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1])) teco_cmdline_rubout(); do @@ -553,7 +657,7 @@ teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa */ while (ctx->parent.current->is_start && (ctx->flags.modifier_at || ctx->flags.modifier_colon) && - teco_cmdline.effective_len > 0) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) > 0) teco_cmdline_rubout(); return TRUE; @@ -568,6 +672,9 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * { teco_state_t *current = ctx->parent.current; + const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); + switch (key) { case TECO_CTL_KEY('W'): { /* rubout/reinsert word */ teco_interface_popup_clear(); @@ -585,15 +692,15 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * if (teco_cmdline.modifier_enabled) { /* reinsert word chars */ while (ctx->parent.current == current && - teco_cmdline.effective_len < teco_cmdline.str.len && - teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len])) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)])) if (!teco_cmdline_rubin(error)) return FALSE; /* reinsert non-word chars */ while (ctx->parent.current == current && - teco_cmdline.effective_len < teco_cmdline.str.len && - !teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len])) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + !teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)])) if (!teco_cmdline_rubin(error)) return FALSE; @@ -607,7 +714,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * * a result string even in parse-only mode. */ if (ctx->result && ctx->result->len > 0) { - gboolean is_wordchar = teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1]); + gboolean is_wordchar = teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1]); teco_cmdline_rubout(); if (ctx->parent.current != current) { /* rub out string building command */ @@ -623,13 +730,13 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * */ if (!is_wordchar) { while (ctx->result->len > 0 && - !teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + !teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1])) teco_cmdline_rubout(); } /* rubout word chars */ while (ctx->result->len > 0 && - teco_string_contains(&wchars, teco_cmdline.str.data[teco_cmdline.effective_len-1])) + teco_string_contains(wchars, macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1])) teco_cmdline_rubout(); return TRUE; @@ -646,8 +753,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * if (teco_cmdline.modifier_enabled) { /* reinsert string */ - while (ctx->parent.current == current && - teco_cmdline.effective_len < teco_cmdline.str.len) + while (ctx->parent.current == current && teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len) if (!teco_cmdline_rubin(error)) return FALSE; @@ -685,7 +791,7 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * return TRUE; } - const gchar *filename = teco_string_last_occurrence(ctx->result, + const gchar *filename = teco_string_last_occurrence(*ctx->result, TECO_DEFAULT_BREAK_CHARS); g_auto(teco_string_t) new_chars, new_chars_escaped; gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars); @@ -716,11 +822,11 @@ teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t * } gboolean -teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error) +teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, teco_string_t str, GError **error) { g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(ctx, str->data, str->len, &str_escaped); - if (!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) + teco_machine_stringbuilding_escape(ctx, str.data, str.len, &str_escaped); + if (!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1])) teco_string_append_c(&str_escaped, ' '); return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); } @@ -765,7 +871,7 @@ teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_ } gboolean -teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; teco_state_t *stringbuilding_current = stringbuilding_ctx->parent.current; @@ -825,18 +931,21 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t case TECO_CTL_KEY('W'): /* rubout/reinsert file names including directories */ teco_interface_popup_clear(); + const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + gsize macro_len = teco_cmdline_ssm(SCI_GETLENGTH, 0, 0); + if (teco_cmdline.modifier_enabled) { /* reinsert one level of file name */ while (stringbuilding_ctx->parent.current == stringbuilding_current && - teco_cmdline.effective_len < teco_cmdline.str.len && - !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len])) + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + !G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)])) if (!teco_cmdline_rubin(error)) return FALSE; /* reinsert final directory separator */ if (stringbuilding_ctx->parent.current == stringbuilding_current && - teco_cmdline.effective_len < teco_cmdline.str.len && - G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len]) && + teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) < macro_len && + G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)]) && !teco_cmdline_rubin(error)) return FALSE; @@ -845,12 +954,12 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t if (ctx->expectstring.string.len > 0) { /* rubout directory separator */ - if (G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1])) + if (G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1])) teco_cmdline_rubout(); /* rubout one level of file name */ while (ctx->expectstring.string.len > 0 && - !G_IS_DIR_SEPARATOR(teco_cmdline.str.data[teco_cmdline.effective_len-1])) + !G_IS_DIR_SEPARATOR(macro[teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0)-1])) teco_cmdline_rubout(); return TRUE; @@ -871,7 +980,7 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t return TRUE; } - if (teco_string_contains(&ctx->expectstring.string, '\0')) + if (teco_string_contains(ctx->expectstring.string, '\0')) /* null-byte not allowed in file names */ return TRUE; @@ -890,13 +999,13 @@ teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean -teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); - if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) && + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); + if ((!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1])) && ctx->expectstring.nesting == 1) teco_string_append_wc(&str_escaped, ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); @@ -927,7 +1036,7 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t return TRUE; } - if (teco_string_contains(&ctx->expectstring.string, '\0')) + if (teco_string_contains(ctx->expectstring.string, '\0')) /* null-byte not allowed in file names */ return TRUE; @@ -958,14 +1067,14 @@ teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean -teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; - g_autofree gchar *pattern_escaped = teco_globber_escape_pattern(str->data); + g_autofree gchar *pattern_escaped = teco_globber_escape_pattern(str.data); g_auto(teco_string_t) str_escaped; teco_machine_stringbuilding_escape(stringbuilding_ctx, pattern_escaped, strlen(pattern_escaped), &str_escaped); - if ((!str->len || !G_IS_DIR_SEPARATOR(str->data[str->len-1])) && + if ((!str.len || !G_IS_DIR_SEPARATOR(str.data[str.len-1])) && ctx->expectstring.nesting == 1) teco_string_append_wc(&str_escaped, ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); @@ -996,7 +1105,7 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t * return TRUE; } - if (teco_string_contains(&ctx->expectstring.string, '\0')) + if (teco_string_contains(ctx->expectstring.string, '\0')) /* null-byte not allowed in file names */ return TRUE; @@ -1016,7 +1125,7 @@ teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t * } gboolean -teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; @@ -1024,7 +1133,7 @@ teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_stri * FIXME: We might terminate the command in case of leaf directories. */ g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); } @@ -1041,7 +1150,7 @@ teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t } gboolean -teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { g_assert(ctx->expectqreg != NULL); /* @@ -1092,9 +1201,9 @@ teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_ } gboolean -teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error) +teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error) { - return teco_cmdline_insert(str->data, str->len, error); + return teco_cmdline_insert(str.data, str.len, error); } gboolean @@ -1138,12 +1247,12 @@ teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_m } gboolean -teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, GError **error) +teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = teco_machine_qregspec_get_stringbuilding(ctx); g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); teco_string_append_c(&str_escaped, ']'); return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); } @@ -1178,7 +1287,7 @@ teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *pa return TRUE; } - const gchar *filename = teco_string_last_occurrence(&ctx->expectstring.string, + const gchar *filename = teco_string_last_occurrence(ctx->expectstring.string, TECO_DEFAULT_BREAK_CHARS); g_auto(teco_string_t) new_chars, new_chars_escaped; gboolean unambiguous = teco_file_auto_complete(filename, G_FILE_TEST_EXISTS, &new_chars); @@ -1217,7 +1326,7 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac return TRUE; } - const gchar *symbol = teco_string_last_occurrence(&ctx->expectstring.string, ","); + const gchar *symbol = teco_string_last_occurrence(ctx->expectstring.string, ","); teco_symbol_list_t *list = symbol == ctx->expectstring.string.data ? &teco_symbol_list_scintilla : &teco_symbol_list_scilexer; @@ -1239,12 +1348,12 @@ teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_mac } gboolean -teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); teco_string_append_c(&str_escaped, ','); return teco_cmdline_insert(str_escaped.data, str_escaped.len, error); } @@ -1274,7 +1383,7 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren } teco_string_t label = ctx->expectstring.string; - gint i = teco_string_rindex(&label, ','); + gint i = teco_string_rindex(label, ','); if (i >= 0) { label.data += i+1; label.len -= i+1; @@ -1297,12 +1406,12 @@ teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren } gboolean -teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_goto_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); /* * FIXME: This does not escape `,`. Cannot be escaped via ^Q currently? */ @@ -1334,7 +1443,7 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren return TRUE; } - if (teco_string_contains(&ctx->expectstring.string, '\0')) + if (teco_string_contains(ctx->expectstring.string, '\0')) /* help term must not contain null-byte */ return TRUE; @@ -1353,12 +1462,12 @@ teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *paren } gboolean -teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_machine_stringbuilding_t *stringbuilding_ctx = &ctx->expectstring.machine; g_auto(teco_string_t) str_escaped; - teco_machine_stringbuilding_escape(stringbuilding_ctx, str->data, str->len, &str_escaped); + teco_machine_stringbuilding_escape(stringbuilding_ctx, str.data, str.len, &str_escaped); if (ctx->expectstring.nesting == 1) teco_string_append_wc(&str_escaped, ctx->expectstring.machine.escape_char == '{' ? '}' : ctx->expectstring.machine.escape_char); @@ -1394,5 +1503,6 @@ teco_state_save_cmdline_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg * Q-Register <q>. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_save_cmdline, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_save_cmdline_got_register ); diff --git a/src/cmdline.h b/src/cmdline.h index ebdf1e1..f6d0345 100644 --- a/src/cmdline.h +++ b/src/cmdline.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,10 +19,13 @@ #include <glib.h> #include "sciteco.h" -#include "string-utils.h" #include "parser.h" +#include "view.h" #include "undo.h" +/** Indicator number used for the rubbed out part of the command line */ +#define INDICATOR_RUBBEDOUT (INDICATOR_CONTAINER+0) + typedef struct { /** * State machine used for interactive mode (commandline macro). @@ -34,16 +37,17 @@ typedef struct { teco_machine_main_t machine; /** - * String containing the current command line - * (both effective and rubbed out). - */ - teco_string_t str; - /** - * Effective command line length. - * The length of the rubbed out part of the command line - * is (teco_cmdline.str.len - teco_cmdline.effective_len). + * Command-line Scintilla view. + * It's document contains the current command line macro. + * The current position (cursor) marks the end of the + * "effective" command line, while everything afterwards + * is the rubbed out part of the command line. + * The rubbed out part should be highlighted with an indicator. */ - gsize effective_len; + teco_view_t *view; + + /** Height of the command line view in lines */ + guint height; /** Program counter within the command-line macro */ gsize pc; @@ -60,6 +64,30 @@ typedef struct { extern teco_cmdline_t teco_cmdline; +void teco_cmdline_init(void); + +static inline sptr_t +teco_cmdline_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam) +{ + return teco_view_ssm(teco_cmdline.view, iMessage, wParam, lParam); +} + +/** + * Update scroll beavior on command line after window resizes. + * + * This should ensure that the caret jumps to the middle of the command line. + * + * @param width Window (command line view) width in pixels or columns. + * + * @fixme + * On the other hand this limits how you can customize the scroll behavior. + */ +static inline void +teco_cmdline_resized(guint width) +{ + teco_cmdline_ssm(SCI_SETXCARETPOLICY, CARET_SLOP | CARET_EVEN, width/2); +} + gboolean teco_cmdline_keypress(const gchar *data, gsize len, GError **error); typedef enum { @@ -84,10 +112,12 @@ teco_cmdline_keymacro_c(gchar key, GError **error) return TRUE; } -extern gboolean teco_quit_requested; +void teco_cmdline_update(void); + +void teco_cmdline_cleanup(void); /* * Command states */ -TECO_DECLARE_STATE(teco_state_save_cmdline); +extern teco_state_t teco_state_save_cmdline; diff --git a/src/core-commands.c b/src/core-commands.c index dbf86bd..f81bdf3 100644 --- a/src/core-commands.c +++ b/src/core-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,7 +18,9 @@ #include "config.h" #endif +#include <time.h> #include <string.h> +#include <stdio.h> #include <glib.h> #include <glib/gstdio.h> @@ -42,31 +44,76 @@ #include "memory.h" #include "eol.h" #include "qreg.h" +#include "stdio-commands.h" #include "qreg-commands.h" #include "goto-commands.h" #include "move-commands.h" #include "core-commands.h" -gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, - gunichar key, GError **error); +static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error); +static teco_state_t *teco_state_ctlc_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error); /** - * @class TECO_DEFINE_STATE_COMMAND - * @implements TECO_DEFINE_STATE_CASEINSENSITIVE - * @ingroup states - * - * Base state for everything that is the beginning of a one or two - * letter command. + * Translate buffer range arguments from the expression stack to + * a from-position and length in bytes. + * + * If only one argument is given, it is interpreted as a number of lines + * beginning with dot. + * If two arguments are given, it is interpreted as two buffer positions + * in glyphs. + * + * @param cmd Name of the command + * @param from_ret Where to store the from-position in bytes + * @param len_ret Where to store the length of the range in bytes + * @param error A GError + * @return FALSE if an error occurred + * + * @fixme There are still redundancies with teco_state_start_kill(). + * But it needs to discern between invalid ranges and other errors. */ -#define TECO_DEFINE_STATE_COMMAND(NAME, ...) \ - TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, \ - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ - teco_state_command_process_edit_cmd, \ - .style = SCE_SCITECO_COMMAND, \ - ##__VA_ARGS__ \ - ) +gboolean +teco_get_range_args(const gchar *cmd, gsize *from_ret, gsize *len_ret, GError **error) +{ + gssize from, len; /* in bytes */ -static teco_state_t *teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error); + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + + if (teco_expressions_args() <= 1) { + teco_int_t line; + + if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) + return FALSE; + + from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); + + if (!teco_validate_line(line)) { + teco_error_range_set(error, cmd); + return FALSE; + } + + len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; + + if (len < 0) { + from += len; + len *= -1; + } + } else { + gssize to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); + from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); + len = to - from; + + if (len < 0 || from < 0 || to < 0) { + teco_error_range_set(error, cmd); + return FALSE; + } + } + + *from_ret = from; + *len_ret = len; + return TRUE; +} /* * NOTE: This needs some extra code in teco_state_start_input(). @@ -213,19 +260,20 @@ teco_state_start_backslash(teco_machine_main_t *ctx, GError **error) return; if (teco_expressions_args()) { - teco_int_t value; - - if (!teco_expressions_pop_num_calc(&value, 0, error)) - return; - gchar buffer[TECO_EXPRESSIONS_FORMAT_LEN]; - gchar *str = teco_expressions_format(buffer, value, + gchar *str = teco_expressions_format(buffer, + teco_expressions_pop_num(0), ctx->qreg_table_locals->radix); g_assert(*str != '\0'); gsize len = strlen(str); - teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + len; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); + /* + * We can assume that `len` is already in glyphs, + * i.e. formatted numbers will never use multi-byte/Unicode characters. + */ + teco_undo_int(teco_ranges[0].to) = teco_ranges[0].from + len; teco_undo_guint(teco_ranges_count) = 1; teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); @@ -276,8 +324,7 @@ static void teco_state_start_loop_open(teco_machine_main_t *ctx, GError **error) { teco_loop_context_t lctx; - if (!teco_expressions_eval(FALSE, error) || - !teco_expressions_pop_num_calc(&lctx.counter, -1, error)) + if (!teco_expressions_pop_num_calc(&lctx.counter, -1, error)) return; lctx.brace_level = teco_brace_level; lctx.pass_through = teco_machine_main_eval_colon(ctx) > 0; @@ -370,7 +417,7 @@ teco_state_start_loop_close(teco_machine_main_t *ctx, GError **error) } } -/*$ ";" break +/*$ ";" ":;" break * [bool]; -- Conditionally break from loop * [bool]:; * @@ -504,9 +551,11 @@ teco_state_start_cmdline_push(teco_machine_main_t *ctx, GError **error) !teco_qreg_table_edit_name(&teco_qreg_table_globals, "\e", 1, error)) return; + const gchar *macro = (const gchar *)teco_cmdline_ssm(SCI_GETCHARACTERPOINTER, 0, 0); + teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); teco_interface_ssm(SCI_CLEARALL, 0, 0); - teco_interface_ssm(SCI_ADDTEXT, teco_cmdline.pc, (sptr_t)teco_cmdline.str.data); + teco_interface_ssm(SCI_ADDTEXT, teco_cmdline.pc, (sptr_t)macro); teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); /* @@ -536,34 +585,6 @@ teco_state_start_cmdline_pop(teco_machine_main_t *ctx, GError **error) g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CMDLINE, ""); } -/*$ "=" print - * <n>= -- Show value as message - * - * Shows integer <n> as a message in the message line and/or - * on the console. - * It is currently always formatted as a decimal integer and - * shown with the user-message severity. - * The command fails if <n> is not given. - */ -/** - * @todo perhaps care about current radix - * @todo colon-modifier to suppress line-break on console? - */ -static void -teco_state_start_print(teco_machine_main_t *ctx, GError **error) -{ - if (!teco_expressions_eval(FALSE, error)) - return; - if (!teco_expressions_args()) { - teco_error_argexpected_set(error, "="); - return; - } - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - teco_interface_msg(TECO_MSG_USER, "%" TECO_INT_FORMAT, v); -} - /*$ A * [n]A -> code -- Get character code from buffer * -A -> code @@ -573,8 +594,6 @@ teco_state_start_print(teco_machine_main_t *ctx, GError **error) * This can be an ASCII <code> or Unicode codepoint * depending on Scintilla's encoding of the current * buffer. - * Invalid Unicode byte sequences are reported as - * -1 or -2. * * - If <n> is 0, return the <code> of the character * pointed to by dot. @@ -585,12 +604,11 @@ teco_state_start_print(teco_machine_main_t *ctx, GError **error) * - If <n> is omitted, the sign prefix is implied. * * If the position of the queried character is off-page, - * the command will yield an error. - * + * the command will return -1. * If the document is encoded as UTF-8 and there is - * an incomplete sequence at the requested position, - * -1 is returned. - * All other invalid Unicode sequences are returned as -2. + * an invalid byte sequence at the requested position, + * -2 is returned. + * Incomplete byte sequences are returned as -3. */ static void teco_state_start_get(teco_machine_main_t *ctx, GError **error) @@ -603,15 +621,11 @@ teco_state_start_get(teco_machine_main_t *ctx, GError **error) gssize get_pos = teco_interface_glyphs2bytes_relative(pos, v); sptr_t len = teco_interface_ssm(SCI_GETLENGTH, 0, 0); - if (get_pos < 0 || get_pos == len) { - teco_error_range_set(error, "A"); - return; - } - - teco_expressions_push(teco_interface_get_character(get_pos, len)); + teco_expressions_push(get_pos < 0 || get_pos == len + ? -1 : teco_interface_get_character(get_pos, len)); } -static teco_state_t * +teco_state_t * teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) { static teco_machine_main_transition_t transitions[] = { @@ -621,7 +635,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) ['$'] = {&teco_state_escape}, ['!'] = {&teco_state_label}, ['O'] = {&teco_state_goto, - .modifier_at = TRUE}, + .modifier_at = TRUE, .modifier_colon = 1}, ['^'] = {&teco_state_control, .modifier_at = TRUE, .modifier_colon = 2}, ['F'] = {&teco_state_fcommand, @@ -629,7 +643,7 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) ['"'] = {&teco_state_condcommand}, ['E'] = {&teco_state_ecommand, .modifier_at = TRUE, .modifier_colon = 2}, - ['I'] = {&teco_state_insert_building, + ['I'] = {&teco_state_insert, .modifier_at = TRUE}, ['?'] = {&teco_state_help, .modifier_at = TRUE}, @@ -639,8 +653,10 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_at = TRUE, .modifier_colon = 1}, ['['] = {&teco_state_pushqreg}, - [']'] = {&teco_state_popqreg}, - ['G'] = {&teco_state_getqregstring}, + [']'] = {&teco_state_popqreg, + .modifier_colon = 1}, + ['G'] = {&teco_state_getqregstring, + .modifier_colon = 1}, ['Q'] = {&teco_state_queryqreg, .modifier_colon = 1}, ['U'] = {&teco_state_setqreginteger, @@ -650,6 +666,8 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_colon = 1}, ['X'] = {&teco_state_copytoqreg, .modifier_at = TRUE, .modifier_colon = 1}, + ['='] = {&teco_state_print_decimal, + .modifier_colon = 1}, /* * Arithmetics @@ -702,28 +720,25 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_colon = 1}, ['D'] = {&teco_state_start, teco_state_start_delete_chars, .modifier_colon = 1}, - ['='] = {&teco_state_start, teco_state_start_print}, - ['A'] = {&teco_state_start, teco_state_start_get} + ['A'] = {&teco_state_start, teco_state_start_get}, + ['T'] = {&teco_state_start, teco_state_start_typeout} }; - switch (chr) { /* - * No-ops (same as TECO_NOOPS): + * Non-operational commands. * These are explicitly not handled in teco_state_control, * so that we can potentially reuse the upcaret notations like ^J. */ - case ' ': - case '\f': - case '\r': - case '\n': - case '\v': + if (teco_is_noop(chr)) { if (ctx->flags.modifier_at || (ctx->flags.mode == TECO_MODE_NORMAL && ctx->flags.modifier_colon)) { teco_error_modifier_set(error, chr); return NULL; } return &teco_state_start; + } + switch (chr) { /*$ 0 1 2 3 4 5 6 7 8 9 digit number * [n]0|1|2|3|4|5|6|7|8|9 -> n*Radix+X -- Append digit * @@ -758,8 +773,10 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) * for beginnings of command-lines? * It could also be used for a corresponding KEYMACRO mask. */ - if (teco_cmdline.effective_len == 1 && teco_cmdline.str.data[0] == '*') + if (teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0) == 1 && + teco_cmdline_ssm(SCI_GETCHARAT, 0, 0) == '*') return &teco_state_save_cmdline; + /* treat as an operator */ break; case '<': @@ -898,20 +915,18 @@ teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error) teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_COMMAND(teco_state_start, - .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ - .is_start = TRUE, - .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE +TECO_DEFINE_STATE_START(teco_state_start, + .input_cb = (teco_state_input_cb_t)teco_state_start_input ); -/*$ F< +/*$ "F<" ":F<" * F< -- Go to loop start or jump to beginning of macro * :F< * * Immediately jumps to the current loop's start. * Also works from inside conditionals. * - * This command behaves exactly like \fB>\fP with regard to + * This command behaves exactly like \fB<\fP with regard to * colon-modifiers. * * Outside of loops \(em or in a macro without @@ -945,7 +960,7 @@ teco_state_fcommand_loop_start(teco_machine_main_t *ctx, GError **error) ctx->macro_pc = lctx->pc; } -/*$ F> continue +/*$ "F>" ":F>" continue * F> -- Go to loop end or return from macro * :F> * @@ -1043,6 +1058,8 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error .modifier_at = TRUE, .modifier_colon = 2}, ['R'] = {&teco_state_replace_default, .modifier_at = TRUE, .modifier_colon = 2}, + ['N'] = {&teco_state_replace_default_all, + .modifier_at = TRUE, .modifier_colon = 1}, ['G'] = {&teco_state_changedir, .modifier_at = TRUE}, @@ -1065,7 +1082,9 @@ teco_state_fcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_COMMAND(teco_state_fcommand); +TECO_DEFINE_STATE_COMMAND(teco_state_fcommand, + .input_cb = (teco_state_input_cb_t)teco_state_fcommand_input +); static void teco_undo_change_dir_action(gchar **dir, gboolean run) @@ -1089,12 +1108,12 @@ teco_undo_change_dir_to_current(void) } static teco_state_t * -teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_changedir_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - g_autofree gchar *dir = teco_file_expand_path(str->data); + g_autofree gchar *dir = teco_file_expand_path(str.data); if (!*dir) { teco_qreg_t *qreg = teco_qreg_table_find(&teco_qreg_table_globals, "$HOME", 5); g_assert(qreg != NULL); @@ -1105,7 +1124,7 @@ teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GE /* * Null-characters must not occur in file names. */ - if (teco_string_contains(&home, '\0')) { + if (teco_string_contains(home, '\0')) { teco_string_clear(&home); g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, "Null-character not allowed in filenames"); @@ -1157,7 +1176,9 @@ teco_state_changedir_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * String-building characters are enabled on this * command and directories can be tab-completed. */ -TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir); +TECO_DEFINE_STATE_EXPECTDIR(teco_state_changedir, + .expectstring.done_cb = teco_state_changedir_done +); static teco_state_t * teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error) @@ -1185,8 +1206,7 @@ teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **er teco_error_argexpected_set(error, "\""); return NULL; } - if (!teco_expressions_pop_num_calc(&value, 0, error)) - return NULL; + value = teco_expressions_pop_num(0); break; default: @@ -1273,7 +1293,8 @@ teco_state_condcommand_input(teco_machine_main_t *ctx, gunichar chr, GError **er } TECO_DEFINE_STATE_COMMAND(teco_state_condcommand, - .style = SCE_SCITECO_OPERATOR + .style = SCE_SCITECO_OPERATOR, + .input_cb = (teco_state_input_cb_t)teco_state_condcommand_input ); /*$ ^_ negate @@ -1287,8 +1308,6 @@ TECO_DEFINE_STATE_COMMAND(teco_state_condcommand, static void teco_state_control_negate(teco_machine_main_t *ctx, GError **error) { - teco_int_t v; - if (!teco_expressions_eval(FALSE, error)) return; @@ -1296,9 +1315,8 @@ teco_state_control_negate(teco_machine_main_t *ctx, GError **error) teco_error_argexpected_set(error, "^_"); return; } - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return; - teco_expressions_push(~v); + + teco_expressions_push(~teco_expressions_pop_num(0)); } static void @@ -1319,35 +1337,6 @@ teco_state_control_xor(teco_machine_main_t *ctx, GError **error) teco_expressions_push_calc(TECO_OP_XOR, error); } -/*$ ^C exit - * ^C -- Exit program immediately - * - * Lets the top-level macro return immediately - * regardless of the current macro invocation frame. - * This command is only allowed in batch mode, - * so it is not invoked accidentally when using - * the CTRL+C immediate editing command to - * interrupt long running operations. - * When using \fB^C\fP in a munged file, - * interactive mode is never started, so it behaves - * effectively just like \(lq-EX\fB$$\fP\(rq - * (when executed in the top-level macro at least). - * - * The \fBquit\fP hook is still executed. - */ -static void -teco_state_control_exit(teco_machine_main_t *ctx, GError **error) -{ - if (teco_undo_enabled) { - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, - "<^C> not allowed in interactive mode"); - return; - } - - teco_quit_requested = TRUE; - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); -} - /*$ ^O octal * ^O -- Set radix to 8 (octal) */ @@ -1401,14 +1390,13 @@ teco_state_control_radix(teco_machine_main_t *ctx, GError **error) return; teco_expressions_push(radix); } else { - if (!teco_expressions_pop_num_calc(&radix, 0, error) || - !qreg->vtable->undo_set_integer(qreg, error) || - !qreg->vtable->set_integer(qreg, radix, error)) + if (!qreg->vtable->undo_set_integer(qreg, error) || + !qreg->vtable->set_integer(qreg, teco_expressions_pop_num(0), error)) return; } } -/*$ ^E glyphs2bytes bytes2glyphs +/*$ "^E" ":^E" glyphs2bytes bytes2glyphs * glyphs^E -> bytes -- Translate between glyph and byte indexes * bytes:^E -> glyphs * ^E -> bytes @@ -1455,9 +1443,7 @@ teco_state_control_glyphs2bytes(teco_machine_main_t *ctx, GError **error) */ res = teco_interface_ssm(colon_modified ? SCI_GETLENGTH : SCI_GETCURRENTPOS, 0, 0); } else { - teco_int_t pos; - if (!teco_expressions_pop_num_calc(&pos, 0, error)) - return; + teco_int_t pos = teco_expressions_pop_num(0); if (colon_modified) { /* teco_interface_bytes2glyphs() does not check addresses */ res = 0 <= pos && pos <= teco_interface_ssm(SCI_GETLENGTH, 0, 0) @@ -1498,8 +1484,8 @@ teco_ranges_init(void) * The default value 0 specifies the entire matched pattern, * while higher numbers refer to \fB^E[\fI...\fB]\fR subpatterns. * \fB^Y\fP can also be used to return the buffer range of the - * last text insertion by any \*(ST command (\fBI\fP, \fBEI\fP, \fB^I\fP, \fBG\fIq\fR, - * \fB\\\fP, \fBEC\fP, \fBEN\fP, etc). + * last text insertion by any \*(ST command (\fBI\fP, \fB^I\fP, \fBG\fIq\fR, + * \fB\\\fP, \fBEC\fP, \fBEN\fP, search replacements, etc). * In this case <n> is only allowed to be 0 or missing. * * For instance, \(lq^YXq\(rq copies the entire matched pattern or text @@ -1522,8 +1508,8 @@ teco_state_control_last_range(teco_machine_main_t *ctx, GError **error) return; } - teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].from)); - teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].to)); + teco_expressions_push(teco_ranges[n].from); + teco_expressions_push(teco_ranges[n].to); } /*$ ^S @@ -1551,17 +1537,20 @@ teco_state_control_last_length(teco_machine_main_t *ctx, GError **error) /* * There is little use in supporting n^S for n != 0. * This is just for consistency with ^Y. + * + * We do not use teco_expressions_pop_num_calc(), + * so as not to reset the sign prefix. */ - if (teco_expressions_args() > 0 && - !teco_expressions_pop_num_calc(&n, 0, error)) + if (!teco_expressions_eval(FALSE, error)) return; + if (teco_expressions_args() > 0) + n = teco_expressions_pop_num(0); if (n < 0 || n >= teco_ranges_count) { - teco_error_subpattern_set(error, "^Y"); + teco_error_subpattern_set(error, "^S"); return; } - teco_expressions_push(teco_interface_bytes2glyphs(teco_ranges[n].from) - - teco_interface_bytes2glyphs(teco_ranges[n].to)); + teco_expressions_push(teco_ranges[n].from - teco_ranges[n].to); } static void TECO_DEBUG_CLEANUP @@ -1570,6 +1559,100 @@ teco_ranges_cleanup(void) g_free(teco_ranges); } +/*$ ^B date + * ^B -> (((year-1900)*16 + month)*32 + day) -- Retrieve date + * + * Returns the current date via the given equation. + */ +/* + * FIXME: Perhaps :^B should directly return the + * decoded year, month and day. + */ +static void +teco_state_control_date(teco_machine_main_t *ctx, GError **error) +{ + GDate date; + + g_date_clear(&date, 1); + g_date_set_time_t(&date, time(NULL)); + teco_expressions_push(((g_date_get_year(&date)-1900)*16 + g_date_get_month(&date))*32 + + g_date_get_day(&date)); +} + +/*$ "^H" ":^H" "::^H" time timestamp + * ^H -> seconds since midnight -- Retrieve time of day or timestamp + * :^H -> seconds + * ::^H -> timestamp + * + * By default returns the current time in seconds since midnight (UTC). + * + * If colon-modified it returns the number of <seconds> since the Epoch, + * 1970-01-01 00:00:00 +0000 (UTC). + * + * If modified by two colons it returns the system's monotonic time in microseconds, + * which can be used as a <timestamp>. + */ +static void +teco_state_control_time(teco_machine_main_t *ctx, GError **error) +{ + switch (teco_machine_main_eval_colon(ctx)) { + case 0: + teco_expressions_push(time(NULL) % (60*60*24)); + break; + case 1: + teco_expressions_push(time(NULL)); + break; + case 2: + /* + * NOTE: Might not be reliable if TECO_INTEGER==32. + */ + teco_expressions_push(g_get_monotonic_time()); + break; + default: + g_assert_not_reached(); + } +} + +/*$ ^W refresh sleep delay wait + * [n]^W -- Wait and refresh screen + * + * First sleep <n> milliseconds before refreshing the view, + * i.e. drawing it. + * By default it sleeps for 10ms. + * This can be added to loops to make progress visible + * in interactive mode. + * In batch mode this command is useful as a sleep command. + * Sleeps can of course be interrupted with CTRL+C. + * + * Since CTRL+W is an immediate editing command, you may + * have to type this command in upcaret mode. + * To enforce a complete screen redraw you can also + * press CTRL+L. + */ +static void +teco_state_control_refresh(teco_machine_main_t *ctx, GError **error) +{ + teco_int_t ms; + + if (!teco_expressions_pop_num_calc(&ms, 10, error)) + return; + + while (ms > 0 && !teco_interface_is_interrupted()) { + /* + * UNIX' usleep() would also be interrupted by + * SIGINT, but polling for interruptions is + * probably precise enough. + * We need this as a fallback anyway. + */ + g_usleep(MIN(ms*1000, TECO_POLL_INTERVAL)); + ms -= TECO_POLL_INTERVAL/1000; + } + + teco_interface_unfold(); + teco_interface_ssm(SCI_SCROLLCARET, 0, 0); + teco_interface_refresh(FALSE); +} + static teco_state_t * teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) { @@ -1583,6 +1666,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_at = TRUE, .modifier_colon = 1}, ['^'] = {&teco_state_ascii}, ['['] = {&teco_state_escape}, + ['C'] = {&teco_state_ctlc}, + ['A'] = {&teco_state_print_string, + .modifier_at = TRUE, .modifier_colon = 1}, /* * Additional numeric operations @@ -1595,7 +1681,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) /* * Commands */ - ['C'] = {&teco_state_start, teco_state_control_exit}, + ['B'] = {&teco_state_start, teco_state_control_date}, + ['H'] = {&teco_state_start, teco_state_control_time, + .modifier_colon = 2}, ['O'] = {&teco_state_start, teco_state_control_octal}, ['D'] = {&teco_state_start, teco_state_control_decimal}, ['R'] = {&teco_state_start, teco_state_control_radix}, @@ -1605,7 +1693,10 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) .modifier_colon = 1}, ['X'] = {&teco_state_start, teco_state_control_search_mode}, ['Y'] = {&teco_state_start, teco_state_control_last_range}, - ['S'] = {&teco_state_start, teco_state_control_last_length} + ['S'] = {&teco_state_start, teco_state_control_last_length}, + ['T'] = {&teco_state_start, teco_state_control_typeout, + .modifier_colon = 1}, + ['W'] = {&teco_state_start, teco_state_control_refresh} }; /* @@ -1617,7 +1708,9 @@ teco_state_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_COMMAND(teco_state_control); +TECO_DEFINE_STATE_COMMAND(teco_state_control, + .input_cb = (teco_state_input_cb_t)teco_state_control_input +); static teco_state_t * teco_state_ascii_input(teco_machine_main_t *ctx, gunichar chr, GError **error) @@ -1640,7 +1733,82 @@ teco_state_ascii_input(teco_machine_main_t *ctx, gunichar chr, GError **error) * Note that this command can be typed CTRL+Caret or * Caret-Caret. */ -TECO_DEFINE_STATE(teco_state_ascii); +TECO_DEFINE_STATE(teco_state_ascii, + .input_cb = (teco_state_input_cb_t)teco_state_ascii_input +); + +/*$ ^[^[ ^[$ $$ ^C terminate return + * [a1,a2,...]$$ -- Terminate command line or return from macro + * [a1,a2,...]^[$ + * [a1,a2,...]^C + * + * Returns from the current macro invocation. + * This will pass control to the calling macro immediately + * and is thus faster than letting control reach the macro's end. + * Also, direct arguments to \fB$$\fP will be left on the expression + * stack when the macro returns. + * \fB$$\fP closes loops automatically and is thus safe to call + * from loop bodies. + * Furthermore, it has defined semantics when executed + * from within braced expressions: + * All braces opened in the current macro invocation will + * be closed and their values discarded. + * Only the direct arguments to \fB$$\fP will be kept. + * + * Returning from the top-level macro in batch mode + * will exit the program or start up interactive mode depending + * on whether program exit has been requested. + * If \fB$$\fP exits the program, any remaining numeric parameter + * is returned by the process as its exit status. + * By default, the success code is returned. + * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit + * prematurely. + * + * In interactive mode, returning from the top-level macro + * (i.e. typing \fB$$\fP at the command line) has the + * effect of command line termination. + * The arguments to \fB$$\fP are currently not used + * when terminating a command line \(em the new command line + * will always start with a clean expression stack. + * + * \fB^C\fP cannot be typed directly on the command-line + * as it could be inserted accidentally after interrupting + * operations with CTRL+C. + * + * The first \fIescape\fP of \fB$$\fP may be typed either + * as an escape character (ASCII 27), in up-arrow mode + * (e.g. \fB^[$\fP) or as a dollar character \(em the + * second character must be either a real escape character + * or a dollar character. + */ +/* + * FIXME: Analogous to ^C^C, we could support ^[^[ typed with carets only + * at the expense of yet another parser state. + */ +static teco_state_t * +teco_return(teco_machine_main_t *ctx, GError **error) +{ + g_assert(ctx->flags.mode == TECO_MODE_NORMAL); + + /* + * This check is not crucial, but a return command would + * terminate the command line and it would be impossible to apply the new + * command line with `}` after command-line termination. + */ + if (G_UNLIKELY(ctx == &teco_cmdline.machine && + teco_qreg_current && !teco_string_cmp(teco_qreg_current->head.name, "\e", 1))) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Not allowed to terminate command-line while " + "editing command-line replacement register"); + return NULL; + } + + ctx->parent.current = &teco_state_start; + if (!teco_expressions_eval(FALSE, error)) + return NULL; + teco_error_return_set(error, teco_expressions_args()); + return NULL; +} /* * The Escape state is special, as it implements @@ -1658,52 +1826,9 @@ TECO_DEFINE_STATE(teco_state_ascii); static teco_state_t * teco_state_escape_input(teco_machine_main_t *ctx, gunichar chr, GError **error) { - /*$ ^[^[ ^[$ $$ terminate return - * [a1,a2,...]$$ -- Terminate command line or return from macro - * [a1,a2,...]^[$ - * - * Returns from the current macro invocation. - * This will pass control to the calling macro immediately - * and is thus faster than letting control reach the macro's end. - * Also, direct arguments to \fB$$\fP will be left on the expression - * stack when the macro returns. - * \fB$$\fP closes loops automatically and is thus safe to call - * from loop bodies. - * Furthermore, it has defined semantics when executed - * from within braced expressions: - * All braces opened in the current macro invocation will - * be closed and their values discarded. - * Only the direct arguments to \fB$$\fP will be kept. - * - * Returning from the top-level macro in batch mode - * will exit the program or start up interactive mode depending - * on whether program exit has been requested. - * \(lqEX\fB$$\fP\(rq is thus a common idiom to exit - * prematurely. - * - * In interactive mode, returning from the top-level macro - * (i.e. typing \fB$$\fP at the command line) has the - * effect of command line termination. - * The arguments to \fB$$\fP are currently not used - * when terminating a command line \(em the new command line - * will always start with a clean expression stack. - * - * The first \fIescape\fP of \fB$$\fP may be typed either - * as an escape character (ASCII 27), in up-arrow mode - * (e.g. \fB^[$\fP) or as a dollar character \(em the - * second character must be either a real escape character - * or a dollar character. - */ - if (chr == '\e' || chr == '$') { - if (ctx->flags.mode > TECO_MODE_NORMAL) - return &teco_state_start; - - ctx->parent.current = &teco_state_start; - if (!teco_expressions_eval(FALSE, error)) - return NULL; - teco_error_return_set(error, teco_expressions_args()); - return NULL; - } + if (chr == '\e' || chr == '$') + return ctx->flags.mode > TECO_MODE_NORMAL + ? &teco_state_start : teco_return(ctx, error); /* * Alternatives: ^[, <CTRL/[>, <ESC>, $ (dollar) @@ -1743,14 +1868,100 @@ teco_state_escape_end_of_macro(teco_machine_t *ctx, GError **error) return teco_expressions_discard_args(error); } -TECO_DEFINE_STATE_COMMAND(teco_state_escape, - .end_of_macro_cb = teco_state_escape_end_of_macro, - /* - * The state should behave like teco_state_start - * when it comes to function key macro masking. +TECO_DEFINE_STATE_START(teco_state_escape, + .input_cb = (teco_state_input_cb_t)teco_state_escape_input, + .end_of_macro_cb = teco_state_escape_end_of_macro +); + +/* + * Just like ^[, ^C actually implements a lookahead, + * so a ^C itself does nothing. + * This does not break the user experience since ^C + * is disallowed to type at the command-line. + */ +static teco_state_t * +teco_state_ctlc_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +{ + switch (chr) { + case TECO_CTL_KEY('C'): return teco_state_ctlc_control_input(ctx, 'C', error); + case '^': return &teco_state_ctlc_control; + } + + return ctx->flags.mode > TECO_MODE_NORMAL + ? teco_state_start_input(ctx, chr, error) : teco_return(ctx, error); +} + +static gboolean +teco_state_ctlc_initial(teco_machine_main_t *ctx, GError **error) +{ + if (G_UNLIKELY(ctx == &teco_cmdline.machine)) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "<^C> is not allowed to terminate command-lines"); + return FALSE; + } + + return TRUE; +} + +TECO_DEFINE_STATE_START(teco_state_ctlc, + .initial_cb = (teco_state_initial_cb_t)teco_state_ctlc_initial, + .input_cb = (teco_state_input_cb_t)teco_state_ctlc_input +); + +static teco_state_t * +teco_state_ctlc_control_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +{ + /*$ ^C^C exit + * [n]^C^C -- Exit program immediately + * + * Lets the top-level macro return immediately + * regardless of the current macro invocation frame. + * This command is only allowed in batch mode, + * so it is not invoked accidentally when using + * the CTRL+C immediate editing command to + * interrupt long running operations. + * When using \fB^C^C\fP in a munged file, + * interactive mode is never started, so it behaves + * effectively just like \(lq-EX\fB$$\fP\(rq + * (when executed in the top-level macro at least). + * + * Any numeric parameter is returned by the process + * as its exit status. + * By default, the success code is returned. + * The \fBquit\fP hook is still executed. + * + * This command is currently disallowed in interactive mode. + * + * Note that both \(lq^C\(rq can be typed either + * as control codes (3) or with carets. */ - .is_start = TRUE, - .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE + if (chr == 'c' || chr == 'C') { + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (teco_undo_enabled) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "<^C^C> not allowed in interactive mode"); + return NULL; + } + + if (!teco_expressions_eval(FALSE, error)) + return NULL; + teco_ed |= TECO_ED_EXIT; + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_QUIT, ""); + return NULL; + } + + return ctx->flags.mode > TECO_MODE_NORMAL + ? teco_state_control_input(ctx, chr, error) : teco_return(ctx, error); +} + +/* + * This state is necessary, so that you can type ^C^C exclusively with carets. + * Otherwise it would be very cumbersome to cause exits with ASCII characters only. + */ +TECO_DEFINE_STATE_COMMAND(teco_state_ctlc_control, + .input_cb = (teco_state_input_cb_t)teco_state_ctlc_control_input ); /*$ ED flags @@ -1772,7 +1983,12 @@ TECO_DEFINE_STATE_COMMAND(teco_state_escape, * Without any argument ED returns the current flags. * * Currently, the following flags are used by \*(ST: - * .IP 4: 5 + * .IP 2: 5 + * Reflects whether program termination has been requested + * by successfully performing the \fBEX\fP command. + * This flag can also be used to cancel the effect of any + * prior \fBEX\fP. + * .IP 4: * If enabled, prefer raw single-byte ANSI encoding * for all new buffers and registers. * This does not change the encoding of any existing @@ -1816,6 +2032,13 @@ TECO_DEFINE_STATE_COMMAND(teco_state_escape, * by the \(lqNerd Fonts\(rq project. * Changes to this flag in interactive mode may not become * effective immediately. + * .IP 1024: + * If set the default clipboard register \(lq~\(rq will refer + * to the primary clipboard (\(lq~P\(rq) instead of the + * clipboard selection (\(lq~C\(rq). + * .IP 2048: + * Enable/Disable redirection of Scintilla messages (\fBES\fP) + * to the command line's Scintilla view. * * The features controlled thus are discribed in other sections * of this manual. @@ -1862,7 +2085,7 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * The current user interface: 1 for Curses, 2 for GTK * (\fBread-only\fP) * .IP 1: - * The current numbfer of buffers: Also the numeric id + * The current number of buffers: Also the numeric id * of the last buffer in the ring. This is implied if * no argument is given, so \(lqEJ\(rq returns the number * of buffers in the ring. @@ -1938,20 +2161,35 @@ teco_state_ecommand_flags(teco_machine_main_t *ctx, GError **error) * The column after the last horizontal movement. * This is only used by \fBfnkeys.tes\fP and is similar to the Scintilla-internal * setting \fBSCI_CHOOSECARETX\fP. - * Unless most other settings, this is on purpose not restored on rubout, - * so it "survives" command line replacements. + * Unlike most other settings, this is on purpose not restored on rubout, + * so it \(lqsurvives\(rq command line replacements. + * .IP 5: + * Height of the command line view in lines (1 by default). + * Must not be smaller than 1. + * .IP 6: + * .SCITECO_TOPIC recovery + * Interval in seconds for the creation of recovery files + * or 0 if those dumps are disabled (the default is 300 seconds). + * When enabled all dirty buffers are dumped to files with hash + * signs around the original basename (\fB#\fIfilename\fB#\fR). + * They are removed automatically when no longer required, + * but may be left around when the \*(ST crashes or terminates + * unexpectedly. + * After changing the interval, the new value may become + * active only after the previous interval expires. + * Recovery files are not dumped in batch mode. * . * .IP -1: * Type of the last mouse event (\fBread-only\fP). * One of the following values will be returned: * .RS - * . IP 1: 4 + * . IP 0: 4 * Some button has been pressed - * . IP 2: + * . IP 1: * Some button has been released - * . IP 3: + * . IP 2: * Scroll up - * . IP 4: + * . IP 3: * Scroll down * .RE * .IP -2: @@ -1992,23 +2230,22 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) EJ_BUFFERS, EJ_MEMORY_LIMIT, EJ_INIT_COLOR, - EJ_CARETX + EJ_CARETX, + EJ_CMDLINE_HEIGHT, + EJ_RECOVERY_INTERVAL }; static teco_int_t caret_x = 0; teco_int_t property; - if (!teco_expressions_eval(FALSE, error) || - !teco_expressions_pop_num_calc(&property, teco_num_sign, error)) + if (!teco_expressions_pop_num_calc(&property, teco_num_sign, error)) return; if (teco_expressions_args() > 0) { /* * Set property */ - teco_int_t value, color; - if (!teco_expressions_pop_num_calc(&value, 0, error)) - return; + teco_int_t value = teco_expressions_pop_num(0); switch (property) { case EJ_MEMORY_LIMIT: @@ -2027,15 +2264,36 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) teco_error_argexpected_set(error, "EJ"); return; } - if (!teco_expressions_pop_num_calc(&color, 0, error)) - return; - teco_interface_init_color((guint)value, (guint32)color); + teco_interface_init_color((guint)value, + (guint32)teco_expressions_pop_num(0)); break; case EJ_CARETX: + /* DON'T undo on rubout */ caret_x = value; break; + case EJ_CMDLINE_HEIGHT: + if (value < 1 || value > G_MAXUINT) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid command line height %" TECO_INT_FORMAT " " + "for <EJ>", value); + return; + } + teco_undo_guint(teco_cmdline.height) = value; + break; + + case EJ_RECOVERY_INTERVAL: + if (value < 0 || value > G_MAXUINT) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Invalid recovery interval %" TECO_INT_FORMAT "s " + "for <EJ>", value); + return; + } + teco_undo_guint(teco_ring_recovery_interval) = value; + /* FIXME: Perhaps signal the interface to reprogram timers */ + break; + default: g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Cannot set property %" TECO_INT_FORMAT " " @@ -2091,6 +2349,14 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) teco_expressions_push(caret_x); break; + case EJ_CMDLINE_HEIGHT: + teco_expressions_push(teco_cmdline.height); + break; + + case EJ_RECOVERY_INTERVAL: + teco_expressions_push(teco_ring_recovery_interval); + break; + default: g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Invalid property %" TECO_INT_FORMAT " " @@ -2099,7 +2365,7 @@ teco_state_ecommand_properties(teco_machine_main_t *ctx, GError **error) } } -/*$ EL eol +/*$ "EL" ":EL" EOL * 0EL -- Set or get End of Line mode * 13,10:EL * 1EL @@ -2150,11 +2416,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error) teco_int_t eol_mode; if (teco_machine_main_eval_colon(ctx) > 0) { - teco_int_t v1, v2; - if (!teco_expressions_pop_num_calc(&v1, 0, error)) - return; - - switch (v1) { + switch (teco_expressions_pop_num(0)) { case '\r': eol_mode = SC_EOL_CR; break; @@ -2163,9 +2425,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error) eol_mode = SC_EOL_LF; break; } - if (!teco_expressions_pop_num_calc(&v2, 0, error)) - return; - if (v2 == '\r') { + if (teco_expressions_pop_num(0) == '\r') { eol_mode = SC_EOL_CRLF; break; } @@ -2176,8 +2436,7 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error) return; } } else { - if (!teco_expressions_pop_num_calc(&eol_mode, 0, error)) - return; + eol_mode = teco_expressions_pop_num(0); switch (eol_mode) { case SC_EOL_CRLF: case SC_EOL_CR: @@ -2195,6 +2454,13 @@ teco_state_ecommand_eol(teco_machine_main_t *ctx, GError **error) undo__teco_interface_ssm(SCI_SETEOLMODE, teco_interface_ssm(SCI_GETEOLMODE, 0, 0), 0); teco_interface_ssm(SCI_SETEOLMODE, eol_mode, 0); + + /* + * While the buffer contents were not changed, + * the result of saving the file may differ, + * so we still dirtify the buffer. + */ + teco_ring_dirtify(); } else if (teco_machine_main_eval_colon(ctx) > 0) { const gchar *eol_seq = teco_eol_get_seq(teco_interface_ssm(SCI_GETEOLMODE, 0, 0)); teco_expressions_push(eol_seq); @@ -2255,7 +2521,7 @@ teco_codepage2str(guint codepage) return NULL; } -/*$ EE encoding codepage charset +/*$ "EE" ":EE" encoding codepage charset * codepageEE -- Edit current document's encoding (codepage/charset) * EE -> codepage * codepage:EE @@ -2319,10 +2585,7 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error) /* * Set code page */ - teco_int_t new_cp; - if (!teco_expressions_pop_num_calc(&new_cp, 0, error)) - return; - + teco_int_t new_cp = teco_expressions_pop_num(0); if (old_cp == SC_CP_UTF8 && new_cp == SC_CP_UTF8) return; @@ -2449,7 +2712,26 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error) teco_interface_ssm(SCI_GOTOPOS, teco_interface_glyphs2bytes(dot_glyphs), 0); } -/*$ EX exit +/*$ EO version + * EO -> major*10000 + minor*100 + micro -- Get program version + * + * Return the version of \*(ST encoded into an integer. + */ +static void +teco_state_ecommand_version(teco_machine_main_t *ctx, GError **error) +{ + /* + * FIXME: This is inefficient and could be done at build-time. + * Or we could have PACKAGE_MAJOR_VERSION, PACKAGE_MINOR_VERSION etc. macros. + * But then, who cares? + */ + guint major, minor, micro; + G_GNUC_UNUSED gint rc = sscanf(PACKAGE_VERSION, "%u.%u.%u", &major, &minor, µ); + g_assert(rc == 3); + teco_expressions_push(major*10000 + minor*100 + micro); +} + +/*$ "EX" ":EX" exit quit * [bool]EX -- Exit program * -EX * :EX @@ -2486,6 +2768,10 @@ teco_state_ecommand_encoding(teco_machine_main_t *ctx, GError **error) * \(lq:EX\fB$$\fP\(rq is nevertheless the usual interactive * command sequence to exit while saving all modified * buffers. + * + * The program termination request is also available in bit 2 + * of the \fBED\fP flags, so \(lqED&2\(rq can be used to + * check whether EX has been successfully called. */ /** @fixme what if changing file after EX? will currently still exit */ static void @@ -2498,14 +2784,22 @@ teco_state_ecommand_exit(teco_machine_main_t *ctx, GError **error) teco_int_t v; if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) return; - if (teco_is_failure(v) && teco_ring_is_any_dirty()) { - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, - "Modified buffers exist"); + guint id; + if (teco_is_failure(v) && (id = teco_ring_get_first_dirty())) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Buffer with id %u is dirty", id); return; } } - teco_undo_gboolean(teco_quit_requested) = TRUE; + teco_undo_int(teco_ed) |= TECO_ED_EXIT; +} + +static void +teco_state_macrofile_deprecated(teco_machine_main_t *ctx, GError **error) +{ + teco_interface_msg(TECO_MSG_WARNING, + "<EM> command is deprecated - use <EI> instead"); } static teco_state_t * @@ -2523,9 +2817,10 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error .modifier_at = TRUE, .modifier_colon = 1}, ['G'] = {&teco_state_egcommand, .modifier_at = TRUE, .modifier_colon = 1}, - ['I'] = {&teco_state_insert_nobuilding, - .modifier_at = TRUE}, - ['M'] = {&teco_state_macrofile, + ['I'] = {&teco_state_indirect, + .modifier_at = TRUE, .modifier_colon = 1}, + /* DEPRECATED: can be repurposed */ + ['M'] = {&teco_state_indirect, teco_state_macrofile_deprecated, .modifier_at = TRUE, .modifier_colon = 1}, ['N'] = {&teco_state_glob_pattern, .modifier_at = TRUE, .modifier_colon = 1}, @@ -2537,6 +2832,8 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error .modifier_at = TRUE, .modifier_colon = 1}, ['W'] = {&teco_state_save_file, .modifier_at = TRUE}, + ['R'] = {&teco_state_read_file, + .modifier_at = TRUE}, /* * Commands @@ -2549,6 +2846,7 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error .modifier_colon = 1}, ['E'] = {&teco_state_start, teco_state_ecommand_encoding, .modifier_colon = 1}, + ['O'] = {&teco_state_start, teco_state_ecommand_version}, ['X'] = {&teco_state_start, teco_state_ecommand_exit, .modifier_colon = 1}, }; @@ -2560,7 +2858,9 @@ teco_state_ecommand_input(teco_machine_main_t *ctx, gunichar chr, GError **error teco_ascii_toupper(chr), error); } -TECO_DEFINE_STATE_COMMAND(teco_state_ecommand); +TECO_DEFINE_STATE_COMMAND(teco_state_ecommand, + .input_cb = (teco_state_input_cb_t)teco_state_ecommand_input +); gboolean teco_state_insert_initial(teco_machine_main_t *ctx, GError **error) @@ -2568,7 +2868,8 @@ teco_state_insert_initial(teco_machine_main_t *ctx, GError **error) if (ctx->flags.mode > TECO_MODE_NORMAL) return TRUE; - teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); teco_undo_guint(teco_ranges_count) = 1; /* @@ -2622,22 +2923,21 @@ teco_state_insert_initial(teco_machine_main_t *ctx, GError **error) undo__teco_interface_ssm(SCI_UNDO, 0, 0); /* This is done only now because it can _theoretically_ fail. */ - for (gint i = args; i > 0; i--) - if (!teco_expressions_pop_num_calc(NULL, 0, error)) - return FALSE; + for (gint i = 0; i < args; i++) + teco_expressions_pop_num(0); return TRUE; } gboolean -teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str, +teco_state_insert_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error) { g_assert(new_chars > 0); teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); teco_interface_ssm(SCI_ADDTEXT, new_chars, - (sptr_t)(str->data + str->len - new_chars)); + (sptr_t)(str.data + str.len - new_chars)); teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); teco_ring_dirtify(); @@ -2648,11 +2948,13 @@ teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str, } teco_state_t * -teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { - if (ctx->flags.mode == TECO_MODE_NORMAL) - teco_undo_gsize(teco_ranges[0].to) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_start; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos); return &teco_state_start; } @@ -2677,21 +2979,7 @@ teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro * may be better, since it has string building characters * disabled. */ -TECO_DEFINE_STATE_INSERT(teco_state_insert_building); - -/*$ EI - * [c1,c2,...]EI[text]$ -- Insert text without string building characters - * - * Inserts text at the current position in the current - * document. - * This command is identical to the \fBI\fP command, - * except that string building characters are \fBdisabled\fP. - * Therefore it may be beneficial when editing \*(ST - * macros. - */ -TECO_DEFINE_STATE_INSERT(teco_state_insert_nobuilding, - .expectstring.string_building = FALSE -); +TECO_DEFINE_STATE_INSERT(teco_state_insert); static gboolean teco_state_insert_indent_initial(teco_machine_main_t *ctx, GError **error) diff --git a/src/core-commands.h b/src/core-commands.h index 523ba28..254c4a7 100644 --- a/src/core-commands.h +++ b/src/core-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,40 +22,65 @@ #include "parser.h" #include "string-utils.h" -/** non-operational characters in teco_state_start */ -#define TECO_NOOPS " \f\r\n\v" +/** Check whether c is a non-operational command in teco_state_start */ +static inline gboolean +teco_is_noop(gunichar c) +{ + return c == ' ' || c == '\f' || c == '\r' || c == '\n' || c == '\v'; +} + +gboolean teco_get_range_args(const gchar *cmd, gsize *from_ret, gsize *len_ret, GError **error); + +/* in cmdline.c */ +gboolean teco_state_command_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); + +/** + * @class TECO_DEFINE_STATE_COMMAND + * @implements TECO_DEFINE_STATE_CASEINSENSITIVE + * @ingroup states + * + * Base state for everything where part of a one or two letter command + * is accepted. + */ +#define TECO_DEFINE_STATE_COMMAND(NAME, ...) \ + TECO_DEFINE_STATE_CASEINSENSITIVE(NAME, \ + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ + teco_state_command_process_edit_cmd, \ + .style = SCE_SCITECO_COMMAND, \ + ##__VA_ARGS__ \ + ) /* * FIXME: Most of these states can probably be private/static * as they are only referenced from teco_state_start. */ -TECO_DECLARE_STATE(teco_state_start); -TECO_DECLARE_STATE(teco_state_fcommand); +extern teco_state_t teco_state_fcommand; void teco_undo_change_dir_to_current(void); -TECO_DECLARE_STATE(teco_state_changedir); +extern teco_state_t teco_state_changedir; -TECO_DECLARE_STATE(teco_state_condcommand); -TECO_DECLARE_STATE(teco_state_control); -TECO_DECLARE_STATE(teco_state_ascii); -TECO_DECLARE_STATE(teco_state_escape); -TECO_DECLARE_STATE(teco_state_ecommand); +extern teco_state_t teco_state_condcommand; +extern teco_state_t teco_state_control; +extern teco_state_t teco_state_ascii; +extern teco_state_t teco_state_ecommand; typedef struct { - gsize from; /*< start position in bytes */ - gsize to; /*< end position in bytes */ + teco_int_t from; /*< start position in glyphs */ + teco_int_t to; /*< end position in glyphs */ } teco_range_t; extern guint teco_ranges_count; extern teco_range_t *teco_ranges; gboolean teco_state_insert_initial(teco_machine_main_t *ctx, GError **error); -gboolean teco_state_insert_process(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_insert_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error); -teco_state_t *teco_state_insert_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); +teco_state_t *teco_state_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error); /* in cmdline.c */ -gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); +gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar chr, GError **error); /** * @class TECO_DEFINE_STATE_INSERT @@ -63,21 +88,39 @@ gboolean teco_state_insert_process_edit_cmd(teco_machine_main_t *ctx, teco_machi * @ingroup states * * @note Also serves as a base class of the replace-insertion commands. - * @fixme Generating the done_cb could be avoided if there simply were a default. */ #define TECO_DEFINE_STATE_INSERT(NAME, ...) \ - static teco_state_t * \ - NAME##_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) \ - { \ - return teco_state_insert_done(ctx, str, error); \ - } \ TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ .initial_cb = (teco_state_initial_cb_t)teco_state_insert_initial, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_insert_process_edit_cmd, \ .expectstring.process_cb = teco_state_insert_process, \ + .expectstring.done_cb = teco_state_insert_done, \ ##__VA_ARGS__ \ ) -TECO_DECLARE_STATE(teco_state_insert_building); -TECO_DECLARE_STATE(teco_state_insert_nobuilding); -TECO_DECLARE_STATE(teco_state_insert_indent); +extern teco_state_t teco_state_insert; +extern teco_state_t teco_state_insert_indent; + +/** + * @class TECO_DEFINE_STATE_START + * @implements TECO_DEFINE_STATE_COMMAND + * @ingroup states + * + * Base state for everything where a new command can begin + * (the start state itself and all lookahead states). + */ +#define TECO_DEFINE_STATE_START(NAME, ...) \ + TECO_DEFINE_STATE_COMMAND(NAME, \ + .end_of_macro_cb = NULL, /* Allowed at the end of a macro! */ \ + .is_start = TRUE, \ + .keymacro_mask = TECO_KEYMACRO_MASK_START | TECO_KEYMACRO_MASK_CASEINSENSITIVE, \ + ##__VA_ARGS__ \ + ) + +teco_state_t *teco_state_start_input(teco_machine_main_t *ctx, gunichar chr, GError **error); + +extern teco_state_t teco_state_start; +extern teco_state_t teco_state_control; +extern teco_state_t teco_state_escape; +extern teco_state_t teco_state_ctlc; +extern teco_state_t teco_state_ctlc_control; @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -205,7 +205,8 @@ teco_doc_get_string(teco_doc_t *ctx, gchar **str, gsize *outlen, guint *codepage gsize len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0); if (str) { *str = g_malloc(len + 1); - teco_view_ssm(teco_qreg_view, SCI_GETTEXT, len + 1, (sptr_t)*str); + /* null-terminates the string */ + teco_view_ssm(teco_qreg_view, SCI_GETTEXT, len, (sptr_t)*str); } if (outlen) *outlen = len; @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -33,6 +33,8 @@ */ typedef struct teco_doc_scintilla_t teco_doc_scintilla_t; +TECO_DECLARE_UNDO_OBJECT(doc_scintilla, teco_doc_scintilla_t *); + /** * A Scintilla document. * @@ -108,7 +110,7 @@ void teco_doc_exchange(teco_doc_t *ctx, teco_doc_t *other); static inline void teco_doc_undo_exchange(teco_doc_t *ctx) { - teco_undo_ptr(ctx->doc); + teco_undo_object_doc_scintilla_push(&ctx->doc); teco_doc_undo_reset(ctx); } @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/error.c b/src/error.c index 6326984..716b60b 100644 --- a/src/error.c +++ b/src/error.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -79,7 +79,7 @@ teco_error_display_short(const GError *error) void teco_error_display_full(const GError *error) { - teco_interface_msg(TECO_MSG_ERROR, "%s", error->message); + teco_interface_msg_literal(TECO_MSG_ERROR, error->message, strlen(error->message)); guint nr = 0; diff --git a/src/error.h b/src/error.h index 61bcce6..67de4aa 100644 --- a/src/error.h +++ b/src/error.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -42,11 +42,13 @@ typedef enum { TECO_ERROR_SYNTAX, TECO_ERROR_MODIFIER, TECO_ERROR_ARGEXPECTED, + TECO_ERROR_LABEL, TECO_ERROR_CODEPOINT, TECO_ERROR_MOVE, TECO_ERROR_WORDS, TECO_ERROR_RANGE, TECO_ERROR_SUBPATTERN, + TECO_ERROR_INVALIDBUF, TECO_ERROR_INVALIDQREG, TECO_ERROR_QREGOPUNSUPPORTED, TECO_ERROR_QREGCONTAINSNULL, @@ -91,10 +93,18 @@ teco_error_argexpected_set(GError **error, const gchar *cmd) } static inline void +teco_error_label_set(GError **error, const gchar *name, gsize len) +{ + g_autofree gchar *label_printable = teco_string_echo(name, len); + g_set_error(error, TECO_ERROR, TECO_ERROR_LABEL, + "Label \"%s\" not found", label_printable); +} + +static inline void teco_error_codepoint_set(GError **error, const gchar *cmd) { g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, - "Invalid Unicode codepoint for <%s>", cmd); + "Invalid codepoint for <%s>", cmd); } static inline void @@ -126,6 +136,13 @@ teco_error_subpattern_set(GError **error, const gchar *cmd) } static inline void +teco_error_invalidbuf_set(GError **error, teco_int_t id) +{ + g_set_error(error, TECO_ERROR, TECO_ERROR_INVALIDBUF, + "Invalid buffer id %" TECO_INT_FORMAT, id); +} + +static inline void teco_error_invalidqreg_set(GError **error, const gchar *name, gsize len, gboolean local) { g_autofree gchar *name_printable = teco_string_echo(name, len); diff --git a/src/expressions.c b/src/expressions.c index f802c6e..9561c46 100644 --- a/src/expressions.c +++ b/src/expressions.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -76,12 +76,22 @@ teco_expressions_push_int(teco_int_t number) undo__remove_index__teco_numbers(teco_numbers->len-1); } +/** Peek into the numbers stack */ teco_int_t teco_expressions_peek_num(guint index) { return g_array_index(teco_numbers, teco_int_t, teco_numbers->len - 1 - index); } +/** + * Pop a value from the number stack. + * + * This must only be called if you are sure that the number at the + * given index exists and is an argument, i.e. only after calling + * teco_expressions_eval() and teco_expressions_args(). + * If you are unsure or want to imply a value, + * use teco_expressions_pop_num_calc() instead. + */ teco_int_t teco_expressions_pop_num(guint index) { @@ -99,6 +109,17 @@ teco_expressions_pop_num(guint index) return n; } +/** + * Pop an argument from the number stack. + * + * This resolves operations and allows implying values + * if there isn't any argument on the stack. + * + * @param ret Where to store the number + * @param imply The fallback value if there is no argument + * @param error A GError + * @return FALSE if an error occurred + */ gboolean teco_expressions_pop_num_calc(teco_int_t *ret, teco_int_t imply, GError **error) { @@ -263,6 +284,13 @@ teco_expressions_calc(GError **error) return TRUE; } +/** + * Resolve all operations on the top of the stack. + * + * @param pop_brace If TRUE this also pops the "brace" operator. + * @param error A GError + * @return FALSE if an error occurred + */ gboolean teco_expressions_eval(gboolean pop_brace, GError **error) { @@ -287,6 +315,14 @@ teco_expressions_eval(gboolean pop_brace, GError **error) return TRUE; } +/** + * Get number of numeric arguments on the top of the stack. + * + * @fixme You must call teco_expressions_eval() to resolve operations + * before this gives sensitive results. + * Overall it might be better to automatically call teco_expressions_eval() + * here or introduce a separate teco_expressions_args_calc(). + */ guint teco_expressions_args(void) { @@ -387,7 +423,7 @@ teco_expressions_clear(void) * @param buffer The output buffer of at least TECO_EXPRESSIONS_FORMAT_LEN characters. * The output string will be null-terminated. * @param number The number to format. - * @param table The local Q-Register table that contains the appropriate radix register (^R). + * @param qreg The radix register (^R). * @return A pointer into buffer to the beginning of the formatted number. */ gchar * diff --git a/src/expressions.h b/src/expressions.h index 631c867..3ef0faf 100644 --- a/src/expressions.h +++ b/src/expressions.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/file-utils.c b/src/file-utils.c index 75bcb48..7c37b27 100644 --- a/src/file-utils.c +++ b/src/file-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -109,6 +109,14 @@ teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attrs) #ifdef G_OS_UNIX +/* + * NOTE: This version does not resolve symlinks to non-existing paths. + * It could be improved by repeating readlink() and g_canonicalize_filename(), + * but it would require glib v2.58.0. + * Alternatively we could also iteratively resolve all path components. + * Currently, we simply do not rely on successful canonicalization of + * yet non-existing paths. + */ gchar * teco_file_get_absolute_path(const gchar *path) { @@ -137,13 +145,12 @@ teco_file_is_visible(const gchar *path) #if GLIB_CHECK_VERSION(2,58,0) /* - * FIXME: This should perhaps be preferred on any platform. - * But it will complicate preprocessing. + * NOTE: Does not resolve symlinks. */ gchar * teco_file_get_absolute_path(const gchar *path) { - return g_canonicalize_filename(path, NULL); + return path ? g_canonicalize_filename(path, NULL) : NULL; } #else /* !GLIB_CHECK_VERSION(2,58,0) */ @@ -353,7 +360,7 @@ teco_file_expand_path(const gchar *path) */ g_auto(teco_string_t) home = {NULL, 0}; if (!qreg->vtable->get_string(qreg, &home.data, &home.len, NULL, NULL) || - teco_string_contains(&home, '\0')) + teco_string_contains(home, '\0')) return g_strdup(path); g_assert(home.data != NULL); @@ -419,7 +426,7 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ while ((cur_basename.data = (gchar *)g_dir_read_name(dir))) { cur_basename.len = strlen(cur_basename.data); - if (string_diff(&cur_basename, basename, basename_len) != basename_len) + if (string_diff(cur_basename, basename, basename_len) != basename_len) /* basename is not a prefix of cur_basename */ continue; @@ -453,7 +460,7 @@ teco_file_auto_complete(const gchar *filename, GFileTest file_test, teco_string_ other_file.data = (gchar *)g_slist_next(files)->data + filename_len; other_file.len = strlen(other_file.data); - gsize len = string_diff(&other_file, cur_filename + filename_len, + gsize len = string_diff(other_file, cur_filename + filename_len, strlen(cur_filename) - filename_len); if (len < prefix_len) prefix_len = len; diff --git a/src/file-utils.h b/src/file-utils.h index 12a9b83..11d6650 100644 --- a/src/file-utils.h +++ b/src/file-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,9 +32,7 @@ void teco_file_set_attributes(const gchar *filename, teco_file_attributes_t attr /** * Get absolute/full version of a possibly relative path. * The path is tried to be canonicalized so it does - * not contain relative components. - * Works with existing and non-existing paths (in the latter case, - * heuristics may be applied). + * not contain relative components and symlinks. * Depending on platform and existence of the path, * canonicalization might fail, but the path returned is * always absolute. @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,10 +38,7 @@ #include "undo.h" #include "glob.h" -/* - * FIXME: This state could be static. - */ -TECO_DECLARE_STATE(teco_state_glob_filename); +static teco_state_t teco_state_glob_filename; /** @memberof teco_globber_t */ void @@ -308,13 +305,13 @@ teco_globber_compile_pattern(const gchar *pattern) */ static teco_state_t * -teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_glob_pattern_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_glob_filename; - if (str->len > 0) { - g_autofree gchar *filename = teco_file_expand_path(str->data); + if (str.len > 0) { + g_autofree gchar *filename = teco_file_expand_path(str.data); teco_qreg_t *glob_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); g_assert(glob_reg != NULL); @@ -327,7 +324,7 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, return &teco_state_glob_filename; } -/*$ EN glob +/*$ "EN" ":EN" glob * [type]EN[pattern]$[filename]$ -- Glob files or match filename and check file type * [type]:EN[pattern]$[filename]$ -> Success|Failure * @@ -373,7 +370,7 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, * \fIfilename\fP does not necessarily have to exist in the * file system for the match to succeed (unless a file type check * is also specified). - * For instance, \(lqENf??/\[**].c\fB$\fPfoo/bar.c\fB$\fP\(rq will + * For instance, \(lqENf??\[sl]*.c\fB$\fPfoo/bar.c\fB$\fP\(rq will * always match and the string \(lqfoo/bar.c\(rq will be inserted * (see below). * @@ -454,11 +451,12 @@ teco_state_glob_pattern_done(teco_machine_main_t *ctx, const teco_string_t *str, * have to edit that register anyway. */ TECO_DEFINE_STATE_EXPECTGLOB(teco_state_glob_pattern, - .expectstring.last = FALSE + .expectstring.last = FALSE, + .expectstring.done_cb = teco_state_glob_pattern_done ); static teco_state_t * -teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_glob_filename_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -470,8 +468,7 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str teco_int_t teco_test_mode; - if (!teco_expressions_eval(FALSE, error) || - !teco_expressions_pop_num_calc(&teco_test_mode, 0, error)) + if (!teco_expressions_pop_num_calc(&teco_test_mode, 0, error)) return NULL; switch (teco_test_mode) { /* @@ -498,35 +495,36 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str if (!glob_reg->vtable->get_string(glob_reg, &pattern_str.data, &pattern_str.len, NULL, error)) return NULL; - if (teco_string_contains(&pattern_str, '\0')) { + if (teco_string_contains(pattern_str, '\0')) { teco_error_qregcontainsnull_set(error, "_", 1, FALSE); return NULL; } - if (str->len > 0) { + if (str.len > 0) { /* * Match pattern against provided file name */ - g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autofree gchar *filename = teco_file_expand_path(str.data); g_autoptr(GRegex) pattern = teco_globber_compile_pattern(pattern_str.data); if (g_regex_match(pattern, filename, 0, NULL) && (teco_test_mode == 0 || g_file_test(filename, file_flags))) { if (!colon_modified) { - gsize len = strlen(filename); - - teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + len + 1; - teco_undo_guint(teco_ranges_count) = 1; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); /* * FIXME: Filenames may contain linefeeds. * But if we add them null-terminated, they will be relatively hard to parse. */ + gsize len = strlen(filename); filename[len] = '\n'; teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); teco_interface_ssm(SCI_ADDTEXT, len+1, (sptr_t)filename); teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos + len + 1); + teco_undo_guint(teco_ranges_count) = 1; } matching = TRUE; @@ -550,16 +548,15 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str g_auto(teco_globber_t) globber; teco_globber_init(&globber, pattern_str.data, file_flags); - teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from; - teco_undo_guint(teco_ranges_count) = 1; + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); gchar *globbed_filename; while ((globbed_filename = teco_globber_next(&globber))) { gsize len = strlen(globbed_filename); - teco_ranges[0].to += len+1; + pos += len+1; /* * FIXME: Filenames may contain linefeeds. @@ -573,6 +570,9 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str } teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); + + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos); + teco_undo_guint(teco_ranges_count) = 1; } if (colon_modified) { @@ -591,4 +591,6 @@ teco_state_glob_filename_done(teco_machine_main_t *ctx, const teco_string_t *str return &teco_state_start; } -TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename); +static TECO_DEFINE_STATE_EXPECTFILE(teco_state_glob_filename, + .expectstring.done_cb = teco_state_glob_filename_done +); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -47,8 +47,9 @@ gchar *teco_globber_escape_pattern(const gchar *pattern); GRegex *teco_globber_compile_pattern(const gchar *pattern); /* in cmdline.c */ -gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); +gboolean teco_state_expectglob_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, + gunichar key, GError **error); +gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTGLOB @@ -68,4 +69,4 @@ gboolean teco_state_expectglob_insert_completion(teco_machine_main_t *ctx, const * Command states */ -TECO_DECLARE_STATE(teco_state_glob_pattern); +extern teco_state_t teco_state_glob_pattern; diff --git a/src/goto-commands.c b/src/goto-commands.c index a0e6634..a9ff3c2 100644 --- a/src/goto-commands.c +++ b/src/goto-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ #include <glib.h> #include "sciteco.h" +#include "error.h" #include "string-utils.h" #include "expressions.h" #include "parser.h" @@ -34,17 +35,20 @@ #include "goto.h" #include "goto-commands.h" -TECO_DECLARE_STATE(teco_state_blockcomment); -TECO_DECLARE_STATE(teco_state_eolcomment); +static teco_state_t teco_state_blockcomment; +static teco_state_t teco_state_eolcomment; +/** + * In TECO_MODE_PARSE_ONLY_GOTO mode, we remain in parse-only mode + * until the given label is encountered. + */ teco_string_t teco_goto_skip_label = {NULL, 0}; - -static gboolean -teco_state_label_initial(teco_machine_main_t *ctx, GError **error) -{ - memset(&ctx->goto_label, 0, sizeof(ctx->goto_label)); - return TRUE; -} +/** + * The program counter to restore if the teco_goto_skip_label + * is \b not found (after :Olabel$). + * If smaller than 0 an error is thrown instead. + */ +gssize teco_goto_backup_pc = -1; /* * NOTE: The comma is theoretically not allowed in a label @@ -71,9 +75,10 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error) teco_goto_table_undo_remove(&ctx->goto_table, ctx->goto_label.data, ctx->goto_label.len); if (teco_goto_skip_label.len > 0 && - !teco_string_cmp(&ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) { + !teco_string_cmp(ctx->goto_label, teco_goto_skip_label.data, teco_goto_skip_label.len)) { teco_undo_string_own(teco_goto_skip_label); memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label)); + teco_undo_gssize(teco_goto_backup_pc) = -1; if (ctx->parent.must_undo) teco_undo_flags(ctx->flags); @@ -108,28 +113,37 @@ teco_state_label_input(teco_machine_main_t *ctx, gunichar chr, GError **error) } TECO_DEFINE_STATE(teco_state_label, - .initial_cb = (teco_state_initial_cb_t)teco_state_label_initial, - .style = SCE_SCITECO_LABEL + .style = SCE_SCITECO_LABEL, + .input_cb = (teco_state_input_cb_t)teco_state_label_input ); static teco_state_t * -teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_goto_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; + if (!str.len) { + /* you can still write @O/,/, though... */ + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "No labels given for <O>"); + return NULL; + } + teco_int_t value; - if (!teco_expressions_pop_num_calc(&value, 1, error)) + if (!teco_expressions_pop_num_calc(&value, 0, error)) return NULL; + gboolean colon_modified = teco_machine_main_eval_colon(ctx) > 0; + /* * Find the comma-separated substring in str indexed by `value`. */ teco_string_t label = {NULL, 0}; - while (value > 0) { - label.data = label.data ? label.data+label.len+1 : str->data; - const gchar *p = label.data ? memchr(label.data, ',', str->len - (label.data - str->data)) : NULL; - label.len = p ? p - label.data : str->len - (label.data - str->data); + while (value >= 0) { + label.data = label.data ? label.data+label.len+1 : str.data; + const gchar *p = label.data ? memchr(label.data, ',', str.len - (label.data - str.data)) : NULL; + label.len = p ? p - label.data : str.len - (label.data - str.data); value--; @@ -137,19 +151,24 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError break; } - if (value == 0) { + if (value < 0 && label.len > 0) { gssize pc = teco_goto_table_find(&ctx->goto_table, label.data, label.len); if (pc >= 0) { ctx->macro_pc = pc; - } else { + } else if (!ctx->goto_table.complete) { /* skip till label is defined */ g_assert(teco_goto_skip_label.len == 0); undo__teco_string_truncate(&teco_goto_skip_label, 0); teco_string_init(&teco_goto_skip_label, label.data, label.len); + teco_undo_gssize(teco_goto_backup_pc) = colon_modified ? ctx->macro_pc : -1; if (ctx->parent.must_undo) teco_undo_flags(ctx->flags); ctx->flags.mode = TECO_MODE_PARSE_ONLY_GOTO; + } else if (!colon_modified) { + /* can happen if we previously executed a colon-modified go-to */ + teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len); + return NULL; } } @@ -159,38 +178,52 @@ teco_state_goto_done(teco_machine_main_t *ctx, const teco_string_t *str, GError /* in cmdline.c */ gboolean teco_state_goto_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); -gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_goto_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); -/*$ O +/*$ "O" ":O" goto * Olabel$ -- Go to label - * [n]Olabel1[,label2,...]$ + * :Olabel$ + * [n]Olabel0[,label1,...]$ * * Go to <label>. * The simple go-to command is a special case of the * computed go-to command. * A comma-separated list of labels may be specified * in the string argument. - * The label to jump to is selected by <n> (1 is <label1>, - * 2 is <label2>, etc.). - * If <n> is omitted, 1 is implied. + * The label to jump to is selected by <n> (0 is <label0>, + * 1 is <label1>, etc.). + * If <n> is omitted, 0 is implied. + * Computed go-tos can be used like switch-case statements + * other languages. * * If the label selected by <n> is does not exist in the - * list of labels, the command does nothing. + * list of labels or is empty, the command does nothing + * and execution continues normally. * Label definitions are cached in a table, so that * if the label to go to has already been defined, the * go-to command will jump immediately. * Otherwise, parsing continues until the <label> * is defined. * The command will yield an error if a label has - * not been defined when the macro or command-line - * is terminated. - * In the latter case, the user will not be able to - * terminate the command-line. + * not been defined when the macro is terminated. + * When jumping to a non-existent <label> in the + * command-line macro, you cannot practically terminate + * the command-line until defining the <label>. + * + * String building constructs are enabled in \fBO\fP + * which allows for a second kind of computed go-to, + * where the label name contains the value to select. + * When colon-modifying the \fBO\fP command, execution + * will continue after the command if the given <label> + * isn't found. + * This is useful to handle the \(lqdefault\(rq case + * when using computed go-tos of the second kind. */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_goto_process_edit_cmd, - .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_goto_insert_completion + .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_goto_insert_completion, + .expectstring.done_cb = teco_state_goto_done ); /** @@ -211,25 +244,34 @@ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_goto, ) static teco_state_t * -teco_state_blockcomment_star_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +teco_state_blockcomment_star_input(teco_machine_t *ctx, gunichar chr, GError **error) { return chr == '!' ? &teco_state_start : &teco_state_blockcomment; } -TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment_star); +static TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment_star, + .input_cb = teco_state_blockcomment_star_input +); static teco_state_t * -teco_state_blockcomment_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +teco_state_blockcomment_input(teco_machine_t *ctx, gunichar chr, GError **error) { return chr == '*' ? &teco_state_blockcomment_star : &teco_state_blockcomment; } -TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment); +static TECO_DEFINE_STATE_COMMENT(teco_state_blockcomment, + .input_cb = teco_state_blockcomment_input +); +/* + * `!!` line comments are inspired by TECO-64. + */ static teco_state_t * -teco_state_eolcomment_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +teco_state_eolcomment_input(teco_machine_t *ctx, gunichar chr, GError **error) { return chr == '\n' ? &teco_state_start : &teco_state_eolcomment; } -TECO_DEFINE_STATE_COMMENT(teco_state_eolcomment); +static TECO_DEFINE_STATE_COMMENT(teco_state_eolcomment, + .input_cb = teco_state_eolcomment_input +); diff --git a/src/goto-commands.h b/src/goto-commands.h index f4f52d5..3b44168 100644 --- a/src/goto-commands.h +++ b/src/goto-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,10 +16,13 @@ */ #pragma once +#include <glib.h> + #include "parser.h" #include "string-utils.h" extern teco_string_t teco_goto_skip_label; +extern gssize teco_goto_backup_pc; -TECO_DECLARE_STATE(teco_state_label); -TECO_DECLARE_STATE(teco_state_goto); +extern teco_state_t teco_state_label; +extern teco_state_t teco_state_goto; @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,10 +26,14 @@ typedef struct { teco_rb3str_tree_t tree; + /** Whether to generate undo tokens (unnecessary in macro invocations) */ + guint must_undo : 1; + /** - * Whether to generate undo tokens (unnecessary in macro invocations) + * Whether the table is guaranteed to be complete because the entire + * macro has already been parsed. */ - gboolean must_undo; + guint complete : 1; } teco_goto_table_t; /** @memberof teco_goto_table_t */ @@ -38,6 +42,7 @@ teco_goto_table_init(teco_goto_table_t *ctx, gboolean must_undo) { rb3_reset_tree(&ctx->tree); ctx->must_undo = must_undo; + ctx->complete = FALSE; } gboolean teco_goto_table_remove(teco_goto_table_t *ctx, const gchar *name, gsize len); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -271,7 +271,7 @@ teco_state_help_initial(teco_machine_main_t *ctx, GError **error) } static teco_state_t * -teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_help_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -281,7 +281,7 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError "Help topic must not contain null-byte"); return NULL; } - const gchar *topic_name = str->data ? : ""; + const gchar *topic_name = str.data ? : ""; teco_help_topic_t *topic = teco_help_find(topic_name); if (!topic) { g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, @@ -316,7 +316,7 @@ teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError /* in cmdline.c */ gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); -gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /*$ "?" help @@ -388,5 +388,6 @@ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_help, .initial_cb = (teco_state_initial_cb_t)teco_state_help_initial, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_help_process_edit_cmd, .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_help_insert_completion, - .expectstring.string_building = FALSE + .expectstring.string_building = FALSE, + .expectstring.done_cb = teco_state_help_done ); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,4 +27,4 @@ gboolean teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert) * Command states */ -TECO_DECLARE_STATE(teco_state_help); +extern teco_state_t teco_state_help; diff --git a/src/interface-curses/curses-icons.c b/src/interface-curses/curses-icons.c index 8a84abe..0e14655 100644 --- a/src/interface-curses/curses-icons.c +++ b/src/interface-curses/curses-icons.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -24,8 +24,6 @@ #include <glib.h> -#include <curses.h> - #include "sciteco.h" #include "curses-icons.h" @@ -364,6 +362,10 @@ teco_curses_icon_cmp(const void *a, const void *b) gunichar teco_curses_icons_lookup_file(const gchar *filename) { + if (!filename || !*filename) + /* "(Unnamed)" file */ + return 0xf1036; /* */ + g_autofree gchar *basename = g_path_get_basename(filename); const teco_curses_icon_t *icon; diff --git a/src/interface-curses/curses-icons.h b/src/interface-curses/curses-icons.h index fce9d75..a12fe88 100644 --- a/src/interface-curses/curses-icons.h +++ b/src/interface-curses/curses-icons.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-info-popup.c b/src/interface-curses/curses-info-popup.c index 332d434..edb6e15 100644 --- a/src/interface-curses/curses-info-popup.c +++ b/src/interface-curses/curses-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,8 @@ #include "config.h" #endif +#include <string.h> + #include <glib.h> #include <curses.h> @@ -26,6 +28,7 @@ #include "list.h" #include "string-utils.h" #include "interface.h" +#include "cmdline.h" #include "curses-utils.h" #include "curses-info-popup.h" #include "curses-icons.h" @@ -37,6 +40,7 @@ typedef struct { teco_stailq_entry_t entry; teco_popup_entry_type_t type; + /** entry name or empty string for the "(Unnamed)" buffer */ teco_string_t name; gboolean highlight; } teco_popup_entry_t; @@ -71,7 +75,6 @@ teco_curses_info_popup_add(teco_curses_info_popup_t *ctx, teco_popup_entry_type_ static void teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) { - int cols = getmaxx(stdscr); /**! screen width */ int pad_lines; /**! pad height */ gint pad_cols; /**! entry columns */ gint pad_colwidth; /**! width per entry column */ @@ -82,10 +85,10 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) * Otherwise 2 characters after the entry. */ gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2; - pad_colwidth = MIN(ctx->longest + reserve, cols - 2); + pad_colwidth = MIN(ctx->longest + reserve, COLS - 2); /* pad_cols = floor((cols - 2) / pad_colwidth) */ - pad_cols = (cols - 2) / pad_colwidth; + pad_cols = (COLS - 2) / pad_colwidth; /* pad_lines = ceil(length / pad_cols) */ pad_lines = (ctx->length+pad_cols-1) / pad_cols; @@ -96,7 +99,7 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) * it will be drawn into the popup window which has left * and right borders. */ - ctx->pad = newpad(pad_lines, cols - 2); + ctx->pad = newpad(pad_lines, COLS - 2); /* * NOTE: attr could contain A_REVERSE on monochrome terminals, @@ -122,25 +125,32 @@ teco_curses_info_popup_init_pad(teco_curses_info_popup_t *ctx, attr_t attr) if (entry->highlight) wattron(ctx->pad, A_BOLD); + teco_string_t name = entry->name; + if (!name.len) { + name.data = TECO_UNNAMED_FILE; + name.len = strlen(name.data); + } + switch (entry->type) { case TECO_POPUP_FILE: - g_assert(!teco_string_contains(&entry->name, '\0')); + g_assert(!teco_string_contains(name, '\0')); if (teco_ed & TECO_ED_ICONS) { + /* "(Unnamed)" buffer is looked up as "" */ teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_file(entry->name.data)); waddch(ctx->pad, ' '); } - teco_curses_format_filename(ctx->pad, entry->name.data, -1); + teco_curses_format_filename(ctx->pad, name.data, -1); break; case TECO_POPUP_DIRECTORY: - g_assert(!teco_string_contains(&entry->name, '\0')); + g_assert(!teco_string_contains(name, '\0')); if (teco_ed & TECO_ED_ICONS) { teco_curses_add_wc(ctx->pad, teco_curses_icons_lookup_dir(entry->name.data)); waddch(ctx->pad, ' '); } - teco_curses_format_filename(ctx->pad, entry->name.data, -1); + teco_curses_format_filename(ctx->pad, name.data, -1); break; default: - teco_curses_format_str(ctx->pad, entry->name.data, entry->name.len, -1); + teco_curses_format_str(ctx->pad, name.data, name.len, -1); break; } @@ -157,9 +167,6 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) /* nothing to display */ return; - int lines, cols; /* screen dimensions */ - getmaxyx(stdscr, lines, cols); - if (ctx->window) delwin(ctx->window); @@ -171,10 +178,10 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) * Popup window can cover all but one screen row. * Another row is reserved for the top border. */ - gint popup_lines = MIN(pad_lines + 1, lines - 1); + gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height); /* window covers message, scintilla and info windows */ - ctx->window = newwin(popup_lines, 0, lines - 1 - popup_lines, 0); + ctx->window = newwin(popup_lines, 0, LINES - teco_cmdline.height - popup_lines, 0); wattrset(ctx->window, attr); @@ -188,7 +195,7 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) copywin(ctx->pad, ctx->window, ctx->pad_first_line, 0, - 1, 1, popup_lines - 1, cols - 2, FALSE); + 1, 1, popup_lines - 1, COLS - 2, FALSE); if (pad_lines <= popup_lines - 1) /* no need for scrollbar */ @@ -200,13 +207,13 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) /* bar_y = floor(pad_first_line/pad_lines * (popup_lines-2)) + 1 */ gint bar_y = ctx->pad_first_line*(popup_lines-2) / pad_lines + 1; - mvwvline(ctx->window, 1, cols-1, ACS_CKBOARD, popup_lines-2); + mvwvline(ctx->window, 1, COLS-1, ACS_CKBOARD, popup_lines-2); /* * We do not use ACS_BLOCK here since it will not * always be drawn as a solid block (e.g. xterm). * Instead, simply draw reverse blanks. */ - wmove(ctx->window, bar_y, cols-1); + wmove(ctx->window, bar_y, COLS-1); wattrset(ctx->window, attr ^ A_REVERSE); wvline(ctx->window, ' ', bar_height); } @@ -227,7 +234,6 @@ teco_curses_info_popup_show(teco_curses_info_popup_t *ctx, attr_t attr) const teco_string_t * teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x) { - int cols = getmaxx(stdscr); /**! screen width */ gint pad_cols; /**! entry columns */ gint pad_colwidth; /**! width per entry column */ @@ -240,10 +246,10 @@ teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x) * Otherwise 2 characters after the entry. */ gint reserve = teco_ed & TECO_ED_ICONS ? 2+1 : 2; - pad_colwidth = MIN(ctx->longest + reserve, cols - 2); + pad_colwidth = MIN(ctx->longest + reserve, COLS - 2); /* pad_cols = floor((cols - 2) / pad_colwidth) */ - pad_cols = (cols - 2) / pad_colwidth; + pad_cols = (COLS - 2) / pad_colwidth; gint cur_col = 0; for (teco_stailq_entry_t *cur = ctx->list.first; cur != NULL; cur = cur->next) { @@ -265,9 +271,8 @@ teco_curses_info_popup_getentry(teco_curses_info_popup_t *ctx, gint y, gint x) void teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx) { - gint lines = getmaxy(stdscr); gint pad_lines = getmaxy(ctx->pad); - gint popup_lines = MIN(pad_lines + 1, lines - 1); + gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height); /* progress scroll position */ ctx->pad_first_line += popup_lines - 1; @@ -281,9 +286,8 @@ teco_curses_info_popup_scroll_page(teco_curses_info_popup_t *ctx) void teco_curses_info_popup_scroll(teco_curses_info_popup_t *ctx, gint delta) { - gint lines = getmaxy(stdscr); gint pad_lines = getmaxy(ctx->pad); - gint popup_lines = MIN(pad_lines + 1, lines - 1); + gint popup_lines = MIN(pad_lines + 1, LINES - teco_cmdline.height); ctx->pad_first_line = MAX(ctx->pad_first_line+delta, 0); if (pad_lines - ctx->pad_first_line < popup_lines - 1) diff --git a/src/interface-curses/curses-info-popup.h b/src/interface-curses/curses-info-popup.h index d845b29..fd923e9 100644 --- a/src/interface-curses/curses-info-popup.h +++ b/src/interface-curses/curses-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-utils.c b/src/interface-curses/curses-utils.c index f94b6dc..875c332 100644 --- a/src/interface-curses/curses-utils.c +++ b/src/interface-curses/curses-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-curses/curses-utils.h b/src/interface-curses/curses-utils.h index 18cdd3d..97fc1cc 100644 --- a/src/interface-curses/curses-utils.h +++ b/src/interface-curses/curses-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,9 @@ #include <curses.h> +/** what is displayed for unnamed buffers in the info line and popups */ +#define TECO_UNNAMED_FILE "(Unnamed)" + guint teco_curses_format_str(WINDOW *win, const gchar *str, gsize len, gint max_width); guint teco_curses_format_filename(WINDOW *win, const gchar *filename, gint max_width); diff --git a/src/interface-curses/interface.c b/src/interface-curses/interface.c index a71ca20..b1c806f 100644 --- a/src/interface-curses/interface.c +++ b/src/interface-curses/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,7 +22,6 @@ #include <string.h> #include <stdio.h> #include <stdlib.h> -#include <stdarg.h> #include <unistd.h> #include <errno.h> @@ -108,6 +107,8 @@ #define CURSES_TTY #endif +//#define DEBUG + #ifdef G_OS_WIN32 /** @@ -135,7 +136,7 @@ teco_console_ctrl_handler(DWORD type) static gint teco_xterm_version(void) G_GNUC_UNUSED; -#define UNNAMED_FILE "(Unnamed)" +static gint teco_interface_blocking_getch(void); /** * Get bright variant of one of the 8 standard @@ -163,19 +164,98 @@ static gint teco_xterm_version(void) G_GNUC_UNUSED; #define COLOR_LCYAN COLOR_LIGHT(COLOR_CYAN) #define COLOR_LWHITE COLOR_LIGHT(COLOR_WHITE) +static struct { + /** + * Mapping of foreground and background curses color tuples + * (encoded into a pointer) to a color pair number. + */ + GHashTable *pair_table; + + /** + * Mapping of the first 16 curses color codes (that may or may not + * correspond with the standard terminal color codes) to + * Scintilla-compatible RGB values (red is LSB) to initialize after + * Curses startup. + * Negative values mean no color redefinition (keep the original + * palette entry). + */ + gint32 color_table[16]; + + /** + * Mapping of the first 16 curses color codes to their + * original values for restoring them on shutdown. + * Unfortunately, this may not be supported on all + * curses ports, so this array may be unused. + */ + struct { + gshort r, g, b; + } orig_color_table[16]; + + int stdin_orig, stdout_orig, stderr_orig; + SCREEN *screen; + FILE *screen_tty; + + WINDOW *info_window; + enum { + TECO_INFO_TYPE_BUFFER = 0, + TECO_INFO_TYPE_QREG + } info_type; + /* current document's name or empty string for "(Unnamed)" buffer */ + teco_string_t info_current; + gboolean info_dirty; + + /** timer to track the recovery interval */ + GTimer *recovery_timer; + + WINDOW *msg_window; + + /** + * Pad used exclusively for wgetch() as it will not + * result in unwanted wrefresh(). + */ + WINDOW *input_pad; + GQueue *input_queue; + + teco_curses_info_popup_t popup; + gsize popup_prefix_len; + + /** + * GError "thrown" by teco_interface_event_loop_iter(). + * Having this in a variable avoids problems with EMScripten. + */ + GError *event_loop_error; +} teco_interface; + /** - * Returns the curses `COLOR_PAIR` for the given curses foreground and background `COLOR`s. - * This is used simply to enumerate every possible color combination. - * Note: only 256 combinations are possible due to curses portability. + * Returns the curses color pair for the given curses foreground and background colors. + * Initializes a new pair if necessary. + * + * Scinterm no longer initializes all color pairs for all combinations of + * the builtin foreground and background colors. + * Since curses guarantees only 256 color pairs, we cannot do that either. + * Instead we allocate color pairs beginnig at 128 on demand + * (similar to what Scinterm does). * - * @param fg The curses foreground `COLOR`. - * @param bg The curses background `COLOR`. - * @return number for defining a curses `COLOR_PAIR`. + * @note Scinterm now also has scintilla_set_color_offsets(), + * so we could use the lower 127 color pairs as well. + * + * @param fg curses foreground color + * @param bg curses background color + * @return curses color pair number */ -static inline gshort +static gshort teco_color_pair(gshort fg, gshort bg) { - return bg * (COLORS < 16 ? 8 : 16) + fg + 1; + static gshort last_pair = 127; + + G_STATIC_ASSERT(sizeof(gshort)*2 <= sizeof(guint)); + gpointer key = GUINT_TO_POINTER(((guint)fg << 16) | bg); + gpointer value = g_hash_table_lookup(teco_interface.pair_table, key); + if (G_LIKELY(value != NULL)) + return GPOINTER_TO_UINT(value); + init_pair(++last_pair, fg, bg); + g_hash_table_insert(teco_interface.pair_table, key, GUINT_TO_POINTER(last_pair)); + return last_pair; } /** @@ -333,61 +413,6 @@ teco_view_free(teco_view_t *ctx) scintilla_delete(ctx); } -static struct { - /** - * Mapping of the first 16 curses color codes (that may or may not - * correspond with the standard terminal color codes) to - * Scintilla-compatible RGB values (red is LSB) to initialize after - * Curses startup. - * Negative values mean no color redefinition (keep the original - * palette entry). - */ - gint32 color_table[16]; - - /** - * Mapping of the first 16 curses color codes to their - * original values for restoring them on shutdown. - * Unfortunately, this may not be supported on all - * curses ports, so this array may be unused. - */ - struct { - gshort r, g, b; - } orig_color_table[16]; - - int stdout_orig, stderr_orig; - SCREEN *screen; - FILE *screen_tty; - - WINDOW *info_window; - enum { - TECO_INFO_TYPE_BUFFER = 0, - TECO_INFO_TYPE_QREG - } info_type; - teco_string_t info_current; - gboolean info_dirty; - - WINDOW *msg_window; - - WINDOW *cmdline_window, *cmdline_pad; - guint cmdline_len, cmdline_rubout_len; - - /** - * Pad used exclusively for wgetch() as it will not - * result in unwanted wrefresh(). - */ - WINDOW *input_pad; - GQueue *input_queue; - - teco_curses_info_popup_t popup; - gsize popup_prefix_len; - - /** - * GError "thrown" by teco_interface_event_loop_iter(). - * Having this in a variable avoids problems with EMScripten. - */ - GError *event_loop_error; -} teco_interface; - static void teco_interface_init_color_safe(guint color, guint32 rgb); static void teco_interface_restore_colors(void); @@ -401,7 +426,6 @@ static void teco_interface_resize_all_windows(void); static void teco_interface_set_window_title(const gchar *title); static void teco_interface_draw_info(void); -static void teco_interface_draw_cmdline(void); void teco_interface_init(void) @@ -411,15 +435,16 @@ teco_interface_init(void) for (guint i = 0; i < G_N_ELEMENTS(teco_interface.orig_color_table); i++) teco_interface.orig_color_table[i].r = -1; - teco_interface.stdout_orig = teco_interface.stderr_orig = -1; + teco_interface.stdin_orig = teco_interface.stdout_orig = teco_interface.stderr_orig = -1; teco_curses_info_popup_init(&teco_interface.popup); + teco_cmdline_init(); /* - * Make sure we have a string for the info line - * even if teco_interface_info_update() is never called. + * The default INDIC_STRIKE wouldn't be visible. + * Instead we use INDIC_SQUIGGLE, which is rendered as A_UNDERLINE. */ - teco_string_init(&teco_interface.info_current, PACKAGE_NAME, strlen(PACKAGE_NAME)); + teco_cmdline_ssm(SCI_INDICSETSTYLE, INDICATOR_RUBBEDOUT, INDIC_SQUIGGLE); /* * On all platforms except Curses/XTerm, it's @@ -558,7 +583,7 @@ teco_interface_init_color(guint color, guint32 rgb) ((color & 0x1) << 2) | ((color & 0x4) >> 2); #endif - if (teco_interface.cmdline_window) { + if (teco_interface.input_pad) { /* interactive mode */ if (!can_change_color()) return; @@ -580,22 +605,46 @@ teco_interface_init_color(guint color, guint32 rgb) static void teco_interface_init_screen(void) { - teco_interface.screen_tty = g_fopen("/dev/tty", "r+"); + teco_interface.screen_tty = g_fopen("/dev/tty", "a"); /* should never fail */ g_assert(teco_interface.screen_tty != NULL); - teco_interface.screen = newterm(NULL, teco_interface.screen_tty, teco_interface.screen_tty); + /* + * At least on NetBSD we loose keypresses when passing in a + * handle for /dev/tty. + * We therefore redirect stdin in interactive mode. + * This works always if stdin was already redirected or not (isatty(0)) + * since we are guaranteed not to read from stdin outside of curses. + * When returning to batch mode, we can restore the original stdin. + */ + teco_interface.stdin_orig = dup(0); + g_assert(teco_interface.stdin_orig >= 0); + G_GNUC_UNUSED FILE *stdin_new = g_freopen("/dev/tty", "r", stdin); + g_assert(stdin_new != NULL); + + teco_interface.screen = newterm(NULL, teco_interface.screen_tty, stdin); if (G_UNLIKELY(!teco_interface.screen)) { g_fprintf(stderr, "Error initializing interactive mode. " "$TERM may be incorrect.\n"); exit(EXIT_FAILURE); } + /* initscr() does that in ncurses */ + def_prog_mode(); + /* * If stdout or stderr would go to the terminal, * redirect it. Otherwise, they are already redirected * (e.g. to a file) and writing to them does not * interrupt terminal interaction. + * + * This cannot of course preserve all messages written to stdout/stderr. + * Only those messages written before flushing will be preserved and + * be visible after program termination since they are still in a user- + * space stdio-buffer. + * All messages could only be preserved if we redirected to a temporary + * file and replayed it afterwards. It wouldn't preserve the order of + * stdout vs. stderr messages. */ if (isatty(1)) { teco_interface.stdout_orig = dup(1); @@ -690,6 +739,9 @@ teco_interface_init_interactive(GError **error) teco_interface_init_screen(); + teco_interface.pair_table = g_hash_table_new(g_direct_hash, g_direct_equal); + start_color(); + /* * On UNIX terminals, the escape key is usually * delivered as the escape character even though function @@ -716,12 +768,8 @@ teco_interface_init_interactive(GError **error) * Disables click-detection. * If we'd want to discern PRESSED and CLICKED events, * we'd have to emulate the same feature on GTK. - * - * On PDCurses/Wincon we currently rely on click detection - * since it does not report BUTTONX_RELEASED unless also - * moving the mouse cursor. */ -#if NCURSES_MOUSE_VERSION >= 2 && !defined(PDCURSES_WINCON) +#if NCURSES_MOUSE_VERSION >= 2 mouseinterval(0); #endif @@ -745,8 +793,12 @@ teco_interface_init_interactive(GError **error) leaveok(stdscr, TRUE); teco_interface.info_window = newwin(1, 0, 0, 0); - teco_interface.msg_window = newwin(1, 0, LINES - 2, 0); - teco_interface.cmdline_window = newwin(0, 0, LINES - 1, 0); + teco_interface.msg_window = newwin(1, 0, LINES - teco_cmdline.height - 1, 0); + + WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view); + wresize(cmdline_win, teco_cmdline.height, COLS); + mvwin(cmdline_win, LINES - teco_cmdline.height, 0); + teco_cmdline_resized(COLS); teco_interface.input_pad = newpad(1, 1); /* @@ -819,10 +871,14 @@ teco_interface_restore_batch(void) teco_interface_restore_colors(); /* - * Restore stdout and stderr, so output goes to + * Restore stdin, stdout and stderr, so output goes to * the terminal again in case we "muted" them. */ #ifdef CURSES_TTY + if (teco_interface.stdin_orig >= 0) { + G_GNUC_UNUSED int fd = dup2(teco_interface.stdin_orig, 0); + g_assert(fd == 0); + } if (teco_interface.stdout_orig >= 0) { G_GNUC_UNUSED int fd = dup2(teco_interface.stdout_orig, 1); g_assert(fd == 1); @@ -834,40 +890,38 @@ teco_interface_restore_batch(void) #endif /* - * cmdline_window determines whether we're in batch mode. + * input_pad determines whether we're in batch mode. */ - if (teco_interface.cmdline_window) { - delwin(teco_interface.cmdline_window); - teco_interface.cmdline_window = NULL; + if (teco_interface.input_pad) { + delwin(teco_interface.input_pad); + teco_interface.input_pad = NULL; } } static void teco_interface_resize_all_windows(void) { - int lines, cols; /* screen dimensions */ - - getmaxyx(stdscr, lines, cols); - - wresize(teco_interface.info_window, 1, cols); + wresize(teco_interface.info_window, 1, COLS); wresize(teco_view_get_window(teco_interface_current_view), - lines - 3, cols); - wresize(teco_interface.msg_window, 1, cols); - mvwin(teco_interface.msg_window, lines - 2, 0); - wresize(teco_interface.cmdline_window, 1, cols); - mvwin(teco_interface.cmdline_window, lines - 1, 0); + LINES - 2 - teco_cmdline.height, COLS); + wresize(teco_interface.msg_window, 1, COLS); + mvwin(teco_interface.msg_window, LINES - 1 - teco_cmdline.height, 0); + + WINDOW *cmdline_win = teco_view_get_window(teco_cmdline.view); + wresize(cmdline_win, teco_cmdline.height, COLS); + mvwin(cmdline_win, LINES - teco_cmdline.height, 0); + teco_cmdline_resized(COLS); teco_interface_draw_info(); teco_interface_msg_clear(); /* FIXME: use saved message */ teco_interface_popup_clear(); - teco_interface_draw_cmdline(); } void -teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) +teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len) { - if (!teco_interface.cmdline_window) { /* batch mode */ - teco_interface_stdio_vmsg(type, fmt, ap); + if (!teco_interface.input_pad) { /* batch mode */ + teco_interface_stdio_msg(type, str, len); return; } @@ -876,10 +930,7 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) * even in interactive mode. */ #if defined(PDCURSES_GUI) || defined(CURSES_TTY) || defined(NCURSES_WIN32) - va_list aq; - va_copy(aq, ap); - teco_interface_stdio_vmsg(type, fmt, aq); - va_end(aq); + teco_interface_stdio_msg(type, str, len); #endif short fg, bg; @@ -903,27 +954,68 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) break; } - /* - * NOTE: This is safe since we don't have to cancel out any A_REVERSE, - * that could be set in the background attributes. - */ wmove(teco_interface.msg_window, 0, 0); - wbkgdset(teco_interface.msg_window, teco_color_attr(fg, bg)); - vw_printw(teco_interface.msg_window, fmt, ap); - wclrtoeol(teco_interface.msg_window); + wattrset(teco_interface.msg_window, teco_color_attr(fg, bg)); + teco_curses_format_str(teco_interface.msg_window, str, len, -1); + teco_curses_clrtobot(teco_interface.msg_window); } void teco_interface_msg_clear(void) { - if (!teco_interface.cmdline_window) /* batch mode */ + if (!teco_interface.input_pad) /* batch mode */ return; short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0)); short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0)); - wbkgdset(teco_interface.msg_window, teco_color_attr(fg, bg)); - werase(teco_interface.msg_window); + wmove(teco_interface.msg_window, 0, 0); + wattrset(teco_interface.msg_window, teco_color_attr(fg, bg)); + teco_curses_clrtobot(teco_interface.msg_window); +} + +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!teco_interface.input_pad) /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_interface_refresh(FALSE); + + /* + * Signal that we accept input by drawing a real cursor in the message bar. + */ + wmove(teco_interface.msg_window, 0, 0); + curs_set(1); + wrefresh(teco_interface.msg_window); + + gchar buf[4]; + gint i = 0; + gint32 cp; + + do { + cp = teco_interface_blocking_getch(); + if (cp == TECO_CTL_KEY('C')) + teco_interrupted = TRUE; + if (cp == TECO_CTL_KEY('C') || cp == TECO_CTL_KEY('D')) { + cp = -1; + break; + } + if (cp < 0 || cp > 0xFF) + continue; + + if (!widechar || !cp) + break; + + /* doesn't work as expected when passed a null byte */ + buf[i] = cp; + cp = g_utf8_get_char_validated(buf, ++i); + if (i >= sizeof(buf) || cp != -2) + i = 0; + } while (cp < 0); + + curs_set(0); + return cp; } void @@ -931,7 +1023,7 @@ teco_interface_show_view(teco_view_t *view) { teco_interface_current_view = view; - if (!teco_interface.cmdline_window) /* batch mode */ + if (!teco_interface.input_pad) /* batch mode */ return; WINDOW *current_view_win = teco_view_get_window(teco_interface_current_view); @@ -940,9 +1032,7 @@ teco_interface_show_view(teco_view_t *view) * screen size might have changed since * this view's WINDOW was last active */ - int lines, cols; /* screen dimensions */ - getmaxyx(stdscr, lines, cols); - wresize(current_view_win, lines - 3, cols); + wresize(current_view_win, LINES - 2 - teco_cmdline.height, COLS); /* Set up window position: never changes */ mvwin(current_view_win, 1, 0); } @@ -972,40 +1062,64 @@ teco_interface_set_window_title(const gchar *title) #elif defined(CURSES_TTY) && defined(HAVE_TIGETSTR) +/* + * Many Modern terminal emulators map the window title to + * the historic status line. + * This feature is not standardized in ncurses, + * so we query the terminfo database. + * This feature may make problems with terminal emulators + * that do support a status line but do not map them + * to the window title. + * Some emulators (like xterm, rxvt and many pseudo-xterms) + * support setting the window title via custom escape + * sequences and via the status line but their + * terminfo entry does not say so. + * Real XTerm can also save and restore window titles but + * there is not even a terminfo capability defined for this. + * Currently, SciTECO just leaves the title set after we quit. + * + * TODO: Once we support customizing the UI, + * there could be a special status line that's sent + * to the terminal that may be set up in the profile + * depending on $TERM. + */ static void teco_interface_set_window_title(const gchar *title) { - if (!has_status_line || !to_status_line || !from_status_line) + static const gchar *term = NULL; + static const gchar *title_start = NULL; + static const gchar *title_end = NULL; + + if (G_UNLIKELY(!term)) { + term = g_getenv("TERM"); + + title_start = to_status_line; + title_end = from_status_line; + + if ((!title_start || !title_end) && term && + (g_str_has_prefix(term, "xterm") || g_str_has_prefix(term, "rxvt"))) { + /* + * Just assume that any whitelisted $TERM has the OSC-0 + * escape sequence or at least ignores it. + * This might also set the window's icon, but it's more widely + * used than OSC-2. + */ + title_start = "\e]0;"; + title_end = "\a"; + } + } + + if (!title_start || !title_end) return; /* - * Modern terminal emulators map the window title to - * the historic status line. - * This feature is not standardized in ncurses, - * so we query the terminfo database. - * This feature may make problems with terminal emulators - * that do support a status line but do not map them - * to the window title. Some emulators (like xterm) - * support setting the window title via custom escape - * sequences and via the status line but their - * terminfo entry does not say so. (xterm can also - * save and restore window titles but there is not - * even a terminfo capability defined for this.) - * Taken the different emulator incompatibilites - * it may be best to make this configurable. - * Once we support configurable status lines, - * there could be a special status line that's sent - * to the terminal that may be set up in the profile - * depending on $TERM. - * * NOTE: The terminfo manpage advises us to use putp() * but on ncurses/UNIX (where terminfo is available), * we do not let curses write to stdout. - * NOTE: This leaves the title set after we quit. */ - fputs(to_status_line, teco_interface.screen_tty); + fputs(title_start, teco_interface.screen_tty); fputs(title, teco_interface.screen_tty); - fputs(from_status_line, teco_interface.screen_tty); + fputs(title_end, teco_interface.screen_tty); fflush(teco_interface.screen_tty); } @@ -1040,26 +1154,30 @@ teco_interface_draw_info(void) waddstr(teco_interface.info_window, PACKAGE_NAME " "); + teco_string_t info_current = teco_interface.info_current; + if (!info_current.len) { + info_current.data = TECO_UNNAMED_FILE; + info_current.len = strlen(info_current.data); + } + switch (teco_interface.info_type) { case TECO_INFO_TYPE_QREG: info_type_str = PACKAGE_NAME " - <QRegister> "; teco_curses_add_wc(teco_interface.info_window, teco_ed & TECO_ED_ICONS ? TECO_CURSES_ICONS_QREG : '-'); waddstr(teco_interface.info_window, " <QRegister> "); - /* same formatting as in command lines */ teco_curses_format_str(teco_interface.info_window, - teco_interface.info_current.data, - teco_interface.info_current.len, -1); + info_current.data, info_current.len, -1); break; case TECO_INFO_TYPE_BUFFER: info_type_str = PACKAGE_NAME " - <Buffer> "; - g_assert(!teco_string_contains(&teco_interface.info_current, '\0')); + g_assert(!teco_string_contains(info_current, '\0')); + /* "(Unnamed)" buffer has to be looked up as "" */ teco_curses_add_wc(teco_interface.info_window, teco_ed & TECO_ED_ICONS ? teco_curses_icons_lookup_file(teco_interface.info_current.data) : '-'); waddstr(teco_interface.info_window, " <Buffer> "); - teco_curses_format_filename(teco_interface.info_window, - teco_interface.info_current.data, + teco_curses_format_filename(teco_interface.info_window, info_current.data, getmaxx(teco_interface.info_window) - getcurx(teco_interface.info_window) - 1); waddch(teco_interface.info_window, teco_interface.info_dirty ? '*' : ' '); @@ -1075,8 +1193,7 @@ teco_interface_draw_info(void) * Make sure the title will consist only of printable characters. */ g_autofree gchar *info_current_printable; - info_current_printable = teco_string_echo(teco_interface.info_current.data, - teco_interface.info_current.len); + info_current_printable = teco_string_echo(info_current.data, info_current.len); g_autofree gchar *title = g_strconcat(info_type_str, info_current_printable, teco_interface.info_dirty ? "*" : "", NULL); teco_interface_set_window_title(title); @@ -1096,123 +1213,14 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg) void teco_interface_info_update_buffer(const teco_buffer_t *buffer) { - const gchar *filename = buffer->filename ? : UNNAMED_FILE; - teco_string_clear(&teco_interface.info_current); - teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_interface.info_dirty = buffer->dirty; + teco_string_init(&teco_interface.info_current, buffer->filename, + buffer->filename ? strlen(buffer->filename) : 0); + teco_interface.info_dirty = buffer->state > TECO_BUFFER_CLEAN; teco_interface.info_type = TECO_INFO_TYPE_BUFFER; /* NOTE: drawn in teco_interface_event_loop_iter() */ } -void -teco_interface_cmdline_update(const teco_cmdline_t *cmdline) -{ - /* - * Especially important on PDCurses, which can crash - * in newpad() when run with --fake-cmdline. - */ - if (!teco_interface.cmdline_window) /* batch mode */ - return; - - /* - * Replace entire pre-formatted command-line. - * We don't know if it is similar to the last one, - * so resizing makes no sense. - * We approximate the size of the new formatted command-line, - * wasting a few bytes for control characters and - * multi-byte Unicode sequences. - */ - if (teco_interface.cmdline_pad) - delwin(teco_interface.cmdline_pad); - - int max_cols = 1; - for (guint i = 0; i < cmdline->str.len; i++) - max_cols += TECO_IS_CTL(cmdline->str.data[i]) ? 3 : 1; - teco_interface.cmdline_pad = newpad(1, max_cols); - - short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0)); - short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0)); - wattrset(teco_interface.cmdline_pad, teco_color_attr(fg, bg)); - - /* format effective command line */ - teco_interface.cmdline_len = - teco_curses_format_str(teco_interface.cmdline_pad, - cmdline->str.data, cmdline->effective_len, -1); - - /* - * A_BOLD should result in either a bold font or a brighter - * color both on 8 and 16 color terminals. - * This is not quite color-scheme-agnostic, but works - * with both the `terminal` and `solarized` themes. - * This problem will be gone once we use a Scintilla view - * as command line, since we can then define a style - * for rubbed out parts of the command line which will - * be user-configurable. - * The attributes, supported by the terminal can theoretically - * be queried with term_attrs(). - */ - wattron(teco_interface.cmdline_pad, A_UNDERLINE | A_BOLD); - - /* - * Format rubbed-out command line. - * NOTE: This formatting will never be truncated since we're - * writing into the pad which is large enough. - */ - teco_interface.cmdline_rubout_len = - teco_curses_format_str(teco_interface.cmdline_pad, cmdline->str.data + cmdline->effective_len, - cmdline->str.len - cmdline->effective_len, -1); - - /* - * Highlight cursor after effective command line - * FIXME: This should use SCI_GETCARETFORE(). - */ - attr_t attr = A_NORMAL; - short pair = 0; - if (teco_interface.cmdline_rubout_len) { - wmove(teco_interface.cmdline_pad, 0, teco_interface.cmdline_len); - wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL); - wchgat(teco_interface.cmdline_pad, 1, - (attr & (A_UNDERLINE | A_REVERSE)) ^ A_REVERSE, pair, NULL); - } else { - teco_interface.cmdline_len++; - wattr_get(teco_interface.cmdline_pad, &attr, &pair, NULL); - wattr_set(teco_interface.cmdline_pad, (attr & ~(A_UNDERLINE | A_BOLD)) ^ A_REVERSE, pair, NULL); - waddch(teco_interface.cmdline_pad, ' '); - } - - teco_interface_draw_cmdline(); -} - -static void -teco_interface_draw_cmdline(void) -{ - /* total width available for command line */ - guint total_width = getmaxx(teco_interface.cmdline_window) - 1; - - /* beginning of command line to show */ - guint disp_offset = teco_interface.cmdline_len - - MIN(teco_interface.cmdline_len, - total_width/2 + teco_interface.cmdline_len % MAX(total_width/2, 1)); - /* - * length of command line to show - * - * NOTE: we do not use getmaxx(cmdline_pad) here since it may be - * larger than the text the pad contains. - */ - guint disp_len = MIN(total_width, teco_interface.cmdline_len + - teco_interface.cmdline_rubout_len - disp_offset); - - short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_DEFAULT, 0)); - short bg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETBACK, STYLE_DEFAULT, 0)); - - wattrset(teco_interface.cmdline_window, teco_color_attr(fg, bg)); - mvwaddch(teco_interface.cmdline_window, 0, 0, '*' | A_BOLD); - teco_curses_clrtobot(teco_interface.cmdline_window); - copywin(teco_interface.cmdline_pad, teco_interface.cmdline_window, - 0, disp_offset, 0, 1, 0, disp_len, FALSE); -} - #if PDCURSES /* @@ -1243,7 +1251,7 @@ teco_interface_init_clipboard(void) if (rc == PDC_CLIP_SUCCESS) PDC_freeclipboard(contents); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); } gboolean @@ -1307,8 +1315,10 @@ get_selection_by_name(const gchar *name) * (everything gets passed down), but currently we * only register the three standard registers * "~", "~P", "~S" and "~C". + * (We are never called with "~", though.) */ - return g_ascii_tolower(*name) ? : 'c'; + g_assert(*name != '\0'); + return g_ascii_tolower(*name); } /* @@ -1507,10 +1517,10 @@ teco_interface_init_clipboard(void) !teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_CLIPBOARD_GET", 22))) return; - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); } gboolean @@ -1520,7 +1530,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, if (teco_interface_osc52_is_enabled()) return teco_interface_osc52_set_clipboard(name, str, str_len, error); - static const gchar *reg_name = "$SCITECO_CLIPBOARD_SET"; + static const gchar reg_name[] = "$SCITECO_CLIPBOARD_SET"; teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); if (!reg) { @@ -1533,7 +1543,7 @@ teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, g_auto(teco_string_t) command; if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) return FALSE; - if (teco_string_contains(&command, '\0')) { + if (teco_string_contains(command, '\0')) { teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); return FALSE; } @@ -1581,7 +1591,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError if (teco_interface_osc52_is_enabled()) return teco_interface_osc52_get_clipboard(name, str, len, error); - static const gchar *reg_name = "$SCITECO_CLIPBOARD_GET"; + static const gchar reg_name[] = "$SCITECO_CLIPBOARD_GET"; teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, reg_name, strlen(reg_name)); if (!reg) { @@ -1594,7 +1604,7 @@ teco_interface_get_clipboard(const gchar *name, gchar **str, gsize *len, GError g_auto(teco_string_t) command; if (!reg->vtable->get_string(reg, &command.data, &command.len, NULL, error)) return FALSE; - if (teco_string_contains(&command, '\0')) { + if (teco_string_contains(command, '\0')) { teco_error_qregcontainsnull_set(error, reg_name, strlen(reg_name), FALSE); return FALSE; } @@ -1680,7 +1690,7 @@ void teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len, gboolean highlight) { - if (teco_interface.cmdline_window) + if (teco_interface.input_pad) /* interactive mode */ teco_curses_info_popup_add(&teco_interface.popup, type, name, name_len, highlight); } @@ -1688,7 +1698,7 @@ teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize void teco_interface_popup_show(gsize prefix_len) { - if (!teco_interface.cmdline_window) + if (!teco_interface.input_pad) /* batch mode */ return; @@ -1702,7 +1712,7 @@ teco_interface_popup_show(gsize prefix_len) void teco_interface_popup_scroll(void) { - if (!teco_interface.cmdline_window) + if (!teco_interface.input_pad) /* batch mode */ return; @@ -1757,8 +1767,8 @@ teco_interface_is_interrupted(void) * filtering out CTRL+C. * It's currently necessary as a fallback e.g. for PDCURSES_GUI or XCurses. * - * NOTE: Theoretically, this can be optimized by doing wgetch() only every X - * microseconds like on Gtk+. + * NOTE: Theoretically, this can be optimized by doing wgetch() only every + * TECO_POLL_INTERVAL microseconds like on Gtk+. * But this turned out to slow things down, at least on PDCurses/WinGUI. */ gboolean @@ -1786,9 +1796,22 @@ teco_interface_is_interrupted(void) #endif -static void -teco_interface_refresh(void) +void +teco_interface_refresh(gboolean force) { + if (!teco_interface.input_pad) + /* batch mode */ + return; + +#ifdef NETBSD_CURSES + /* works around crashes in doupdate() */ + if (G_UNLIKELY(COLS <= 1 || LINES <= 1)) + return; +#endif + + if (G_UNLIKELY(force)) + clearok(curscr, TRUE); + /* * Info window is updated very often which is very * costly, especially when using PDC_set_title(), @@ -1799,7 +1822,7 @@ teco_interface_refresh(void) wnoutrefresh(teco_interface.info_window); teco_view_noutrefresh(teco_interface_current_view); wnoutrefresh(teco_interface.msg_window); - wnoutrefresh(teco_interface.cmdline_window); + teco_view_noutrefresh(teco_cmdline.view); teco_curses_info_popup_noutrefresh(&teco_interface.popup); doupdate(); } @@ -1813,42 +1836,50 @@ teco_interface_refresh(void) (BUTTON1_##X | BUTTON2_##X | BUTTON3_##X | BUTTON4_##X | BUTTON5_##X) static gboolean -teco_interface_getmouse(GError **error) +teco_interface_process_mevent(MEVENT *event, GError **error) { - MEVENT event; - - if (getmouse(&event) != OK) - return TRUE; +#ifdef DEBUG + g_printf("EVENT: 0x%016X -> bit %02d [%c%c%c%c%c]\n", + event->bstate, ffs(event->bstate)-1, + event->bstate & BUTTON_NUM(4) ? 'U' : ' ', + event->bstate & BUTTON_NUM(5) ? 'D' : ' ', + event->bstate & BUTTON_EVENT(PRESSED) ? 'P' : ' ', + event->bstate & BUTTON_EVENT(RELEASED) ? 'R' : ' ', + event->bstate & REPORT_MOUSE_POSITION ? 'M' : ' '); +#endif if (teco_curses_info_popup_is_shown(&teco_interface.popup) && - wmouse_trafo(teco_interface.popup.window, &event.y, &event.x, FALSE)) { + wmouse_trafo(teco_interface.popup.window, &event->y, &event->x, FALSE)) { /* * NOTE: Not all curses variants report the RELEASED event, * but may also return REPORT_MOUSE_POSITION. * So we might react to all button presses as well. - * Others will still report CLICKED. */ - if (event.bstate & (BUTTON1_RELEASED | BUTTON1_CLICKED | REPORT_MOUSE_POSITION)) { + if (event->bstate & (BUTTON1_RELEASED | REPORT_MOUSE_POSITION)) { teco_machine_t *machine = &teco_cmdline.machine.parent; - const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, event.y, event.x); + const teco_string_t *insert = teco_curses_info_popup_getentry(&teco_interface.popup, + event->y, event->x); if (insert && machine->current->insert_completion_cb) { - /* successfully clicked popup item */ + /* + * Successfully clicked popup item. + * `insert` is the empty string for the "(Unnamed)" buffer. + */ const teco_string_t insert_suffix = {insert->data + teco_interface.popup_prefix_len, insert->len - teco_interface.popup_prefix_len}; - if (!machine->current->insert_completion_cb(machine, &insert_suffix, error)) + if (!machine->current->insert_completion_cb(machine, insert_suffix, error)) return FALSE; teco_interface_popup_clear(); teco_interface_msg_clear(); - teco_interface_cmdline_update(&teco_cmdline); + teco_cmdline_update(); } return TRUE; } - if (event.bstate & BUTTON_NUM(4)) + if (event->bstate & BUTTON_NUM(4)) teco_curses_info_popup_scroll(&teco_interface.popup, -2); - else if (event.bstate & BUTTON_NUM(5)) + else if (event->bstate & BUTTON_NUM(5)) teco_curses_info_popup_scroll(&teco_interface.popup, +2); short fg = teco_rgb2curses(teco_interface_ssm(SCI_STYLEGETFORE, STYLE_CALLTIP, 0)); @@ -1859,106 +1890,144 @@ teco_interface_getmouse(GError **error) } /* - * Return mouse coordinates relative to the view. - * They will be in characters, but that's what SCI_POSITIONFROMPOINT - * expects on Scinterm anyway. - */ - WINDOW *current = teco_view_get_window(teco_interface_current_view); - if (!wmouse_trafo(current, &event.y, &event.x, FALSE)) - /* no event inside of current view */ - return TRUE; - - /* * NOTE: There will only be one of the button bits * set in bstate, so we don't loose information translating * them to enums. * - * At least on ncurses, we don't always get a RELEASED event. - * It instead sends only REPORT_MOUSE_POSITION, - * so make sure not to overwrite teco_mouse.button in this case. + * At least on ncurses, this enables the "Normal tracking mode" + * which only reports PRESSEND and RELEASED events, but no mouse + * position tracing. */ - if (event.bstate & BUTTON_NUM(4)) + if (event->bstate & BUTTON_NUM(4)) /* scroll up - there will be no RELEASED event */ teco_mouse.type = TECO_MOUSE_SCROLLUP; - else if (event.bstate & BUTTON_NUM(5)) + else if (event->bstate & BUTTON_NUM(5)) /* scroll down - there will be no RELEASED event */ teco_mouse.type = TECO_MOUSE_SCROLLDOWN; - else if (event.bstate & BUTTON_EVENT(RELEASED)) + else if (event->bstate & BUTTON_EVENT(RELEASED)) teco_mouse.type = TECO_MOUSE_RELEASED; - else if (event.bstate & BUTTON_EVENT(PRESSED)) + else if (event->bstate & BUTTON_EVENT(PRESSED)) teco_mouse.type = TECO_MOUSE_PRESSED; else - /* can also be REPORT_MOUSE_POSITION */ - teco_mouse.type = TECO_MOUSE_RELEASED; + return TRUE; - teco_mouse.x = event.x; - teco_mouse.y = event.y; + /* + * Return mouse coordinates relative to the view. + * They will be in characters, but that's what SCI_POSITIONFROMPOINT + * expects on Scinterm anyway. + */ + WINDOW *current = teco_view_get_window(teco_interface_current_view); + if (!wmouse_trafo(current, &event->y, &event->x, FALSE)) + /* no event inside of current view */ + return TRUE; - if (event.bstate & BUTTON_NUM(1)) + teco_mouse.x = event->x; + teco_mouse.y = event->y; + + if (event->bstate & BUTTON_NUM(1)) teco_mouse.button = 1; - else if (event.bstate & BUTTON_NUM(2)) + else if (event->bstate & BUTTON_NUM(2)) teco_mouse.button = 2; - else if (event.bstate & BUTTON_NUM(3)) + else if (event->bstate & BUTTON_NUM(3)) teco_mouse.button = 3; - else if (!(event.bstate & REPORT_MOUSE_POSITION)) + else if (!(event->bstate & REPORT_MOUSE_POSITION)) teco_mouse.button = -1; teco_mouse.mods = 0; - if (event.bstate & BUTTON_SHIFT) + if (event->bstate & BUTTON_SHIFT) teco_mouse.mods |= TECO_MOUSE_SHIFT; - if (event.bstate & BUTTON_CTRL) + if (event->bstate & BUTTON_CTRL) teco_mouse.mods |= TECO_MOUSE_CTRL; - if (event.bstate & BUTTON_ALT) + if (event->bstate & BUTTON_ALT) teco_mouse.mods |= TECO_MOUSE_ALT; - if (event.bstate & BUTTON_EVENT(CLICKED)) { - /* - * Click detection __should__ be disabled, - * but some Curses implementations report them anyway. - * This has been observed on PDCurses/WinGUI. - * On PDCurses/Wincon we especially did not disable - * click detection since it doesn't report - * BUTTONX_RELEASED at all. - * We emulate separate PRESSED/RELEASE events on those - * platforms. - */ - teco_mouse.type = TECO_MOUSE_PRESSED; - if (!teco_cmdline_keymacro("MOUSE", -1, error)) - return FALSE; - teco_mouse.type = TECO_MOUSE_RELEASED; +#if defined(NCURSES_UNIX) && NCURSES_VERSION_PATCH < 20250913 + /* + * FIXME: Some terminal emulators do not send separate + * middle click PRESSED and RELEASED buttons + * (both are sent when releasing the button). + * Furthermore due to ncurses bugs the order + * of events is arbitrary. + * Therefore we ignore BUTTON2_PRESSED and synthesize + * PRESSED and RELEASED evnts on BUTTON2_RELEASED: + */ + if (teco_mouse.button == 2) { + if (teco_mouse.type == TECO_MOUSE_PRESSED) + /* ignore BUTTON2_PRESSED events */ + return TRUE; + if (teco_mouse.type == TECO_MOUSE_RELEASED) { + teco_mouse.type = TECO_MOUSE_PRESSED; + if (!teco_cmdline_keymacro("MOUSE", -1, error)) + return FALSE; + teco_mouse.type = TECO_MOUSE_RELEASED; + } } +#endif /* NCURSES_UNIX && NCURSES_VERSION_PATCH < 20250913 */ + return teco_cmdline_keymacro("MOUSE", -1, error); } +static gboolean +teco_interface_getmouse(GError **error) +{ + MEVENT event; + + while (getmouse(&event) == OK) + if (!teco_interface_process_mevent(&event, error)) + return FALSE; + + return TRUE; +} + #endif /* NCURSES_MOUSE_VERSION >= 2 */ static gint teco_interface_blocking_getch(void) { + if (!g_queue_is_empty(teco_interface.input_queue)) + return GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + #if NCURSES_MOUSE_VERSION >= 2 -#if defined(NCURSES_VERSION) || defined(PDCURSES_WINCON) /* - * REPORT_MOUSE_POSITION is necessary at least on - * ncurses, so that BUTTONX_RELEASED events are reported. - * At least we interpret REPORT_MOUSE_POSITION - * like BUTTONX_RELEASED. - * It does NOT report every cursor movement, though. - * - * FIXME: On PDCurses/Wincon we enable it, so we at least - * receive something that will be interpreted as - * BUTTONX_RELEASED, although it really just reports - * cursor movements. - */ - static const mmask_t mmask = ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION; -#else - static const mmask_t mmask = ALL_MOUSE_EVENTS; + * FIXME: Due to an ncurses bug, we can receive bogus BUTTON3_PRESSED events after + * left scrolling. + * If we would reset the mouse mask with every wgetch(), which resets + * the internal button state, we would receive bogus BUTTON3_PRESSED events + * repeatedly. + * An upstream ncurses patch will probably be merged soon. + */ + static gboolean old_mousekey = FALSE; + gboolean new_mousekey = (teco_ed & TECO_ED_MOUSEKEY) != 0; + if (new_mousekey != old_mousekey) { + old_mousekey = new_mousekey; + mmask_t mmask = BUTTON_EVENT(PRESSED) | BUTTON_EVENT(RELEASED); +#ifdef __PDCURSES__ + /* + * On PDCurses it's crucial NOT to mask for BUTTONX_CLICKED. + * Also, scroll events are not reported without the non-standard + * MOUSE_WHEEL_SCROLL. + */ + mmask |= MOUSE_WHEEL_SCROLL; #endif - mousemask(teco_ed & TECO_ED_MOUSEKEY ? mmask : 0, NULL); + mousemask(new_mousekey ? mmask : 0, NULL); + } #endif /* NCURSES_MOUSE_VERSION >= 2 */ /* no special <CTRL/C> handling */ raw(); nodelay(teco_interface.input_pad, FALSE); + + /* + * Make sure we return when it's time to create recovery dumps. + */ + if (teco_ring_recovery_interval != 0) { + if (G_UNLIKELY(!teco_interface.recovery_timer)) + teco_interface.recovery_timer = g_timer_new(); + gdouble elapsed = g_timer_elapsed(teco_interface.recovery_timer, NULL); + wtimeout(teco_interface.input_pad, + MAX((gdouble)teco_ring_recovery_interval - elapsed, 0)*1000); + } + /* * Memory limiting is stopped temporarily, since it might otherwise * constantly place 100% load on the CPU. @@ -1974,6 +2043,12 @@ teco_interface_blocking_getch(void) cbreak(); #endif + if (key == ERR && teco_ring_recovery_interval != 0 && + g_timer_elapsed(teco_interface.recovery_timer, NULL) >= teco_ring_recovery_interval) { + teco_ring_dump_recovery(); + g_timer_start(teco_interface.recovery_timer); + } + return key; } @@ -1995,12 +2070,11 @@ teco_interface_event_loop_iter(void) GError **error = &teco_interface.event_loop_error; - gint key = g_queue_is_empty(teco_interface.input_queue) - ? teco_interface_blocking_getch() - : GPOINTER_TO_INT(g_queue_pop_head(teco_interface.input_queue)); + gint key = teco_interface_blocking_getch(); const teco_view_t *last_view = teco_interface_current_view; sptr_t last_vpos = teco_interface_ssm(SCI_GETFIRSTVISIBLELINE, 0, 0); + guint last_cmdline_height = teco_cmdline.height; switch (key) { case ERR: @@ -2008,10 +2082,11 @@ teco_interface_event_loop_iter(void) return; #ifdef KEY_RESIZE case KEY_RESIZE: -#ifdef __PDCURSES__ - /* NOTE: No longer necessary since PDCursesMod v4.3.3. */ - resize_term(0, 0); -#endif + /* + * At least on PDCurses/Wincon, the hardware cursor is sometimes + * reactivated. + */ + curs_set(0); teco_interface_resize_all_windows(); break; #endif @@ -2078,9 +2153,10 @@ teco_interface_event_loop_iter(void) * Do not auto-scroll on mouse events, so you can scroll the view manually * in the ^KMOUSE macro, allowing dot to be outside of the view. */ - teco_interface_refresh(); + teco_interface_unfold(); + teco_interface_refresh(FALSE); return; -#endif +#endif /* NCURSES_MOUSE_VERSION >= 2 */ /* * Control keys and keys with printable representation @@ -2125,6 +2201,10 @@ teco_interface_event_loop_iter(void) } } + if (G_UNLIKELY(teco_cmdline.height != last_cmdline_height)) + /* command line height was changed with h,5EJ */ + teco_interface_resize_all_windows(); + /* * Scintilla has been patched to avoid any automatic scrolling since that * has been benchmarked to be a very costly operation. @@ -2134,9 +2214,10 @@ teco_interface_event_loop_iter(void) */ if (teco_interface_current_view == last_view) teco_interface_ssm(SCI_SETFIRSTVISIBLELINE, last_vpos, 0); + teco_interface_unfold(); teco_interface_ssm(SCI_SCROLLCARET, 0, 0); - teco_interface_refresh(); + teco_interface_refresh(FALSE); } gboolean @@ -2148,11 +2229,14 @@ teco_interface_event_loop(GError **error) if (!teco_interface_init_interactive(error)) return FALSE; - static const teco_cmdline_t empty_cmdline; // FIXME - teco_interface_cmdline_update(&empty_cmdline); teco_interface_msg_clear(); teco_interface_ssm(SCI_SCROLLCARET, 0, 0); - teco_interface_refresh(); + /* + * NetBSD's Curses needs the hard refresh as it would + * otherwise draw the info window in the wrong row. + * Shouldn't cause any slowdown on ncurses. + */ + teco_interface_refresh(TRUE); #ifdef EMCURSES PDC_emscripten_set_handler(teco_interface_event_loop_iter, TRUE); @@ -2199,10 +2283,6 @@ teco_interface_cleanup(void) teco_string_clear(&teco_interface.info_current); if (teco_interface.input_queue) g_queue_free(teco_interface.input_queue); - if (teco_interface.cmdline_window) - delwin(teco_interface.cmdline_window); - if (teco_interface.cmdline_pad) - delwin(teco_interface.cmdline_pad); if (teco_interface.msg_window) delwin(teco_interface.msg_window); if (teco_interface.input_pad) @@ -2227,4 +2307,10 @@ teco_interface_cleanup(void) close(teco_interface.stderr_orig); if (teco_interface.stdout_orig >= 0) close(teco_interface.stdout_orig); + + if (teco_interface.pair_table) + g_hash_table_destroy(teco_interface.pair_table); + + if (teco_interface.recovery_timer) + g_timer_destroy(teco_interface.recovery_timer); } diff --git a/src/interface-gtk/gtk-info-popup.c b/src/interface-gtk/gtk-info-popup.c index aaa0a65..769f772 100644 --- a/src/interface-gtk/gtk-info-popup.c +++ b/src/interface-gtk/gtk-info-popup.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -37,6 +37,7 @@ typedef struct { teco_stailq_entry_t entry; teco_popup_entry_type_t type; + /** entry name or empty string for the "(Unnamed)" buffer */ teco_string_t name; gboolean highlight; } teco_popup_entry_t; @@ -109,10 +110,10 @@ teco_gtk_info_popup_activated_cb(GtkFlowBox *box, GtkFlowBoxChild *child, gpoint GList *entry; for (entry = child_list; entry != NULL && !TECO_IS_GTK_LABEL(entry->data); entry = g_list_next(entry)); g_assert(entry != NULL); - const teco_string_t *str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data)); + teco_string_t str = teco_gtk_label_get_text(TECO_GTK_LABEL(entry->data)); g_signal_emit(popup, teco_gtk_info_popup_clicked_signal, 0, - str->data, (gulong)str->len); + str.data, (gulong)str.len); } static void @@ -249,6 +250,10 @@ teco_gtk_info_popup_new(void) GIcon * teco_gtk_info_popup_get_icon_for_path(const gchar *path, const gchar *fallback_name) { + if (!path || !*path) + /* "(Unnamed)" file */ + return g_icon_new_for_string(fallback_name, NULL); + GIcon *icon = NULL; g_autoptr(GFile) file = g_file_new_for_path(path); @@ -299,7 +304,7 @@ teco_gtk_info_popup_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, static void teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t type, - const gchar *name, gssize len, gboolean highlight) + const gchar *name, gsize len, gboolean highlight) { g_return_if_fail(self != NULL); g_return_if_fail(TECO_IS_GTK_INFO_POPUP(self)); @@ -318,12 +323,8 @@ teco_gtk_info_popup_idle_add(TecoGtkInfoPopup *self, teco_popup_entry_type_t typ const gchar *fallback = type == TECO_POPUP_FILE ? "text-x-generic" : "folder"; - /* - * `name` is not guaranteed to be null-terminated. - */ - g_autofree gchar *path = len < 0 ? g_strdup(name) : g_strndup(name, len); - - g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(path, fallback); + /* name comes from a teco_string_t and is guaranteed to be null-terminated */ + g_autoptr(GIcon) icon = teco_gtk_info_popup_get_icon_for_path(name, fallback); if (icon) { gint width, height; gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height); diff --git a/src/interface-gtk/gtk-info-popup.h b/src/interface-gtk/gtk-info-popup.h index ad79b84..3ce8e1f 100644 --- a/src/interface-gtk/gtk-info-popup.h +++ b/src/interface-gtk/gtk-info-popup.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/interface-gtk/gtk-label.c b/src/interface-gtk/gtk-label.c index ef370a2..5052cdc 100644 --- a/src/interface-gtk/gtk-label.c +++ b/src/interface-gtk/gtk-label.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -32,7 +32,9 @@ #include "gtk-label.h" -#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) +#define TECO_UNNAMED_FILE "(Unnamed)" + +#define GDK_TO_PANGO_COLOR(X) ((guint16)((X) * G_MAXUINT16)) struct _TecoGtkLabel { GtkLabel parent_instance; @@ -40,6 +42,7 @@ struct _TecoGtkLabel { PangoColor fg, bg; guint16 fg_alpha, bg_alpha; + /** text backing the label or empty string for "(Unnamed)" buffer */ teco_string_t string; }; @@ -143,7 +146,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui * even in Pango v1.38. * Perhaps, this has been fixed in later versions. */ -#if PANGO_VERSION_CHECK(1,38,0) attr = pango_attr_foreground_alpha_new(fg_alpha); attr->start_index = index; attr->end_index = index + len; @@ -153,7 +155,6 @@ teco_gtk_label_add_highlight_attribs(PangoAttrList *attribs, PangoColor *fg, gui attr->start_index = index; attr->end_index = index + len; pango_attr_list_insert(attribs, attr); -#endif attr = pango_attr_foreground_new(fg->red, fg->green, fg->blue); attr->start_index = index; @@ -253,25 +254,52 @@ teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len) teco_string_clear(&self->string); teco_string_init(&self->string, str, len < 0 ? strlen(str) : len); - g_autofree gchar *plaintext = NULL; + teco_string_t string = self->string; + if (!string.len) { + string.data = TECO_UNNAMED_FILE; + string.len = strlen(string.data); + } - if (self->string.len > 0) { - PangoAttrList *attribs = NULL; + g_autofree gchar *plaintext = NULL; + PangoAttrList *attribs = NULL; - teco_gtk_label_parse_string(self->string.data, self->string.len, - &self->fg, self->fg_alpha, - &self->bg, self->bg_alpha, - &attribs, &plaintext); + teco_gtk_label_parse_string(string.data, string.len, + &self->fg, self->fg_alpha, + &self->bg, self->bg_alpha, + &attribs, &plaintext); - gtk_label_set_attributes(GTK_LABEL(self), attribs); - pango_attr_list_unref(attribs); - } + gtk_label_set_attributes(GTK_LABEL(self), attribs); + pango_attr_list_unref(attribs); gtk_label_set_text(GTK_LABEL(self), plaintext); } -const teco_string_t * +teco_string_t teco_gtk_label_get_text(TecoGtkLabel *self) { - return &self->string; + return self->string; +} + +/** + * Signal that a keypress is expected (after executing ^T) + * by printing the first character in reverse. + * + * @fixme This mimics the current Curses implementation. + * Perhaps better show an icon? + */ +void +teco_gtk_label_highlight_getch(TecoGtkLabel *self) +{ + const gchar *plaintext = gtk_label_get_text(GTK_LABEL(self)); + g_assert(plaintext != NULL); + if (!*plaintext || !strcmp(plaintext, "\u258C")) { + gtk_label_set_text(GTK_LABEL(self), "\u258C"); + } else { + PangoAttrList *attribs = gtk_label_get_attributes(GTK_LABEL(self)); + teco_gtk_label_add_highlight_attribs(attribs, + &self->fg, self->fg_alpha, + &self->bg, self->bg_alpha, + 0, 1); + gtk_label_set_attributes(GTK_LABEL(self), attribs); + } } diff --git a/src/interface-gtk/gtk-label.h b/src/interface-gtk/gtk-label.h index c52d073..ad39c6e 100644 --- a/src/interface-gtk/gtk-label.h +++ b/src/interface-gtk/gtk-label.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,9 +27,11 @@ G_DECLARE_FINAL_TYPE(TecoGtkLabel, teco_gtk_label, TECO, GTK_LABEL, GtkLabel) GtkWidget *teco_gtk_label_new(const gchar *str, gssize len); void teco_gtk_label_set_text(TecoGtkLabel *self, const gchar *str, gssize len); -const teco_string_t *teco_gtk_label_get_text(TecoGtkLabel *self); +teco_string_t teco_gtk_label_get_text(TecoGtkLabel *self); void teco_gtk_label_parse_string(const gchar *str, gssize len, PangoColor *fg, guint16 fg_alpha, PangoColor *bg, guint16 bg_alpha, PangoAttrList **attribs, gchar **text); + +void teco_gtk_label_highlight_getch(TecoGtkLabel *self); diff --git a/src/interface-gtk/interface.c b/src/interface-gtk/interface.c index 045f9d7..0e1507f 100644 --- a/src/interface-gtk/interface.c +++ b/src/interface-gtk/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ #include "config.h" #endif -#include <stdarg.h> +#include <stdlib.h> #include <string.h> #include <signal.h> @@ -28,6 +28,7 @@ #include <glib/gstdio.h> #ifdef G_OS_UNIX +#include <unistd.h> #include <glib-unix.h> #endif @@ -61,6 +62,7 @@ //#define DEBUG static gboolean teco_interface_busy_timeout_cb(gpointer user_data); +static gboolean teco_interface_dump_recovery_cb(gpointer user_data); static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data); static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, @@ -69,33 +71,22 @@ static void teco_interface_cmdline_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data); static gboolean teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data); +static void teco_interface_scroll_cb(GtkEventControllerScroll *controller, + double dx, double dy, gpointer data); static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data); static gboolean teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer user_data); static gboolean teco_interface_sigterm_handler(gpointer user_data) G_GNUC_UNUSED; +static gchar teco_interface_get_ansi_key(GdkEventKey *event); -/** - * Interval between polling for keypresses. - * In other words, this is the maximum latency to detect CTRL+C interruptions. - */ -#define TECO_POLL_INTERVAL 100000 /* microseconds */ - -#define UNNAMED_FILE "(Unnamed)" +#define TECO_UNNAMED_FILE "(Unnamed)" #define USER_CSS_FILE ".teco_css" /** printf() format for CSS RGB colors given as guint32 */ #define CSS_COLOR_FORMAT "#%06" G_GINT32_MODIFIER "X" -/** Style used for the asterisk at the beginning of the command line */ -#define STYLE_ASTERISK 16 - -/** Indicator number used for control characters in the command line */ -#define INDIC_CONTROLCHAR (INDIC_CONTAINER+0) -/** Indicator number used for the rubbed out part of the command line */ -#define INDIC_RUBBEDOUT (INDIC_CONTAINER+1) - /** Convert Scintilla-style BGR color triple to RGB. */ static inline guint32 teco_bgr2rgb(guint32 bgr) @@ -113,9 +104,10 @@ static struct { TECO_INFO_TYPE_BUFFER_DIRTY, TECO_INFO_TYPE_QREG } info_type; + /* current document's name or empty string for "(Unnamed)" buffer */ teco_string_t info_current; - gboolean no_csd; + gboolean no_csd, detach; gint xembed_id; GtkWidget *info_bar_widget; @@ -124,11 +116,11 @@ static struct { GtkWidget *info_name_widget; GtkWidget *event_box_widget; + GtkEventController *scroll_controller; GtkWidget *message_bar_widget; GtkWidget *message_widget; - teco_view_t *cmdline_view; GtkIMContext *input_method; GtkWidget *popup_widget; @@ -142,6 +134,26 @@ static struct { void teco_interface_init(void) { +#ifdef G_OS_UNIX + if (teco_interface.detach) { + /* + * NOTE: There is also daemon() on BSD/Linux, + * but the following should be more portable. + */ + pid_t pid = fork(); + g_assert(pid >= 0); + if (pid != 0) + /* parent process */ + exit(EXIT_SUCCESS); + + setsid(); + + g_freopen("/dev/null", "r", stdin); + g_freopen("/dev/null", "a+", stdout); + g_freopen("/dev/null", "a+", stderr); + } +#endif + /* * gtk_init() is not necessary when using gtk_get_option_group(), * but this will open the default display. @@ -157,10 +169,10 @@ teco_interface_init(void) * clipboards/selections are supported on this system, * so we register only some default ones. */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("P")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("S")); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_clipboard_new("C")); teco_interface.event_queue = g_queue_new(); @@ -190,7 +202,7 @@ teco_interface_init(void) */ teco_interface.info_bar_widget = gtk_header_bar_new(); gtk_widget_set_name(teco_interface.info_bar_widget, "sciteco-info-bar"); - teco_interface.info_name_widget = teco_gtk_label_new(NULL, 0); + teco_interface.info_name_widget = teco_gtk_label_new("", 0); gtk_widget_set_valign(teco_interface.info_name_widget, GTK_ALIGN_CENTER); /* eases writing portable fallback.css that avoids CSS element names */ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.info_name_widget), @@ -230,7 +242,7 @@ teco_interface_init(void) /* * Overlay widget will allow overlaying the Scintilla view * and message widgets with the info popup. - * Therefore overlay_vbox (containing the view and popup) + * Therefore overlay_vbox (containing the view and message line) * will be the main child of the overlay. */ GtkWidget *overlay_widget = gtk_overlay_new(); @@ -253,22 +265,30 @@ teco_interface_init(void) gint events = gtk_widget_get_events(teco_interface.event_box_widget); events |= GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | - GDK_SCROLL_MASK; + GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK; gtk_widget_set_events(teco_interface.event_box_widget, events); g_signal_connect(teco_interface.event_box_widget, "button-press-event", G_CALLBACK(teco_interface_input_cb), NULL); g_signal_connect(teco_interface.event_box_widget, "button-release-event", G_CALLBACK(teco_interface_input_cb), NULL); - g_signal_connect(teco_interface.event_box_widget, "scroll-event", - G_CALLBACK(teco_interface_input_cb), NULL); + + /* + * On some platforms only GDK_SCROLL_SMOOTH events are reported, which are hard + * to translate to discrete scroll events, as required by the `4EJ` API. + * This work is therefore delegated to a scroll controller. + */ + teco_interface.scroll_controller = gtk_event_controller_scroll_new(teco_interface.event_box_widget, + GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | + GTK_EVENT_CONTROLLER_SCROLL_DISCRETE); + g_signal_connect(teco_interface.scroll_controller, "scroll", + G_CALLBACK(teco_interface_scroll_cb), NULL); teco_interface.message_bar_widget = gtk_info_bar_new(); gtk_widget_set_name(teco_interface.message_bar_widget, "sciteco-message-bar"); GtkWidget *message_bar_content = gtk_info_bar_get_content_area(GTK_INFO_BAR(teco_interface.message_bar_widget)); - /* NOTE: Messages are always pre-canonicalized */ - teco_interface.message_widget = gtk_label_new(NULL); + teco_interface.message_widget = teco_gtk_label_new(NULL, 0); /* eases writing portable fallback.css that avoids CSS element names */ gtk_style_context_add_class(gtk_widget_get_style_context(teco_interface.message_widget), "label"); @@ -281,23 +301,9 @@ teco_interface_init(void) gtk_container_add(GTK_CONTAINER(overlay_widget), overlay_vbox); gtk_box_pack_start(GTK_BOX(vbox), overlay_widget, TRUE, TRUE, 0); - teco_interface.cmdline_view = teco_view_new(); - teco_view_setup(teco_interface.cmdline_view); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETUNDOCOLLECTION, FALSE, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETVSCROLLBAR, FALSE, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINTYPEN, 1, SC_MARGIN_TEXT); - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETSTYLE, 0, STYLE_ASTERISK); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETMARGINWIDTHN, 1, - teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTWIDTH, STYLE_ASTERISK, (sptr_t)"*")); - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*"); - /* only required as long as we avoid ordinary character representations */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_CONTROLCHAR, INDIC_ROUNDBOX); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETALPHA, INDIC_CONTROLCHAR, 128); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETSTYLE, INDIC_RUBBEDOUT, INDIC_STRIKE); - /* we will forward key events, so the view should only react to text insertion */ - teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALLCMDKEYS, 0, 0); - - GtkWidget *cmdline_widget = GTK_WIDGET(teco_interface.cmdline_view); + teco_cmdline_init(); + + GtkWidget *cmdline_widget = GTK_WIDGET(teco_cmdline.view); gtk_widget_set_name(cmdline_widget, "sciteco-cmdline"); g_signal_connect(cmdline_widget, "size-allocate", G_CALLBACK(teco_interface_cmdline_size_allocate_cb), NULL); @@ -332,10 +338,6 @@ teco_interface_init(void) */ gtk_widget_set_can_focus(teco_interface.message_widget, FALSE); gtk_widget_set_can_focus(teco_interface.info_name_widget, FALSE); - - teco_cmdline_t empty_cmdline; - memset(&empty_cmdline, 0, sizeof(empty_cmdline)); - teco_interface_cmdline_update(&empty_cmdline); } static void @@ -361,6 +363,11 @@ teco_interface_get_options(void) G_OPTION_ARG_INT, &teco_interface.xembed_id, "Embed into an existing X11 Window.", "ID"}, #endif +#ifdef G_OS_UNIX + {"detach", 'd', G_OPTION_FLAG_IN_MAIN, + G_OPTION_ARG_NONE, &teco_interface.detach, + "Detach from controlling terminal (daemonize).", NULL}, +#endif {NULL} }; @@ -379,7 +386,7 @@ teco_interface_get_options(void) void teco_interface_init_color(guint color, guint32 rgb) {} void -teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) +teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len) { /* * The message types are chosen such that there is a CSS class @@ -395,21 +402,11 @@ teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) g_assert(type < G_N_ELEMENTS(type2gtk)); - gchar buf[256]; - - /* - * stdio_vmsg() leaves `ap` undefined and we are expected - * to do the same and behave like vprintf(). - */ - va_list aq; - va_copy(aq, ap); - teco_interface_stdio_vmsg(type, fmt, ap); - g_vsnprintf(buf, sizeof(buf), fmt, aq); - va_end(aq); + teco_interface_stdio_msg(type, str, len); gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget), type2gtk[type]); - gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), buf); + teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), str, len); if (type == TECO_MSG_ERROR) gtk_widget_error_bell(teco_interface.window); @@ -420,7 +417,100 @@ teco_interface_msg_clear(void) { gtk_info_bar_set_message_type(GTK_INFO_BAR(teco_interface.message_bar_widget), GTK_MESSAGE_QUESTION); - gtk_label_set_text(GTK_LABEL(teco_interface.message_widget), ""); + teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.message_widget), "", 0); +} + +static void +teco_interface_getch_commit_cb(GtkIMContext *context, gchar *str, gpointer user_data) +{ + teco_int_t *cp = user_data; + + /* + * FIXME: What if str contains several characters? + */ + *cp = g_utf8_get_char_validated(str, -1); + g_assert(*cp >= 0); + gtk_main_quit(); +} + +static gboolean +teco_interface_getch_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) +{ + teco_int_t *cp = user_data; + + g_assert(event->type == GDK_KEY_PRESS); + + switch (event->key.keyval) { + case GDK_KEY_Escape: *cp = '\e'; break; + case GDK_KEY_BackSpace: *cp = TECO_CTL_KEY('H'); break; + case GDK_KEY_Tab: *cp = '\t'; break; + case GDK_KEY_Return: *cp = '\n'; break; + default: + /* + * NOTE: Alt-Gr key-combinations are sometimes reported as + * Ctrl+Alt, so we filter those out. + */ + if ((event->key.state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) == GDK_CONTROL_MASK && + (*cp = teco_interface_get_ansi_key(&event->key))) { + *cp = TECO_CTL_KEY(g_ascii_toupper(*cp)); + switch (*cp) { + case TECO_CTL_KEY('C'): + teco_interrupted = TRUE; + /* fall through */ + case TECO_CTL_KEY('D'): + *cp = -1; + } + break; + } + + gtk_im_context_filter_keypress(teco_interface.input_method, &event->key); + return TRUE; + } + + gtk_main_quit(); + return TRUE; +} + +teco_int_t +teco_interface_getch(gboolean widechar) +{ + if (!gtk_main_level()) + /* batch mode */ + return teco_interface_stdio_getch(widechar); + + teco_int_t cp = -1; + gulong key_handler, commit_handler; + + /* temporarily replace the "key-press-event" and "commit" handlers */ + g_signal_handlers_block_by_func(teco_interface.window, + teco_interface_input_cb, NULL); + key_handler = g_signal_connect(teco_interface.window, "key-press-event", + G_CALLBACK(teco_interface_getch_input_cb), &cp); + g_signal_handlers_block_by_func(teco_interface.input_method, + teco_interface_cmdline_commit_cb, NULL); + commit_handler = g_signal_connect(teco_interface.input_method, "commit", + G_CALLBACK(teco_interface_getch_commit_cb), &cp); + + /* + * Highlights the first character in the label. + * This mimics what the Curses UI does. + * Is there a better way to signal that we expect input? + */ + teco_gtk_label_highlight_getch(TECO_GTK_LABEL(teco_interface.message_widget)); + + GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); + gdk_window_thaw_updates(top_window); + + gtk_main(); + + gdk_window_freeze_updates(top_window); + + g_signal_handler_disconnect(teco_interface.input_method, commit_handler); + g_signal_handlers_unblock_by_func(teco_interface.input_method, teco_interface_cmdline_commit_cb, NULL); + g_signal_handler_disconnect(teco_interface.window, key_handler); + g_signal_handlers_unblock_by_func(teco_interface.window, teco_interface_input_cb, NULL); + + return cp; } void @@ -439,8 +529,13 @@ teco_interface_refresh_info(void) gtk_style_context_remove_class(style, "dirty"); g_auto(teco_string_t) info_current_temp; - teco_string_init(&info_current_temp, - teco_interface.info_current.data, teco_interface.info_current.len); + + if (!teco_interface.info_current.len) + teco_string_init(&info_current_temp, TECO_UNNAMED_FILE, strlen(TECO_UNNAMED_FILE)); + else + teco_string_init(&info_current_temp, + teco_interface.info_current.data, teco_interface.info_current.len); + if (teco_interface.info_type == TECO_INFO_TYPE_BUFFER_DIRTY) teco_string_append_c(&info_current_temp, '*'); teco_gtk_label_set_text(TECO_GTK_LABEL(teco_interface.info_name_widget), @@ -508,92 +603,18 @@ teco_interface_info_update_qreg(const teco_qreg_t *reg) void teco_interface_info_update_buffer(const teco_buffer_t *buffer) { - const gchar *filename = buffer->filename ? : UNNAMED_FILE; - teco_string_clear(&teco_interface.info_current); - teco_string_init(&teco_interface.info_current, filename, strlen(filename)); - teco_interface.info_type = buffer->dirty ? TECO_INFO_TYPE_BUFFER_DIRTY - : TECO_INFO_TYPE_BUFFER; -} - -/** - * Insert a single character into the command line. - * - * @fixme - * Control characters should be inserted verbatim since the Scintilla - * representations of them should be preferred. - * However, Scintilla would break the line on every CR/LF and there is - * currently no way to prevent this. - * Scintilla needs to be patched. - * - * @see teco_view_set_representations() - * @see teco_curses_format_str() - */ -static void -teco_interface_cmdline_insert_c(gchar chr) -{ - gchar buffer[3+1] = ""; - - /* - * NOTE: This mapping is similar to teco_view_set_representations() - */ - switch (chr) { - case '\e': strcpy(buffer, "$"); break; - case '\r': strcpy(buffer, "CR"); break; - case '\n': strcpy(buffer, "LF"); break; - case '\t': strcpy(buffer, "TAB"); break; - default: - if (TECO_IS_CTL(chr)) { - buffer[0] = '^'; - buffer[1] = TECO_CTL_ECHO(chr); - buffer[2] = '\0'; - } - } - - if (*buffer) { - gsize len = strlen(buffer); - teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, len, (sptr_t)buffer); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_CONTROLCHAR, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, - teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - len, len); - } else { - teco_view_ssm(teco_interface.cmdline_view, SCI_APPENDTEXT, 1, (sptr_t)&chr); - } -} - -void -teco_interface_cmdline_update(const teco_cmdline_t *cmdline) -{ - /* - * We don't know if the new command line is similar to - * the old one, so we can just as well rebuild it. - * - * NOTE: teco_view_ssm() already locks the GDK lock. - */ - teco_view_ssm(teco_interface.cmdline_view, SCI_CLEARALL, 0, 0); - - /* format effective command line */ - for (guint i = 0; i < cmdline->effective_len; i++) - teco_interface_cmdline_insert_c(cmdline->str.data[i]); - - /* cursor should be after effective command line */ - guint pos = teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_GOTOPOS, pos, 0); - - /* format rubbed out command line */ - for (guint i = cmdline->effective_len; i < cmdline->str.len; i++) - teco_interface_cmdline_insert_c(cmdline->str.data[i]); - - teco_view_ssm(teco_interface.cmdline_view, SCI_SETINDICATORCURRENT, INDIC_RUBBEDOUT, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICATORFILLRANGE, pos, - teco_view_ssm(teco_interface.cmdline_view, SCI_GETLENGTH, 0, 0) - pos); - - teco_view_ssm(teco_interface.cmdline_view, SCI_SCROLLCARET, 0, 0); + teco_string_init(&teco_interface.info_current, buffer->filename, + buffer->filename ? strlen(buffer->filename) : 0); + teco_interface.info_type = buffer->state > TECO_BUFFER_CLEAN + ? TECO_INFO_TYPE_BUFFER_DIRTY : TECO_INFO_TYPE_BUFFER; } static GdkAtom teco_interface_get_selection_by_name(const gchar *name) { + g_assert(*name != '\0'); + /* * We can use gdk_atom_intern() to support arbitrary X11 selection * names. However, since we cannot find out which selections are @@ -602,11 +623,9 @@ teco_interface_get_selection_by_name(const gchar *name) * Checking them here avoids expensive X server roundtrips. */ switch (*name) { - case '\0': return GDK_NONE; case 'P': return GDK_SELECTION_PRIMARY; case 'S': return GDK_SELECTION_SECONDARY; case 'C': return GDK_SELECTION_CLIPBOARD; - default: break; } return gdk_atom_intern(name, FALSE); @@ -767,6 +786,27 @@ teco_interface_is_interrupted(void) return teco_interrupted != FALSE; } +void +teco_interface_refresh(gboolean force) +{ + if (!gtk_main_level()) /* batch mode */ + return; + + if (G_UNLIKELY(force)) + gtk_widget_queue_draw(teco_interface.window); + + GdkWindow *top_window = gdk_window_get_toplevel(gtk_widget_get_window(teco_interface.window)); + gdk_window_thaw_updates(top_window); + + /* + * FIXME: Why do we need two iterations to see any updates? + */ + for (gint i = 0; i < 2; i++) + gtk_main_iteration_do(FALSE); + + gdk_window_freeze_updates(top_window); +} + static void teco_interface_set_css_variables(teco_view_t *view) { @@ -776,40 +816,6 @@ teco_interface_set_css_variables(teco_view_t *view) guint32 calltip_bg_color = teco_view_ssm(view, SCI_STYLEGETBACK, STYLE_CALLTIP, 0); /* - * FIXME: Font and colors of Scintilla views cannot be set via CSS. - * But some day, there will be a way to send messages to the commandline view - * from SciTECO code via ES. - * Configuration will then be in the hands of color schemes. - * - * NOTE: We don't actually know apriori how large the font_size buffer should be, - * but luckily SCI_STYLEGETFONT with a sptr==0 will return only the size. - * This is undocumented in the Scintilla docs. - */ - g_autofree gchar *font_name = g_malloc(teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, 0) + 1); - teco_view_ssm(view, SCI_STYLEGETFONT, STYLE_DEFAULT, (sptr_t)font_name); - - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_DEFAULT, default_fg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_DEFAULT, default_bg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFONT, STYLE_DEFAULT, (sptr_t)font_name); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETSIZE, STYLE_DEFAULT, - teco_view_ssm(view, SCI_STYLEGETSIZE, STYLE_DEFAULT, 0)); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLECLEARALL, 0, 0); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETFORE, STYLE_CALLTIP, calltip_fg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBACK, STYLE_CALLTIP, calltip_bg_color); - teco_view_ssm(teco_interface.cmdline_view, SCI_SETCARETFORE, - teco_view_ssm(view, SCI_GETCARETFORE, 0, 0), 0); - /* used for the asterisk at the beginning of the command line */ - teco_view_ssm(teco_interface.cmdline_view, SCI_STYLESETBOLD, STYLE_ASTERISK, TRUE); - /* used for character representations */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_CONTROLCHAR, default_fg_color); - /* used for the rubbed out command line */ - teco_view_ssm(teco_interface.cmdline_view, SCI_INDICSETFORE, INDIC_RUBBEDOUT, default_fg_color); - /* this somehow gets reset */ - teco_view_ssm(teco_interface.cmdline_view, SCI_MARGINSETTEXT, 0, (sptr_t)"*"); - - guint text_height = teco_view_ssm(teco_interface.cmdline_view, SCI_TEXTHEIGHT, 0, 0); - - /* * Generates a CSS that sets some predefined color variables. * This effectively "exports" Scintilla styles into the CSS * world. @@ -835,16 +841,17 @@ teco_interface_set_css_variables(teco_view_t *view) gtk_css_provider_load_from_data(teco_interface.css_var_provider, css, -1, NULL); /* - * The font and size of the commandline view might have changed, + * The font and size and height of the command-line view might have changed, * so we resize it. * This cannot be done via CSS or Scintilla messages. - * Currently, it is always exactly one line high in order to mimic the Curses UI. */ - gtk_widget_set_size_request(GTK_WIDGET(teco_interface.cmdline_view), -1, text_height); + g_assert(teco_cmdline.height > 0); + gtk_widget_set_size_request(GTK_WIDGET(teco_cmdline.view), -1, + teco_cmdline.height*teco_cmdline_ssm(SCI_TEXTHEIGHT, 0, 0)); } static void -teco_interface_refresh(gboolean current_view_changed) +teco_interface_update(gboolean current_view_changed) { /* * The styles configured via Scintilla might change @@ -1116,9 +1123,6 @@ teco_interface_handle_scroll(GdkEventScroll *event, GError **error) { g_assert(event->type == GDK_SCROLL); - /* - * FIXME: Do we have to support GDK_SCROLL_SMOOTH? - */ switch (event->direction) { case GDK_SCROLL_UP: teco_mouse.type = TECO_MOUSE_SCROLLUP; @@ -1157,7 +1161,7 @@ teco_interface_event_loop(GError **error) if (!scitecoconfig_reg->vtable->get_string(scitecoconfig_reg, &scitecoconfig.data, &scitecoconfig.len, NULL, error)) return FALSE; - if (teco_string_contains(&scitecoconfig, '\0')) { + if (teco_string_contains(scitecoconfig, '\0')) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, "Null-character not allowed in filenames"); return FALSE; @@ -1195,7 +1199,7 @@ teco_interface_event_loop(GError **error) GTK_STYLE_PROVIDER(user_css_provider), GTK_STYLE_PROVIDER_PRIORITY_USER); - teco_interface_refresh(TRUE); + teco_interface_update(TRUE); gtk_widget_show_all(teco_interface.window); /* don't show popup by default */ @@ -1215,7 +1219,7 @@ teco_interface_event_loop(GError **error) * This is not necessary on Windows since the icon included * as a resource will be used by default. */ - static const gchar *icon_files[] = { + static const gchar *const icon_files[] = { "sciteco-48.png", "sciteco-32.png", "sciteco-16.png" }; GList *icon_list = NULL; @@ -1249,6 +1253,10 @@ teco_interface_event_loop(GError **error) g_unix_signal_add(SIGTERM, teco_interface_sigterm_handler, NULL); #endif + /* the interval might have been changed in the profile */ + g_timeout_add_seconds(teco_ring_recovery_interval, + teco_interface_dump_recovery_cb, NULL); + /* don't limit while waiting for input as this might be a busy operation */ teco_memory_stop_limiting(); @@ -1274,6 +1282,8 @@ teco_interface_cleanup(void) if (teco_interface.window) gtk_widget_destroy(teco_interface.window); + if (teco_interface.scroll_controller) + g_object_unref(teco_interface.scroll_controller); scintilla_release_resources(); @@ -1305,6 +1315,20 @@ teco_interface_busy_timeout_cb(gpointer user_data) return G_SOURCE_REMOVE; } +static gboolean +teco_interface_dump_recovery_cb(gpointer user_data) +{ + teco_ring_dump_recovery(); + + /* + * The backup interval could have changed (6EJ). + * New intervals will not be effective immediately, though. + */ + g_timeout_add_seconds(teco_ring_recovery_interval, + teco_interface_dump_recovery_cb, NULL); + return G_SOURCE_REMOVE; +} + static void teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) { @@ -1312,17 +1336,12 @@ teco_interface_event_box_realized_cb(GtkWidget *widget, gpointer user_data) teco_interface_set_cursor(widget, "text"); } -/** - * Called when the commandline widget is resized. - * This should ensure that the caret jumps to the middle of the command line, - * imitating the behaviour of the current Curses command line. - */ +/** Called when the commandline widget is resized */ static void teco_interface_cmdline_size_allocate_cb(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data) { - teco_view_ssm(teco_interface.cmdline_view, SCI_SETXCARETPOLICY, - CARET_SLOP | CARET_EVEN, allocation->width/2); + teco_cmdline_resized(allocation->width); } static gboolean @@ -1415,7 +1434,9 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) } teco_interrupted = FALSE; - teco_interface_refresh(teco_interface_current_view != last_view); + teco_interface_update(teco_interface_current_view != last_view); + /* always expand folds, even after mouse clicks */ + teco_interface_unfold(); /* * Scintilla has been patched to avoid any automatic scrolling since that * has been benchmarked to be a very costly operation. @@ -1458,10 +1479,34 @@ teco_interface_input_cb(GtkWidget *widget, GdkEvent *event, gpointer user_data) } static void +teco_interface_scroll_cb(GtkEventControllerScroll *controller, double dx, double dy, gpointer data) +{ + /* + * FIXME: Using teco_interface.event_box_widget will cause crashes in + * teco_interface_input_cb() + */ + GtkWidget *widget = teco_interface.window; + + /* + * Emulate a GDK_SCROLL event to make use of the existing + * event queuing in teco_interface_input_cb(). + */ + g_autoptr(GdkEvent) scroll_event = gdk_event_new(GDK_SCROLL); + scroll_event->scroll.window = gtk_widget_get_parent_window(widget); + scroll_event->scroll.direction = dy > 0 ? GDK_SCROLL_DOWN : GDK_SCROLL_UP; + scroll_event->scroll.delta_x = dx; + scroll_event->scroll.delta_y = dy; + + teco_interface_input_cb(widget, scroll_event, NULL); +} + +static void teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpointer user_data) { g_assert(len >= teco_interface.popup_prefix_len); - const teco_string_t insert = {str+teco_interface.popup_prefix_len, len-teco_interface.popup_prefix_len}; + /* str is an empty string for the "(Unnamed)" buffer */ + const teco_string_t insert = {str+teco_interface.popup_prefix_len, + len-teco_interface.popup_prefix_len}; teco_machine_t *machine = &teco_cmdline.machine.parent; const teco_view_t *last_view = teco_interface_current_view; @@ -1471,12 +1516,12 @@ teco_interface_popup_clicked_cb(GtkWidget *popup, gchar *str, gulong len, gpoint * A auto completion should never result in program termination. */ if (machine->current->insert_completion_cb && - !machine->current->insert_completion_cb(machine, &insert, NULL)) + !machine->current->insert_completion_cb(machine, insert, NULL)) return; teco_interface_popup_clear(); - teco_interface_cmdline_update(&teco_cmdline); + teco_cmdline_update(); - teco_interface_refresh(teco_interface_current_view != last_view); + teco_interface_update(teco_interface_current_view != last_view); } static gboolean @@ -1484,7 +1529,6 @@ teco_interface_window_delete_cb(GtkWidget *widget, GdkEventAny *event, gpointer { /* * Emulate that the "close" key was pressed - * which may then be handled by the execution thread * which invokes the appropriate "function key macro" * if it exists. Its default action will ensure that * the execution thread shuts down and the main loop @@ -1503,8 +1547,11 @@ teco_interface_sigterm_handler(gpointer user_data) /* * Similar to window deletion - emulate "close" key press. */ + GtkWidget *widget = teco_interface.window; + g_autoptr(GdkEvent) close_event = gdk_event_new(GDK_KEY_PRESS); + close_event->key.window = gtk_widget_get_parent_window(widget); close_event->key.keyval = GDK_KEY_Close; - return teco_interface_input_cb(teco_interface.window, close_event, NULL); + return teco_interface_input_cb(widget, close_event, NULL); } diff --git a/src/interface-gtk/view.c b/src/interface-gtk/view.c index 81db3d7..3a18f33 100644 --- a/src/interface-gtk/view.c +++ b/src/interface-gtk/view.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -102,12 +102,8 @@ teco_view_new(void) gint events = gtk_widget_get_events(GTK_WIDGET(ctx)); events &= ~(GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK | GDK_TOUCH_MASK | -#ifdef GDK_VERSION_3_18 GDK_TOUCHPAD_GESTURE_MASK | -#endif -#ifdef GDK_VERSION_3_22 GDK_TABLET_PAD_MASK | -#endif GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK); gtk_widget_set_events(GTK_WIDGET(ctx), events); diff --git a/src/interface.c b/src/interface.c index 9ec1bed..2343a16 100644 --- a/src/interface.c +++ b/src/interface.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #endif #include <stdarg.h> +#include <string.h> #include <stdio.h> #include <glib.h> @@ -36,6 +37,9 @@ //#define DEBUG +/** minimum level of messages to print to stdout/stderr */ +teco_msg_t teco_interface_msg_level = TECO_MSG_USER; + teco_view_t *teco_interface_current_view = NULL; TECO_DEFINE_UNDO_CALL(teco_interface_show_view, teco_view_t *); @@ -81,35 +85,82 @@ teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len) } } +void +teco_interface_msg(teco_msg_t type, const gchar *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + g_autofree gchar *buf = g_strdup_vprintf(fmt, ap); + va_end(ap); + + teco_interface_msg_literal(type, buf, strlen(buf)); +} + /** - * Print a message to the appropriate stdio streams. + * Print a raw message to the appropriate stdio streams. * - * This method has similar semantics to `vprintf`, i.e. - * it leaves `ap` undefined. Therefore to pass the format - * string and arguments to another `vprintf`-like function, - * you have to copy the arguments via `va_copy`. + * This deliberately does not echo (i.e. escape non-printable characters) + * the string. Either they are supposed to be written verbatim + * (TECO_MSG_USER) or are already echoed. + * Everything higher than TECO_MSG_USER is also terminated by LF. + * + * @fixme TECO_MSG_USER could always be flushed. + * This however makes the message disappear on UNIX since stdout/stderr + * have been redirected to /dev/null. + * Also it would probably be detrimental for performance in scripts + * that write individual characters. + * Perhaps we should put flushing under control of the language instead. */ void -teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap) +teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len) { - FILE *stream = stdout; + /* "user"-level messages are always printed */ + if (type != TECO_MSG_USER && type < teco_interface_msg_level) + return; switch (type) { case TECO_MSG_USER: + fwrite(str, 1, len, stdout); + //fflush(stdout); break; case TECO_MSG_INFO: - fputs("Info: ", stream); + g_fprintf(stdout, "Info: %.*s\n", (gint)len, str); break; case TECO_MSG_WARNING: - stream = stderr; - fputs("Warning: ", stream); + g_fprintf(stderr, "Warning: %.*s\n", (gint)len, str); break; case TECO_MSG_ERROR: - stream = stderr; - fputs("Error: ", stream); + g_fprintf(stderr, "Error: %.*s\n", (gint)len, str); break; } +} - g_vfprintf(stream, fmt, ap); - fputc('\n', stream); +/** + * Get character from stdin. + * + * @param widechar If TRUE reads one glyph encoded in UTF-8. + * If FALSE, returns exactly one byte. + * @return Codepoint or -1 in case of EOF. + */ +teco_int_t +teco_interface_stdio_getch(gboolean widechar) +{ + gchar buf[4]; + gint i = 0; + gint32 cp; + + do { + if (G_UNLIKELY(fread(buf+i, 1, 1, stdin) < 1)) + return -1; /* EOF */ + if (!widechar || !buf[i]) + return (guchar)buf[i]; + + /* doesn't work as expected when passed a null byte */ + cp = g_utf8_get_char_validated(buf, ++i); + if (i >= sizeof(buf) || cp != -2) + i = 0; + } while (cp < 0); + + return cp; } diff --git a/src/interface.h b/src/interface.h index 33b094b..f196a83 100644 --- a/src/interface.h +++ b/src/interface.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,7 +16,6 @@ */ #pragma once -#include <stdarg.h> #include <signal.h> #include <glib.h> @@ -42,6 +41,12 @@ * feature. */ +/** + * Interval between polling for keypresses (if necessary). + * In other words, this is the maximum latency to detect CTRL+C interruptions. + */ +#define TECO_POLL_INTERVAL 100000 /* microseconds */ + /** @protected */ extern teco_view_t *teco_interface_current_view; @@ -61,18 +66,15 @@ typedef enum { TECO_MSG_ERROR } teco_msg_t; +extern teco_msg_t teco_interface_msg_level; + /** @pure */ -void teco_interface_vmsg(teco_msg_t type, const gchar *fmt, va_list ap); +void teco_interface_msg_literal(teco_msg_t type, const gchar *str, gsize len); -static inline void G_GNUC_PRINTF(2, 3) -teco_interface_msg(teco_msg_t type, const gchar *fmt, ...) -{ - va_list ap; +void teco_interface_msg(teco_msg_t type, const gchar *fmt, ...) G_GNUC_PRINTF(2, 3); - va_start(ap, fmt); - teco_interface_vmsg(type, fmt, ap); - va_end(ap); -} +/** @pure */ +teco_int_t teco_interface_getch(gboolean widechar); /** @pure */ void teco_interface_msg_clear(void); @@ -93,6 +95,14 @@ teco_interface_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam) */ void undo__teco_interface_ssm(unsigned int, uptr_t, sptr_t); +/** Expand folds, so that dot is always visible. */ +static inline void +teco_interface_unfold(void) +{ + sptr_t dot = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_interface_ssm(SCI_ENSUREVISIBLE, teco_interface_ssm(SCI_LINEFROMPOSITION, dot, 0), 0); +} + /** @pure */ void teco_interface_info_update_qreg(const teco_qreg_t *reg); /** @pure */ @@ -108,9 +118,6 @@ void undo__teco_interface_info_update_qreg(const teco_qreg_t *); void undo__teco_interface_info_update_buffer(const teco_buffer_t *); /** @pure */ -void teco_interface_cmdline_update(const teco_cmdline_t *cmdline); - -/** @pure */ gboolean teco_interface_set_clipboard(const gchar *name, const gchar *str, gsize str_len, GError **error); void teco_interface_undo_set_clipboard(const gchar *name, gchar *str, gsize len); @@ -128,7 +135,17 @@ typedef enum { TECO_POPUP_DIRECTORY } teco_popup_entry_type_t; -/** @pure */ +/** + * Add entry to popup. + * + * @param type Entry type + * @param name + * Name string of the entry or NULL for "(Unnamed)". + * It is not necessarily null-terminated. + * @param name_len Length of string in name + * @param highlight Whether to highlight the entry + * @pure + */ void teco_interface_popup_add(teco_popup_entry_type_t type, const gchar *name, gsize name_len, gboolean highlight); /** @pure */ @@ -145,7 +162,7 @@ gboolean teco_interface_is_interrupted(void); typedef struct { enum { - TECO_MOUSE_PRESSED = 1, + TECO_MOUSE_PRESSED = 0, TECO_MOUSE_RELEASED, TECO_MOUSE_SCROLLUP, TECO_MOUSE_SCROLLDOWN @@ -172,7 +189,13 @@ gboolean teco_interface_event_loop(GError **error); * Interfacing to the external SciTECO world */ /** @protected */ -void teco_interface_stdio_vmsg(teco_msg_t type, const gchar *fmt, va_list ap); +void teco_interface_stdio_msg(teco_msg_t type, const gchar *str, gsize len); + +/** @protected */ +teco_int_t teco_interface_stdio_getch(gboolean widechar); + +/** @protected */ +void teco_interface_refresh(gboolean force); /** @pure */ void teco_interface_cleanup(void); diff --git a/src/lexer.c b/src/lexer.c index 5e6202d..25ea8f2 100644 --- a/src/lexer.c +++ b/src/lexer.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ #include "sciteco.h" #include "view.h" #include "parser.h" +#include "core-commands.h" #include "lexer.h" static teco_style_t @@ -37,16 +38,21 @@ teco_lexer_getstyle(teco_view_t *view, teco_machine_main_t *machine, /* * FIXME: At least this special workaround for numbers might be * unnecessary once we get a special parser state for parsing numbers. - * - * FIXME: What about ^* and ^/? - * They are currently highlighted as commands. */ if (machine->parent.current->keymacro_mask & TECO_KEYMACRO_MASK_START && chr <= 0xFF) { if (g_ascii_isdigit(chr)) style = SCE_SCITECO_NUMBER; - else if (strchr("+-*/#&", chr)) + else if (strchr(",+-*/#&()", chr)) style = SCE_SCITECO_OPERATOR; + } else if (machine->parent.current == &teco_state_control) { + /* + * Two-character operators must always begin with caret + * They get a separate style, so we can extend it back to + * the caret in teco_lexter_step. + */ + if (strchr("*/#", chr)) + style = SCE_SCITECO_OPERATOR2; } /* @@ -126,13 +132,40 @@ teco_lexer_step(teco_view_t *view, teco_machine_main_t *machine, machine->macro_pc = g_utf8_next_char(macro+machine->macro_pc) - macro; gunichar escape_char = machine->expectstring.machine.escape_char; + guint fold_level = SC_FOLDLEVELBASE+machine->expectstring.nesting-1+ + (escape_char == '{' ? 1 : 0); + style = teco_lexer_getstyle(view, machine, chr); /* + * Apply folding. This currently folds only {...} string arguments + * and all its embedded braces. + * We could fold loops and IF-statements as well, but that would + * require manually keeping track of the nesting in parse-only mode, + * which should better be in the parser itself. + * + * FIXME: You cannot practically disable folding via properties. + */ + if (teco_view_ssm(view, SCI_GETPROPERTYINT, (uptr_t)"fold", TRUE)) { + guint next_fold_level = SC_FOLDLEVELBASE+machine->expectstring.nesting-1+ + (machine->expectstring.machine.escape_char == '{' ? 1 : 0); + + if (next_fold_level > fold_level) + /* `chr` opened a {...} string argument */ + teco_view_ssm(view, SCI_SETFOLDLEVEL, *cur_line, + fold_level | SC_FOLDLEVELHEADERFLAG); + else if (!*cur_col) + teco_view_ssm(view, SCI_SETFOLDLEVEL, *cur_line, fold_level); + } + + /* * Optionally style @^Uq{ ... } contents like macro definitions. * The curly braces will be styled like regular commands. * - * FIXME: This will not work with nested macro definitions. + * FIXME: This works only for top-level macro definitions, + * not for nested definitions. + * FIXME: The macrodef_machine's end-of-macro callback could be used + * to detect and highlight an error on the closing `}`. * FIXME: This cannot currently be disabled, not even with SCI_SETPROPERTY. * We could only map it to an ED flag or * rewrite the lexer against the ILexer5 interface, which requires C++. @@ -147,8 +180,9 @@ teco_lexer_step(teco_view_t *view, teco_machine_main_t *machine, /* * True comments begin with `!*` or `!!`, but only the second character gets * the correct style by default, so we extend it backwards. + * The same is true for two-letter operators. */ - if (style == SCE_SCITECO_COMMENT) + if (style == SCE_SCITECO_COMMENT || style == SCE_SCITECO_OPERATOR2) old_pc--; teco_view_ssm(view, SCI_STARTSTYLING, start+old_pc, 0); @@ -185,22 +219,35 @@ teco_lexer_style(teco_view_t *view, gsize end) gsize start = teco_view_ssm(view, SCI_GETENDSTYLED, 0, 0); guint start_line = teco_view_ssm(view, SCI_LINEFROMPOSITION, start, 0); - gint start_col = 0; /* * The line state stores the laster character (column) in bytes, * that starts from a fresh parser state. * It's -1 if the line does not have a clean parser state. - * Therefore we search for the first line before `start` that has a - * known clean parser state. + * If the cached position on start_line does not fit our needs, + * we backtrack and search in previous lines + * for a known clean parser state. + * + * NOTE: It's crucial to consider the line state of the first possible + * line since we might be styling for a single-line command line view. + * + * FIXME: During rubout of regular commands we will frequently have the + * situation that the cached line state points after the last styled position + * forcing us to restyle the entire command line macro. + * If this turns out to be problematic, we might detect that + * view == teco_cmdline.view and inspect teco_cmdline.machine. */ - if (start_line > 0) { + gint start_col = teco_view_ssm(view, SCI_GETLINESTATE, start_line, 0); + if (start_col > start - teco_view_ssm(view, SCI_POSITIONFROMLINE, start_line, 0)) + /* we are asked to style __before__ the last known start state */ + start_col = -1; + if (start_col < 0 && start_line > 0) { do start_line--; while ((start_col = teco_view_ssm(view, SCI_GETLINESTATE, start_line, 0)) < 0 && start_line > 0); - start_col = MAX(start_col, 0); } + start_col = MAX(start_col, 0); start = teco_view_ssm(view, SCI_POSITIONFROMLINE, start_line, 0) + start_col; g_assert(end > start); diff --git a/src/lexer.h b/src/lexer.h index 2b011be..e7073c2 100644 --- a/src/lexer.h +++ b/src/lexer.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,12 +25,14 @@ typedef enum { SCE_SCITECO_DEFAULT = 0, SCE_SCITECO_COMMAND = 1, SCE_SCITECO_OPERATOR = 2, - SCE_SCITECO_QREG = 3, - SCE_SCITECO_STRING = 4, - SCE_SCITECO_NUMBER = 5, - SCE_SCITECO_LABEL = 6, - SCE_SCITECO_COMMENT = 7, - SCE_SCITECO_INVALID = 8 + /** two-character operators */ + SCE_SCITECO_OPERATOR2 = 3, + SCE_SCITECO_QREG = 4, + SCE_SCITECO_STRING = 5, + SCE_SCITECO_NUMBER = 6, + SCE_SCITECO_LABEL = 7, + SCE_SCITECO_COMMENT = 8, + SCE_SCITECO_INVALID = 9 } teco_style_t; void teco_lexer_style(teco_view_t *view, gsize end); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -30,17 +30,24 @@ #include <glib/gprintf.h> #include <glib/gstdio.h> +#ifdef G_OS_WIN32 +#include <fcntl.h> +#include <io.h> +#endif + #ifdef HAVE_SYS_CAPSICUM_H #include <sys/capsicum.h> #endif #include "sciteco.h" +#include "expressions.h" #include "file-utils.h" #include "cmdline.h" #include "interface.h" #include "parser.h" #include "goto.h" #include "qreg.h" +#include "view.h" #include "ring.h" #include "undo.h" #include "error.h" @@ -107,6 +114,10 @@ teco_get_default_config_path(void) #endif +static gboolean teco_show_version = FALSE; +static gboolean teco_quiet = FALSE; +static gboolean teco_stdin = FALSE; +static gboolean teco_stdout = FALSE; static gchar *teco_eval_macro = NULL; static gboolean teco_mung_file = FALSE; static gboolean teco_mung_profile = TRUE; @@ -118,6 +129,14 @@ static gchar * teco_process_options(gchar ***argv) { static const GOptionEntry option_entries[] = { + {"version", 'v', 0, G_OPTION_ARG_NONE, &teco_show_version, + "Show version"}, + {"quiet", 'q', 0, G_OPTION_ARG_NONE, &teco_quiet, + "Don't print any non-user-level messages to stdout"}, + {"stdin", 'i', 0, G_OPTION_ARG_NONE, &teco_stdin, + "Read stdin into the unnamed buffer"}, + {"stdout", 'o', 0, G_OPTION_ARG_NONE, &teco_stdout, + "Print current buffer to stdout before program termination"}, {"eval", 'e', 0, G_OPTION_ARG_STRING, &teco_eval_macro, "Evaluate macro", "macro"}, {"mung", 'm', 0, G_OPTION_ARG_NONE, &teco_mung_file, @@ -150,7 +169,7 @@ teco_process_options(gchar ***argv) g_option_context_set_description( options, "Bug reports should go to <" PACKAGE_BUGREPORT "> or " - "<" PACKAGE_URL ">." + "<rhaberkorn@fmsbw.de>." ); g_option_context_add_main_entries(options, option_entries, NULL); @@ -186,6 +205,15 @@ teco_process_options(gchar ***argv) exit(EXIT_FAILURE); } + if (teco_show_version) { + puts(PACKAGE_VERSION); + exit(EXIT_SUCCESS); + } + + if (teco_quiet) + /* warnings and errors will still be printed to stderr */ + teco_interface_msg_level = TECO_MSG_WARNING; + if ((*argv)[0] && !g_strcmp0((*argv)[1], "-S")) { /* translate -S to --, this is always passed down */ (*argv)[1][1] = '-'; @@ -337,6 +365,21 @@ main(int argc, char **argv) #endif { g_autoptr(GError) error = NULL; + teco_int_t ret = EXIT_SUCCESS; + +#ifdef G_OS_WIN32 + /* + * Windows might by default perform EOL translations, especially + * when writing to stdout, i.e. translate LF to CRLF. + * This would break at the very least --stdout, where you are + * expected to get the linebreaks configured on the current buffer via EL. + * It would also break binary filters on Windows. + * Since printing LF to the console is safe nowadays, we just do that + * globally. + */ + for (gint fd = 0; fd <= 2; fd++) + _setmode(fd, _O_BINARY); +#endif #ifdef DEBUG_PAUSE /* Windows debugging hack (see above) */ @@ -417,15 +460,20 @@ main(int argc, char **argv) * DEC TECO has them in the global table, though. */ /* search string and status register */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("_", 1)); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, + teco_qreg_plain_new("_", 1)); /* replacement string register */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("-", 1)); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, + teco_qreg_plain_new("-", 1)); /* current document's dot (":") */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_dot_new()); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, + teco_qreg_dot_new()); /* current buffer name and number ("*") */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_bufferinfo_new()); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, + teco_qreg_bufferinfo_new()); /* current working directory ("$") */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_workingdir_new()); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, + teco_qreg_workingdir_new()); /* environment defaults and registers */ teco_initialize_environment(); @@ -438,15 +486,37 @@ main(int argc, char **argv) } /* - * Add remaining arguments to unnamed buffer. + * Load stdin into the unnamed buffer. + * This will also perform EOL normalization. + * This is not done automatically when isatty(0) == 0 + * since you might want to read from stdin manually (^T). + * Loading stdin also won't work if the stream is infinite. * - * FIXME: This is not really robust since filenames may contain linefeeds. - * Also, the Unnamed Buffer should be kept empty for piping. - * Therefore, it would be best to store the arguments in Q-Regs, e.g. $0,$1,$2... + * NOTE: The profile hasn't run yet, so it cannot guess the + * documents encoding. This should therefore be done by the profile + * for any preexisting unnamed buffer. + */ + if (teco_stdin) { + if (!teco_view_load_from_stdin(teco_ring_current->view, TRUE, &error)) + goto cleanup; + + if (teco_interface_ssm(SCI_GETLENGTH, 0, 0) > 0) + teco_ring_dirtify(); + } + + /* + * Initialize the commandline-argument Q-registers (^Ax). */ - for (gint i = 1; argv_utf8[i]; i++) { - teco_interface_ssm(SCI_APPENDTEXT, strlen(argv_utf8[i]), (sptr_t)argv_utf8[i]); - teco_interface_ssm(SCI_APPENDTEXT, 1, (sptr_t)"\n"); + for (guint i = 0; argv_utf8[i]; i++) { + gchar buf[32+1]; + gint len = g_snprintf(buf, sizeof(buf), "\1%u", i); + g_assert(len < sizeof(buf)); + + teco_qreg_t *qreg = teco_qreg_plain_new(buf, len); + teco_qreg_table_insert_unique(&teco_qreg_table_globals, qreg); + if (!qreg->vtable->set_string(qreg, argv_utf8[i], strlen(argv_utf8[i]), + teco_default_codepage(), &error)) + goto cleanup; } /* @@ -461,7 +531,8 @@ main(int argc, char **argv) } g_clear_error(&error); - if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error)) + if (!teco_expressions_pop_num_calc(&ret, EXIT_SUCCESS, &error) || + !teco_ed_hook(TECO_ED_HOOK_QUIT, &error)) goto cleanup; goto cleanup; } @@ -498,8 +569,10 @@ main(int argc, char **argv) goto cleanup; g_clear_error(&error); - if (teco_quit_requested) { - if (!teco_ed_hook(TECO_ED_HOOK_QUIT, &error)) + if (teco_ed & TECO_ED_EXIT) { + /* exit was requested using the EX command */ + if (!teco_expressions_pop_num_calc(&ret, EXIT_SUCCESS, &error) || + !teco_ed_hook(TECO_ED_HOOK_QUIT, &error)) goto cleanup; goto cleanup; } @@ -509,7 +582,7 @@ main(int argc, char **argv) * If munged file didn't quit, switch into interactive mode */ /* commandline replacement string register */ - teco_qreg_table_insert(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1)); + teco_qreg_table_replace(&teco_qreg_table_globals, teco_qreg_plain_new("\e", 1)); teco_undo_enabled = TRUE; teco_ring_set_scintilla_undo(TRUE); @@ -553,8 +626,13 @@ main(int argc, char **argv) goto cleanup; cleanup: - if (error != NULL) + if (!error && teco_stdout) + teco_view_save_to_stdout(teco_ring_current->view, &error); + + if (error != NULL) { teco_error_display_full(error); + ret = EXIT_FAILURE; + } #ifndef NDEBUG teco_ring_cleanup(); @@ -562,8 +640,9 @@ cleanup: teco_qreg_table_clear(&teco_qreg_table_globals); teco_qreg_stack_clear(); teco_view_free(teco_qreg_view); + teco_cmdline_cleanup(); #endif teco_interface_cleanup(); - return error ? EXIT_FAILURE : EXIT_SUCCESS; + return ret; } diff --git a/src/memory.c b/src/memory.c index ea056bc..d8de483 100644 --- a/src/memory.c +++ b/src/memory.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -251,13 +251,19 @@ * It is of course possible to query the program's RSS via OS APIs. * This has long been avoided because it is naturally platform-dependant and * some of the APIs have proven to be too slow for frequent polling. + * Also, this will only reliably work if malloc_trim(0) does what it's + * supposed to do. * * - Windows has GetProcessMemoryInfo() which is quite slow. * When polled on a separate thread, the slow down is very acceptable. + * - POSIX has getrusage(). + * __Its performance on different OS is still untested!__ + * It reports in different units on different systems, see + * mimalloc/src/prim/unix/prim.c. + * The maxrss field does not shrink even with a working malloc_trim(). * - OS X has task_info(). * __Its performance is still untested!__ * - FreeBSD has sysctl(). - * __Its performance is still untested!__ * - Linux has no APIs but /proc/self/statm. * Reading it is naturally very slow, but at least of constant time. * When polled on a separate thread, the slow down is very acceptable. @@ -298,6 +304,10 @@ static guint teco_memory_usage = 0; */ #ifdef REPLACE_MALLOC +#ifndef G_ATOMIC_LOCK_FREE +#warning "malloc() replacement will be very slow!" +#endif + void * __attribute__((used)) malloc(size_t size) { @@ -471,10 +481,8 @@ teco_memory_get_usage(void) /* * Practically only for FreeBSD. * - * The malloc replacement via dlmalloc also works on FreeBSD, - * but this implementation has been benchmarked to be up to 4 times faster - * (but only if we poll in a separate thread). - * On the downside, this will of course be less precise. + * Since FreeBSD supports dlmalloc(), which is generally faster than + * jemalloc with a poll thread, this is not usually required. */ static gsize teco_memory_get_usage(void) diff --git a/src/memory.h b/src/memory.h index ae7b506..9826073 100644 --- a/src/memory.h +++ b/src/memory.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/move-commands.c b/src/move-commands.c index 6324131..45afc4e 100644 --- a/src/move-commands.c +++ b/src/move-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -36,7 +36,7 @@ #include "core-commands.h" #include "move-commands.h" -/*$ J jump +/*$ "J" ":J" jump * [position]J -- Go to position in buffer * [position]:J -> Success|Failure * @@ -90,7 +90,7 @@ teco_move_chars(teco_int_t n) return TECO_SUCCESS; } -/*$ C move +/*$ "C" ":C" move * [n]C -- Move dot <n> characters * -C * [n]:C -> Success|Failure @@ -117,7 +117,7 @@ teco_state_start_move(teco_machine_main_t *ctx, GError **error) } } -/*$ R reverse +/*$ "R" ":R" reverse * [n]R -- Move dot <n> characters backwards * -R * [n]:R -> Success|Failure @@ -158,7 +158,7 @@ teco_move_lines(teco_int_t n) return TECO_SUCCESS; } -/*$ L line +/*$ "L" ":L" line * [n]L -- Move dot <n> lines forwards * -L * [n]:L -> Success|Failure @@ -193,7 +193,7 @@ teco_state_start_line(teco_machine_main_t *ctx, GError **error) } } -/*$ B backwards +/*$ "B" ":B" backwards * [n]B -- Move dot <n> lines backwards * -B * [n]:B -> Success|Failure @@ -276,7 +276,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word) /* * FIXME: Is this safe or do we have to look up Unicode code points? */ - if ((!teco_string_contains(&wchars, *p)) == skip_word) { + if ((!teco_string_contains(wchars, *p)) == skip_word) { if (skip_word == end_of_word) break; skip_word = !skip_word; @@ -314,7 +314,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word) /* * FIXME: Is this safe or do we have to look up Unicode code points? */ - if ((!teco_string_contains(&wchars, p[-1])) == skip_word) { + if ((!teco_string_contains(wchars, p[-1])) == skip_word) { if (skip_word != end_of_word) break; skip_word = !skip_word; @@ -328,7 +328,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word) return TRUE; } -/*$ W word +/*$ "W" ":W" "@W" ":@W" word * [n]W -- Move dot <n> words forwards * -W * [n]:W -> Success|Failure @@ -353,7 +353,7 @@ teco_find_words(gsize *pos, teco_int_t n, gboolean end_of_word) * buffer, the command yields an error. * If colon-modified it instead returns a condition code. */ -/*$ P +/*$ "P" ":P" "@P" ":@P" * [n]P -- Move dot <n> words backwards * -P * [n]:P -> Success|Failure @@ -401,7 +401,7 @@ teco_state_start_words(teco_machine_main_t *ctx, const gchar *cmd, gint factor, return &teco_state_start; } -/*$ V +/*$ "V" ":V" "@V" ":@V" * [n]V -- Delete words forwards * -V * [n]:V -> Success|Failure @@ -417,7 +417,7 @@ teco_state_start_words(teco_machine_main_t *ctx, const gchar *cmd, gint factor, * \(lq@V\(rq is especially useful to remove the remainder of the * current word. */ -/*$ Y +/*$ "Y" ":Y" "@Y" ":@Y" * [n]Y -- Delete word backwards * -Y * [n]:Y -> Success|Failure @@ -544,7 +544,7 @@ teco_state_start_kill(teco_machine_main_t *ctx, const gchar *cmd, gboolean by_li return TRUE; } -/*$ K kill +/*$ "K" ":K" kill * [n]K -- Kill lines * -K * from,to K @@ -572,7 +572,7 @@ teco_state_start_kill_lines(teco_machine_main_t *ctx, GError **error) teco_state_start_kill(ctx, "K", TRUE, error); } -/*$ D delete +/*$ "D" ":D" delete * [n]D -- Delete characters * -D * from,to D @@ -600,7 +600,7 @@ teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error) teco_state_start_kill(ctx, "D", FALSE, error); } -/*$ ^Q lines2glyphs glyphs2lines +/*$ "^Q" ":^Q" lines2glyphs glyphs2lines * [n]^Q -> glyphs -- Convert between lines and glyph lengths or positions * [position]:^Q -> line * @@ -620,21 +620,16 @@ teco_state_start_delete_chars(teco_machine_main_t *ctx, GError **error) void teco_state_control_lines2glyphs(teco_machine_main_t *ctx, GError **error) { - if (!teco_expressions_eval(FALSE, error)) - return; - if (teco_machine_main_eval_colon(ctx)) { gssize pos; + if (!teco_expressions_eval(FALSE, error)) + return; + if (!teco_expressions_args()) { pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); } else { - teco_int_t v; - - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return; - - pos = teco_interface_glyphs2bytes(v); + pos = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); if (pos < 0) { teco_error_range_set(error, "^Q"); return; diff --git a/src/move-commands.h b/src/move-commands.h index 1f32151..cc92961 100644 --- a/src/move-commands.h +++ b/src/move-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/src/parser.c b/src/parser.c index c1d22b2..747249d 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -19,6 +19,7 @@ #include "config.h" #endif +#include <errno.h> #include <string.h> #include <glib.h> @@ -80,7 +81,7 @@ teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error) gboolean teco_state_end_of_macro(teco_machine_t *ctx, GError **error) { - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_SYNTAX, "Unterminated command"); return FALSE; } @@ -161,9 +162,7 @@ gboolean teco_execute_macro(const gchar *macro, gsize macro_len, teco_qreg_table_t *qreg_table_locals, GError **error) { - const teco_string_t str = {(gchar *)macro, macro_len}; - - if (!teco_string_validate_utf8(&str)) { + if (!teco_string_validate_utf8((teco_string_t){(gchar *)macro, macro_len})) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT, "Invalid UTF-8 byte sequence in macro"); return FALSE; @@ -185,41 +184,60 @@ teco_execute_macro(const gchar *macro, gsize macro_len, GError *tmp_error = NULL; - if (!teco_machine_main_step(¯o_machine, macro, macro_len, &tmp_error)) { - if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) { - /* passes ownership of tmp_error */ - g_propagate_error(error, tmp_error); - goto error_cleanup; + for (;;) { + if (!teco_machine_main_step(¯o_machine, macro, macro_len, &tmp_error)) { + if (!g_error_matches(tmp_error, TECO_ERROR, TECO_ERROR_RETURN)) { + /* passes ownership of tmp_error */ + g_propagate_error(error, tmp_error); + goto error_cleanup; + } + g_error_free(tmp_error); + + /* + * Macro returned - handle like regular + * end of macro, even though some checks + * are unnecessary here. + * macro_pc will still point to the return PC. + */ + g_assert(macro_machine.parent.current == &teco_state_start); + + /* + * Discard all braces, except the current one. + */ + if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error)) + goto error_cleanup; + + /* + * Clean up the loop stack. + * We are allowed to return in loops. + * NOTE: This does not have to be undone. + */ + g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp, + teco_loop_stack->len - macro_machine.loop_stack_fp); } - g_error_free(tmp_error); - /* - * Macro returned - handle like regular - * end of macro, even though some checks - * are unnecessary here. - * macro_pc will still point to the return PC. - */ - g_assert(macro_machine.parent.current == &teco_state_start); + if (G_LIKELY(teco_goto_backup_pc < 0)) + break; - /* - * Discard all braces, except the current one. - */ - if (!teco_expressions_brace_return(parent_brace_level, teco_error_return_args, error)) - goto error_cleanup; + /* continue after :Olabel$ */ + macro_machine.macro_pc = teco_goto_backup_pc; + /* macro could have ended in a "lookahead" state */ + macro_machine.parent.current = &teco_state_start; - /* - * Clean up the loop stack. - * We are allowed to return in loops. - * NOTE: This does not have to be undone. - */ - g_array_remove_range(teco_loop_stack, macro_machine.loop_stack_fp, - teco_loop_stack->len - macro_machine.loop_stack_fp); + teco_undo_string_own(teco_goto_skip_label); + memset(&teco_goto_skip_label, 0, sizeof(teco_goto_skip_label)); + teco_undo_gssize(teco_goto_backup_pc) = -1; + + if (macro_machine.parent.must_undo) + teco_undo_flags(macro_machine.flags); + macro_machine.flags.mode = TECO_MODE_NORMAL; + + /* no need to reparse everything in the future */ + macro_machine.goto_table.complete = TRUE; } if (G_UNLIKELY(teco_goto_skip_label.len > 0)) { - g_autofree gchar *label_printable = teco_string_echo(teco_goto_skip_label.data, teco_goto_skip_label.len); - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Label \"%s\" not found", label_printable); + teco_error_label_set(error, teco_goto_skip_label.data, teco_goto_skip_label.len); goto error_attach; } @@ -385,6 +403,8 @@ teco_machine_main_clear(teco_machine_main_t *ctx) teco_goto_table_clear(&ctx->goto_table); teco_string_clear(&ctx->expectstring.string); teco_machine_stringbuilding_clear(&ctx->expectstring.machine); + teco_string_clear(&ctx->goto_label); + teco_machine_qregspec_free(ctx->expectqreg); } /** Append string to result with case folding. */ @@ -394,9 +414,6 @@ teco_machine_stringbuilding_append(teco_machine_stringbuilding_t *ctx, const gch g_assert(ctx->result != NULL); switch (ctx->mode) { - case TECO_STRINGBUILDING_MODE_NORMAL: - teco_string_append(ctx->result, str, len); - break; case TECO_STRINGBUILDING_MODE_UPPER: { g_autofree gchar *folded = ctx->codepage == SC_CP_UTF8 ? g_utf8_strup(str, len) : g_ascii_strup(str, len); @@ -409,46 +426,91 @@ teco_machine_stringbuilding_append(teco_machine_stringbuilding_t *ctx, const gch teco_string_append(ctx->result, folded, strlen(folded)); break; } + default: + teco_string_append(ctx->result, str, len); + break; } } -/* - * FIXME: All teco_state_stringbuilding_* states could be static? +/** + * Append codepoint to result string with case folding. + * + * This also takes the target encoding into account and checks the value + * range accordingly. + * + * @return FALSE if the codepoint is not valid in the target encoding. */ +static gboolean +teco_machine_stringbuilding_append_c(teco_machine_stringbuilding_t *ctx, teco_int_t value) +{ + g_assert(ctx->result != NULL); + + if (ctx->codepage == SC_CP_UTF8) { + if (value < 0 || !g_unichar_validate(value)) + return FALSE; + switch (ctx->mode) { + case TECO_STRINGBUILDING_MODE_UPPER: + value = g_unichar_toupper(value); + break; + case TECO_STRINGBUILDING_MODE_LOWER: + value = g_unichar_tolower(value); + break; + } + teco_string_append_wc(ctx->result, value); + } else { + if (value < 0 || value > 0xFF) + return FALSE; + switch (ctx->mode) { + case TECO_STRINGBUILDING_MODE_UPPER: + value = g_ascii_toupper(value); + break; + case TECO_STRINGBUILDING_MODE_LOWER: + value = g_ascii_tolower(value); + break; + } + teco_string_append_c(ctx->result, value); + } + + return TRUE; +} + static teco_state_t *teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctl); +static teco_state_t teco_state_stringbuilding_ctl; static teco_state_t *teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error); -TECO_DECLARE_STATE(teco_state_stringbuilding_escaped); +static teco_state_t teco_state_stringbuilding_escaped; -TECO_DECLARE_STATE(teco_state_stringbuilding_lower); -TECO_DECLARE_STATE(teco_state_stringbuilding_upper); +static teco_state_t teco_state_stringbuilding_lower; +static teco_state_t teco_state_stringbuilding_upper; -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_num); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_u); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_q); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_quote); -TECO_DECLARE_STATE(teco_state_stringbuilding_ctle_n); +static teco_state_t teco_state_stringbuilding_ctle; +static teco_state_t teco_state_stringbuilding_ctle_num; +static teco_state_t teco_state_stringbuilding_ctle_u; +static teco_state_t teco_state_stringbuilding_ctle_code; +static teco_state_t teco_state_stringbuilding_ctle_q; +static teco_state_t teco_state_stringbuilding_ctle_quote; +static teco_state_t teco_state_stringbuilding_ctle_n; static teco_state_t * teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) { - switch (chr) { - case '^': - return &teco_state_stringbuilding_ctl; - case TECO_CTL_KEY('^'): - /* - * Ctrl+^ is inserted verbatim as code 30. - * Otherwise it would expand to a single caret - * just like caret+caret (^^). - */ - break; - default: - if (TECO_IS_CTL(chr)) - return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error); + if (ctx->mode != TECO_STRINGBUILDING_MODE_DISABLED) { + switch (chr) { + case '^': + return &teco_state_stringbuilding_ctl; + case TECO_CTL_KEY('^'): + /* + * Ctrl+^ is inserted verbatim as code 30. + * Otherwise it would expand to a single caret + * just like caret+caret (^^). + */ + break; + default: + if (TECO_IS_CTL(chr)) + return teco_state_stringbuilding_ctl_input(ctx, TECO_CTL_ECHO(chr), error); + } } return teco_state_stringbuilding_escaped_input(ctx, chr, error); @@ -457,14 +519,15 @@ teco_state_stringbuilding_start_input(teco_machine_stringbuilding_t *ctx, gunich /* in cmdline.c */ gboolean teco_state_stringbuilding_start_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, const teco_string_t *str, GError **error); - -TECO_DEFINE_STATE(teco_state_stringbuilding_start, - .is_start = TRUE, - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) - teco_state_stringbuilding_start_process_edit_cmd, - .insert_completion_cb = (teco_state_insert_completion_cb_t) - teco_state_stringbuilding_insert_completion +gboolean teco_state_stringbuilding_insert_completion(teco_machine_stringbuilding_t *ctx, teco_string_t str, GError **error); + +static TECO_DEFINE_STATE(teco_state_stringbuilding_start, + .is_start = TRUE, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_start_input, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) + teco_state_stringbuilding_start_process_edit_cmd, + .insert_completion_cb = (teco_state_insert_completion_cb_t) + teco_state_stringbuilding_insert_completion ); static teco_state_t * @@ -481,6 +544,11 @@ teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar * be abolished altogether. */ break; + case 'P': + if (ctx->parent.must_undo) + teco_undo_guint(ctx->mode); + ctx->mode = TECO_STRINGBUILDING_MODE_DISABLED; + return &teco_state_stringbuilding_start; case 'Q': case 'R': return &teco_state_stringbuilding_escaped; case 'V': return &teco_state_stringbuilding_lower; @@ -509,7 +577,9 @@ teco_state_stringbuilding_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl); +static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctl, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctl_input, +); static teco_state_t * teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -523,8 +593,6 @@ teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, guni * is that we don't try to casefold non-ANSI characters in single-byte mode. */ switch (ctx->mode) { - case TECO_STRINGBUILDING_MODE_NORMAL: - break; case TECO_STRINGBUILDING_MODE_UPPER: chr = ctx->codepage == SC_CP_UTF8 || chr < 0x80 ? g_unichar_toupper(chr) : chr; @@ -543,7 +611,8 @@ teco_state_stringbuilding_escaped_input(teco_machine_stringbuilding_t *ctx, guni gboolean teco_state_stringbuilding_escaped_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -TECO_DEFINE_STATE(teco_state_stringbuilding_escaped, +static TECO_DEFINE_STATE(teco_state_stringbuilding_escaped, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_escaped_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) teco_state_stringbuilding_escaped_process_edit_cmd ); @@ -569,7 +638,9 @@ teco_state_stringbuilding_lower_ctl_input(teco_machine_stringbuilding_t *ctx, gu return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_lower_ctl); +static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_lower_ctl, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_lower_ctl_input +); static teco_state_t * teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -587,7 +658,9 @@ teco_state_stringbuilding_lower_input(teco_machine_stringbuilding_t *ctx, gunich return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE(teco_state_stringbuilding_lower); +static TECO_DEFINE_STATE(teco_state_stringbuilding_lower, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_lower_input +); static teco_state_t * teco_state_stringbuilding_upper_ctl_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -610,7 +683,9 @@ teco_state_stringbuilding_upper_ctl_input(teco_machine_stringbuilding_t *ctx, gu return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_upper_ctl); +static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_upper_ctl, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_upper_ctl_input +); static teco_state_t * teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -628,7 +703,9 @@ teco_state_stringbuilding_upper_input(teco_machine_stringbuilding_t *ctx, gunich return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE(teco_state_stringbuilding_upper); +static TECO_DEFINE_STATE(teco_state_stringbuilding_upper, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_upper_input +); static teco_state_t * teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -638,6 +715,7 @@ teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunicha switch (teco_ascii_toupper(chr)) { case '\\': next = &teco_state_stringbuilding_ctle_num; break; case 'U': next = &teco_state_stringbuilding_ctle_u; break; + case '<': next = &teco_state_stringbuilding_ctle_code; break; case 'Q': next = &teco_state_stringbuilding_ctle_q; break; case '@': next = &teco_state_stringbuilding_ctle_quote; break; case 'N': next = &teco_state_stringbuilding_ctle_n; break; @@ -660,7 +738,9 @@ teco_state_stringbuilding_ctle_input(teco_machine_stringbuilding_t *ctx, gunicha return next; } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle); +static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_stringbuilding_ctle, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_input +); /* in cmdline.c */ gboolean teco_state_stringbuilding_qreg_process_edit_cmd(teco_machine_stringbuilding_t *ctx, teco_machine_t *parent_ctx, @@ -711,7 +791,9 @@ teco_state_stringbuilding_ctle_num_input(teco_machine_stringbuilding_t *ctx, gun return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num); +static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_num, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_num_input +); static teco_state_t * teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -736,47 +818,73 @@ teco_state_stringbuilding_ctle_u_input(teco_machine_stringbuilding_t *ctx, gunic if (!qreg->vtable->get_integer(qreg, &value, error)) return NULL; - if (ctx->codepage == SC_CP_UTF8) { - if (value < 0 || !g_unichar_validate(value)) - goto error_codepoint; - switch (ctx->mode) { - case TECO_STRINGBUILDING_MODE_NORMAL: - break; - case TECO_STRINGBUILDING_MODE_UPPER: - value = g_unichar_toupper(value); - break; - case TECO_STRINGBUILDING_MODE_LOWER: - value = g_unichar_tolower(value); - break; + if (!teco_machine_stringbuilding_append_c(ctx, value)) { + g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len); + g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, + "Q-Register \"%s\" does not contain a valid codepoint", name_printable); + return NULL; + } + + return &teco_state_stringbuilding_start; +} + +static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_u_input +); + +static teco_state_t * +teco_state_stringbuilding_ctle_code_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) +{ + if (chr == '>') { + if (!ctx->result) + /* parse-only mode */ + return &teco_state_stringbuilding_start; + + if (!ctx->code.data) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_CODEPOINT, + "Invalid empty ^E<> specified"); + return NULL; } - teco_string_append_wc(ctx->result, value); - } else { - if (value < 0 || value > 0xFF) - goto error_codepoint; - switch (ctx->mode) { - case TECO_STRINGBUILDING_MODE_NORMAL: - break; - case TECO_STRINGBUILDING_MODE_UPPER: - value = g_ascii_toupper(value); - break; - case TECO_STRINGBUILDING_MODE_LOWER: - value = g_ascii_tolower(value); - break; + + /* + * FIXME: Once we support hexadecimal constants in the SciTECO + * language itself, we might support this syntax as well. + * Or should we perhaps always consider the current radix? + */ + gchar *endp = ctx->code.data; + errno = 0; + gint64 code = g_ascii_strtoll(ctx->code.data, &endp, 0); + if (errno || endp - ctx->code.data != ctx->code.len || + !teco_machine_stringbuilding_append_c(ctx, code)) { + /* will also catch embedded nulls */ + g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, + "Invalid code ^E<%s> specified", ctx->code.data); + return NULL; } - teco_string_append_c(ctx->result, value); + + if (ctx->parent.must_undo) + teco_undo_string_own(ctx->code); + else + teco_string_clear(&ctx->code); + memset(&ctx->code, 0, sizeof(ctx->code)); + + return &teco_state_stringbuilding_start; } - return &teco_state_stringbuilding_start; + if (!ctx->result) + /* parse-only mode */ + return &teco_state_stringbuilding_ctle_code; -error_codepoint: { - g_autofree gchar *name_printable = teco_string_echo(qreg->head.name.data, qreg->head.name.len); - g_set_error(error, TECO_ERROR, TECO_ERROR_CODEPOINT, - "Q-Register \"%s\" does not contain a valid codepoint", name_printable); - return NULL; -} + if (ctx->parent.must_undo) + undo__teco_string_truncate(&ctx->code, ctx->code.len); + teco_string_append_wc(&ctx->code, chr); + + return &teco_state_stringbuilding_ctle_code; } -TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_u); +static TECO_DEFINE_STATE(teco_state_stringbuilding_ctle_code, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_code_input +); static teco_state_t * teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -804,7 +912,9 @@ teco_state_stringbuilding_ctle_q_input(teco_machine_stringbuilding_t *ctx, gunic return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q); +static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_q, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_q_input +); static teco_state_t * teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -836,7 +946,7 @@ teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, g * in command line arguments anyway. * Otherwise, we'd have to implement our own POSIX shell escape function. */ - if (teco_string_contains(&str, '\0')) { + if (teco_string_contains(str, '\0')) { teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len, table != &teco_qreg_table_globals); return NULL; @@ -847,7 +957,9 @@ teco_state_stringbuilding_ctle_quote_input(teco_machine_stringbuilding_t *ctx, g return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote); +static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_quote, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_quote_input +); static teco_state_t * teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunichar chr, GError **error) @@ -872,7 +984,7 @@ teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunic g_auto(teco_string_t) str = {NULL, 0}; if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error)) return NULL; - if (teco_string_contains(&str, '\0')) { + if (teco_string_contains(str, '\0')) { teco_error_qregcontainsnull_set(error, qreg->head.name.data, qreg->head.name.len, table != &teco_qreg_table_globals); return NULL; @@ -884,7 +996,9 @@ teco_state_stringbuilding_ctle_n_input(teco_machine_stringbuilding_t *ctx, gunic return &teco_state_stringbuilding_start; } -TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n); +static TECO_DEFINE_STATE_STRINGBUILDING_QREG(teco_state_stringbuilding_ctle_n, + .input_cb = (teco_state_input_cb_t)teco_state_stringbuilding_ctle_n_input +); void teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gunichar escape_char, @@ -922,6 +1036,11 @@ teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gch for (guint i = 0; i < len; ) { gunichar chr = g_utf8_get_char(str+i); + /* + * NOTE: We support both `[` and `{`, so this works for autocompleting + * long Q-register specifications as well. + * This may therefore insert unnecessary ^Q, but they won't hurt. + */ if (g_unichar_toupper(chr) == ctx->escape_char || (ctx->escape_char == '[' && chr == ']') || (ctx->escape_char == '{' && chr == '}')) @@ -939,8 +1058,8 @@ teco_machine_stringbuilding_escape(teco_machine_stringbuilding_t *ctx, const gch void teco_machine_stringbuilding_clear(teco_machine_stringbuilding_t *ctx) { - if (ctx->machine_qregspec) - teco_machine_qregspec_free(ctx->machine_qregspec); + teco_machine_qregspec_free(ctx->machine_qregspec); + teco_string_clear(&ctx->code); } gboolean @@ -958,30 +1077,28 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e teco_state_t *current = ctx->parent.current; /* - * String termination handling + * Ignore whitespace immediately after @-modified commands. + * This is inspired by TECO-64. + * The alternative would have been to throw an error, + * as allowing whitespace escape_chars is harmful. */ - if (ctx->flags.modifier_at) { - if (current->expectstring.last) - /* also clears the "@" modifier flag */ - teco_machine_main_eval_at(ctx); + if (ctx->flags.modifier_at && teco_is_noop(chr)) + return current; + /* + * String termination handling + */ + if (teco_machine_main_eval_at(ctx)) { /* - * FIXME: Exclude setting at least whitespace characters as the - * new string escape character to avoid accidental errors? - * * FIXME: Should we perhaps restrict case folding escape characters * to the ANSI range (teco_ascii_toupper())? - * This would be faster than case folding each and every character + * This would be faster than case folding almost all characters * of a string argument to check against the escape char. */ - switch (ctx->expectstring.machine.escape_char) { - case '\e': - case '{': - if (ctx->parent.must_undo) - teco_undo_gunichar(ctx->expectstring.machine.escape_char); - ctx->expectstring.machine.escape_char = g_unichar_toupper(chr); - return current; - } + if (ctx->parent.must_undo) + teco_undo_gunichar(ctx->expectstring.machine.escape_char); + ctx->expectstring.machine.escape_char = g_unichar_toupper(chr); + return current; } /* @@ -1019,11 +1136,11 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e * so they may do their main activity in process_cb(). */ if (ctx->expectstring.insert_len && current->expectstring.process_cb && - !current->expectstring.process_cb(ctx, &ctx->expectstring.string, + !current->expectstring.process_cb(ctx, ctx->expectstring.string, ctx->expectstring.insert_len, error)) return NULL; - teco_state_t *next = current->expectstring.done_cb(ctx, &ctx->expectstring.string, error); + teco_state_t *next = current->expectstring.done_cb(ctx, ctx->expectstring.string, error); if (ctx->parent.must_undo) teco_undo_string_own(ctx->expectstring.string); @@ -1035,6 +1152,14 @@ teco_state_expectstring_input(teco_machine_main_t *ctx, gunichar chr, GError **e if (ctx->parent.must_undo) teco_undo_gunichar(ctx->expectstring.machine.escape_char); ctx->expectstring.machine.escape_char = '\e'; + } else if (ctx->expectstring.machine.escape_char == '{') { + /* + * Makes sure that after all but the last string argument, + * the escape character is reset, as in @FR{foo}{bar}. + */ + if (ctx->parent.must_undo) + teco_undo_flags(ctx->flags); + ctx->flags.modifier_at = TRUE; } ctx->expectstring.nesting = 1; @@ -1090,7 +1215,7 @@ teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error) /* never calls process_cb() in parse-only mode */ if (ctx->expectstring.insert_len && current->expectstring.process_cb && - !current->expectstring.process_cb(ctx, &ctx->expectstring.string, + !current->expectstring.process_cb(ctx, ctx->expectstring.string, ctx->expectstring.insert_len, error)) return FALSE; @@ -1102,10 +1227,10 @@ teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **error) } gboolean -teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str, +teco_state_expectfile_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error) { - g_assert(str->data != NULL); + g_assert(str.data != NULL); /* * Null-chars must not occur in filename/path strings and at some point @@ -1114,7 +1239,7 @@ teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str * Doing it here ensures that teco_file_expand_path() can be safely called * from the done_cb(). */ - if (memchr(str->data + str->len - new_chars, '\0', new_chars)) { + if (memchr(str.data + str.len - new_chars, '\0', new_chars)) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, "Null-character not allowed in filenames"); return FALSE; diff --git a/src/parser.h b/src/parser.h index 5477150..0c389cc 100644 --- a/src/parser.h +++ b/src/parser.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -75,7 +75,9 @@ void undo__remove_index__teco_loop_stack(guint); * FIXME: Maybe use TECO_DECLARE_VTABLE_METHOD()? */ typedef const struct { + /** whether string building characters are enabled by default */ guint string_building : 1; + /** whether this string argument is the last of the command */ guint last : 1; /** @@ -83,7 +85,7 @@ typedef const struct { * * Can be NULL if no interactive feedback is required. */ - gboolean (*process_cb)(teco_machine_main_t *ctx, const teco_string_t *str, + gboolean (*process_cb)(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error); /** @@ -91,7 +93,7 @@ typedef const struct { * Commands that don't give interactive feedback can use this callback * to perform their main processing. */ - teco_state_t *(*done_cb)(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); + teco_state_t *(*done_cb)(teco_machine_main_t *ctx, teco_string_t str, GError **error); } teco_state_expectstring_t; typedef const struct { @@ -108,7 +110,7 @@ typedef gboolean (*teco_state_refresh_cb_t)(teco_machine_t *ctx, GError **error) typedef gboolean (*teco_state_end_of_macro_cb_t)(teco_machine_t *ctx, GError **error); typedef gboolean (*teco_state_process_edit_cmd_cb_t)(teco_machine_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -typedef gboolean (*teco_state_insert_completion_cb_t)(teco_machine_t *ctx, const teco_string_t *str, GError **error); +typedef gboolean (*teco_state_insert_completion_cb_t)(teco_machine_t *ctx, teco_string_t str, GError **error); typedef enum { TECO_KEYMACRO_MASK_START = (1 << 0), @@ -246,15 +248,18 @@ gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent * @implements teco_state_t * @ingroup states * - * @todo Should we eliminate required callbacks, this could be turned into a - * struct initializer TECO_INIT_STATE() and TECO_DECLARE_STATE() would become pointless. - * This would also ease declaring static states. + * Base class of all states. + * + * Since states are constant, you can append static assertions for required callbacks + * and other conditions. + * You should use TECO_ASSERT_SAFE(), but it won't be checked on all supported compilers. + * You should not put anything in front of the definition, though, so you + * can write `static TECO_DEFINE_STATE(...)`. */ #define TECO_DEFINE_STATE(NAME, ...) \ /** @ingroup states */ \ teco_state_t NAME = { \ .initial_cb = NULL, /* do nothing */ \ - .input_cb = (teco_state_input_cb_t)NAME##_input, /* always required */ \ .refresh_cb = NULL, /* do nothing */ \ .end_of_macro_cb = teco_state_end_of_macro, \ .process_edit_cmd_cb = teco_state_process_edit_cmd, \ @@ -263,11 +268,8 @@ gboolean teco_state_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent .keymacro_mask = TECO_KEYMACRO_MASK_DEFAULT, \ .style = SCE_SCITECO_DEFAULT, \ ##__VA_ARGS__ \ - } - -/** @ingroup states */ -#define TECO_DECLARE_STATE(NAME) \ - extern teco_state_t NAME + }; \ + TECO_ASSERT_SAFE(NAME.input_cb != NULL) /* in cmdline.c */ gboolean teco_state_caseinsensitive_process_edit_cmd(teco_machine_t *ctx, teco_machine_t *parent_ctx, gunichar chr, GError **error); @@ -332,7 +334,8 @@ gboolean teco_machine_input(teco_machine_t *ctx, gunichar chr, GError **error); typedef enum { TECO_STRINGBUILDING_MODE_NORMAL = 0, TECO_STRINGBUILDING_MODE_UPPER, - TECO_STRINGBUILDING_MODE_LOWER + TECO_STRINGBUILDING_MODE_LOWER, + TECO_STRINGBUILDING_MODE_DISABLED } teco_stringbuilding_mode_t; /** @@ -383,6 +386,13 @@ typedef struct teco_machine_stringbuilding_t { * the buffer's or Q-Register's encoding. */ guint codepage; + + /** + * String to collect code from `^E<...>` constructs. + * This could waste some memory for string arguments with nested Q-Reg specs, + * but we better keep it here than adding another global variable. + */ + teco_string_t code; } teco_machine_stringbuilding_t; void teco_machine_stringbuilding_init(teco_machine_stringbuilding_t *ctx, gunichar escape_char, @@ -484,6 +494,9 @@ struct teco_machine_main_t { * This is tracked even in parse-only mode. */ guint modifier_at : 1; + + /** whether <EB> command accepts a filename */ + guint allow_filename : 1; } flags; /** The nesting level of braces */ @@ -506,17 +519,19 @@ struct teco_machine_main_t { /* * teco_state_t-dependent state. * - * Some of these cannot be used concurrently and are therefore - * grouped into unions. - * We could further optimize memory usage by dynamically allocating - * some of these structures on demand. + * Some cannot theoretically be used at the same time + * but it's hard to prevent memory leaks if putting them into + * a common union. */ - teco_machine_expectstring_t expectstring; - union { - teco_string_t goto_label; - teco_machine_qregspec_t *expectqreg; - teco_machine_scintilla_t scintilla; - }; + teco_machine_expectstring_t expectstring; + /** + * State machine for parsing Q-reg specifications. + * This could theoretically be inlined, but it would introduce + * a recursive dependency between qreg.h and parser.h. + */ + teco_machine_qregspec_t *expectqreg; + teco_string_t goto_label; + teco_machine_scintilla_t scintilla; }; typedef struct teco_machine_main_flags_t teco_machine_main_flags_t; @@ -584,7 +599,7 @@ gboolean teco_state_expectstring_refresh(teco_machine_main_t *ctx, GError **erro /* in cmdline.c */ gboolean teco_state_expectstring_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /** @@ -595,18 +610,11 @@ gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, con * Super-class for states accepting string arguments * Opaquely cares about alternative-escape characters, * string building commands and accumulation into a string - * - * @note Generating the input_cb could be avoided if there were a default - * implementation. */ #define TECO_DEFINE_STATE_EXPECTSTRING(NAME, ...) \ - static teco_state_t * \ - NAME##_input(teco_machine_main_t *ctx, gunichar chr, GError **error) \ - { \ - return teco_state_expectstring_input(ctx, chr, error); \ - } \ TECO_DEFINE_STATE(NAME, \ .initial_cb = (teco_state_initial_cb_t)teco_state_expectstring_initial, \ + .input_cb = (teco_state_input_cb_t)teco_state_expectstring_input, \ .refresh_cb = (teco_state_refresh_cb_t)teco_state_expectstring_refresh, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectstring_process_edit_cmd, \ @@ -616,17 +624,17 @@ gboolean teco_state_expectstring_insert_completion(teco_machine_main_t *ctx, con .style = SCE_SCITECO_STRING, \ .expectstring.string_building = TRUE, \ .expectstring.last = TRUE, \ - .expectstring.process_cb = NULL, /* do nothing */ \ - .expectstring.done_cb = NAME##_done, /* always required */ \ + .expectstring.process_cb = NULL, /* do nothing */ \ ##__VA_ARGS__ \ - ) + ); \ + TECO_ASSERT_SAFE(NAME.expectstring.done_cb != NULL) -gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_expectfile_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error); /* in cmdline.c */ gboolean teco_state_expectfile_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); +gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTFILE @@ -645,7 +653,7 @@ gboolean teco_state_expectfile_insert_completion(teco_machine_main_t *ctx, const /* in cmdline.c */ gboolean teco_state_expectdir_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, GError **error); +gboolean teco_state_expectdir_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /** * @interface TECO_DEFINE_STATE_EXPECTDIR diff --git a/src/qreg-commands.c b/src/qreg-commands.c index a4019a0..4ede403 100644 --- a/src/qreg-commands.c +++ b/src/qreg-commands.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,8 +39,8 @@ teco_state_expectqreg_initial(teco_machine_main_t *ctx, GError **error) teco_state_t *current = ctx->parent.current; /* - * NOTE: We have to allocate a new instance always since `expectqreg` - * is part of an union. + * NOTE: This could theoretically be allocated once in + * teco_machine_main_init(), but we'd have to set the type here anyway. */ ctx->expectqreg = teco_machine_qregspec_new(current->expectqreg.type, ctx->qreg_table_locals, ctx->parent.must_undo); @@ -54,8 +54,8 @@ teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr, GError **err { teco_state_t *current = ctx->parent.current; - teco_qreg_t *qreg; - teco_qreg_table_t *table; + teco_qreg_t *qreg = NULL; + teco_qreg_table_t *table = NULL; switch (teco_machine_qregspec_input(ctx->expectqreg, chr, ctx->flags.mode == TECO_MODE_NORMAL ? &qreg : NULL, &table, error)) { @@ -69,7 +69,7 @@ teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr, GError **err /* * NOTE: ctx->expectqreg is preserved since we may want to query it from follow-up - * states. This means, it must usually be stored manually in got_register_cb() via: + * states. This means, it must usually be reset manually in got_register_cb() via: * teco_state_expectqreg_reset(ctx); */ return current->expectqreg.got_register_cb(ctx, qreg, table, error); @@ -91,7 +91,9 @@ teco_state_pushqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, * Save Q-Register <q> contents on the global Q-Register push-down * stack. */ -TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg); +TECO_DEFINE_STATE_EXPECTQREG(teco_state_pushqreg, + .expectqreg.got_register_cb = teco_state_pushqreg_got_register +); static teco_state_t * teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, @@ -99,25 +101,37 @@ teco_state_popqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, { teco_state_expectqreg_reset(ctx); - return ctx->flags.mode == TECO_MODE_NORMAL && - !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start; + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (!teco_machine_main_eval_colon(ctx)) + return !teco_qreg_stack_pop(qreg, error) ? NULL : &teco_state_start; + teco_expressions_push(teco_bool(teco_qreg_stack_pop(qreg, NULL))); + return &teco_state_start; } -/*$ "]" "]q" pop +/*$ "]" "]q" ":]q" pop * ]q -- Restore Q-Register + * :]q -> Success|Failure * * Restore Q-Register <q> by replacing its contents * with the contents of the register saved on top of * the Q-Register push-down stack. * The stack entry is popped. * + * When colon-modified, \fB]\fP returns a success boolean + * (-1) if there was a register to pop. + * If the stack was empty, a failure boolean (0) is returned + * instead of throwing an error. + * * In interactive mode, the original contents of <q> * are not immediately reclaimed but are kept in memory * to support rubbing out the command. * Memory is reclaimed on command-line termination. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_popqreg, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_popqreg_got_register ); static teco_state_t * @@ -131,11 +145,12 @@ teco_state_eqcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, } TECO_DEFINE_STATE_EXPECTQREG(teco_state_eqcommand, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_eqcommand_got_register ); static teco_state_t * -teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_loadqreg_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_qreg_t *qreg; teco_qreg_table_t *table; @@ -146,9 +161,9 @@ teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - if (str->len > 0) { + if (str.len > 0) { /* Load file into Q-Register */ - g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autofree gchar *filename = teco_file_expand_path(str.data); if (!qreg->vtable->load(qreg, filename, error)) return NULL; } else { @@ -176,7 +191,9 @@ teco_state_loadqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr * Undefined Q-Registers will be defined. * The command fails if <file> could not be read. */ -TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg); +TECO_DEFINE_STATE_EXPECTFILE(teco_state_loadqreg, + .expectstring.done_cb = teco_state_loadqreg_done +); static teco_state_t * teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, @@ -188,10 +205,12 @@ teco_state_epctcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, return &teco_state_saveqreg; } -TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand); +TECO_DEFINE_STATE_EXPECTQREG(teco_state_epctcommand, + .expectqreg.got_register_cb = teco_state_epctcommand_got_register +); static teco_state_t * -teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_saveqreg_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { teco_qreg_t *qreg; @@ -201,7 +220,7 @@ teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autofree gchar *filename = teco_file_expand_path(str.data); return qreg->vtable->save(qreg, filename, error) ? &teco_state_start : NULL; } @@ -220,7 +239,9 @@ teco_state_saveqreg_done(teco_machine_main_t *ctx, const teco_string_t *str, GEr * File names may also be tab-completed and string building * characters are enabled by default. */ -TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg); +TECO_DEFINE_STATE_EXPECTFILE(teco_state_saveqreg, + .expectstring.done_cb = teco_state_saveqreg_done +); static gboolean teco_state_queryqreg_initial(teco_machine_main_t *ctx, GError **error) @@ -273,9 +294,7 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, if (teco_expressions_args() > 0) { /* Query character from Q-Register string */ - teco_int_t pos; - if (!teco_expressions_pop_num_calc(&pos, 0, error)) - return NULL; + teco_int_t pos = teco_expressions_pop_num(0); if (pos < 0) { teco_error_range_set(error, "Q"); return NULL; @@ -297,24 +316,24 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, return &teco_state_start; } -/*$ Q Qq query +/*$ "Q" "Qq" ":Qq" query * Qq -> n -- Query Q-Register existence, its integer or string characters * -Qq -> -n - * <position>Qq -> character + * <position>Qq -> code * :Qq -> -1 | size * * Without any arguments, get and return the integer-part of * Q-Register <q>. * - * With one argument, return the <character> code at <position> + * With one argument, return the character <code> at <position> * from the string-part of Q-Register <q>. * Positions are handled like buffer positions \(em they * begin at 0 up to the length of the string minus 1. - * An error is thrown for invalid positions. + * -1 is returned for invalid positions. * If <q> is encoded as UTF-8 and there is - * an incomplete sequence at the requested position, - * -1 is returned. - * All other invalid Unicode sequences are returned as -2. + * an invalid byte sequence at the requested position, + * -2 is returned. + * Incomplete UTF-8 byte sequences are returned as -3. * Both non-colon-modified forms of Q require register <q> * to be defined and fail otherwise. * @@ -337,7 +356,8 @@ teco_state_queryqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, * boolean. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_queryqreg, - .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial + .initial_cb = (teco_state_initial_cb_t)teco_state_queryqreg_initial, + .expectqreg.got_register_cb = teco_state_queryqreg_got_register ); static teco_state_t * @@ -351,12 +371,13 @@ teco_state_ctlucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, } TECO_DEFINE_STATE_EXPECTQREG(teco_state_ctlucommand, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_ctlucommand_got_register ); static teco_state_t * teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, - const teco_string_t *str, GError **error) + teco_string_t str, GError **error) { teco_qreg_t *qreg; @@ -378,43 +399,46 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, return NULL; g_autofree gchar *buffer = NULL; + const gchar *start; gsize len = 0; if (codepage == SC_CP_UTF8) { - /* the glib docs wrongly claim that one character can take 6 bytes */ - buffer = g_malloc(4*args); + /* 4 bytes should be enough for UTF-8, but we better follow the documentation */ + start = buffer = g_malloc(args*6); + for (gint i = args; i > 0; i--) { - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return NULL; - if (v < 0 || !g_unichar_validate(v)) { + teco_int_t chr = teco_expressions_peek_num(i-1); + if (chr < 0 || !g_unichar_validate(chr)) { teco_error_codepoint_set(error, "^U"); return NULL; } - len += g_unichar_to_utf8(v, buffer+len); + len += g_unichar_to_utf8(chr, buffer+len); } + /* we pop only now since we had to peek in reverse order */ + for (gint i = 0; i < args; i++) + teco_expressions_pop_num(0); } else { buffer = g_malloc(args); - for (gint i = args; i > 0; i--) { - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, 0, error)) - return NULL; - if (v < 0 || v > 0xFF) { + + for (gint i = 0; i < args; i++) { + teco_int_t chr = teco_expressions_pop_num(0); + if (chr < 0 || chr > 0xFF) { teco_error_codepoint_set(error, "^U"); return NULL; } - buffer[len++] = v; + buffer[args-(++len)] = chr; } + start = buffer+args-len; } if (colon_modified) { /* append to register */ - if (!qreg->vtable->append_string(qreg, buffer, len, error)) + if (!qreg->vtable->append_string(qreg, start, len, error)) return NULL; } else { /* set register */ if (!qreg->vtable->undo_set_string(qreg, error) || - !qreg->vtable->set_string(qreg, buffer, len, + !qreg->vtable->set_string(qreg, start, len, codepage, error)) return NULL; } @@ -422,12 +446,12 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, if (args > 0 || colon_modified) { /* append to register */ - if (!qreg->vtable->append_string(qreg, str->data, str->len, error)) + if (!qreg->vtable->append_string(qreg, str.data, str.len, error)) return NULL; } else { /* set register */ if (!qreg->vtable->undo_set_string(qreg, error) || - !qreg->vtable->set_string(qreg, str->data, str->len, + !qreg->vtable->set_string(qreg, str.data, str.len, teco_default_codepage(), error)) return NULL; } @@ -435,7 +459,7 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, return &teco_state_start; } -/*$ ^Uq +/*$ "^Uq" ":^Uq" "set string" append * [c1,c2,...]^Uq[string]$ -- Set or append to Q-Register string without string building * [c1,c2,...]:^Uq[string]$ * @@ -462,7 +486,8 @@ teco_state_setqregstring_nobuilding_done(teco_machine_main_t *ctx, * is desired. */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_nobuilding, - .expectstring.string_building = FALSE + .expectstring.string_building = FALSE, + .expectstring.done_cb = teco_state_setqregstring_nobuilding_done ); static teco_state_t * @@ -476,7 +501,8 @@ teco_state_eucommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, } TECO_DEFINE_STATE_EXPECTQREG(teco_state_eucommand, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_eucommand_got_register ); static gboolean @@ -499,13 +525,7 @@ teco_state_setqregstring_building_initial(teco_machine_main_t *ctx, GError **err return TRUE; } -static teco_state_t * -teco_state_setqregstring_building_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) -{ - return teco_state_setqregstring_nobuilding_done(ctx, str, error); -} - -/*$ EU EUq +/*$ "EU" "EUq" ":EUq" * [c1,c2,...]EUq[string]$ -- Set or append to Q-Register string with string building characters * [c1,c2,...]:EUq[string]$ * @@ -517,7 +537,8 @@ teco_state_setqregstring_building_done(teco_machine_main_t *ctx, const teco_stri */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_setqregstring_building, .initial_cb = (teco_state_initial_cb_t)teco_state_setqregstring_building_initial, - .expectstring.string_building = TRUE + .expectstring.string_building = TRUE, + .expectstring.done_cb = teco_state_setqregstring_nobuilding_done ); static teco_state_t * @@ -534,9 +555,12 @@ teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qre if (!qreg->vtable->get_string(qreg, &str.data, &str.len, NULL, error)) return NULL; - teco_undo_gsize(teco_ranges[0].from) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - teco_undo_gsize(teco_ranges[0].to) = teco_ranges[0].from + str.len; - teco_undo_guint(teco_ranges_count) = 1; + if (teco_machine_main_eval_colon(ctx)) { + teco_interface_msg_literal(TECO_MSG_USER, str.data, str.len); + return &teco_state_start; + } + + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); if (str.len > 0) { teco_interface_ssm(SCI_BEGINUNDOACTION, 0, 0); @@ -548,17 +572,28 @@ teco_state_getqregstring_got_register(teco_machine_main_t *ctx, teco_qreg_t *qre undo__teco_interface_ssm(SCI_UNDO, 0, 0); } + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos + str.len); + teco_undo_guint(teco_ranges_count) = 1; + return &teco_state_start; } -/*$ G Gq get - * Gq -- Insert Q-Register string +/*$ G Gq get paste + * Gq -- Insert or print Q-Register string + * :Gq * * Inserts the string of Q-Register <q> into the buffer * at its current position. + * If colon-modified prints the string as a message + * (i.e. to the terminal and/or in the message area) instead + * of modifying the current buffer. + * * Specifying an undefined <q> yields an error. */ -TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring); +TECO_DEFINE_STATE_EXPECTQREG(teco_state_getqregstring, + .expectqreg.got_register_cb = teco_state_getqregstring_got_register +); static teco_state_t * teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, @@ -590,7 +625,7 @@ teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qr return &teco_state_start; } -/*$ U Uq +/*$ "U" "Uq" ":Uq" set * nUq -- Set Q-Register integer * -Uq * [n]:Uq -> Success|Failure @@ -607,7 +642,8 @@ teco_state_setqreginteger_got_register(teco_machine_main_t *ctx, teco_qreg_t *qr * The register is defined if it does not exist. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_setqreginteger, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_setqreginteger_got_register ); static teco_state_t * @@ -641,7 +677,8 @@ teco_state_increaseqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg * <q> will be defined if it does not exist. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_increaseqreg, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_increaseqreg_got_register ); static teco_state_t * @@ -674,7 +711,7 @@ teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, return &teco_state_start; } -/*$ M Mq eval +/*$ "M" "Mq" ":Mq" call eval macro * Mq -- Execute macro * :Mq * @@ -700,15 +737,17 @@ teco_state_macro_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, * (as reported by \fBEE\fP), its contents must be and are checked to be in * valid UTF-8. */ -TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro); +TECO_DEFINE_STATE_EXPECTQREG(teco_state_macro, + .expectqreg.got_register_cb = teco_state_macro_got_register +); static teco_state_t * -teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_indirect_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autofree gchar *filename = teco_file_expand_path(str.data); if (teco_machine_main_eval_colon(ctx) > 0) { /* don't create new local Q-Registers if colon modifier is given */ @@ -725,9 +764,9 @@ teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GE return &teco_state_start; } -/*$ EM - * EMfile$ -- Execute macro from file - * :EMfile$ +/*$ "EI" ":EI" indirect include + * EIfile$ -- Execute from indirect command file + * :EIfile$ * * Read the file with name <file> into memory and execute its contents * as a macro. @@ -738,7 +777,9 @@ teco_state_macrofile_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * As all \*(ST code, the contents of <file> must be in valid UTF-8 * even if operating in the \(lqdefault ANSI\(rq mode as configured by \fBED\fP. */ -TECO_DEFINE_STATE_EXPECTFILE(teco_state_macrofile); +TECO_DEFINE_STATE_EXPECTFILE(teco_state_indirect, + .expectstring.done_cb = teco_state_indirect_done +); static teco_state_t * teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, @@ -757,39 +798,10 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - gssize from, len; /* in bytes */ + gsize from, len; - if (!teco_expressions_eval(FALSE, error)) + if (!teco_get_range_args("X", &from, &len, error)) return NULL; - if (teco_expressions_args() <= 1) { - teco_int_t line; - - from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) - return NULL; - line += teco_interface_ssm(SCI_LINEFROMPOSITION, from, 0); - - if (!teco_validate_line(line)) { - teco_error_range_set(error, "X"); - return NULL; - } - - len = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0) - from; - - if (len < 0) { - from += len; - len *= -1; - } - } else { - gssize to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); - from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); - len = to - from; - - if (len < 0 || from < 0 || to < 0) { - teco_error_range_set(error, "X"); - return NULL; - } - } /* * NOTE: This does not use SCI_GETRANGEPOINTER+SCI_GETGAPPOSITION @@ -837,7 +849,7 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, return &teco_state_start; } -/*$ X Xq +/*$ "X" "Xq" ":Xq" "@Xq" ":@Xq" copy extract * [lines]Xq -- Copy into or append or cut to Q-Register * -Xq * from,toXq @@ -870,5 +882,6 @@ teco_state_copytoqreg_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, * Register <q> will be created if it is undefined. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_copytoqreg, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_copytoqreg_got_register ); diff --git a/src/qreg-commands.h b/src/qreg-commands.h index f6ad82a..51f792b 100644 --- a/src/qreg-commands.h +++ b/src/qreg-commands.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -40,7 +40,7 @@ teco_state_t *teco_state_expectqreg_input(teco_machine_main_t *ctx, gunichar chr /* in cmdline.c */ gboolean teco_state_expectqreg_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /** @@ -51,48 +51,44 @@ gboolean teco_state_expectqreg_insert_completion(teco_machine_main_t *ctx, const * Super class for states accepting Q-Register specifications. */ #define TECO_DEFINE_STATE_EXPECTQREG(NAME, ...) \ - static teco_state_t * \ - NAME##_input(teco_machine_main_t *ctx, gunichar chr, GError **error) \ - { \ - return teco_state_expectqreg_input(ctx, chr, error); \ - } \ TECO_DEFINE_STATE(NAME, \ .initial_cb = (teco_state_initial_cb_t)teco_state_expectqreg_initial, \ + .input_cb = (teco_state_input_cb_t)teco_state_expectqreg_input, \ .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t) \ teco_state_expectqreg_process_edit_cmd, \ .insert_completion_cb = (teco_state_insert_completion_cb_t) \ teco_state_expectqreg_insert_completion, \ .style = SCE_SCITECO_QREG, \ .expectqreg.type = TECO_QREG_REQUIRED, \ - .expectqreg.got_register_cb = NAME##_got_register, /* always required */ \ ##__VA_ARGS__ \ - ) + ); \ + TECO_ASSERT_SAFE(NAME.expectqreg.got_register_cb != NULL) /* * FIXME: Some of these states are referenced only in qreg-commands.c, * so they should be moved there? */ -TECO_DECLARE_STATE(teco_state_pushqreg); -TECO_DECLARE_STATE(teco_state_popqreg); +extern teco_state_t teco_state_pushqreg; +extern teco_state_t teco_state_popqreg; -TECO_DECLARE_STATE(teco_state_eqcommand); -TECO_DECLARE_STATE(teco_state_loadqreg); +extern teco_state_t teco_state_eqcommand; +extern teco_state_t teco_state_loadqreg; -TECO_DECLARE_STATE(teco_state_epctcommand); -TECO_DECLARE_STATE(teco_state_saveqreg); +extern teco_state_t teco_state_epctcommand; +extern teco_state_t teco_state_saveqreg; -TECO_DECLARE_STATE(teco_state_queryqreg); +extern teco_state_t teco_state_queryqreg; -TECO_DECLARE_STATE(teco_state_ctlucommand); -TECO_DECLARE_STATE(teco_state_setqregstring_nobuilding); -TECO_DECLARE_STATE(teco_state_eucommand); -TECO_DECLARE_STATE(teco_state_setqregstring_building); +extern teco_state_t teco_state_ctlucommand; +extern teco_state_t teco_state_setqregstring_nobuilding; +extern teco_state_t teco_state_eucommand; +extern teco_state_t teco_state_setqregstring_building; -TECO_DECLARE_STATE(teco_state_getqregstring); -TECO_DECLARE_STATE(teco_state_setqreginteger); -TECO_DECLARE_STATE(teco_state_increaseqreg); +extern teco_state_t teco_state_getqregstring; +extern teco_state_t teco_state_setqreginteger; +extern teco_state_t teco_state_increaseqreg; -TECO_DECLARE_STATE(teco_state_macro); -TECO_DECLARE_STATE(teco_state_macrofile); +extern teco_state_t teco_state_macro; +extern teco_state_t teco_state_indirect; -TECO_DECLARE_STATE(teco_state_copytoqreg); +extern teco_state_t teco_state_copytoqreg; @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -39,6 +39,7 @@ #include "ring.h" #include "eol.h" #include "error.h" +#include "rb3str.h" #include "qreg.h" /** @@ -239,18 +240,12 @@ teco_qreg_plain_get_character(teco_qreg_t *qreg, teco_int_t position, sptr_t len = teco_view_ssm(teco_qreg_view, SCI_GETLENGTH, 0, 0); gssize off = teco_view_glyphs2bytes(teco_qreg_view, position); - gboolean ret = off >= 0 && off != len; - if (!ret) - g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, - "Position %" TECO_INT_FORMAT " out of range", position); - /* make sure we still restore the current Q-Register */ - else - *chr = teco_view_get_character(teco_qreg_view, off, len); + *chr = off >= 0 && off != len ? teco_view_get_character(teco_qreg_view, off, len) : -1; if (teco_qreg_current) teco_doc_edit(&teco_qreg_current->string, 0); - return ret; + return TRUE; } static teco_int_t @@ -340,7 +335,7 @@ teco_qreg_plain_load(teco_qreg_t *qreg, const gchar *filename, GError **error) * So if loading fails, teco_qreg_current will be * made the current document again. */ - if (!teco_view_load(teco_qreg_view, filename, error)) + if (!teco_view_load(teco_qreg_view, filename, TRUE, error)) return FALSE; if (teco_qreg_current) @@ -492,6 +487,19 @@ teco_qreg_external_edit(teco_qreg_t *qreg, GError **error) } static gboolean +teco_qreg_external_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) +{ + g_auto(teco_string_t) buf = {NULL, 0}; + guint codepage; + + if (!qreg->vtable->undo_set_string(qreg, error) || + !qreg->vtable->get_string(qreg, &buf.data, &buf.len, &codepage, error)) + return FALSE; + teco_string_append(&buf, str, len); + return qreg->vtable->set_string(qreg, buf.data, buf.len, codepage, error); +} + +static gboolean teco_qreg_external_exchange_string(teco_qreg_t *qreg, teco_doc_t *src, GError **error) { g_auto(teco_string_t) other_str, own_str = {NULL, 0}; @@ -527,9 +535,8 @@ teco_qreg_external_get_character(teco_qreg_t *qreg, teco_int_t position, return FALSE; if (position < 0 || position >= g_utf8_strlen(str.data, str.len)) { - g_set_error(error, TECO_ERROR, TECO_ERROR_RANGE, - "Position %" TECO_INT_FORMAT " out of range", position); - return FALSE; + *chr = -1; + return TRUE; } const gchar *p = g_utf8_offset_to_pointer(str.data, position); @@ -610,6 +617,7 @@ teco_qreg_external_save(teco_qreg_t *qreg, const gchar *filename, GError **error .exchange_string = teco_qreg_external_exchange_string, \ .undo_exchange_string = teco_qreg_external_undo_exchange_string, \ .edit = teco_qreg_external_edit, \ + .append_string = teco_qreg_external_append_string, \ .get_character = teco_qreg_external_get_character, \ .get_length = teco_qreg_external_get_length, \ .load = teco_qreg_external_load, \ @@ -642,6 +650,8 @@ teco_qreg_bufferinfo_get_integer(teco_qreg_t *qreg, teco_int_t *ret, GError **er /* * FIXME: Something could be implemented here. There are 2 possibilities: * Either it renames the current buffer, or opens a file (alternative to EB). + * Should we implement it, we can probably remove the append_string + * implementation below. */ static gboolean teco_qreg_bufferinfo_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, @@ -722,7 +732,7 @@ teco_qreg_workingdir_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, g_auto(teco_string_t) dir; teco_string_init(&dir, str, len); - if (teco_string_contains(&dir, '\0')) { + if (teco_string_contains(dir, '\0')) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, "Directory contains null-character"); return FALSE; @@ -747,17 +757,6 @@ teco_qreg_workingdir_undo_set_string(teco_qreg_t *qreg, GError **error) return TRUE; } -/* - * FIXME: Redundant with teco_qreg_bufferinfo_append_string()... - * Best solution would be to simply implement them. - */ -static gboolean -teco_qreg_workingdir_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) -{ - teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); - return FALSE; -} - static gboolean teco_qreg_workingdir_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, guint *codepage, GError **error) @@ -791,7 +790,6 @@ teco_qreg_workingdir_new(void) static teco_qreg_vtable_t vtable = TECO_INIT_QREG_EXTERNAL( .set_string = teco_qreg_workingdir_set_string, .undo_set_string = teco_qreg_workingdir_undo_set_string, - .append_string = teco_qreg_workingdir_append_string, .get_string = teco_qreg_workingdir_get_string ); @@ -801,19 +799,27 @@ teco_qreg_workingdir_new(void) * the "\e" register also exists. * Not to mention that environment variable regs also start with dollar. * Perhaps "~" would be a better choice, although it is also already used? - * Most logical would be ".", but this should probably map to to Dot and - * is also ugly to write in practice. + * Most logical would be ".", but it is also ugly to write in practice. * Perhaps "@"... */ return teco_qreg_new(&vtable, "$", 1); } +static inline const gchar * +teco_qreg_clipboard_get_name(const teco_qreg_t *qreg) +{ + g_assert(1 <= qreg->head.name.len && qreg->head.name.len <= 2 && + *qreg->head.name.data == '~'); + if (qreg->head.name.len > 1) + return qreg->head.name.data+1; + return teco_ed & TECO_ED_CLIP_PRIMARY ? "P" : "C"; +} + static gboolean teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, guint codepage, GError **error) { - g_assert(!teco_string_contains(&qreg->head.name, '\0')); - const gchar *clipboard_name = qreg->head.name.data + 1; + const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg); if (teco_ed & TECO_ED_AUTOEOL) { /* @@ -849,17 +855,6 @@ teco_qreg_clipboard_set_string(teco_qreg_t *qreg, const gchar *str, gsize len, return TRUE; } -/* - * FIXME: Redundant with teco_qreg_bufferinfo_append_string()... - * Best solution would be to simply implement them. - */ -static gboolean -teco_qreg_clipboard_append_string(teco_qreg_t *qreg, const gchar *str, gsize len, GError **error) -{ - teco_error_qregopunsupported_set(error, qreg->head.name.data, qreg->head.name.len, FALSE); - return FALSE; -} - static gboolean teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error) { @@ -873,8 +868,7 @@ teco_qreg_clipboard_undo_set_string(teco_qreg_t *qreg, GError **error) if (!teco_undo_enabled) return TRUE; - g_assert(!teco_string_contains(&qreg->head.name, '\0')); - const gchar *clipboard_name = qreg->head.name.data + 1; + const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg); /* * Ownership of str is passed to the undo token. @@ -892,8 +886,7 @@ static gboolean teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, guint *codepage, GError **error) { - g_assert(!teco_string_contains(&qreg->head.name, '\0')); - const gchar *clipboard_name = qreg->head.name.data + 1; + const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg); if (!(teco_ed & TECO_ED_AUTOEOL)) /* @@ -937,8 +930,7 @@ teco_qreg_clipboard_get_string(teco_qreg_t *qreg, gchar **str, gsize *len, static gboolean teco_qreg_clipboard_load(teco_qreg_t *qreg, const gchar *filename, GError **error) { - g_assert(!teco_string_contains(&qreg->head.name, '\0')); - const gchar *clipboard_name = qreg->head.name.data + 1; + const gchar *clipboard_name = teco_qreg_clipboard_get_name(qreg); g_auto(teco_string_t) str = {NULL, 0}; @@ -954,13 +946,18 @@ teco_qreg_clipboard_new(const gchar *name) static teco_qreg_vtable_t vtable = TECO_INIT_QREG_EXTERNAL( .set_string = teco_qreg_clipboard_set_string, .undo_set_string = teco_qreg_clipboard_undo_set_string, - .append_string = teco_qreg_clipboard_append_string, .get_string = teco_qreg_clipboard_get_string, .load = teco_qreg_clipboard_load ); teco_qreg_t *qreg = teco_qreg_new(&vtable, "~", 1); teco_string_append(&qreg->head.name, name, strlen(name)); + /* + * Register "~" is the default clipboard, which defaults to "~C". + * This is configurable via the integer cell. + */ + if (qreg->head.name.len == 1) + qreg->integer = 'C'; return qreg; } @@ -974,9 +971,9 @@ teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo) /* general purpose registers */ for (gchar q = 'A'; q <= 'Z'; q++) - teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q))); + teco_qreg_table_insert_unique(table, teco_qreg_plain_new(&q, sizeof(q))); for (gchar q = '0'; q <= '9'; q++) - teco_qreg_table_insert(table, teco_qreg_plain_new(&q, sizeof(q))); + teco_qreg_table_insert_unique(table, teco_qreg_plain_new(&q, sizeof(q))); } /** @memberof teco_qreg_table_t */ @@ -986,10 +983,10 @@ teco_qreg_table_init_locals(teco_qreg_table_t *table, gboolean must_undo) teco_qreg_table_init(table, must_undo); /* search mode ("^X") */ - teco_qreg_table_insert(table, teco_qreg_plain_new("\x18", 1)); + teco_qreg_table_insert_unique(table, teco_qreg_plain_new("\x18", 1)); /* numeric radix ("^R") */ table->radix = teco_qreg_radix_new(); - teco_qreg_table_insert(table, table->radix); + teco_qreg_table_insert_unique(table, table->radix); } static inline void @@ -1110,7 +1107,7 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error) for (teco_qreg_t *cur = first; cur && cur->head.name.data[0] == '$'; cur = (teco_qreg_t *)teco_rb3str_get_next(&cur->head)) { - const teco_string_t *name = &cur->head.name; + const teco_string_t name = cur->head.name; /* * Ignore the "$" register (not an environment @@ -1118,7 +1115,7 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error) * name contains "=" or null (not allowed in environment * variable names). */ - if (name->len == 1 || + if (name.len == 1 || teco_string_contains(name, '=') || teco_string_contains(name, '\0')) continue; @@ -1127,16 +1124,16 @@ teco_qreg_table_get_environ(teco_qreg_table_t *table, GError **error) g_strfreev(envp); return NULL; } - if (teco_string_contains(&value, '\0')) { + if (teco_string_contains(value, '\0')) { g_strfreev(envp); g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Environment register \"%s\" must not contain null characters", - name->data); + name.data); return NULL; } /* more efficient than g_environ_setenv() */ - *p++ = g_strconcat(name->data+1, "=", value.data, NULL); + *p++ = g_strconcat(name.data+1, "=", value.data, NULL); } *p = NULL; @@ -1336,16 +1333,16 @@ teco_ed_hook(teco_ed_hook_t type, GError **error) return teco_expressions_discard_args(error) && teco_expressions_brace_close(error); - static const gchar *type2name[] = { - [TECO_ED_HOOK_ADD-1] = "ADD", - [TECO_ED_HOOK_EDIT-1] = "EDIT", - [TECO_ED_HOOK_CLOSE-1] = "CLOSE", - [TECO_ED_HOOK_QUIT-1] = "QUIT" + static const gchar *const type2name[] = { + [TECO_ED_HOOK_ADD] = "ADD", + [TECO_ED_HOOK_EDIT] = "EDIT", + [TECO_ED_HOOK_CLOSE] = "CLOSE", + [TECO_ED_HOOK_QUIT] = "QUIT" }; error_add_frame: - g_assert(0 <= type-1 && type-1 < G_N_ELEMENTS(type2name)); - teco_error_add_frame_edhook(type2name[type-1]); + g_assert(0 <= type && type < G_N_ELEMENTS(type2name)); + teco_error_add_frame_edhook(type2name[type]); return FALSE; } @@ -1380,15 +1377,12 @@ TECO_DEFINE_UNDO_SCALAR(teco_machine_qregspec_flags_t); #define teco_undo_qregspec_flags(VAR) \ (*teco_undo_object_teco_machine_qregspec_flags_t_push(&(VAR))) -/* - * FIXME: All teco_state_qregspec_* states could be static? - */ -TECO_DECLARE_STATE(teco_state_qregspec_start); -TECO_DECLARE_STATE(teco_state_qregspec_start_global); -TECO_DECLARE_STATE(teco_state_qregspec_caret); -TECO_DECLARE_STATE(teco_state_qregspec_firstchar); -TECO_DECLARE_STATE(teco_state_qregspec_secondchar); -TECO_DECLARE_STATE(teco_state_qregspec_string); +static teco_state_t teco_state_qregspec_start; +static teco_state_t teco_state_qregspec_start_global; +static teco_state_t teco_state_qregspec_caret; +static teco_state_t teco_state_qregspec_firstchar; +static teco_state_t teco_state_qregspec_secondchar; +static teco_state_t teco_state_qregspec_string; static teco_state_t *teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gunichar chr, GError **error); @@ -1416,7 +1410,7 @@ teco_state_qregspec_done(teco_machine_qregspec_t *ctx, GError **error) case TECO_QREG_OPTIONAL_INIT: if (!ctx->result) { ctx->result = teco_qreg_plain_new(ctx->name.data, ctx->name.len); - teco_qreg_table_insert(ctx->result_table, ctx->result); + teco_qreg_table_insert_unique(ctx->result_table, ctx->result); teco_qreg_table_undo_remove(ctx->result); } break; @@ -1445,11 +1439,12 @@ teco_state_qregspec_start_input(teco_machine_qregspec_t *ctx, gunichar chr, GErr /* in cmdline.c */ gboolean teco_state_qregspec_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, +gboolean teco_state_qregspec_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error); -TECO_DEFINE_STATE(teco_state_qregspec_start, +static TECO_DEFINE_STATE(teco_state_qregspec_start, .is_start = TRUE, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_start_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd, .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_insert_completion ); @@ -1485,7 +1480,8 @@ teco_state_qregspec_start_global_input(teco_machine_qregspec_t *ctx, gunichar ch * Alternatively, we'd have to introduce a teco_machine_qregspec_t::status attribute. * Or even better, why not use special pointers like ((teco_state_t *)"teco_state_qregspec_done")? */ -TECO_DEFINE_STATE(teco_state_qregspec_start_global, +static TECO_DEFINE_STATE(teco_state_qregspec_start_global, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_start_global_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd ); @@ -1506,7 +1502,9 @@ teco_state_qregspec_caret_input(teco_machine_qregspec_t *ctx, gunichar chr, GErr return teco_state_qregspec_done(ctx, error); } -TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_qregspec_caret); +static TECO_DEFINE_STATE_CASEINSENSITIVE(teco_state_qregspec_caret, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_caret_input +); static teco_state_t * teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gunichar chr, GError **error) @@ -1522,7 +1520,8 @@ teco_state_qregspec_firstchar_input(teco_machine_qregspec_t *ctx, gunichar chr, return &teco_state_qregspec_secondchar; } -TECO_DEFINE_STATE(teco_state_qregspec_firstchar, +static TECO_DEFINE_STATE(teco_state_qregspec_firstchar, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_firstchar_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd ); @@ -1540,7 +1539,8 @@ teco_state_qregspec_secondchar_input(teco_machine_qregspec_t *ctx, gunichar chr, return teco_state_qregspec_done(ctx, error); } -TECO_DEFINE_STATE(teco_state_qregspec_secondchar, +static TECO_DEFINE_STATE(teco_state_qregspec_secondchar, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_secondchar_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_process_edit_cmd ); @@ -1587,10 +1587,11 @@ teco_state_qregspec_string_input(teco_machine_qregspec_t *ctx, gunichar chr, GEr /* in cmdline.c */ gboolean teco_state_qregspec_string_process_edit_cmd(teco_machine_qregspec_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, const teco_string_t *str, +gboolean teco_state_qregspec_string_insert_completion(teco_machine_qregspec_t *ctx, teco_string_t str, GError **error); -TECO_DEFINE_STATE(teco_state_qregspec_string, +static TECO_DEFINE_STATE(teco_state_qregspec_string, + .input_cb = (teco_state_input_cb_t)teco_state_qregspec_string_input, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_qregspec_string_process_edit_cmd, .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_qregspec_string_insert_completion ); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,6 +18,8 @@ #include <glib.h> +//#include <rb3ptr.h> + #include "sciteco.h" #include "view.h" #include "doc.h" @@ -43,6 +45,8 @@ extern teco_view_t *teco_qreg_view; * teco_qreg_set_integer_t set_integer; * ... * teco_qreg_set_integer(qreg, 23, error); + * + * But this probably won't work. Perhaps use the X-macro pattern. */ typedef const struct { gboolean (*set_integer)(teco_qreg_t *qreg, teco_int_t value, GError **error); @@ -156,7 +160,13 @@ struct teco_qreg_table_t { void teco_qreg_table_init(teco_qreg_table_t *table, gboolean must_undo); void teco_qreg_table_init_locals(teco_qreg_table_t *table, gboolean must_undo); -/** @memberof teco_qreg_table_t */ +/** + * Insert Q-Register into table. + * + * @return If non-NULL a register with the same name as qreg already + * existed in table. In this case qreg is __not__ automatically freed. + * @memberof teco_qreg_table_t + */ static inline teco_qreg_t * teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg) { @@ -165,6 +175,35 @@ teco_qreg_table_insert(teco_qreg_table_t *table, teco_qreg_t *qreg) } /** @memberof teco_qreg_table_t */ +static inline void +teco_qreg_table_insert_unique(teco_qreg_table_t *table, teco_qreg_t *qreg) +{ + G_GNUC_UNUSED teco_qreg_t *found = teco_qreg_table_insert(table, qreg); + g_assert(found == NULL); +} + +/** + * Insert Q-register into table, possibly replacing a register with the same name. + * + * This is useful for initializing Q-registers late when the user could have + * already created one in the profile. + * + * @param table Table to insert into + * @param qreg Q-Register to insert + * + * @memberof teco_qreg_table_t + */ +static inline void +teco_qreg_table_replace(teco_qreg_table_t *table, teco_qreg_t *qreg) +{ + teco_qreg_t *found = teco_qreg_table_insert(table, qreg); + if (found) { + rb3_replace(&found->head.head, &qreg->head.head); + teco_qreg_free(found); + } +} + +/** @memberof teco_qreg_table_t */ static inline teco_qreg_t * teco_qreg_table_find(teco_qreg_table_t *table, const gchar *name, gsize len) { @@ -200,7 +239,7 @@ gboolean teco_qreg_stack_pop(teco_qreg_t *qreg, GError **error); void teco_qreg_stack_clear(void); typedef enum { - TECO_ED_HOOK_ADD = 1, + TECO_ED_HOOK_ADD = 0, TECO_ED_HOOK_EDIT, TECO_ED_HOOK_CLOSE, TECO_ED_HOOK_QUIT diff --git a/src/rb3str.c b/src/rb3str.c index 276b624..f4c16fa 100644 --- a/src/rb3str.c +++ b/src/rb3str.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,13 +34,13 @@ static gint teco_rb3str_cmp(const teco_rb3str_head_t *head, const teco_string_t *data) { - return teco_string_cmp(&head->key, data->data, data->len); + return teco_string_cmp(head->key, data->data, data->len); } static gint teco_rb3str_casecmp(const teco_rb3str_head_t *head, const teco_string_t *data) { - return teco_string_casecmp(&head->key, data->data, data->len); + return teco_string_casecmp(head->key, data->data, data->len); } /** @memberof teco_rb3str_tree_t */ @@ -113,7 +113,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, guint prefixed_entries = 0; for (teco_rb3str_head_t *cur = teco_rb3str_nfind(tree, case_sensitive, str, str_len); - cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len; + cur && cur->key.len >= str_len && diff(cur->key, str, str_len) == str_len; cur = teco_rb3str_get_next(cur)) { if (restrict_len && g_utf8_strlen(cur->key.data, cur->key.len) != restrict_len) continue; @@ -122,7 +122,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, first = cur; prefix_len = cur->key.len - str_len; } else { - gsize len = diff(&cur->key, first->key.data, first->key.len) - str_len; + gsize len = diff(cur->key, first->key.data, first->key.len) - str_len; if (len < prefix_len) prefix_len = len; } @@ -134,7 +134,7 @@ teco_rb3str_auto_complete(teco_rb3str_tree_t *tree, gboolean case_sensitive, teco_string_init(insert, first->key.data + str_len, prefix_len); } else if (prefixed_entries > 1) { for (teco_rb3str_head_t *cur = first; - cur && cur->key.len >= str_len && diff(&cur->key, str, str_len) == str_len; + cur && cur->key.len >= str_len && diff(cur->key, str, str_len) == str_len; cur = teco_rb3str_get_next(cur)) { if (restrict_len && g_utf8_strlen(cur->key.data, cur->key.len) != restrict_len) continue; diff --git a/src/rb3str.h b/src/rb3str.h index 466cf90..00d1791 100644 --- a/src/rb3str.h +++ b/src/rb3str.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ #include <glib.h> #include <glib/gprintf.h> +#include <glib/gstdio.h> #include <Scintilla.h> @@ -55,7 +56,8 @@ teco_buffer_set_filename(teco_buffer_t *ctx, const gchar *filename) gchar *resolved = teco_file_get_absolute_path(filename); g_free(ctx->filename); ctx->filename = resolved; - teco_interface_info_update(ctx); + if (ctx == teco_ring_current && !teco_qreg_current) + teco_interface_info_update(ctx); } /** @memberof teco_buffer_t */ @@ -74,16 +76,23 @@ teco_buffer_undo_edit(teco_buffer_t *ctx) } /** @private @memberof teco_buffer_t */ +static inline gchar * +teco_buffer_get_recovery(teco_buffer_t *ctx) +{ + g_autofree gchar *dirname = g_path_get_dirname(ctx->filename); + g_autofree gchar *basename = g_path_get_basename(ctx->filename); + return g_strconcat(dirname, G_DIR_SEPARATOR_S, "#", basename, "#", NULL); +} + +/** @private @memberof teco_buffer_t */ static gboolean teco_buffer_load(teco_buffer_t *ctx, const gchar *filename, GError **error) { - if (!teco_view_load(ctx->view, filename, error)) + if (!teco_view_load(ctx->view, filename, TRUE, error)) return FALSE; -#if 0 /* NOTE: currently buffer cannot be dirty */ - undo__teco_interface_info_update_buffer(ctx); - teco_undo_gboolean(ctx->dirty) = FALSE; -#endif + /* currently buffer cannot be dirty */ + g_assert(ctx->state == TECO_BUFFER_CLEAN); teco_buffer_set_filename(ctx, filename); return TRUE; @@ -107,8 +116,15 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error) * Undirtify * NOTE: info update is performed by set_filename() */ - undo__teco_interface_info_update_buffer(ctx); - teco_undo_gboolean(ctx->dirty) = FALSE; + if (ctx == teco_ring_current && !teco_qreg_current) + undo__teco_interface_info_update_buffer(ctx); + if (ctx->state > TECO_BUFFER_DIRTY_NO_DUMP) { + g_autofree gchar *filename_recovery = teco_buffer_get_recovery(ctx); + g_unlink(filename_recovery); + /* on rubout, we do not restore the recovery file */ + ctx->state = TECO_BUFFER_DIRTY_NO_DUMP; + } + teco_undo_guint(ctx->state) = TECO_BUFFER_CLEAN; /* * FIXME: necessary also if the filename was not specified but the file @@ -127,6 +143,11 @@ teco_buffer_save(teco_buffer_t *ctx, const gchar *filename, GError **error) static inline void teco_buffer_free(teco_buffer_t *ctx) { + if (ctx->state > TECO_BUFFER_DIRTY_NO_DUMP) { + g_autofree gchar *filename_recovery = teco_buffer_get_recovery(ctx); + g_unlink(filename_recovery); + } + teco_view_free(ctx->view); g_free(ctx->filename); g_free(ctx); @@ -151,7 +172,7 @@ teco_ring_last(void) } static void -teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run) +teco_undo_ring_reinsert_action(teco_buffer_t **buffer, gboolean run) { if (run) { /* @@ -162,22 +183,19 @@ teco_undo_ring_edit_action(teco_buffer_t **buffer, gboolean run) teco_tailq_insert_before((*buffer)->entry.next, &(*buffer)->entry); else teco_tailq_insert_tail(&teco_ring_head, &(*buffer)->entry); - - teco_ring_current = *buffer; - teco_buffer_edit(*buffer); } else { teco_buffer_free(*buffer); } } -/* - * Emitted after a buffer close - * The pointer is the only remaining reference to the buffer! +/** + * Insert buffer during undo (for closing buffers). + * Ownership of the buffer is passed to the undo token. */ static void -teco_undo_ring_edit(teco_buffer_t *buffer) +teco_undo_ring_reinsert(teco_buffer_t *buffer) { - teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_edit_action, + teco_buffer_t **ctx = teco_undo_push_size((teco_undo_action_t)teco_undo_ring_reinsert_action, sizeof(buffer)); if (ctx) *ctx = buffer; @@ -223,27 +241,57 @@ teco_ring_find_by_id(teco_int_t id) return NULL; } +static void +teco_ring_undirtify(void) +{ + if (teco_ring_current->state > TECO_BUFFER_DIRTY_NO_DUMP) { + g_autofree gchar *filename_recovery = teco_buffer_get_recovery(teco_ring_current); + g_unlink(filename_recovery); + } + + teco_ring_current->state = TECO_BUFFER_CLEAN; + teco_interface_info_update(teco_ring_current); +} + +TECO_DEFINE_UNDO_CALL(teco_ring_undirtify); + void teco_ring_dirtify(void) { - if (teco_qreg_current || teco_ring_current->dirty) + if (teco_qreg_current) return; - undo__teco_interface_info_update_buffer(teco_ring_current); - teco_undo_gboolean(teco_ring_current->dirty) = TRUE; - teco_interface_info_update(teco_ring_current); + switch ((teco_buffer_state_t)teco_ring_current->state) { + case TECO_BUFFER_CLEAN: + teco_ring_current->state = TECO_BUFFER_DIRTY_NO_DUMP; + teco_interface_info_update(teco_ring_current); + undo__teco_ring_undirtify(); + break; + case TECO_BUFFER_DIRTY_NO_DUMP: + case TECO_BUFFER_DIRTY_OUTDATED_DUMP: + break; + case TECO_BUFFER_DIRTY_RECENT_DUMP: + teco_ring_current->state = TECO_BUFFER_DIRTY_OUTDATED_DUMP; + /* set to TECO_BUFFER_DIRTY_OUTDATED_DUMP on rubout */ + teco_undo_guint(teco_ring_current->state); + break; + } } -gboolean -teco_ring_is_any_dirty(void) +/** Get id of first dirty buffer, or otherwise 0 */ +guint +teco_ring_get_first_dirty(void) { + guint id = 1; + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { teco_buffer_t *buffer = (teco_buffer_t *)cur; - if (buffer->dirty) - return TRUE; + if (buffer->state > TECO_BUFFER_CLEAN) + return id; + id++; } - return FALSE; + return 0; } gboolean @@ -252,13 +300,72 @@ teco_ring_save_all_dirty_buffers(GError **error) for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { teco_buffer_t *buffer = (teco_buffer_t *)cur; /* NOTE: Will fail for a dirty unnamed file */ - if (buffer->dirty && !teco_buffer_save(buffer, NULL, error)) + if (buffer->state > TECO_BUFFER_CLEAN && + !teco_buffer_save(buffer, NULL, error)) return FALSE; } return TRUE; } +/** + * Recovery creation interval in seconds or 0 if disabled. + * It's not currently enforced in batch mode. + */ +guint teco_ring_recovery_interval = 5*60; + +/** + * Create recovery files for all dirty buffers. + * + * Should be called by the interface every teco_ring_recovery_interval seconds. + * This does not generate or expect undo tokens, so it can be called + * even when idlying. + */ +void +teco_ring_dump_recovery(void) +{ + g_assert(teco_ring_recovery_interval > 0); + + for (teco_tailq_entry_t *cur = teco_ring_head.first; cur != NULL; cur = cur->next) { + teco_buffer_t *buffer = (teco_buffer_t *)cur; + /* already dumped buffers don't have to be written again */ + if (buffer->state != TECO_BUFFER_DIRTY_NO_DUMP && + buffer->state != TECO_BUFFER_DIRTY_OUTDATED_DUMP) + continue; + + /* + * Dirty unnamed buffers cannot be backed up. + * FIXME: Perhaps they should be dumped under ~/#UNNAMED#? + */ + if (!buffer->filename) + continue; + + g_autofree gchar *filename_recovery = teco_buffer_get_recovery(buffer); + + g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename_recovery, "w", NULL); + if (!channel) + continue; + + /* + * teco_view_save_to_channel() expects a buffered and blocking channel. + */ + g_io_channel_set_encoding(channel, NULL, NULL); + g_io_channel_set_buffered(channel, TRUE); + + /* + * This does not use teco_view_save_to_file() since we must not + * emit undo tokens. + * + * FIXME: Errors are silently ignored. + * Should we log warnings instead? + */ + if (!teco_view_save_to_channel(buffer->view, channel, NULL)) + continue; + + buffer->state = TECO_BUFFER_DIRTY_RECENT_DUMP; + } +} + gboolean teco_ring_edit_by_name(const gchar *filename, GError **error) { @@ -306,8 +413,7 @@ teco_ring_edit_by_id(teco_int_t id, GError **error) { teco_buffer_t *buffer = teco_ring_find(id); if (!buffer) { - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Invalid buffer id %" TECO_INT_FORMAT, id); + teco_error_invalidbuf_set(error, id); return FALSE; } @@ -320,7 +426,7 @@ teco_ring_edit_by_id(teco_int_t id, GError **error) } static void -teco_ring_close_buffer(teco_buffer_t *buffer) +teco_ring_remove_buffer(teco_buffer_t *buffer) { teco_tailq_remove(&teco_ring_head, &buffer->entry); @@ -333,32 +439,48 @@ teco_ring_close_buffer(teco_buffer_t *buffer) "Removed unnamed file from the ring."); } -TECO_DEFINE_UNDO_CALL(teco_ring_close_buffer, teco_buffer_t *); +TECO_DEFINE_UNDO_CALL(teco_ring_remove_buffer, teco_buffer_t *); +/** + * Close the given buffer. + * Executes close hooks and changes the current buffer if necessary. + * It already pushes undo tokens. + */ gboolean -teco_ring_close(GError **error) +teco_ring_close(teco_buffer_t *buffer, GError **error) { - teco_buffer_t *buffer = teco_ring_current; + if (buffer == teco_ring_current) { + if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error)) + return FALSE; - if (!teco_ed_hook(TECO_ED_HOOK_CLOSE, error)) - return FALSE; - teco_ring_close_buffer(buffer); - teco_ring_current = teco_buffer_next(buffer) ? : teco_buffer_prev(buffer); - /* Transfer responsibility to the undo token object. */ - teco_undo_ring_edit(buffer); + teco_ring_undo_edit(); + teco_ring_remove_buffer(buffer); + + teco_ring_current = teco_buffer_next(buffer) ? : teco_buffer_prev(buffer); + if (!teco_ring_current) { + /* edit new unnamed buffer */ + if (!teco_ring_edit_by_name(NULL, error)) + return FALSE; + } else { + teco_buffer_edit(teco_ring_current); + if (!teco_ed_hook(TECO_ED_HOOK_EDIT, error)) + return FALSE; + } + } else { + teco_ring_remove_buffer(buffer); + } - if (!teco_ring_current) - return teco_ring_edit_by_name(NULL, error); + /* transfer responsibility to the undo token object */ + teco_undo_ring_reinsert(buffer); - teco_buffer_edit(teco_ring_current); - return teco_ed_hook(TECO_ED_HOOK_EDIT, error); + return TRUE; } void teco_ring_undo_close(void) { undo__teco_buffer_free(teco_ring_current); - undo__teco_ring_close_buffer(teco_ring_current); + undo__teco_ring_remove_buffer(teco_ring_current); } void @@ -387,13 +509,6 @@ teco_ring_cleanup(void) * Command states */ -/* - * FIXME: Should be part of the teco_machine_main_t? - * Unfortunately, we cannot just merge initial() with done(), - * since we want to react immediately to xEB without waiting for $. - */ -static gboolean allow_filename = FALSE; - static gboolean teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error) { @@ -404,18 +519,17 @@ teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error) if (!teco_expressions_pop_num_calc(&id, -1, error)) return FALSE; - allow_filename = TRUE; + ctx->flags.allow_filename = TRUE; if (id == 0) { - for (teco_buffer_t *cur = teco_ring_first(); cur; cur = teco_buffer_next(cur)) { - const gchar *filename = cur->filename ? : "(Unnamed)"; - teco_interface_popup_add(TECO_POPUP_FILE, filename, - strlen(filename), cur == teco_ring_current); - } + for (teco_buffer_t *cur = teco_ring_first(); cur; cur = teco_buffer_next(cur)) + teco_interface_popup_add(TECO_POPUP_FILE, cur->filename, + cur->filename ? strlen(cur->filename) : 0, + cur == teco_ring_current); teco_interface_popup_show(0); } else if (id > 0) { - allow_filename = FALSE; + ctx->flags.allow_filename = FALSE; if (!teco_current_doc_undo_edit(error) || !teco_ring_edit(id, error)) return FALSE; @@ -424,24 +538,35 @@ teco_state_edit_file_initial(teco_machine_main_t *ctx, GError **error) return TRUE; } +gboolean +teco_state_edit_file_process(teco_machine_main_t *ctx, teco_string_t str, + gsize new_chars, GError **error) +{ + g_assert(new_chars > 0); + + if (!ctx->flags.allow_filename) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "If a buffer is selected by id, the <EB> " + "string argument must be empty"); + return FALSE; + } + + return TRUE; +} + static teco_state_t * -teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_edit_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - if (!allow_filename) { - if (str->len > 0) { - g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, - "If a buffer is selected by id, the <EB> " - "string argument must be empty"); - return NULL; - } - + if (!ctx->flags.allow_filename) { + /* process_cb() already throws error if str.len > 0 */ + g_assert(str.len == 0); return &teco_state_start; } - g_autofree gchar *filename = teco_file_expand_path(str->data); + g_autofree gchar *filename = teco_file_expand_path(str.data); if (teco_globber_is_pattern(filename)) { g_auto(teco_globber_t) globber; teco_globber_init(&globber, filename, G_FILE_TEST_IS_REGULAR); @@ -465,7 +590,7 @@ teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE /*$ EB edit * [n]EB[file]$ -- Open or edit file - * nEB$ + * [n]EB$ * * Opens or edits the file with name <file>. * If <file> is not in the buffer ring it is opened, @@ -518,45 +643,60 @@ teco_state_edit_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * ecetera. */ TECO_DEFINE_STATE_EXPECTGLOB(teco_state_edit_file, - .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial + .initial_cb = (teco_state_initial_cb_t)teco_state_edit_file_initial, + .expectstring.process_cb = teco_state_edit_file_process, + .expectstring.done_cb = teco_state_edit_file_done ); static teco_state_t * -teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_save_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; - g_autofree gchar *filename = teco_file_expand_path(str->data); - if (teco_qreg_current) { - if (!teco_qreg_current->vtable->save(teco_qreg_current, filename, error)) - return NULL; - } else { - if (!teco_buffer_save(teco_ring_current, *filename ? filename : NULL, error)) + if (!teco_expressions_eval(FALSE, error)) + return NULL; + + g_autofree gchar *filename = teco_file_expand_path(str.data); + + /* + * This is like implying teco_ring_get_id(teco_ring_current) + * but avoids the O(n) ring iterations. + */ + teco_buffer_t *buffer = teco_ring_current; + if (teco_expressions_args() > 0) { + teco_int_t id = teco_expressions_pop_num(0); + buffer = teco_ring_find(id); + if (!buffer) { + teco_error_invalidbuf_set(error, id); return NULL; + } + } else if (teco_qreg_current) { + return !teco_qreg_current->vtable->save(teco_qreg_current, filename, error) + ? NULL : &teco_state_start; } - return &teco_state_start; + return !teco_buffer_save(buffer, *filename ? filename : NULL, error) ? NULL : &teco_state_start; } /*$ EW write save - * EW$ -- Save current buffer or Q-Register - * EWfile$ + * EW$ -- Save buffer or Q-Register + * [n]EW[file]$ * - * Saves the current buffer to disk. + * Saves the chosen buffer with id <n> to disk + * By default, the current buffer is saved. * If the buffer was dirty, it will be clean afterwards. * If the string argument <file> is not empty, * the buffer is saved with the specified file name * and is renamed in the ring. * - * The EW command also works if the current document - * is a Q-Register, i.e. a Q-Register is edited. - * In this case, the string contents of the current - * Q-Register are saved to <file>. + * If the current document is a Q-Register and <n> is not given, + * the string contents of the current Q-Register are saved to <file> + * (cf. \fBE%\fIq\fR command).. * Q-Registers have no notion of associated file names, - * so <file> must be always specified. + * so <file> must be always specified in this case. * - * In interactive mode, EW is executed immediately and + * In interactive mode, \fBEW\fP is executed immediately and * may be rubbed out. * In order to support that, \*(ST creates so called * save point files. @@ -568,9 +708,9 @@ teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * Save point files are always created in the same directory * as the original file to ensure that no copying of the file * on disk is necessary but only a rename of the file. - * When rubbing out the EW command, \*(ST restores the latest + * When rubbing out the \fBEW\fP command, \*(ST restores the latest * save point file by moving (renaming) it back to its - * original path \(em also not requiring any on-disk copying. + * original path -- also not requiring any on-disk copying. * \*(ST is impossible to crash, but just in case it still * does it may leave behind these save point files which * must be manually deleted by the user. @@ -580,62 +720,119 @@ teco_state_save_file_done(teco_machine_main_t *ctx, const teco_string_t *str, GE * File names may also be tab-completed and string building * characters are enabled by default. */ -TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file); +TECO_DEFINE_STATE_EXPECTFILE(teco_state_save_file, + .expectstring.done_cb = teco_state_save_file_done +); + +static teco_state_t * +teco_state_read_file_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_start; + + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + + g_autofree gchar *filename = teco_file_expand_path(str.data); + /* FIXME: Add wrapper to interface.h? */ + if (!teco_view_load(teco_interface_current_view, filename, FALSE, error)) + return NULL; + + if (teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0) != pos) { + teco_ring_dirtify(); + + if (teco_current_doc_must_undo()) + undo__teco_interface_ssm(SCI_UNDO, 0, 0); + } + + return &teco_state_start; +} + +/*$ ER read + * ER<file>$ -- Read and insert file into current buffer + * + * Reads and inserts the given <file> into the current buffer or Q-Register at dot. + * Dot is left immediately after the given file. + */ +/* + * NOTE: Video TECO allows glob patterns as an argument. + */ +TECO_DEFINE_STATE_EXPECTFILE(teco_state_read_file, + .expectstring.done_cb = teco_state_read_file_done +); -/*$ EF close - * [bool]EF -- Remove buffer from ring +/*$ "EF" ":EF" close + * [n]EF -- Remove buffer from ring * -EF - * :EF + * [n]:EF * * Removes buffer from buffer ring, effectively * closing it. - * If the buffer is dirty (modified), EF will yield + * The optional argument <n> specifies the id of the buffer + * to close -- by default the current buffer will be closed. + * If the selected buffer is dirty (modified), \fBEF\fP will yield * an error. - * <bool> may be a specified to enforce closing dirty - * buffers. - * If it is a Failure condition boolean (negative), - * the buffer will be closed unconditionally. - * If <bool> is absent, the sign prefix (1 or -1) will - * be implied, so \(lq-EF\(rq will always close the buffer. + * If <n> is negative (success boolean), buffer <-n> will be closed + * even if it is dirty. + * \(lq-EF\(rq will force-close the current buffer. * - * When colon-modified, <bool> is ignored and \fBEF\fP - * will save the buffer before closing. + * When colon-modified, the selected buffer is saved before closing. * The file is always written, unlike \(lq:EX\(rq which * saves only dirty buffers. * This can fail of course, e.g. when called on the unnamed * buffer. * - * It is noteworthy that EF will be executed immediately in + * It is noteworthy that \fBEF\fP will be executed immediately in * interactive mode but can be rubbed out at a later time * to reopen the file. * Closed files are kept in memory until the command line * is terminated. + * + * Close and edit hooks are only executed when closing the current buffer. */ void teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error) { - if (teco_qreg_current) { + if (!teco_expressions_eval(FALSE, error)) + return; + + /* + * This is like implying teco_num_sign*teco_ring_get_id(teco_ring_current) + * but avoids the O(n) ring iterations. + */ + teco_buffer_t *buffer; + gboolean force; + if (teco_expressions_args() > 0) { + teco_int_t id = teco_expressions_pop_num(0); + buffer = teco_ring_find(ABS(id)); + if (!buffer) { + teco_error_invalidbuf_set(error, ABS(id)); + return; + } + force = id < 0; + } else if (teco_qreg_current) { + /* + * TODO: Should perhaps remove the register like FQq. + */ const teco_string_t *name = &teco_qreg_current->head.name; g_autofree gchar *name_printable = teco_string_echo(name->data, name->len); g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Q-Register \"%s\" currently edited", name_printable); return; + } else { + buffer = teco_ring_current; + force = teco_num_sign < 0; + teco_set_num_sign(1); } if (teco_machine_main_eval_colon(ctx) > 0) { - if (!teco_buffer_save(teco_ring_current, NULL, error)) - return; - } else { - teco_int_t v; - if (!teco_expressions_pop_num_calc(&v, teco_num_sign, error)) - return; - if (teco_is_failure(v) && teco_ring_current->dirty) { - g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Buffer \"%s\" is dirty", - teco_ring_current->filename ? : "(Unnamed)"); + if (!teco_buffer_save(buffer, NULL, error)) return; - } + } else if (!force && buffer->state > TECO_BUFFER_CLEAN) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Buffer \"%s\" is dirty", + buffer->filename ? : "(Unnamed)"); + return; } - teco_ring_close(error); + teco_ring_close(buffer, error); } @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,13 +25,29 @@ #include "parser.h" #include "list.h" +typedef enum { + /** buffer is freshly opened or saved */ + TECO_BUFFER_CLEAN = 0, + /** buffer modified, but a recovery file does not yet exist */ + TECO_BUFFER_DIRTY_NO_DUMP, + /** buffer modified, recovery file outdated */ + TECO_BUFFER_DIRTY_OUTDATED_DUMP, + /** buffer modified and recovery file is up to date */ + TECO_BUFFER_DIRTY_RECENT_DUMP +} teco_buffer_state_t; + typedef struct teco_buffer_t { teco_tailq_entry_t entry; teco_view_t *view; gchar *filename; - gboolean dirty; + + /** + * A teco_buffer_state_t. + * This is still a guint, so you can call teco_undo_guint(). + */ + guint state; } teco_buffer_t; /** @memberof teco_buffer_t */ @@ -67,9 +83,13 @@ teco_buffer_t *teco_ring_find_by_id(teco_int_t id); teco_int_t : teco_ring_find_by_id)(X)) void teco_ring_dirtify(void); -gboolean teco_ring_is_any_dirty(void); +guint teco_ring_get_first_dirty(void); gboolean teco_ring_save_all_dirty_buffers(GError **error); +extern guint teco_ring_recovery_interval; + +void teco_ring_dump_recovery(void); + gboolean teco_ring_edit_by_name(const gchar *filename, GError **error); gboolean teco_ring_edit_by_id(teco_int_t id, GError **error); @@ -86,7 +106,7 @@ teco_ring_undo_edit(void) teco_buffer_undo_edit(teco_ring_current); } -gboolean teco_ring_close(GError **error); +gboolean teco_ring_close(teco_buffer_t *buffer, GError **error); void teco_ring_undo_close(void); void teco_ring_set_scintilla_undo(gboolean state); @@ -97,8 +117,9 @@ void teco_ring_cleanup(void); * Command states */ -TECO_DECLARE_STATE(teco_state_edit_file); -TECO_DECLARE_STATE(teco_state_save_file); +extern teco_state_t teco_state_edit_file; +extern teco_state_t teco_state_save_file; +extern teco_state_t teco_state_read_file; void teco_state_ecommand_close(teco_machine_main_t *ctx, GError **error); diff --git a/src/sciteco.h b/src/sciteco.h index 4868303..16dba69 100644 --- a/src/sciteco.h +++ b/src/sciteco.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -25,10 +25,12 @@ #if TECO_INTEGER == 32 typedef gint32 teco_int_t; -#define TECO_INT_FORMAT G_GINT32_FORMAT +#define TECO_INT_MODIFIER G_GINT32_MODIFIER +#define TECO_INT_FORMAT G_GINT32_FORMAT #elif TECO_INTEGER == 64 typedef gint64 teco_int_t; -#define TECO_INT_FORMAT G_GINT64_FORMAT +#define TECO_INT_MODIFIER G_GINT64_MODIFIER +#define TECO_INT_FORMAT G_GINT64_FORMAT #else #error Invalid TECO integer storage size #endif @@ -85,6 +87,7 @@ teco_is_failure(teco_bool_t x) * This is not a bitfield, since it is set from SciTECO. */ enum { + TECO_ED_EXIT = (1 << 1), TECO_ED_DEFAULT_ANSI = (1 << 2), TECO_ED_AUTOCASEFOLD = (1 << 3), TECO_ED_AUTOEOL = (1 << 4), @@ -92,7 +95,9 @@ enum { TECO_ED_MOUSEKEY = (1 << 6), TECO_ED_SHELLEMU = (1 << 7), TECO_ED_OSC52 = (1 << 8), - TECO_ED_ICONS = (1 << 9) + TECO_ED_ICONS = (1 << 9), + TECO_ED_CLIP_PRIMARY = (1 << 10), + TECO_ED_MINIBUF_SSM = (1 << 11) }; /* in main.c */ @@ -112,6 +117,16 @@ extern volatile sig_atomic_t teco_interrupted; */ G_DEFINE_AUTOPTR_CLEANUP_FUNC(FILE, fclose); +/** + * A "safe" compile-time assertion, which also passes if the expression is not constant. + * + * Can be useful since different compilers have different ideas about what's a constant expression. + * In particular GCC does not treat `static const` objects as constant (in the way it qualifies + * for _Static_assert()), while constexpr is only available since C23. + */ +#define TECO_ASSERT_SAFE(EXPR) \ + G_STATIC_ASSERT(!__builtin_constant_p(EXPR) || (EXPR)) + /* * BEWARE DRAGONS! */ diff --git a/src/search.c b/src/search.c index 7fcf10e..856d079 100644 --- a/src/search.c +++ b/src/search.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -56,7 +56,7 @@ TECO_DEFINE_UNDO_SCALAR(teco_search_parameters_t); */ static teco_search_parameters_t teco_search_parameters; -/*$ ^X search-mode +/*$ "^X" "search mode" * mode^X -- Set or get search mode flag * -^X * ^X -> mode @@ -111,8 +111,7 @@ teco_state_search_initial(teco_machine_main_t *ctx, GError **error) return FALSE; if (teco_expressions_args()) { /* TODO: optional count argument? */ - if (!teco_expressions_pop_num_calc(&v1, 0, error)) - return FALSE; + v1 = teco_expressions_pop_num(0); if (v1 <= v2) { teco_search_parameters.count = 1; teco_search_parameters.from = teco_interface_glyphs2bytes(v1); @@ -538,7 +537,7 @@ teco_pattern2regexp(teco_string_t *pattern, teco_machine_qregspec_t *qreg_machin if (state == TECO_SEARCH_STATE_ALT) teco_string_append_c(&re, ')'); - g_assert(!teco_string_contains(&re, '\0')); + g_assert(!teco_string_contains(re, '\0')); return g_steal_pointer(&re.data) ? : g_strdup(""); } @@ -548,6 +547,16 @@ TECO_DEFINE_UNDO_OBJECT_OWN(ranges, teco_range_t *, g_free); #define teco_undo_ranges_own(VAR) \ (*teco_undo_object_ranges_push(&(VAR))) +/** + * Extract the ranges of the given GMatchInfo. + * + * @param match_info The result of g_regex_match(). + * @param offset The beginning of the match operation in bytes. + * Match results will be relative to this offset. + * @param count Where to store the number of ranges (subpatterns). + * @returns Ranges (subpatterns) in absolute byte positions. + * They \b must still be converted to glyph positions afterwards. + */ static teco_range_t * teco_get_ranges(const GMatchInfo *match_info, gsize offset, guint *count) { @@ -661,7 +670,7 @@ teco_do_search(GRegex *re, gsize from, gsize to, gint *count, GError **error) matched[i].ranges = NULL; } - for (int i = 0; i < matched_num; i++) + for (gint i = 0; i < matched_num; i++) g_free(matched[i].ranges); } @@ -671,14 +680,23 @@ teco_do_search(GRegex *re, gsize from, gsize to, gint *count, GError **error) teco_undo_guint(teco_ranges_count) = num_ranges; g_assert(teco_ranges_count > 0); - teco_interface_ssm(SCI_SETSEL, matched_ranges[0].from, matched_ranges[0].to); + teco_interface_ssm(SCI_SETSEL, teco_ranges[0].from, teco_ranges[0].to); + + /* + * teco_get_ranges() returned byte positions, + * while everything else expects glyph offsets. + */ + for (guint i = 0; i < teco_ranges_count; i++) { + teco_ranges[i].from = teco_interface_bytes2glyphs(teco_ranges[i].from); + teco_ranges[i].to = teco_interface_bytes2glyphs(teco_ranges[i].to); + } } return TRUE; } static gboolean -teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gsize new_chars, GError **error) +teco_state_search_process(teco_machine_main_t *ctx, teco_string_t str, gsize new_chars, GError **error) { /* FIXME: Should G_REGEX_OPTIMIZE be added under certain circumstances? */ GRegexCompileFlags flags = G_REGEX_MULTILINE | G_REGEX_DOTALL; @@ -723,10 +741,9 @@ teco_state_search_process(teco_machine_main_t *ctx, const teco_string_t *str, gs qreg_machine = teco_machine_qregspec_new(TECO_QREG_REQUIRED, ctx->qreg_table_locals, FALSE); g_autoptr(GRegex) re = NULL; - teco_string_t pattern = *str; g_autofree gchar *re_pattern; /* NOTE: teco_pattern2regexp() modifies str pointer */ - re_pattern = teco_pattern2regexp(&pattern, qreg_machine, + re_pattern = teco_pattern2regexp(&str, qreg_machine, ctx->expectstring.machine.codepage, FALSE, error); if (!re_pattern) return FALSE; @@ -811,7 +828,7 @@ failure: } static teco_state_t * -teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_search_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -819,14 +836,14 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro teco_qreg_t *search_reg = teco_qreg_table_find(&teco_qreg_table_globals, "_", 1); g_assert(search_reg != NULL); - if (str->len > 0) { + if (str.len > 0) { /* workaround: preserve selection (also on rubout) */ gint anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0); if (teco_current_doc_must_undo()) undo__teco_interface_ssm(SCI_SETANCHOR, anchor, 0); if (!search_reg->vtable->undo_set_string(search_reg, error) || - !search_reg->vtable->set_string(search_reg, str->data, str->len, + !search_reg->vtable->set_string(search_reg, str.data, str.len, teco_default_codepage(), error)) return NULL; @@ -835,7 +852,7 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro g_auto(teco_string_t) search_str = {NULL, 0}; if (!search_reg->vtable->get_string(search_reg, &search_str.data, &search_str.len, NULL, error) || - !teco_state_search_process(ctx, &search_str, search_str.len, error)) + !teco_state_search_process(ctx, search_str, search_str.len, error)) return NULL; } @@ -864,11 +881,10 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro TECO_DEFINE_STATE_EXPECTSTRING(NAME, \ .initial_cb = (teco_state_initial_cb_t)teco_state_search_initial, \ .expectstring.process_cb = teco_state_search_process, \ - .expectstring.done_cb = NAME##_done, \ ##__VA_ARGS__ \ ) -/*$ S search pattern compare +/*$ "S" ":S" "::S" search pattern compare * [n]S[pattern]$ -- Search for pattern * -S[pattern]$ * from,toS[pattern]$ @@ -938,7 +954,9 @@ teco_state_search_done(teco_machine_main_t *ctx, const teco_string_t *str, GErro * Changing the <pattern> results in the search being reperformed * from the beginning. */ -TECO_DEFINE_STATE_SEARCH(teco_state_search); +TECO_DEFINE_STATE_SEARCH(teco_state_search, + .expectstring.done_cb = teco_state_search_done +); static gboolean teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error) @@ -958,8 +976,7 @@ teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error) return FALSE; if (teco_expressions_args()) { /* TODO: optional count argument? */ - if (!teco_expressions_pop_num_calc(&v1, 0, error)) - return FALSE; + v1 = teco_expressions_pop_num(0); if (v1 <= v2) { teco_search_parameters.count = 1; teco_search_parameters.from_buffer = teco_ring_find(v1); @@ -998,17 +1015,20 @@ teco_state_search_all_initial(teco_machine_main_t *ctx, GError **error) } static teco_state_t * -teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_search_all_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { - if (ctx->flags.mode <= TECO_MODE_NORMAL && - (!teco_state_search_done(ctx, str, error) || - !teco_ed_hook(TECO_ED_HOOK_EDIT, error))) + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_start; + + const teco_buffer_t *curbuf = teco_ring_current; + if (!teco_state_search_done(ctx, str, error) || + (teco_ring_current != curbuf && !teco_ed_hook(TECO_ED_HOOK_EDIT, error))) return NULL; return &teco_state_start; } -/*$ N +/*$ "N" ":N" "search all" * [n]N[pattern]$ -- Search over buffer-boundaries * -N[pattern]$ * from,toN[pattern]$ @@ -1054,11 +1074,12 @@ teco_state_search_all_done(teco_machine_main_t *ctx, const teco_string_t *str, G * This is probably not very useful in practice, so it's not documented. */ TECO_DEFINE_STATE_SEARCH(teco_state_search_all, - .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial + .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial, + .expectstring.done_cb = teco_state_search_all_done ); static teco_state_t * -teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_search_kill_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -1080,13 +1101,15 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, if (teco_search_parameters.dot < dot) { /* kill forwards */ sptr_t anchor = teco_interface_ssm(SCI_GETANCHOR, 0, 0); - gsize len = anchor - teco_search_parameters.dot; + teco_int_t len_glyphs = teco_interface_bytes2glyphs(anchor) - + teco_interface_bytes2glyphs(teco_search_parameters.dot); if (teco_current_doc_must_undo()) undo__teco_interface_ssm(SCI_GOTOPOS, dot, 0); teco_interface_ssm(SCI_GOTOPOS, anchor, 0); - teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot, len); + teco_interface_ssm(SCI_DELETERANGE, teco_search_parameters.dot, + anchor - teco_search_parameters.dot); /* NOTE: An undo action is not always created. */ if (teco_current_doc_must_undo() && @@ -1095,8 +1118,8 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, /* fix up ranges (^Y) */ for (guint i = 0; i < teco_ranges_count; i++) { - teco_ranges[i].from -= len; - teco_ranges[i].to -= len; + teco_ranges[i].from -= len_glyphs; + teco_ranges[i].to -= len_glyphs; } } else { /* kill backwards */ @@ -1113,7 +1136,7 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, return &teco_state_start; } -/*$ FK +/*$ "FK" ":FK" * FK[pattern]$ -- Delete up to occurrence of pattern * [n]FK[pattern]$ * -FK[pattern]$ @@ -1135,10 +1158,12 @@ teco_state_search_kill_done(teco_machine_main_t *ctx, const teco_string_t *str, /* * ::FK is possible but doesn't make much sense, so it's undocumented. */ -TECO_DEFINE_STATE_SEARCH(teco_state_search_kill); +TECO_DEFINE_STATE_SEARCH(teco_state_search_kill, + .expectstring.done_cb = teco_state_search_kill_done +); static teco_state_t * -teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_search_delete_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -1164,7 +1189,7 @@ teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str return &teco_state_start; } -/*$ FD +/*$ "FD" ":FD" "::FD" * [n]FD[pattern]$ -- Delete occurrence of pattern * -FD[pattern]$ * from,toFD[pattern]$ @@ -1178,37 +1203,50 @@ teco_state_search_delete_done(teco_machine_main_t *ctx, const teco_string_t *str * Searches for <pattern> just like the regular search command * (\fBS\fP) but when found deletes the entire occurrence. */ -TECO_DEFINE_STATE_SEARCH(teco_state_search_delete); +TECO_DEFINE_STATE_SEARCH(teco_state_search_delete, + .expectstring.done_cb = teco_state_search_delete_done +); static gboolean teco_state_replace_insert_initial(teco_machine_main_t *ctx, GError **error) { - if (ctx->flags.mode == TECO_MODE_NORMAL) - teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine, - teco_interface_get_codepage()); + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + + /* + * Overwrites teco_ranges set by the preceding search. + * FIXME: Wastes undo tokens in teco_do_search(). + * Perhaps make this configurable in the state. + */ + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(pos); + teco_undo_guint(teco_ranges_count) = 1; + + /* + * Current document's encoding determines the behaviour of + * string building constructs. + */ + teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine, + teco_interface_get_codepage()); return TRUE; } -/* - * FIXME: Could be static - */ -TECO_DEFINE_STATE_INSERT(teco_state_replace_insert, +static TECO_DEFINE_STATE_INSERT(teco_state_replace_insert, .initial_cb = (teco_state_initial_cb_t)teco_state_replace_insert_initial ); static teco_state_t * -teco_state_replace_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_replace_ignore_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { return &teco_state_start; } -/* - * FIXME: Could be static - */ -TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore); +static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_ignore, + .expectstring.done_cb = teco_state_replace_ignore_done +); static teco_state_t * -teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_replace_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_replace_ignore; @@ -1225,7 +1263,7 @@ teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr : &teco_state_replace_ignore; } -/*$ FS +/*$ "FS" ":FS" "::FS" * [n]FS[pattern]$[string]$ -- Search and replace * -FS[pattern]$[string]$ * from,toFS[pattern]$[string]$ @@ -1248,16 +1286,12 @@ teco_state_replace_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr * immediately and interactively. */ TECO_DEFINE_STATE_SEARCH(teco_state_replace, - .expectstring.last = FALSE + .expectstring.last = FALSE, + .expectstring.done_cb = teco_state_replace_done ); -/* - * FIXME: TECO_DEFINE_STATE_INSERT() already defines a done_cb(), - * so we had to name this differently. - * Perhaps it simply shouldn't define it. - */ static teco_state_t * -teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_replace_default_insert_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -1265,55 +1299,53 @@ teco_state_replace_default_insert_done_overwrite(teco_machine_main_t *ctx, const teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1); g_assert(replace_reg != NULL); - if (str->len > 0) { + if (str.len > 0) { if (!replace_reg->vtable->undo_set_string(replace_reg, error) || - !replace_reg->vtable->set_string(replace_reg, str->data, str->len, + !replace_reg->vtable->set_string(replace_reg, str.data, str.len, teco_default_codepage(), error)) return NULL; } else { g_auto(teco_string_t) replace_str = {NULL, 0}; if (!replace_reg->vtable->get_string(replace_reg, &replace_str.data, &replace_str.len, NULL, error) || - (replace_str.len > 0 && !teco_state_insert_process(ctx, &replace_str, replace_str.len, error))) + (replace_str.len > 0 && !teco_state_insert_process(ctx, replace_str, replace_str.len, error))) return NULL; } + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos); return &teco_state_start; } -/* - * FIXME: Could be static - */ -TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert, - .initial_cb = NULL, - .expectstring.done_cb = teco_state_replace_default_insert_done_overwrite +static TECO_DEFINE_STATE_INSERT(teco_state_replace_default_insert, + .initial_cb = (teco_state_initial_cb_t)teco_state_replace_insert_initial, + .expectstring.done_cb = teco_state_replace_default_insert_done ); static teco_state_t * -teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_replace_default_ignore_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL || - !str->len) + !str.len) return &teco_state_start; teco_qreg_t *replace_reg = teco_qreg_table_find(&teco_qreg_table_globals, "-", 1); g_assert(replace_reg != NULL); if (!replace_reg->vtable->undo_set_string(replace_reg, error) || - !replace_reg->vtable->set_string(replace_reg, str->data, str->len, + !replace_reg->vtable->set_string(replace_reg, str.data, str.len, teco_default_codepage(), error)) return NULL; return &teco_state_start; } -/* - * FIXME: Could be static - */ -TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore); +static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_replace_default_ignore, + .expectstring.done_cb = teco_state_replace_default_ignore_done +); static teco_state_t * -teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_replace_default_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_replace_default_ignore; @@ -1330,7 +1362,7 @@ teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *s : &teco_state_replace_default_ignore; } -/*$ FR +/*$ "FR" ":FR" "::FR" search-replace * [n]FR[pattern]$[string]$ -- Search and replace with default * -FR[pattern]$[string]$ * from,toFR[pattern]$[string]$ @@ -1352,5 +1384,51 @@ teco_state_replace_default_done(teco_machine_main_t *ctx, const teco_string_t *s * register is implied instead. */ TECO_DEFINE_STATE_SEARCH(teco_state_replace_default, - .expectstring.last = FALSE + .expectstring.last = FALSE, + .expectstring.done_cb = teco_state_replace_default_done +); + +static teco_state_t * +teco_state_replace_default_all_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return &teco_state_replace_default_ignore; + + const teco_buffer_t *curbuf = teco_ring_current; + teco_state_t *state = teco_state_replace_default_done(ctx, str, error); + if (!state || (curbuf != teco_ring_current && !teco_ed_hook(TECO_ED_HOOK_EDIT, error))) + return NULL; + + return state; +} + +/*$ "FN" ":FN" "::FN" "search-replace all" + * [n]FN[pattern]$[string]$ -- Search and replace with default over buffer-boundaries + * -FN[pattern]$[string]$ + * from,toFN[pattern]$[string]$ + * [n]:FN[pattern]$[string]$ -> Success|Failure + * -:FN[pattern]$[string]$ -> Success|Failure + * from,to:FN[pattern]$[string]$ -> Success|Failure + * [n]::FN[pattern]$[string]$ -> Success|Failure + * -::FN[pattern]$[string]$ -> Success|Failure + * from,to::FN[pattern]$[string]$ -> Success|Failure + * + * The \fBFN\fP command is similar to the \fBFR\fP command + * but will continue to search for occurrences of <pattern> when the + * end or beginning of the current buffer is reached. + * It searches for <pattern> just like the search over buffer-boundaries + * command (\fBN\fP) and replaces the occurrence with <string> + * similar to what \fBFR\fP does. + * If <string> is empty the string in the global replacement + * register is implied instead. + * + * \fBFN\fP also differs from \fBFR\fP in the interpretation of two arguments. + * Using two arguments the search will be bounded between the + * buffer with number <from>, up to the buffer with number + * <to>. + */ +TECO_DEFINE_STATE_SEARCH(teco_state_replace_default_all, + .initial_cb = (teco_state_initial_cb_t)teco_state_search_all_initial, + .expectstring.last = FALSE, + .expectstring.done_cb = teco_state_replace_default_all_done ); diff --git a/src/search.h b/src/search.h index 621fdd1..9bd62f7 100644 --- a/src/search.h +++ b/src/search.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -22,9 +22,10 @@ void teco_state_control_search_mode(teco_machine_main_t *ctx, GError **error); -TECO_DECLARE_STATE(teco_state_search); -TECO_DECLARE_STATE(teco_state_search_all); -TECO_DECLARE_STATE(teco_state_search_kill); -TECO_DECLARE_STATE(teco_state_search_delete); -TECO_DECLARE_STATE(teco_state_replace); -TECO_DECLARE_STATE(teco_state_replace_default); +extern teco_state_t teco_state_search; +extern teco_state_t teco_state_search_all; +extern teco_state_t teco_state_search_kill; +extern teco_state_t teco_state_search_delete; +extern teco_state_t teco_state_replace; +extern teco_state_t teco_state_replace_default; +extern teco_state_t teco_state_replace_default_all; diff --git a/src/spawn.c b/src/spawn.c index d51dbb1..61718fd 100644 --- a/src/spawn.c +++ b/src/spawn.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -129,7 +129,7 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error) teco_string_t comspec; if (!reg->vtable->get_string(reg, &comspec.data, &comspec.len, NULL, error)) return NULL; - if (teco_string_contains(&comspec, '\0')) { + if (teco_string_contains(comspec, '\0')) { teco_string_clear(&comspec); teco_error_qregcontainsnull_set(error, "$COMSPEC", 8, FALSE); return NULL; @@ -150,7 +150,7 @@ teco_parse_shell_command_line(const gchar *cmdline, GError **error) teco_string_t shell; if (!reg->vtable->get_string(reg, &shell.data, &shell.len, NULL, error)) return NULL; - if (teco_string_contains(&shell, '\0')) { + if (teco_string_contains(shell, '\0')) { teco_string_clear(&shell); teco_error_qregcontainsnull_set(error, "$SHELL", 6, FALSE); return NULL; @@ -204,7 +204,7 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error) teco_int_t line; teco_spawn_ctx.from = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); - if (!teco_expressions_pop_num_calc(&line, 0, error)) + if (!teco_expressions_pop_num_calc(&line, teco_num_sign, error)) return FALSE; line += teco_interface_ssm(SCI_LINEFROMPOSITION, teco_spawn_ctx.from, 0); teco_spawn_ctx.to = teco_interface_ssm(SCI_POSITIONFROMLINE, line, 0); @@ -219,18 +219,13 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error) break; } - default: { + default: /* pipe and replace character range */ - teco_int_t from, to; - if (!teco_expressions_pop_num_calc(&to, 0, error) || - !teco_expressions_pop_num_calc(&from, 0, error)) - return FALSE; - teco_spawn_ctx.from = teco_interface_glyphs2bytes(from); - teco_spawn_ctx.to = teco_interface_glyphs2bytes(to); + teco_spawn_ctx.to = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); + teco_spawn_ctx.from = teco_interface_glyphs2bytes(teco_expressions_pop_num(0)); rc = teco_bool(teco_spawn_ctx.from <= teco_spawn_ctx.to && teco_spawn_ctx.from >= 0 && teco_spawn_ctx.to >= 0); } - } if (teco_is_failure(rc)) { if (!teco_machine_main_eval_colon(ctx)) { @@ -247,7 +242,7 @@ teco_state_execute_initial(teco_machine_main_t *ctx, GError **error) } static teco_state_t * -teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_execute_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { /* * NOTE: With G_SPAWN_LEAVE_DESCRIPTORS_OPEN and without G_SPAWN_SEARCH_PATH_FROM_ENVP, @@ -290,13 +285,13 @@ teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr } #endif - if (!str->len || teco_string_contains(str, '\0')) { + if (!str.len || teco_string_contains(str, '\0')) { g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Command line must not be empty or contain null-bytes"); goto gerror; } - argv = teco_parse_shell_command_line(str->data, error); + argv = teco_parse_shell_command_line(str.data, error); if (!argv) goto gerror; @@ -420,8 +415,9 @@ teco_state_execute_done(teco_machine_main_t *ctx, const teco_string_t *str, GErr teco_interface_ssm(SCI_DELETERANGE, teco_spawn_ctx.from, teco_spawn_ctx.to - teco_spawn_ctx.from); - teco_undo_gsize(teco_ranges[0].from) = teco_spawn_ctx.from; - teco_undo_gsize(teco_ranges[0].to) = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + sptr_t pos = teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0); + teco_undo_int(teco_ranges[0].from) = teco_interface_bytes2glyphs(teco_spawn_ctx.from); + teco_undo_int(teco_ranges[0].to) = teco_interface_bytes2glyphs(pos); teco_undo_guint(teco_ranges_count) = 1; } teco_interface_ssm(SCI_ENDUNDOACTION, 0, 0); @@ -490,7 +486,7 @@ cleanup: /* in cmdline.c */ gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -/*$ EC pipe filter +/*$ "EC" :EC" pipe filter * ECcommand$ -- Execute operating system command and filter buffer contents * linesECcommand$ * -ECcommand$ @@ -609,7 +605,8 @@ gboolean teco_state_execute_process_edit_cmd(teco_machine_main_t *ctx, teco_mach */ TECO_DEFINE_STATE_EXPECTSTRING(teco_state_execute, .initial_cb = (teco_state_initial_cb_t)teco_state_execute_initial, - .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_execute_process_edit_cmd + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_execute_process_edit_cmd, + .expectstring.done_cb = teco_state_execute_done ); static teco_state_t * @@ -623,7 +620,7 @@ teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, return &teco_state_execute; } -/*$ EG EGq +/*$ "EG" "EGq" ":EGq" * EGq command$ -- Set Q-Register to output of operating system command * linesEGq command$ * -EGq command$ @@ -651,7 +648,8 @@ teco_state_egcommand_got_register(teco_machine_main_t *ctx, teco_qreg_t *qreg, * The register <q> is defined if it does not already exist. */ TECO_DEFINE_STATE_EXPECTQREG(teco_state_egcommand, - .expectqreg.type = TECO_QREG_OPTIONAL_INIT + .expectqreg.type = TECO_QREG_OPTIONAL_INIT, + .expectqreg.got_register_cb = teco_state_egcommand_got_register ); /* diff --git a/src/spawn.h b/src/spawn.h index ef210e9..09764bd 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,5 +18,5 @@ #include "parser.h" -TECO_DECLARE_STATE(teco_state_execute); -TECO_DECLARE_STATE(teco_state_egcommand); +extern teco_state_t teco_state_execute; +extern teco_state_t teco_state_egcommand; diff --git a/src/stdio-commands.c b/src/stdio-commands.c new file mode 100644 index 0000000..abb6566 --- /dev/null +++ b/src/stdio-commands.c @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2012-2026 Robin Haberkorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <glib.h> + +#include "sciteco.h" +#include "parser.h" +#include "error.h" +#include "undo.h" +#include "expressions.h" +#include "interface.h" +#include "cmdline.h" +#include "core-commands.h" +#include "stdio-commands.h" + +/** + * Check whether we are executing directly from the end of the command line. + * This works \b only when invoked from the initial_cb. + */ +static inline gboolean +teco_cmdline_is_executing(teco_machine_main_t *ctx) +{ + return ctx == &teco_cmdline.machine && + ctx->macro_pc == teco_cmdline_ssm(SCI_GETCURRENTPOS, 0, 0); +} + +static gboolean is_executing = FALSE; + +/** + * Print number from stack in the given radix. + * + * It must be popped manually, so we can call it multiple times + * on the same number. + */ +static gboolean +teco_print(teco_machine_main_t *ctx, guint radix, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return FALSE; + if (!teco_expressions_args()) { + teco_error_argexpected_set(error, "="); + return FALSE; + } + + /* + * teco_expressions_format() cannot easily be used + * to format __unsigned__ integers. + */ + const gchar *fmt = "%" TECO_INT_MODIFIER "d"; + switch (radix) { + case 8: fmt = "%" TECO_INT_MODIFIER "o"; break; + case 16: fmt = "%" TECO_INT_MODIFIER "X"; break; + } + gchar buf[32]; + gint len = g_snprintf(buf, sizeof(buf), fmt, teco_expressions_peek_num(0)); + g_assert(len > 0); + if (!teco_machine_main_eval_colon(ctx)) + buf[len++] = '\n'; + + teco_interface_msg_literal(TECO_MSG_USER, buf, len); + return TRUE; +} + +/*$ "=" "==" "===" ":=" ":==" ":===" "print number" + * <n>= -- Print integer as message + * <n>== + * <n>=== + * <n>:= + * <n>:== + * <n>:=== + * + * Shows integer <n> as a message in the message line and/or + * on the console. + * One \(lq=\(rq formats the integer as a signed decimal number, + * \(lq==\(rq formats as an unsigned octal number and + * \(lq===\(rq as an unsigned hexadecimal number. + * It is logged with the user-message severity. + * The command fails if <n> is not given. + * + * A noteworthy quirk is that \(lq==\(rq and \(lq===\(rq + * will print 2 or 3 numbers in succession when executed + * from interactive mode at the end of the command line + * in order to guarantee immediate feedback. + * + * If you want to print multiple values from the stack, + * you have to put the \(lq=\(rq into a pass-through loop + * or separate the commands with + * whitespace (e.g. \(lq^Y= =\(rq). + * + * If colon-modified the number is printed without a trailing + * linefeed. + */ +/* + * In order to imitate TECO-11 closely, we apply the lookahead + * strategy -- `=` and `==` are not executed immediately but only + * when a non-`=` character is parsed (cf. `$$` and `^C^C`). + * However, this would be very annoying during interactive + * execution, therefore we still print the number immediately + * and perhaps multiple times: + * Typing `===` prints the number first in decimal, + * then octal and finally in hexadecimal. + * This won't happen e.g. in a loop that is closed on the command-line. + */ +static teco_state_t teco_state_print_octal; + +static gboolean +teco_state_print_decimal_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + is_executing = teco_cmdline_is_executing(ctx); + if (G_LIKELY(!is_executing)) + return TRUE; + /* + * Interactive invocation: + * don't yet pop number as we may have to print it repeatedly + */ + return teco_print(ctx, 10, error); +} + +static teco_state_t * +teco_state_print_decimal_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +{ + if (chr == '=') + return &teco_state_print_octal; + + if (ctx->flags.mode == TECO_MODE_NORMAL) { + if (G_LIKELY(!is_executing) && !teco_print(ctx, 10, error)) + return NULL; + teco_expressions_pop_num(0); + } + return teco_state_start_input(ctx, chr, error); +} + +/* + * Due to the deferred nature of `=`, + * it is valid to end in this state as well. + */ +static gboolean +teco_state_print_decimal_end_of_macro(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + if (G_UNLIKELY(is_executing)) + return TRUE; + if (!teco_print(ctx, 10, error)) + return FALSE; + teco_expressions_pop_num(0); + return TRUE; +} + +TECO_DEFINE_STATE_START(teco_state_print_decimal, + .initial_cb = (teco_state_initial_cb_t)teco_state_print_decimal_initial, + .input_cb = (teco_state_input_cb_t)teco_state_print_decimal_input, + .end_of_macro_cb = (teco_state_end_of_macro_cb_t) + teco_state_print_decimal_end_of_macro +); + +static gboolean +teco_state_print_octal_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + is_executing = teco_cmdline_is_executing(ctx); + if (G_LIKELY(!is_executing)) + return TRUE; + /* + * Interactive invocation: + * don't yet pop number as we may have to print it repeatedly + */ + return teco_print(ctx, 8, error); +} + +static teco_state_t * +teco_state_print_octal_input(teco_machine_main_t *ctx, gunichar chr, GError **error) +{ + if (chr == '=') { + if (ctx->flags.mode == TECO_MODE_NORMAL) { + if (!teco_print(ctx, 16, error)) + return NULL; + teco_expressions_pop_num(0); + } + return &teco_state_start; + } + + if (ctx->flags.mode == TECO_MODE_NORMAL) { + if (G_LIKELY(!is_executing) && !teco_print(ctx, 8, error)) + return NULL; + teco_expressions_pop_num(0); + } + return teco_state_start_input(ctx, chr, error); +} + +/* + * Due to the deferred nature of `==`, + * it is valid to end in this state as well. + */ +static gboolean +teco_state_print_octal_end_of_macro(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + if (G_UNLIKELY(is_executing)) + return TRUE; + if (!teco_print(ctx, 8, error)) + return FALSE; + teco_expressions_pop_num(0); + return TRUE; +} + +static TECO_DEFINE_STATE_START(teco_state_print_octal, + .initial_cb = (teco_state_initial_cb_t)teco_state_print_octal_initial, + .input_cb = (teco_state_input_cb_t)teco_state_print_octal_input, + .end_of_macro_cb = (teco_state_end_of_macro_cb_t) + teco_state_print_octal_end_of_macro +); + +static gboolean +teco_state_print_string_initial(teco_machine_main_t *ctx, GError **error) +{ + /* + * ^A differs from all other string-taking commands in having + * a default ^A escape char. + */ + if (ctx->parent.must_undo) + teco_undo_gunichar(ctx->expectstring.machine.escape_char); + ctx->expectstring.machine.escape_char = TECO_CTL_KEY('A'); + + if (ctx->flags.mode > TECO_MODE_NORMAL) + return TRUE; + + teco_machine_stringbuilding_set_codepage(&ctx->expectstring.machine, + teco_machine_main_eval_colon(ctx) + ? SC_CHARSET_ANSI : teco_default_codepage()); + return TRUE; +} + +static teco_state_t * +teco_state_print_string_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) +{ + teco_interface_msg_literal(TECO_MSG_USER, str.data, str.len); + return &teco_state_start; +} + +/*$ "^A" ":^A" print "print string" + * ^A<string>^A -- Print string as message + * @^A/string/ + * :^A<string>^A + * + * Print <string> as a message, i.e. in the message line + * in interactive mode and if possible on the terminal (stdout) as well. + * + * \fB^A\fP differs from all other commands in the way <string> + * is terminated. + * It is terminated by ^A (CTRL+A, ASCII 1) by default. + * While the initial \fB^A\fP can be written with upcarets, + * the terminating ^A must always be ASCII 1. + * You can however overwrite the <string> terminator as usual + * by \fB@\fP-modifying the command. + * + * String-building characters are enabled for this command. + * \fB^A\fP outputs strings in the default codepage, + * but when colon modified raw ANSI encoding is enforced. + */ +/* + * NOTE: Codepage is among other things important for + * ^EUq, ^E<...> and case folding. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_print_string, + .initial_cb = (teco_state_initial_cb_t)teco_state_print_string_initial, + .expectstring.done_cb = teco_state_print_string_done +); + +/*$ T type typeout + * [lines]T -- Type out buffer contents as messages + * -T + * from,toT + * + * Type out the next or previous number of <lines> from the buffer + * as a message, i.e. in the message line in interactive mode + * and if possible on the terminal (stdout) as well. + * If <lines> is omitted, the sign prefix is implied. + * If two arguments are specified, the characters beginning + * at position <from> up to the character at position <to> + * are copied. + * + * The semantics of the arguments is analogous to the \fBK\fP + * command's arguments. + */ +void +teco_state_start_typeout(teco_machine_main_t *ctx, GError **error) +{ + gsize from, len; + + if (!teco_get_range_args("T", &from, &len, error)) + return; + + /* + * NOTE: This may remove the buffer gap since we need a consecutive + * piece of memory to log as a single message. + * FIXME: In batch mode even this could theoretically be avoided. + * Need to add a function like teco_interface_is_batch(). + * Still, this is probably more efficient than using a temporary + * allocation with SCI_GETTEXTRANGEFULL. + */ + const gchar *str = (const gchar *)teco_interface_ssm(SCI_GETRANGEPOINTER, from, len); + teco_interface_msg_literal(TECO_MSG_USER, str, len); +} + +/*$ "^T" ":^T" "typeout glyph" "get char" + * <c1,c2,...>^T -- Type out the numeric arguments as a message or get character from user + * <c1,c2,...>:^T + * ^T -> codepoint + * :^T -> byte + * + * Types out characters for all the values + * on the argument stack (interpreted as codepoints) as messages, + * i.e. in the message line in interactive mode + * and if possible on the terminal (stdout) as well. + * It does so in the order of the arguments, i.e. + * <c1> is inserted before <c2>, ecetera. + * By default the codepoints are expected to be in the default + * codepage, but you can force raw ANSI encoding (for arbitrary + * bytes) by colon-modifying the command. + * + * When called without any argument, \fB^T\fP reads a key from the + * user (or from stdin) and returns the corresponding codepoint. + * If the default encoding is UTF-8, this will not work + * for function keys. + * If the default encoding is raw ANSI or if the command is + * colon-modified, \fB^T\fP returns raw bytes. + * When run in batch mode, this will return whatever byte is + * delivered by the attached terminal. + * In case stdin is closed, -1 is returned. + * In interactive mode, pressing CTRL+D or CTRL+C will also + * return -1. + */ +void +teco_state_control_typeout(teco_machine_main_t *ctx, GError **error) +{ + if (!teco_expressions_eval(FALSE, error)) + return; + + gboolean utf8 = !teco_machine_main_eval_colon(ctx) && + teco_default_codepage() == SC_CP_UTF8; + + guint args = teco_expressions_args(); + if (!args) { + teco_expressions_push(teco_interface_getch(utf8)); + return; + } + + if (!utf8) { + /* assume raw ANSI byte output */ + g_autofree gchar *buf = g_malloc(args); + gchar *p = buf+args; + + for (gint i = 0; i < args; i++) { + teco_int_t chr = teco_expressions_pop_num(0); + if (chr < 0 || chr > 0xFF) { + teco_error_codepoint_set(error, "^T"); + return; + } + *--p = chr; + } + + teco_interface_msg_literal(TECO_MSG_USER, p, args); + return; + } + + /* 4 bytes should be enough for UTF-8, but we better follow the documentation */ + g_autofree gchar *buf = g_malloc(args*6); + gchar *p = buf; + + for (gint i = args; i > 0; i--) { + teco_int_t chr = teco_expressions_peek_num(i-1); + if (chr < 0 || !g_unichar_validate(chr)) { + teco_error_codepoint_set(error, "^T"); + return; + } + p += g_unichar_to_utf8(chr, p); + } + /* we pop only now since we had to peek in reverse order */ + for (gint i = 0; i < args; i++) + teco_expressions_pop_num(0); + + teco_interface_msg_literal(TECO_MSG_USER, buf, p-buf); +} diff --git a/src/stdio-commands.h b/src/stdio-commands.h new file mode 100644 index 0000000..cf04b34 --- /dev/null +++ b/src/stdio-commands.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012-2026 Robin Haberkorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +#pragma once + +#include <glib.h> + +#include "parser.h" + +void teco_state_start_typeout(teco_machine_main_t *ctx, GError **error); +void teco_state_control_typeout(teco_machine_main_t *ctx, GError **error); + +/* + * Command states + */ +extern teco_state_t teco_state_print_decimal; +extern teco_state_t teco_state_print_string; diff --git a/src/string-utils.c b/src/string-utils.c index 10e34a8..e9dd148 100644 --- a/src/string-utils.c +++ b/src/string-utils.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -98,12 +98,12 @@ teco_string_get_coord(const gchar *str, gsize off, guint *pos, guint *line, guin * @memberof teco_string_t */ gsize -teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len) +teco_string_diff(teco_string_t a, const gchar *b, gsize b_len) { gsize len = 0; - while (len < a->len && len < b_len && - a->data[len] == b[len]) + while (len < a.len && len < b_len && + a.data[len] == b[len]) len++; return len; @@ -124,12 +124,12 @@ teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len) * @memberof teco_string_t */ gsize -teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len) +teco_string_casediff(teco_string_t a, const gchar *b, gsize b_len) { gsize len = 0; - while (len < a->len && len < b_len) { - gunichar a_chr = g_utf8_get_char(a->data+len); + while (len < a.len && len < b_len) { + gunichar a_chr = g_utf8_get_char(a.data+len); gunichar b_chr = g_utf8_get_char(b+len); if (g_unichar_tolower(a_chr) != g_unichar_tolower(b_chr)) break; @@ -141,36 +141,36 @@ teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len) /** @memberof teco_string_t */ gint -teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len) +teco_string_cmp(teco_string_t a, const gchar *b, gsize b_len) { - for (guint i = 0; i < a->len; i++) { + for (guint i = 0; i < a.len; i++) { if (i == b_len) /* b is a prefix of a */ return 1; - gint ret = (gint)a->data[i] - (gint)b[i]; + gint ret = (gint)a.data[i] - (gint)b[i]; if (ret != 0) /* a and b have a common prefix of length i */ return ret; } - return a->len == b_len ? 0 : -1; + return a.len == b_len ? 0 : -1; } /** @memberof teco_string_t */ gint -teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len) +teco_string_casecmp(teco_string_t a, const gchar *b, gsize b_len) { - for (guint i = 0; i < a->len; i++) { + for (guint i = 0; i < a.len; i++) { if (i == b_len) /* b is a prefix of a */ return 1; - gint ret = (gint)g_ascii_tolower(a->data[i]) - (gint)g_ascii_tolower(b[i]); + gint ret = (gint)g_ascii_tolower(a.data[i]) - (gint)g_ascii_tolower(b[i]); if (ret != 0) /* a and b have a common prefix of length i */ return ret; } - return a->len == b_len ? 0 : -1; + return a.len == b_len ? 0 : -1; } /** @@ -184,22 +184,20 @@ teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len) * @memberof teco_string_t */ const gchar * -teco_string_last_occurrence(const teco_string_t *str, const gchar *chars) +teco_string_last_occurrence(teco_string_t str, const gchar *chars) { - teco_string_t ret = *str; - - if (!ret.len) + if (!str.len) return NULL; do { - gint i = teco_string_rindex(&ret, *chars); + gint i = teco_string_rindex(str, *chars); if (i >= 0) { - ret.data += i+1; - ret.len -= i+1; + str.data += i+1; + str.len -= i+1; } } while (*chars++); - return ret.data; + return str.data; } TECO_DEFINE_UNDO_CALL(teco_string_truncate, teco_string_t *, gsize); diff --git a/src/string-utils.h b/src/string-utils.h index 2491d07..a1eda4e 100644 --- a/src/string-utils.h +++ b/src/string-utils.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -63,6 +63,9 @@ teco_strv_remove(gchar **strv, guint i) * to functions expecting traditional null-terminated C strings if you can * guarantee that it contains no null-character other than the trailing one. * + * Since string objects are just two words, they can and should be passed by + * value if the callee doesn't have to modify it. + * * @warning For consistency with C idioms the underlying character type is * `char`, which might be signed! * Accessing individual characters may yield signed integers and that sign @@ -102,7 +105,7 @@ teco_string_init(teco_string_t *target, const gchar *str, gsize len) static inline void teco_string_init_chunk(teco_string_t *target, const gchar *str, gssize len, GStringChunk *chunk) { - target->data = g_string_chunk_insert_len(chunk, str, len); + target->data = g_string_chunk_insert_len(chunk, str ? : "", len); target->len = len; } @@ -164,19 +167,19 @@ gchar *teco_string_echo(const gchar *str, gsize len); void teco_string_get_coord(const gchar *str, gsize off, guint *pos, guint *line, guint *column); -typedef gsize (*teco_string_diff_t)(const teco_string_t *a, const gchar *b, gsize b_len); -gsize teco_string_diff(const teco_string_t *a, const gchar *b, gsize b_len); -gsize teco_string_casediff(const teco_string_t *a, const gchar *b, gsize b_len); +typedef gsize (*teco_string_diff_t)(teco_string_t a, const gchar *b, gsize b_len); +gsize teco_string_diff(teco_string_t a, const gchar *b, gsize b_len); +gsize teco_string_casediff(teco_string_t a, const gchar *b, gsize b_len); -typedef gint (*teco_string_cmp_t)(const teco_string_t *a, const gchar *b, gsize b_len); -gint teco_string_cmp(const teco_string_t *a, const gchar *b, gsize b_len); -gint teco_string_casecmp(const teco_string_t *a, const gchar *b, gsize b_len); +typedef gint (*teco_string_cmp_t)(teco_string_t a, const gchar *b, gsize b_len); +gint teco_string_cmp(teco_string_t a, const gchar *b, gsize b_len); +gint teco_string_casecmp(teco_string_t a, const gchar *b, gsize b_len); /** @memberof teco_string_t */ static inline gboolean -teco_string_contains(const teco_string_t *str, gchar chr) +teco_string_contains(teco_string_t str, gchar chr) { - return str->data && memchr(str->data, chr, str->len); + return str.data && memchr(str.data, chr, str.len); } /** @@ -188,26 +191,26 @@ teco_string_contains(const teco_string_t *str, gchar chr) * @memberof teco_string_t */ static inline gint -teco_string_rindex(const teco_string_t *str, gchar chr) +teco_string_rindex(teco_string_t str, gchar chr) { gint i; - for (i = str->len-1; i >= 0 && str->data[i] != chr; i--); + for (i = str.len-1; i >= 0 && str.data[i] != chr; i--); return i; } -const gchar *teco_string_last_occurrence(const teco_string_t *str, const gchar *chars); +const gchar *teco_string_last_occurrence(teco_string_t str, const gchar *chars); /** * Validate whether string consists exclusively of valid UTF-8, but accept null bytes. * @note there is g_utf8_validate_len() in Glib 2.60 */ static inline gboolean -teco_string_validate_utf8(const teco_string_t *str) +teco_string_validate_utf8(teco_string_t str) { - const gchar *p = str->data; - while (!g_utf8_validate(p, str->len - (p - str->data), &p) && !*p) + const gchar *p = str.data; + while (!g_utf8_validate(p, str.len - (p - str.data), &p) && !*p) p++; - return p - str->data == str->len; + return p - str.data == str.len; } /** @memberof teco_string_t */ diff --git a/src/symbols-extract.tes b/src/symbols-extract.tes index 9a8a270..6c7ef05 100755 --- a/src/symbols-extract.tes +++ b/src/symbols-extract.tes @@ -6,23 +6,23 @@ 0,2EJ !* FIXME: Memory limiting is too slow *! -:EMQ[$SCITECOPATH]/getopt.tes -EMQ[$SCITECOPATH]/string.tes +:EIQ[$SCITECOPATH]/getopt.tes +EIQ[$SCITECOPATH]/string.tes !* read commandline arguments *! [getopt.p] -[optstring]p:n: M[getopt]"F (0/0) ' -LR 0X#ou 2LR 0X#in HK +[optstring]p:n: +M[getopt]U#ou Q#ou"< Invalid command-line^J 1 ' Q#ou+1U#in !* copy all defines in input file beginning with prefix *! -EBN#in <S#defineS[[Q[getopt.p]]M ]; 1:Xa 10:a> EF +EBN[\#in] <S#defineS[[Q[getopt.p]]M ]; 1:Xa 10:a> EF !* sort all defines *! Ga ZJB 0,.M[qsort] J !* format as C/C++ array *! I/* - * AUTOGENERATED FROM Q#in + * AUTOGENERATED FROM Q[\#in] * DO NOT EDIT */ #ifdef HAVE_CONFIG_H @@ -31,7 +31,7 @@ I/* #include <glib.h> -#include "Q#in" +#include "Q[\#in]" #include "sciteco.h" #include "symbols.h" @@ -56,6 +56,6 @@ teco_cmdline_cleanup(void) !* write output file *! -2EL EWQ#ou +2EL EWQ[\#ou] EX diff --git a/src/symbols.c b/src/symbols.c index 4028b7e..dd5856e 100644 --- a/src/symbols.c +++ b/src/symbols.c @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -38,6 +38,7 @@ #include "undo.h" #include "expressions.h" #include "interface.h" +#include "cmdline.h" #include "symbols.h" teco_symbol_list_t teco_symbol_list_scintilla = {NULL, 0}; @@ -138,7 +139,7 @@ teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, tec glist_str.data = (gchar *)glist->data + symbol_len; glist_str.len = strlen(glist_str.data); - gsize len = teco_string_casediff(&glist_str, (gchar *)entry->data + symbol_len, + gsize len = teco_string_casediff(glist_str, (gchar *)entry->data + symbol_len, strlen(entry->data) - symbol_len); if (!prefix_len || len < prefix_len) prefix_len = len; @@ -166,13 +167,17 @@ teco_symbol_list_auto_complete(teco_symbol_list_t *ctx, const gchar *symbol, tec * Command states */ -/* - * FIXME: This state could be static. - */ -TECO_DECLARE_STATE(teco_state_scintilla_lparam); +static inline sptr_t +teco_scintilla_ssm(unsigned int iMessage, uptr_t wParam, sptr_t lParam) +{ + return teco_view_ssm(teco_ed & TECO_ED_MINIBUF_SSM ? teco_cmdline.view : teco_interface_current_view, + iMessage, wParam, lParam); +} + +static teco_state_t teco_state_scintilla_lparam; static gboolean -teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_string_t *str, GError **error) +teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, teco_string_t str, GError **error) { if (teco_string_contains(str, '\0')) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, @@ -180,7 +185,7 @@ teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_str return FALSE; } - g_auto(GStrv) symbols = g_strsplit(str->data, ",", -1); + g_auto(GStrv) symbols = g_strsplit(str.data, ",", -1); if (!symbols[0]) return TRUE; @@ -212,7 +217,7 @@ teco_scintilla_parse_symbols(teco_machine_scintilla_t *scintilla, const teco_str } static teco_state_t * -teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_scintilla_lparam; @@ -226,12 +231,10 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t teco_undo_scintilla_message(ctx->scintilla); memset(&ctx->scintilla, 0, sizeof(ctx->scintilla)); - if ((str->len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) || + if ((str.len > 0 && !teco_scintilla_parse_symbols(&ctx->scintilla, str, error)) || !teco_expressions_eval(FALSE, error)) return NULL; - teco_int_t value; - if (!ctx->scintilla.iMessage) { if (!teco_expressions_args()) { g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, @@ -239,9 +242,7 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t return NULL; } - if (!teco_expressions_pop_num_calc(&value, 0, error)) - return NULL; - ctx->scintilla.iMessage = value; + ctx->scintilla.iMessage = teco_expressions_pop_num(0); } return &teco_state_scintilla_lparam; @@ -250,7 +251,7 @@ teco_state_scintilla_symbols_done(teco_machine_main_t *ctx, const teco_string_t /* in cmdline.c */ gboolean teco_state_scintilla_symbols_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gunichar key, GError **error); -gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, const teco_string_t *str, +gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /*$ ES scintilla message @@ -320,6 +321,13 @@ gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx * second string argument of \fBES\fP, i.e. it allows you * to look up style ids by name. * + * By default Scintilla messages are sent to the current buffer's + * view or the Q-register view \(em there is only one view for + * all Q-registers. + * If bit 11 is set in the \fBED\fP flags, the messages will be + * sent to the command-line view instead, which allows you to + * set up \*(ST syntax highlighting and other styles. + * * .BR Warning : * Almost all Scintilla messages may be dispatched using * this command. @@ -344,17 +352,18 @@ gboolean teco_state_scintilla_symbols_insert_completion(teco_machine_main_t *ctx TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_symbols, .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_scintilla_symbols_process_edit_cmd, .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_scintilla_symbols_insert_completion, - .expectstring.last = FALSE + .expectstring.last = FALSE, + .expectstring.done_cb = teco_state_scintilla_symbols_done ); #ifdef HAVE_LEXILLA static gpointer -teco_create_lexer(const teco_string_t *str, GError **error) +teco_create_lexer(teco_string_t str, GError **error) { CreateLexerFn CreateLexerFn = CreateLexer; - const gchar *lexer = memchr(str->data ? : "", '\0', str->len); + const gchar *lexer = memchr(str.data ? : "", '\0', str.len); if (lexer) { /* external lexer */ lexer++; @@ -363,7 +372,7 @@ teco_create_lexer(const teco_string_t *str, GError **error) * NOTE: The same module can be opened multiple times. * They are internally reference counted. */ - GModule *module = g_module_open(str->data, G_MODULE_BIND_LAZY); + GModule *module = g_module_open(str.data, G_MODULE_BIND_LAZY); if (!module) { teco_error_module_set(error, "Error opening lexer module"); return NULL; @@ -385,7 +394,7 @@ teco_create_lexer(const teco_string_t *str, GError **error) * * FIXME: In Scintillua distributions, the lexers are usually contained in the * same directory as the prebuilt shared libraries. - * Perhaps we should default scintillua.lexers to the dirname in str->data? + * Perhaps we should default scintillua.lexers to the dirname in str.data? */ teco_qreg_t *reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECO_SCINTILLUA_LEXERS", 26); if (reg) { @@ -397,7 +406,7 @@ teco_create_lexer(const teco_string_t *str, GError **error) } } else { /* Lexilla lexer */ - lexer = str->data ? : ""; + lexer = str.data ? : ""; } ILexer5 *ret = CreateLexerFn(lexer); @@ -413,9 +422,9 @@ teco_create_lexer(const teco_string_t *str, GError **error) #else /* !HAVE_LEXILLA */ static gpointer -teco_create_lexer(const teco_string_t *str, GError **error) +teco_create_lexer(teco_string_t str, GError **error) { - g_autofree gchar *str_printable = teco_string_echo(str->data, str->len); + g_autofree gchar *str_printable = teco_string_echo(str.data, str.len); g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Cannot load lexer \"%s\": Lexilla disabled", str_printable); return NULL; @@ -424,7 +433,7 @@ teco_create_lexer(const teco_string_t *str, GError **error) #endif static teco_state_t * -teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, teco_string_t str, GError **error) { if (ctx->flags.mode > TECO_MODE_NORMAL) return &teco_state_start; @@ -446,10 +455,10 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t * /* * FIXME: Should we cache the name to style id? */ - guint count = teco_interface_ssm(SCI_GETNAMEDSTYLES, 0, 0); + guint count = teco_scintilla_ssm(SCI_GETNAMEDSTYLES, 0, 0); for (guint id = 0; id < count; id++) { gchar style[128] = ""; - teco_interface_ssm(SCI_NAMEOFSTYLE, id, (sptr_t)style); + teco_scintilla_ssm(SCI_NAMEOFSTYLE, id, (sptr_t)style); if (!teco_string_cmp(str, style, strlen(style))) { teco_expressions_push(id); return &teco_state_start; @@ -457,13 +466,13 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t * } g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, - "Style name \"%s\" not found.", str->data ? : ""); + "Style name \"%s\" not found.", str.data ? : ""); return NULL; } else if (ctx->scintilla.iMessage == SCI_SETILEXER) { lParam = (sptr_t)teco_create_lexer(str, error); if (!lParam) return NULL; - } else if (str->len > 0) { + } else if (str.len > 0) { /* * Theoretically, Scintilla could use null bytes from the string specified. * This could only be the case for messages where the string length is @@ -473,13 +482,13 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t * * which unlocks useful messages like * SCI_SETREPRESENTATIONS and SCI_SETPROPERTY. */ - const gchar *p = memchr(str->data, '\0', str->len); + const gchar *p = memchr(str.data, '\0', str.len); if (p) { - ctx->scintilla.wParam = (uptr_t)str->data; - if (str->len > p - str->data + 1) + ctx->scintilla.wParam = (uptr_t)str.data; + if (str.len > p - str.data + 1) lParam = (sptr_t)(p+1); } else { - lParam = (sptr_t)str->data; + lParam = (sptr_t)str.data; } } @@ -495,10 +504,12 @@ teco_state_scintilla_lparam_done(teco_machine_main_t *ctx, const teco_string_t * lParam = v; } - teco_expressions_push(teco_interface_ssm(ctx->scintilla.iMessage, + teco_expressions_push(teco_scintilla_ssm(ctx->scintilla.iMessage, ctx->scintilla.wParam, lParam)); return &teco_state_start; } -TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam); +static TECO_DEFINE_STATE_EXPECTSTRING(teco_state_scintilla_lparam, + .expectstring.done_cb = teco_state_scintilla_lparam_done +); diff --git a/src/symbols.h b/src/symbols.h index 1d0af12..c7db610 100644 --- a/src/symbols.h +++ b/src/symbols.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -61,4 +61,4 @@ extern teco_symbol_list_t teco_symbol_list_scilexer; * Command states */ -TECO_DECLARE_STATE(teco_state_scintilla_symbols); +extern teco_state_t teco_state_scintilla_symbols; @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -34,6 +34,7 @@ TECO_DEFINE_UNDO_SCALAR(gunichar); TECO_DEFINE_UNDO_SCALAR(gint); TECO_DEFINE_UNDO_SCALAR(guint); TECO_DEFINE_UNDO_SCALAR(gsize); +TECO_DEFINE_UNDO_SCALAR(gssize); TECO_DEFINE_UNDO_SCALAR(teco_int_t); TECO_DEFINE_UNDO_SCALAR(gboolean); TECO_DEFINE_UNDO_SCALAR(gconstpointer); @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -182,6 +182,9 @@ TECO_DECLARE_UNDO_SCALAR(guint); TECO_DECLARE_UNDO_SCALAR(gsize); #define teco_undo_gsize(VAR) (*teco_undo_object_gsize_push(&(VAR))) +TECO_DECLARE_UNDO_SCALAR(gssize); +#define teco_undo_gssize(VAR) (*teco_undo_object_gssize_push(&(VAR))) + TECO_DECLARE_UNDO_SCALAR(teco_int_t); #define teco_undo_int(VAR) (*teco_undo_object_teco_int_t_push(&(VAR))) @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -145,11 +145,18 @@ teco_view_setup(teco_view_t *ctx) TECO_DEFINE_UNDO_CALL(teco_view_ssm, teco_view_t *, unsigned int, uptr_t, sptr_t); -/** @memberof teco_view_t */ +/** + * Configure typical TECO representations for control characters. + * + * You may have to SCI_SETVIEWEOL(TRUE) to see the CR and LF characters. + * In order to see the TAB character use SCI_SETTABDRAWMODE(SCTD_CONTROLCHAR). + * + * @memberof teco_view_t + */ void teco_view_set_representations(teco_view_t *ctx) { - static const char *reps[] = { + static const gchar reps[][4] = { "^@", "^A", "^B", "^C", "^D", "^E", "^F", "^G", "^H", "TAB" /* ^I */, "LF" /* ^J */, "^K", "^L", "CR" /* ^M */, "^N", "^O", "^P", "^Q", "^R", "^S", "^T", "^U", "^V", "^W", @@ -198,16 +205,22 @@ teco_view_set_representations(teco_view_t *ctx) * * @param ctx The view to load. * @param channel Channel to read from. + * @param clear Whether to completely replace document + * (leaving dot at the beginning of the document) or insert at dot + * (leaving dot at the end of the insertion). * @param error A GError. * @return FALSE in case of a GError. * * @memberof teco_view_t */ gboolean -teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error) +teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, + gboolean clear, GError **error) { gboolean ret = TRUE; + unsigned int message = SCI_ADDTEXT; + g_auto(teco_eol_reader_t) reader; teco_eol_reader_init_gio(&reader, channel); @@ -225,22 +238,27 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro SC_LINECHARACTERINDEX_UTF32, 0); teco_view_ssm(ctx, SCI_BEGINUNDOACTION, 0, 0); - teco_view_ssm(ctx, SCI_CLEARALL, 0, 0); + if (clear) { + teco_view_ssm(ctx, SCI_CLEARALL, 0, 0); - /* - * Preallocate memory based on the file size. - * May waste a few bytes if file contains DOS EOLs - * and EOL translation is enabled, but is faster. - * NOTE: g_io_channel_unix_get_fd() should report the correct fd - * on Windows, too. - */ - struct stat stat_buf = {.st_size = 0}; - if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) && - stat_buf.st_size > 0) { - ret = teco_memory_check(stat_buf.st_size, error); - if (!ret) - goto cleanup; - teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0); + /* + * Preallocate memory based on the file size. + * May waste a few bytes if file contains DOS EOLs + * and EOL translation is enabled, but is faster. + * NOTE: g_io_channel_unix_get_fd() should report the correct fd + * on Windows, too. + */ + struct stat stat_buf = {.st_size = 0}; + if (!fstat(g_io_channel_unix_get_fd(channel), &stat_buf) && + stat_buf.st_size > 0) { + ret = teco_memory_check(stat_buf.st_size, error); + if (!ret) + goto cleanup; + teco_view_ssm(ctx, SCI_ALLOCATE, stat_buf.st_size, 0); + } + + /* keep dot at beginning of document */ + message = SCI_APPENDTEXT; } for (;;) { @@ -258,7 +276,7 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro if (rc == G_IO_STATUS_EOF) break; - teco_view_ssm(ctx, SCI_APPENDTEXT, str.len, (sptr_t)str.data); + teco_view_ssm(ctx, message, str.len, (sptr_t)str.data); /* * Even if we checked initially, knowing the file size, @@ -285,7 +303,7 @@ teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **erro * If it is enabled but the stream does not contain any * EOL characters, the platform default is still assumed. */ - if (reader.eol_style >= 0) + if (clear && reader.eol_style >= 0) teco_view_ssm(ctx, SCI_SETEOLMODE, reader.eol_style, 0); if (reader.eol_style_inconsistent) @@ -303,12 +321,21 @@ cleanup: } /** - * Load view's document from file. + * Load file into view's document. + * + * @param ctx The view to load. + * @param filename File name to read + * @param clear Whether to completely replace document + * (leaving dot at the beginning of the document) or insert at dot + * (leaving dot at the end of the insertion). + * @param error A GError. + * @return FALSE in case of a GError. * * @memberof teco_view_t */ gboolean -teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error) +teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, + gboolean clear, GError **error) { g_autoptr(GIOChannel) channel = g_io_channel_new_file(filename, "r", error); if (!channel) @@ -322,7 +349,7 @@ teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error g_io_channel_set_encoding(channel, NULL, NULL); g_io_channel_set_buffered(channel, FALSE); - if (!teco_view_load_from_channel(ctx, channel, error)) { + if (!teco_view_load_from_channel(ctx, channel, clear, error)) { g_prefix_error(error, "Error reading file \"%s\": ", filename); return FALSE; } @@ -330,6 +357,44 @@ teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error return TRUE; } +/** + * Load stdin until EOF into view's document. + * + * @param ctx The view to load. + * @param clear Whether to completely replace document + * (leaving dot at the beginning of the document) or insert at dot + * (leaving dot at the end of the insertion). + * @param error A GError. + * @return FALSE in case of a GError. + * + * @memberof teco_view_t + */ +gboolean +teco_view_load_from_stdin(teco_view_t *ctx, gboolean clear, GError **error) +{ +#ifdef G_OS_WIN32 + g_autoptr(GIOChannel) channel = g_io_channel_win32_new_fd(0); +#else + g_autoptr(GIOChannel) channel = g_io_channel_unix_new(0); +#endif + g_assert(channel != NULL); + + /* + * The file loading algorithm does not need buffered + * streams, so disabling buffering should increase + * performance (slightly). + */ + g_io_channel_set_encoding(channel, NULL, NULL); + g_io_channel_set_buffered(channel, FALSE); + + if (!teco_view_load_from_channel(ctx, channel, clear, error)) { + g_prefix_error(error, "Error reading stdin: "); + return FALSE; + } + + return TRUE; +} + #if 0 /* @@ -401,13 +466,10 @@ teco_undo_restore_savepoint_push(gchar *savepoint, const gchar *filename) static void teco_make_savepoint(const gchar *filename) { - gchar savepoint_basename[FILENAME_MAX]; - g_autofree gchar *basename = g_path_get_basename(filename); - g_snprintf(savepoint_basename, sizeof(savepoint_basename), - ".teco-%d-%s~", savepoint_id, basename); g_autofree gchar *dirname = g_path_get_dirname(filename); - gchar *savepoint = g_build_filename(dirname, savepoint_basename, NULL); + gchar *savepoint = g_strdup_printf("%s%c.teco-%d-%s~", dirname, G_DIR_SEPARATOR, + savepoint_id, basename); if (g_rename(filename, savepoint)) { teco_interface_msg(TECO_MSG_WARNING, @@ -446,6 +508,13 @@ teco_undo_remove_file_push(const gchar *filename) strcpy(ctx, filename); } +/** + * Save the view's document to the given IO channel. + * + * @note This must not emit undo tokens as it is also used by teco_ring_dump_recovery(). + * + * @memberof teco_view_t + */ gboolean teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error) { @@ -485,16 +554,16 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error) file_stat.st_gid = -1; #endif teco_file_attributes_t attributes = TECO_FILE_INVALID_ATTRIBUTES; + gboolean undo_remove_file = FALSE; if (teco_undo_enabled) { - if (g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { + undo_remove_file = !g_file_test(filename, G_FILE_TEST_IS_REGULAR); + if (!undo_remove_file) { #ifdef G_OS_UNIX g_stat(filename, &file_stat); #endif attributes = teco_file_get_attributes(filename); teco_make_savepoint(filename); - } else { - teco_undo_remove_file_push(filename); } } @@ -503,6 +572,18 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error) if (!channel) return FALSE; + if (undo_remove_file) { + /* + * The file is new, so has to be removed on undo. + * If `filename` is a symlink, it's crucial to resolve it now, + * since early canonicalization may have failed (for non-existent + * path segments). + * Now, `filename` is guaranteed to exist. + */ + g_autofree gchar *filename_canon = teco_file_get_absolute_path(filename); + teco_undo_remove_file_push(filename_canon); + } + /* * teco_view_save_to_channel() expects a buffered and blocking channel */ @@ -511,6 +592,7 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error) if (!teco_view_save_to_channel(ctx, channel, error)) { g_prefix_error(error, "Error writing file \"%s\": ", filename); + /* file might also be removed (in interactive mode) */ return FALSE; } @@ -534,6 +616,31 @@ teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error) return TRUE; } +/** @memberof teco_view_t */ +gboolean +teco_view_save_to_stdout(teco_view_t *ctx, GError **error) +{ +#ifdef G_OS_WIN32 + g_autoptr(GIOChannel) channel = g_io_channel_win32_new_fd(1); +#else + g_autoptr(GIOChannel) channel = g_io_channel_unix_new(1); +#endif + g_assert(channel != NULL); + + /* + * teco_view_save_to_channel() expects a buffered and blocking channel + */ + g_io_channel_set_encoding(channel, NULL, NULL); + g_io_channel_set_buffered(channel, TRUE); + + if (!teco_view_save_to_channel(ctx, channel, error)) { + g_prefix_error(error, "Error writing to stdout: "); + return FALSE; + } + + return TRUE; +} + /** * Convert a glyph index to a byte offset as used by Scintilla. * @@ -628,8 +735,8 @@ teco_view_glyphs2bytes_relative(teco_view_t *ctx, gsize pos, teco_int_t n) * @param pos The glyph's byte position * @param len The length of the document in bytes * @return The requested codepoint. - * In UTF-8 encoded documents, this might be -1 (incomplete sequence) - * or -2 (invalid byte sequence). + * In UTF-8 encoded documents, this might be -2 (invalid byte sequence) + * or -3 (incomplete sequence). */ teco_int_t teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len) @@ -653,12 +760,15 @@ teco_view_get_character(teco_view_t *ctx, gsize pos, gsize len) * or repeatedly calling SCI_GETCHARAT. */ teco_view_ssm(ctx, SCI_GETTEXTRANGEFULL, 0, (sptr_t)&range); + if (!*buf) + return 0; /* * Make sure that the -1/-2 error values are preserved. * The sign bit in UCS-4/UTF-32 is unused, so this will even * suffice if TECO_INTEGER == 32. */ - return *buf ? (gint32)g_utf8_get_char_validated(buf, -1) : 0; + gint32 rc = g_utf8_get_char_validated(buf, -1); + return rc < 0 ? rc-1 : rc; } void @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012-2025 Robin Haberkorn + * Copyright (C) 2012-2026 Robin Haberkorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -52,20 +52,27 @@ teco_view_set_scintilla_undo(teco_view_t *ctx, gboolean state) teco_view_ssm(ctx, SCI_SETUNDOCOLLECTION, state, 0); } -gboolean teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, GError **error); -gboolean teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, GError **error); +gboolean teco_view_load_from_channel(teco_view_t *ctx, GIOChannel *channel, + gboolean clear, GError **error); +gboolean teco_view_load_from_file(teco_view_t *ctx, const gchar *filename, + gboolean clear, GError **error); +gboolean teco_view_load_from_stdin(teco_view_t *ctx, gboolean clear, GError **error); /** @memberof teco_view_t */ -#define teco_view_load(CTX, FROM, ERROR) \ +#define teco_view_load(CTX, FROM, CLEAR, ERROR) \ (_Generic((FROM), GIOChannel * : teco_view_load_from_channel, \ - const gchar * : teco_view_load_from_file)((CTX), (FROM), (ERROR))) + gchar * : teco_view_load_from_file, \ + const gchar * : teco_view_load_from_file)((CTX), (FROM), \ + (CLEAR), (ERROR))) gboolean teco_view_save_to_channel(teco_view_t *ctx, GIOChannel *channel, GError **error); gboolean teco_view_save_to_file(teco_view_t *ctx, const gchar *filename, GError **error); +gboolean teco_view_save_to_stdout(teco_view_t *ctx, GError **error); /** @memberof teco_view_t */ #define teco_view_save(CTX, TO, ERROR) \ (_Generic((TO), GIOChannel * : teco_view_save_to_channel, \ + gchar * : teco_view_save_to_file, \ const gchar * : teco_view_save_to_file)((CTX), (TO), (ERROR))) /** @pure @memberof teco_view_t */ diff --git a/tests/atlocal.in b/tests/atlocal.in index 1992c54..b2ceda1 100644 --- a/tests/atlocal.in +++ b/tests/atlocal.in @@ -4,49 +4,36 @@ host=@host@ # built. # Using the $BOOTSTRAP_SCITECO wouldn't make sense # anyway as we don't want to test some preinstalled SciTECO. -SCITECO="@abs_top_builddir@/src/sciteco" +SCITECO="@LAUNCHER@ @abs_top_builddir@/src/sciteco@EXEEXT@" if [ $at_arg_valgrind != false ]; then SCITECO="valgrind --tool=memcheck --leak-check=full --error-exitcode=66 $SCITECO" fi -# For testing command-line editing: -SCITECO_CMDLINE="$SCITECO --no-profile --fake-cmdline" - -# Control characters for testing immediate editing commands with $SCITECO_CMDLINE. -# Often, we can use {...} for testing rubout, but sometimes this is not enough. -# Directly embedding escapes into strings is not portable. -# Theoretically, we could directly embed control codes, but for the time being -# I am trying to keep non-TECO sources clean of non-printable characters. -RUBOUT=`printf '\8'` -RUBOUT_WORD=`printf '\27'` -ESCAPE=`printf '\33'` - # Make sure that the standard library from the source package # is used. -SCITECOPATH="@abs_top_srcdir@/lib" +export SCITECOPATH="@abs_top_srcdir@/lib" -TECO_INTEGER=@TECO_INTEGER@ +# Some test cases may access files from the tests/ source directory. +export srcdir -MAXINT32=2147483647 -MININT32=-2147483648 -MAXINT64=9223372036854775807 -MININT64=-9223372036854775808 +TECO_INTEGER=@TECO_INTEGER@ +SCITECO_VERSION="@PACKAGE_VERSION@" GREP="@GREP@" # Glib debug options -G_SLICE=debug-blocks -G_ENABLE_DIAGNOSTIC=1 +export G_SLICE=debug-blocks +export G_ENABLE_DIAGNOSTIC=1 # For the Unicode tests - makes sure that UTF-8 characters # are accepted on command lines. case $host in *-*-darwin*) - LC_ALL=`defaults read -g AppleLocale | @SED@ 's/@.*$//g'`.UTF-8 + export LC_ALL=`defaults read -g AppleLocale | @SED@ 's/@.*$//g'`.UTF-8 ;; *) - LC_ALL=C.UTF-8 + export LC_ALL=C.UTF-8 ;; esac @@ -54,6 +41,3 @@ esac # Some platforms allow very large stack sizes, making it hard to test # against potential stack overflows. ulimit -s 8192 - -# Test strings used by multiple test cases -WORDS_EXAMPLE="navigating (words is useful" diff --git a/tests/testsuite.at b/tests/testsuite.at index b0ca8c0..f34eee3 100644 --- a/tests/testsuite.at +++ b/tests/testsuite.at @@ -1,155 +1,269 @@ AT_INIT AT_COLOR_TESTS +# Will usually be called as +# make check TESTSUITEFLAGS=--valgrind AT_ARG_OPTION([valgrind], AS_HELP_STRING([--valgrind], [Run tests under Valgrind (memcheck)])) -# NOTE: There is currently no way to influence the return -# code of SciTECO, except to provoke an error. -# Since errors cannot be yielded explicitly, we use the -# idiom "(0/0)" to enforce a "Division by zero" error -# whenever we want to fail. +# NOTE: We could use 1^C or 1^C^C to get an unsuccessful return code. +# However, this won't print any stack trace or error message. +# Therefore, we still use the idiom "(0/0)" to enforce a "Division by zero" +# error whenever we want to fail. +# A proper error throwing construct should be used instead once it's available. # -# NOTE: Square brackets are significant for M4 but -# often required in TECO code as well. -# We therefore use double brackets [[ ... ]] -# (translated to [ ... ]) in simple cases where balanced -# brackets are required in TECO code as well and -# quadrigraphs (@<:@ and @:>@) in all other cases. -# Single round brackets also have to be replaced with the -# quadrigraphs @{:@ and @:}@. - -AT_BANNER([Features]) +# NOTE: By convention, we double quote the SciTECO test case +# snippets, ie. put them between [[ and ]]. +# The advantage is that single brackets are preserved and do not +# have to be written as quadrigraphs. +# Single brackets must still be balanced, so if you need an unbalanced +# opening or closing bracket, you can add a ![! or !]! TECO comment +# to balance the braces from the viewpoint of M4. +# Round braces are preserved and do not have to be balanced. +# Most test cases can use the TE_CHECK() macro below, which takes +# care of escaping shell constructs. +# Effectively, you can put arbitrary TECO code into TE_CHECK([[...]]) +# calls and only have to take additional actions in case of unbalanced +# square brackets. + +m4_define([TE_CHECK], [ + AT_CHECK([$SCITECO --quiet --eval ']m4_bpatsubst([[$1]], ['], ['\\''])['], [$2], [$3], [$4]) +]) +m4_define([TE_CHECK_CMDLINE], [ + AT_CHECK([$SCITECO --quiet --no-profile --fake-cmdline ']m4_bpatsubst([[$1]], ['], ['\\''])['], [$2], [$3], [$4]) +]) + +# Control characters for testing immediate editing commands with TE_CHECK_CMDLINE(). +# Often, we can use {...} for testing rubout, but sometimes this is not enough. +# Theoretically, we could directly embed control codes, but for the time being +# I am trying to keep non-TECO sources clean of non-printable characters. +m4_define([TE_ESCAPE], [m4_format([%c], 27)]) +m4_define([TE_RUBOUT], [m4_format([%c], 8)]) +m4_define([TE_RUBOUT_WORD], [m4_format([%c], 23)]) + +AT_BANNER([Language features]) AT_SETUP([Number stack]) -AT_CHECK([$SCITECO -e "2%a,%a - 3\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[2%a,%a - 3"N(0/0)' $]], 0, ignore, ignore) # It's not quite clear what would be the best semantics for comma: # a) Superfluous commas as in ",," or "(1,)" should be an error. # b) Superfluous commas should be ignored which is effectively what we do now. # Even then it might be advisable to treat (1,) like (1). # c) The empty "list" element is equivalent to 0, so # "1,,2" is equivalent to "1,0,2" and (1,) to (1,0). -AT_CHECK([$SCITECO -e "(1,) \"~|(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "1,(2)=="], 0, ignore, ignore) +TE_CHECK([[(1,) "~|(0/0)']], 0, ignore, ignore) +TE_CHECK([[1,(2)= =]], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Exit status]) +TE_CHECK([[23]], 23, ignore, ignore) +TE_CHECK([[42^C (0/0)]], 42, ignore, ignore) +TE_CHECK([[13$$ (0/0)]], 13, ignore, ignore) +TE_CHECK([[@^Um{9^C^C} Mm (0/0)]], 9, ignore, ignore) AT_CLEANUP AT_SETUP([Radix]) -AT_CHECK([$SCITECO -e "0^R"], 1, ignore, ignore) -AT_CHECK([$SCITECO -e "0U.^R"], 1, ignore, ignore) -AT_CHECK([$SCITECO -e "23 (2^R)\^D .-5\"N(0/0)"], 0, ignore, ignore) +TE_CHECK([[0^R]], 1, ignore, ignore) +TE_CHECK([[0U.^R]], 1, ignore, ignore) +TE_CHECK([[23 (2^R)\^D .-5"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Exponentiation]) -AT_CHECK([$SCITECO -e "-1^*0 - (-1)\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "-1^*-5 - (-1)\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "0^*-5="], 1, ignore, ignore) -AT_CHECK([$SCITECO -e "0^*0 - 1\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "1^*-5 - 1\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "2^*-5 - 0\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[-1^*0 - (-1)"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[-1^*-5 - (-1)"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[0^*-5=]], 1, ignore, ignore) +TE_CHECK([[0^*0 - 1"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[1^*-5 - 1"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[2^*-5 - 0"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Missing left operand]) -AT_CHECK([$SCITECO -e '+23='], 1, ignore, ignore) +TE_CHECK([[+23=]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Operator precedence]) -AT_CHECK([$SCITECO -e "(10-2-3)-5\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "(1-6*5)+29\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "(1-6*5-1)+30\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "(1-6*5-1*2*2)+33\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[(10-2-3)-5"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[(1-6*5)+29"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[(1-6*5-1)+30"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[(1-6*5-1*2*2)+33"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Modifiers]) -AT_CHECK([$SCITECO -e '@:W$ :@W$'], 0, ignore, ignore) +TE_CHECK([[@:W$ :@W$]], 0, ignore, ignore) # Detect invalid modifiers -AT_CHECK([$SCITECO -e '@J'], 1, ignore, ignore) -AT_CHECK([$SCITECO -e ': '], 1, ignore, ignore) -AT_CHECK([$SCITECO -e '::C$'], 1, ignore, ignore) +TE_CHECK([[@J]], 1, ignore, ignore) +TE_CHECK([[: ]], 1, ignore, ignore) +TE_CHECK([[::C$]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Closing loops at the correct macro level]) -AT_CHECK([$SCITECO -e '@^Ua{>} <Ma'], 1, ignore, ignore) +TE_CHECK([[@^Ua{>} <Ma]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Braces in loops]) -AT_CHECK([$SCITECO -e "1<23@{:@42>"], 1, ignore, ignore) -AT_CHECK([$SCITECO -e "1<23(1;)> \"~|(0/0)'"], 0, ignore, ignore) +TE_CHECK([[1<23(42>]], 1, ignore, ignore) +TE_CHECK([[1<23(1;)> "~|(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Pass-through loops]) # NOTE: This requires the <=>, so that values get consumed from the stack. # More elegant would be a command for popping exactly one argument like <:$>. -AT_CHECK([$SCITECO -e "1,2,3,-1:<\"~1;'%a=> Qa-6\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "1,2,3,-1:<\"~1;'%a= F>(0/0)> Qa-6\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "3<%a:>-3\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "3<%a :F>(0/0):>-3\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[1,2,3,-1:<"~1;'%a=> Qa-6"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[1,2,3,-1:<"~1;'%a= F>(0/0)> Qa-6"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[3<%a:>-3"N(0/0)' $]], 0, ignore, ignore) +TE_CHECK([[3<%a :F>(0/0):>-3"N(0/0)' $]], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Gotos and labels]) +TE_CHECK([[@O//]], 1, ignore, ignore) +TE_CHECK([[^^XUq @O/s.^EUq/ (0/0) !s.X!]], 0, ignore, ignore) +TE_CHECK([[:@O/foo/]], 0, ignore, ignore) +TE_CHECK([[1@O/foo,bar/ (0/0) !bar!]], 0, ignore, ignore) +# No-op gotos +TE_CHECK([[-1@O/foo/ 1@O/foo/ @O/,foo/]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([String arguments]) -AT_CHECK([$SCITECO -e $'Ifoo^Q\e(0/0)\e'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '@I"foo^Q"(0/0)"'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '@I{foo{bar}foo^Q{(0/0)}'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '@Ia^EQa(0/0)a'], 0, ignore, ignore) -# TODO: String building characters +TE_CHECK([[Ifoo^Q]]TE_ESCAPE[[(0/0)]]TE_ESCAPE, 0, ignore, ignore) +TE_CHECK([[@I"foo^Q"(0/0)"]], 0, ignore, ignore) +TE_CHECK([[@I{foo{bar}foo^Q{(0/0)}]], 0, ignore, ignore) +TE_CHECK([[@^Ua + {12345} :Qa-5"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/X/ H@FR{X}/12345/ Z-5"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@Ia^EQa(0/0)a]], 0, ignore, ignore) +# Video-TECO-like syntax - might change in the future +TE_CHECK([[@I/^E<65>^E<0x41>^E<0101>/ <-A:; -A-^^A"N(0/0)' R>]], 0, ignore, ignore) +# TODO: More string building constructs AT_CLEANUP AT_SETUP([Q-Register definitions]) -AT_CHECK([$SCITECO -e '0Ua'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U.a'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U.^X'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U#ab'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U.#ab'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U[[AB]]'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U.[[AB]]'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '0U[[AB^Q@:>@(0/0)]]'], 0, ignore, ignore) +TE_CHECK([[0Ua]], 0, ignore, ignore) +TE_CHECK([[0U.a]], 0, ignore, ignore) +TE_CHECK([[0U.^X]], 0, ignore, ignore) +TE_CHECK([[0U#ab]], 0, ignore, ignore) +TE_CHECK([[0U.#ab]], 0, ignore, ignore) +TE_CHECK([[0U[AB] ]], 0, ignore, ignore) +TE_CHECK([[0U.[AB] ]], 0, ignore, ignore) +TE_CHECK([[] ]], 0, ignore, ignore) # TODO: String building in Q-Register definitions AT_CLEANUP AT_SETUP([Copy, append and cut to Q-Registers]) -AT_CHECK([$SCITECO -e "@I/12^J123/J Xa :Xa L-:@Xa :Qa-9\"N(0/0)' Z-3\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/ABCDE/ 1,4Xa 0,3:Xa 3,5:@Xa :Qa-8\"N(0/0)' Z-3\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@I/12^J123/J Xa :Xa L-:@Xa :Qa-9"N(0/0)' Z-3"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/ABCDE/ 1,4Xa 0,3:Xa 3,5:@Xa :Qa-8"N(0/0)' Z-3"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Q-Register stack]) -AT_CHECK([$SCITECO -e "[[a 23Ub ]]b Qb\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[ [a 23Ub ]b Qb"N(0/0)']], 0, ignore, ignore) # FG will temporarily change the working directory to tests/testsuite.dir. -AT_CHECK([$SCITECO -e "[[\$ @FG'..' ]]\$ :Q\$-1Q\$-^^r\"=(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "[[: @I/XXX/ ]]: .\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[ [$ @FG'..' ]$ :Q$-1Q$-^^r"=(0/0)']], 0, ignore, ignore) +TE_CHECK([[ [: @I/XXX/ ]: ."N(0/0)']], 0, ignore, ignore) +TE_CHECK([[ [a :]a"F(0/0)' ![! :]a"S(0/0)']], 0, ignore, ignore) AT_CLEANUP +m4_define([TE_MAXINT32], [2147483647]) +m4_define([TE_MININT32], [-2147483648]) +m4_define([TE_MAXINT64], [9223372036854775807]) +m4_define([TE_MININT64], [-9223372036854775808]) + +# TODO: Also check different radixes. AT_SETUP([Formatting numbers]) # MAXINT32/MININT32: should always work. -AT_CHECK([$SCITECO -e "$MAXINT32\\ J::@S/$MAXINT32/\"F(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "$MININT32\\ J::@S/$MININT32/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK(TE_MAXINT32[[\ J::@S/]]TE_MAXINT32[[/"F(0/0)']], 0, ignore, ignore) +TE_CHECK(TE_MININT32[[\ J::@S/]]TE_MININT32[[/"F(0/0)']], 0, ignore, ignore) AT_SKIP_IF([test $TECO_INTEGER -lt 64]) -AT_CHECK([$SCITECO -e "$MAXINT64\\ J::@S/$MAXINT64/\"F(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "$MININT64\\ J::@S/$MININT64/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK(TE_MAXINT64[[\ J::@S/]]TE_MAXINT64[[/"F(0/0)']], 0, ignore, ignore) +TE_CHECK(TE_MININT64[[\ J::@S/]]TE_MININT64[[/"F(0/0)']], 0, ignore, ignore) AT_CLEANUP # This should always work, at least on systems with a two's complement # representation of negative integers. # We will probably never meet anything else, but at least we check. AT_SETUP([Integer comparisons]) -AT_CHECK([$SCITECO -e "($MAXINT32)-($MAXINT32)\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "($MININT32)-($MININT32)\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[(]]TE_MAXINT32[[)-(]]TE_MAXINT32[[)"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[(]]TE_MININT32[[)-(]]TE_MININT32[[)"N(0/0)']], 0, ignore, ignore) AT_SKIP_IF([test $TECO_INTEGER -lt 64]) -AT_CHECK([$SCITECO -e "($MAXINT64)-($MAXINT64)\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "($MININT64)-($MININT64)\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[(]]TE_MAXINT64[[)-(]]TE_MAXINT64[[)"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[(]]TE_MININT64[[)-(]]TE_MININT64[[)"N(0/0)']], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Piping from stdin to stdout]) +AT_DATA([expout], [[ТЕСТ +]]) +# Also tests case-folding via ^W^W. +AT_CHECK([[echo 'тест' | $SCITECO -qioe 'H@Xa @I/^W^W^EQa/']], 0, expout, ignore) +AT_CLEANUP + +AT_SETUP([Printing numbers]) +# Must print only one hexadecimal number. +AT_DATA([expout], [[FF +]]) +TE_CHECK([[255===]], 0, expout, ignore) +# Without LF: No line will be counted. +TE_CHECK([[255:===]], 0, stdout, ignore) +AT_FAIL_IF([test `wc -l <stdout` -ne 0]) +# Will print a decimal, octal and 2 hexadecimal numbers. +AT_DATA([expout], [[255 +377 +FF +FF +]]) +TE_CHECK_CMDLINE([[2<255===>]], 0, expout, ignore) +AT_DATA([expout], [[255 +255 +255 +]]) +TE_CHECK_CMDLINE([[3<255=>]], 0, expout, ignore) +AT_CLEANUP + +AT_SETUP([Printing strings]) +AT_DATA([expout], [[TEST +Line 2 +]]) +TE_CHECK([[@^A/TEST^JLine 2^J/]], 0, expout, ignore) +AT_CLEANUP + +AT_SETUP([Type out buffer contents]) +AT_DATA([expout], [[Line 1 +Line 2 +Line 3 +]]) +TE_CHECK([[@EB'expout' HT]], 0, expout, ignore) +AT_CLEANUP + +AT_SETUP([Type out and get char]) +AT_DATA([expout], [[ТЕСТ +]]) +TE_CHECK([[1058,1045,1057,1058,10^T]], 0, expout, ignore) +AT_DATA([expout], [[1058 +1045 +1057 +1058 +]]) +AT_CHECK([[printf "ТЕСТ" | $SCITECO -qe '<^TUa Qa:; Qa=>']], 0, expout, ignore) +# Writing to stdout should not perform any unexpected EOL translations. +# When using --stdin/--stdout, we can rely on the builtin EOL normalization. +TE_CHECK([[10^T]], 0, stdout, ignore) +TE_CHECK([[16,0ED @EB/stdout/ Z-1"N(0/0)' 0A-10"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Convert between line and glyph positions]) -AT_CHECK([$SCITECO -e "@I/1^J2^J3/J 2^QC :^Q-3\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@I/1^J2^J3/J 2^QC :^Q-3"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Moving by words]) -AT_CHECK([$SCITECO -e "3J 2W @P .-17\"N(0/0)'" "$WORDS_EXAMPLE"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/foo ^J bar/ JW @W .-Z\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "Z-4J 3P .-12\"N(0/0)'" "$WORDS_EXAMPLE"], 0, ignore, ignore) +AT_DATA([words-example.txt], [[navigating (words is useful +]]) +TE_CHECK([[@EB'words-example.txt' 3J 2W @P .-17"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/foo ^J bar/ JW @W .-Z"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@EB'words-example.txt' Z-4J 3P .-12"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Deleting words]) -AT_CHECK([$SCITECO -e "3J 2V .-3\"N(0/0)' @V Z-11\"N(0/0)'" "$WORDS_EXAMPLE"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "Z-4J 2Y .-18\"N(0/0)' 2C @Y Z-19\"N(0/0)'" "$WORDS_EXAMPLE"], 0, ignore, ignore) +AT_DATA([words-example.txt], [[navigating (words is useful +]]) +TE_CHECK([[@EB'words-example.txt' 3J 2V .-3"N(0/0)' @V Z-11"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@EB'words-example.txt' Z-4J 2Y .-18"N(0/0)' 2C @Y Z-19"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Searches]) @@ -157,73 +271,142 @@ AT_SETUP([Searches]) # by a 2nd character. It can be quoted, but cannot be written as Caret+E. # You also cannot search for a single ASCII 5 using Caret+E. # 2 additional ^Q are translated to a single ^Q and interpreted at the search-pattern layer. -AT_CHECK([$SCITECO -e "@I/^Q\05/ J @:S/^Q^Q^Q\05/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK(m4_format([[@I/^Q%c/ J @:S/^Q^Q^Q%c/"F(0/0)']], 5, 5), 0, ignore, ignore) # Canse-sensitive search -AT_CHECK([$SCITECO -e "@I/XXX/J -^X @:S/xxx/\"S(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@I/XXX/J -^X @:S/xxx/"S(0/0)']], 0, ignore, ignore) # Search mode should be local to the macro frame. -AT_CHECK([$SCITECO -e "-^X @^Um{^X} Mm-0\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/XYZ/ J ::@S/X/\"F(0/0)' H::@S/Z/\"S(0/0)'"], 0, ignore, ignore) +TE_CHECK([[-^X @^Um{^X} Mm-0"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/XYZ/ J ::@S/X/"F(0/0)' H::@S/Z/"S(0/0)']], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Searches over buffer boundaries]) +TE_CHECK([[@I/XYZ/J @EB/foo/ @I/XZY/J @:N/Z/"F(0/0)' Q*-2"N(0/0)' + @:N//"F(0/0)' Q*-1"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/XYZ/J @EB/foo/ @I/XZY/J @:FN/Z/0/"F(0/0)' Q*-2"N(0/0)' + @:FN///"F(0/0)' Q*-1"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Search and insertion ranges]) -AT_CHECK([$SCITECO -e "@I/XXYYZZ/^SC .\"N(0/0)' C @S/YY/^YU1U0 Q0-2\"N(0/0)' Q1-4\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/XXYYZZ/J @S/XX^E[[^EMY]]/ 1^YXa :Qa-2\"N(0/0)'"], 0, ignore, ignore) +# When deleting characters, the result of ^S/^Y must not change. +TE_CHECK([[@I/XXYYZZ/^SC ."N(0/0)' C @S/YY/ HK ^YU1U0 Q0-2"N(0/0)' Q1-4"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/XXYYZZ/J @S/XX^E[^EMY]/ 1^YXa :Qa-2"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/XXYYZZ/J @FD/^EMZ/ ^S+2"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/ABCDEF/J @^U-/1234/ @FR/ABC// ^S+4"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/ABCDEF/J @FS/ABC/1234/ ^S+4"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@^Ua/XYZ/ Ga ^S+3"N(0/0)']], 0, ignore, ignore) +# NOTE: EN currently inserts another trailing linefeed. +TE_CHECK([[@EN/*/XYZ/ ^S+4"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Editing local registers in macro calls]) -AT_CHECK([$SCITECO -e '@^Ua{@EQ.x//} :Ma @^U.x/FOO/'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '@^Ua{@EQ.x//} Ma @^U.x/FOO/'], 1, ignore, ignore) -AT_CHECK([$SCITECO -e '@^Ua{@EQ.x// Mb Q*U*} Ma'], 0, ignore, ignore) +TE_CHECK([[@^Ua{@EQ.x//} :Ma @^U.x/FOO/]], 0, ignore, ignore) +TE_CHECK([[@^Ua{@EQ.x//} Ma @^U.x/FOO/]], 1, ignore, ignore) +TE_CHECK([[@^Ua{@EQ.x// Mb Q*U*} Ma]], 0, ignore, ignore) +AT_CLEANUP + +# This is also for detecting leaks under Valgrind. +AT_SETUP([Unterminated commands]) +TE_CHECK([[G[foo^Q] ]], 1, ignore, ignore) +TE_CHECK([[!foo ]], 1, ignore, ignore) +TE_CHECK([[^Ua ]], 1, ignore, ignore) +TE_CHECK([[EGa ]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Loading files into Q-Registers]) -AT_CHECK([$SCITECO -e "@I/../ @EW/loadqreg.txt/ @EQa/loadqreg.txt/ :Qa-2\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@I/../ @EW/loadqreg.txt/ @EQa/loadqreg.txt/ :Qa-2"N(0/0)']], 0, ignore, ignore) # Does the same as FG..$. Afterwards, the parent directory should be shorter. -AT_CHECK([$SCITECO -e ":Q\$Ul @EQ\$/loadqreg.txt/ :Q\$-Ql+1\">(0/0)'"], 0, ignore, ignore) +TE_CHECK([[:Q$Ul @EQ$/loadqreg.txt/ :Q$-Ql+1">(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Saving Q-Registers contents to files]) -AT_CHECK([$SCITECO -e "@^Ua/test/ @E%a/saveqreg.txt/ @EB/saveqreg.txt/ Z-4\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@E%\$/saveqreg.txt/ @EB/saveqreg.txt/ Z-:Q\$\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@^Ua/test/ @E%a/saveqreg.txt/ @EB/saveqreg.txt/ Z-4"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@E%$/saveqreg.txt/ @EB/saveqreg.txt/ Z-:Q$"N(0/0)']], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Saving documents]) +TE_CHECK([[@I/test/ @EW/savebuf.txt/ :Q*"=(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/test/ @EB/foo/ 1@EW/savebuf.txt/]], 0, ignore, ignore) +AT_CHECK([test `wc -c <savebuf.txt` -eq 4], 0, ignore, ignore) +TE_CHECK([[@EQa// @I/XYZ/ @EW/saveqreg.txt/ @EB/saveqreg.txt/ ::@S/XYZ/"F(0/0)']], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Opening/closing buffers]) +TE_CHECK([[@EB/foo/ @I/XXX/ -EF :Q*"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@EB/foo/ @I/XXX/ :EF :Q*"N(0/0)' @EB/foo/ Z-3"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@EB/foo/ 1EF :Q*"=(0/0)']], 0, ignore, ignore) +# Open by id +TE_CHECK([[1@EB//]], 0, ignore, ignore) +TE_CHECK([[1@EB/foo/]], 1, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Read file into current buffer]) +AT_DATA([test.txt], [[0123456789 +]]) +TE_CHECK([[@I/Helloworld/5R @ER"test.txt" .-5-11"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([8-bit cleanliness]) -AT_CHECK([$SCITECO -e "0@I//J 0A\"N(0/0)' :@S/^@/\"F(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@EQa//0EE 1U*0EE 0:@EUa/f^@^@/ :Qa-4\"N(0/0)' Ga Z-4\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "0EE 129@I// -A-129\"N(0/0)' HXa @EQa// EE\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -8e "129@:^Ua// 0Qa-129\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "1EE 167Ua @I/^EUa/ .-1\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -8e "194Ua Qa@I//J :@S/^EUa/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK([[0@I//J 0A"N(0/0)' :@S/^@/"F(0/0)']], 0, ignore, ignore) +TE_CHECK([[@EQa//0EE 1U*0EE 0:@EUa/f^@^@/ :Qa-4"N(0/0)' Ga Z-4"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[0EE 129@I// -A-129"N(0/0)' HXa @EQa// EE"N(0/0)']], 0, ignore, ignore) +AT_CHECK([[$SCITECO -8e "129@:^Ua// 0Qa-129\"N(0/0)'"]], 0, ignore, ignore) +TE_CHECK([[1EE 167Ua @I/^EUa/ .-1"N(0/0)']], 0, ignore, ignore) +AT_CHECK([[$SCITECO -8e "194Ua Qa@I//J :@S/^EUa/\"F(0/0)'"]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Unicode]) -AT_CHECK([$SCITECO -e "8594@I/Здравствуй, мир!/ Z-17\"N(0/0)' J0A-8594\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "8594@^Ua/Здравствуй, мир!/ :Qa-17\"N(0/0)' 0Qa-8594\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/Здравствуй, мир!/ JW .-12\"N(0/0)' ^E-22\"N(0/0)' 204:EE .-12\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@I/TEST/ @EW/юникод.txt/"], 0, ignore, ignore) -AT_CHECK([test -f юникод.txt], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "^^ß-223\"N(0/0) 23Uъ Q[Ъ]-23\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@O/метка/ !метка!"], 0, ignore, ignore) +TE_CHECK([[8594,8592@I/Здравствуй, мир!/ Z-18"N(0/0)' J0A-8594"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[8594,8592@^Ua/Здравствуй, мир!/ :Qa-18"N(0/0)' 0Qa-8594"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/Здравствуй, мир!/ JW .-12"N(0/0)' ^E-22"N(0/0)' 204:EE .-12"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/TEST/ @EW/юникод.txt/]], 0, ignore, ignore) +AT_CHECK([[test -f юникод.txt]], 0, ignore, ignore) +TE_CHECK([[^^ß-223"N(0/0)' 23Uъ Q[Ъ]-23"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@O/метка/ !метка!]], 0, ignore, ignore) + +# Test the "error" return codes of <A>: +TE_CHECK([[0EE 255@I/A/J 65001EE 0A-(-2)"N(0/0)' 1A-^^A"N(0/0)' 2A-(-1)"N(0/0)']], 0, ignore, ignore) +# FIXME: Byte 128 should probably return -3 (incomplete sequence). +TE_CHECK([[@EQa// 0EE 128@I/A/J 65001EE 0Qa-(-2)"N(0/0)' 1Qa-^^A"N(0/0)' 2Qa-(-1)"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Automatic EOL normalization]) -AT_CHECK([$SCITECO -e "@EB'${srcdir}/autoeol-input.txt' EL-2\"N(0/0)' 2LR 13@I'' 0EL @EW'autoeol-sciteco.txt'"], +TE_CHECK([[@EB'^EQ[$srcdir]/autoeol-input.txt' EL-2"N(0/0)' 2LR 13@I'' 0EL @EW'autoeol-sciteco.txt']], 0, ignore, ignore) -AT_CHECK([cmp autoeol-sciteco.txt ${srcdir}/autoeol-output.txt], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "@EB'autoeol-sciteco.txt' EL-0\"N(0/0)' 2EL @EW''"], 0, ignore, ignore) -AT_CHECK([cmp autoeol-sciteco.txt ${srcdir}/autoeol-input.txt], 0, ignore, ignore) +AT_CHECK([[cmp autoeol-sciteco.txt ${srcdir}/autoeol-output.txt]], 0, ignore, ignore) +TE_CHECK([[@EB'autoeol-sciteco.txt' EL-0"N(0/0)' 2EL @EW'']], 0, ignore, ignore) +AT_CHECK([[cmp autoeol-sciteco.txt ${srcdir}/autoeol-input.txt]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Memory limiting]) -AT_CHECK([$SCITECO -e "50*1000*1000,2EJ <@<:@a>"], 1, ignore, ignore) +# FIXME: Requires too much time and memory at least when running in a CI job on my fmsbw.de server +# (.fmsbw/10-freebsd14-sciteco). +AT_SKIP_IF([test $at_arg_valgrind != false]) +TE_CHECK([[50*1000*1000,2EJ <[a> !]!]], 1, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Change working directory]) +TE_CHECK([[:Q$Ul @FG'..' Ql-:Q$-1"<(0/0)']], 0, ignore, ignore) +TE_CHECK([[:Q$Ul :@^U$'/..' Ql-:Q$-1"<(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Execute external command]) # TODO: It would be a better test to generate a random number of bytes. # Unfortunately, neither $RANDOM, shuf nor jot are portable. # So we have to wait until SciTECO supports a random number generator. -AT_CHECK([$SCITECO -e "@EC'dd if=/dev/zero bs=512 count=1' Z= Z-512\"N(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e "0,128ED @EC'dd if=/dev/zero bs=512 count=1' Z= Z-512\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[@EC'dd if=/dev/zero bs=512 count=1' Z= Z-512"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[0,128ED @EC'dd if=/dev/zero bs=512 count=1' Z= Z-512"N(0/0)']], 0, ignore, ignore) +TE_CHECK([[@I/hello/ H@EC'tr a-z A-Z' J<0A"V(0/0)' :C;>]], 0, ignore, ignore) +TE_CHECK([[@I/hello^J/ -@EC'tr a-z A-Z' J<0A"V(0/0)' :C;>]], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Timestamps]) +# TODO: Test the date (^B) and time (^H and :^H) variants as well. +TE_CHECK([[::^HUt 100^W (::^H-Qt)-100"<(0/0)']], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Program version]) +TE_CHECK([[EO/10000\@I"." EO/100^/100\@I"." EO^/100\ HT]], 0, stdout, ignore) +AT_FAIL_IF([test "`cat stdout`" != "$SCITECO_VERSION"]) AT_CLEANUP # @@ -236,68 +419,118 @@ AT_CLEANUP # AT_SETUP([Rub out with immediate editing commands]) # Must rub out @, but not the colon from the Q-Reg specification. -AT_CHECK([$SCITECO_CMDLINE "Q:@I/XXX/ ${RUBOUT_WORD}{Z-2\"N(0/0)'}"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[Q:@I/XXX/ ]]TE_RUBOUT_WORD[[{Z-2"N(0/0)'}]], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) # Should not rub out @ and : characters. -AT_CHECK([$SCITECO_CMDLINE "@I/ @:foo ${RUBOUT_WORD}/ Z-3\"N(0/0)'"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[@I/ @:foo ]]TE_RUBOUT_WORD[[/ Z-3"N(0/0)']], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) AT_CLEANUP +AT_SETUP([Disallowed interactive commands]) +# Command-line termination while editing the replacement register would +# be hard to recover from. +TE_CHECK_CMDLINE([[{$$}]], 0, ignore, stderr) +AT_FAIL_IF([! $GREP "^Error:" stderr]) +# ^C interruption should not terminate the command-line accidentally. +TE_CHECK_CMDLINE([[^C]], 0, ignore, stderr) +AT_FAIL_IF([! $GREP "^Error:" stderr]) +# ^C^C is generally disallowed in interactive mode. +TE_CHECK_CMDLINE([[@^Um{^C^C} Mm]], 0, ignore, stderr) +AT_FAIL_IF([! $GREP "^Error:" stderr]) +AT_CLEANUP + +AT_BANNER([Standard library]) + +AT_SETUP([Option parser]) +AT_CHECK([[$SCITECO -e "@EI'^EQ[\$SCITECOPATH]/getopt.tes' @^U[optstring]/XY/ M[getopt]-3\"N(0/0)' \ + Q[getopt.X]+1\"N(0/0)' :Q[^A3]-2\"N(0/0)' :Q[^A4]-6\"N(0/0)'" \ + -- -X -- -Y foobar]], 0, ignore, ignore) +AT_CHECK([[$SCITECO -e "@EI'^EQ[\$SCITECOPATH]/getopt.tes' @^U[optstring]/F:z::X/ M[getopt]-4\"N(0/0)' \ + Q[getopt.X]+1\"N(0/0)' :Q[getopt.F]-8\"N(0/0)' :Q[getopt.z]\"N(0/0)'" \ + -- -X -Ffilename -z]], 0, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Command line opener]) +AT_DATA([data.123], [0123456789 +9876543210 +]) +AT_CHECK([[$SCITECO -e "@EI'^EQ[\$SCITECOPATH]/opener.tes' M[opener] EF .-13\"N(0/0)'" +2,3 data.123]], 0, ignore, ignore) +AT_CHECK([[$SCITECO -e "@EI'^EQ[\$SCITECOPATH]/opener.tes' M[opener] EF .-11\"N(0/0)'" data.123:2]], 0, ignore, ignore) +# `-S` stops processing of special arguments +AT_CHECK([[$SCITECO -e "@EI'^EQ[\$SCITECOPATH]/opener.tes' M[opener] EF EJ-2\"N(0/0)'" -S +1 data.123]], 0, ignore, ignore) +AT_CLEANUP + +# FIXME: Does not work on OBS runners, perhaps because of Xvfb? +# It's not reproducible on FreeBSD. +#AT_SETUP([Detaching from terminal]) +#AT_SKIP_IF([$SCITECO --help | $GREP -qvz -e '--detach']) +#AT_CHECK([[timeout 10 $SHELL -c "$SCITECO --detach -e '23= @EW/test.txt/' && while [ ! -f test.txt ]; do sleep 1; done"]], 0, [], []) +#AT_CLEANUP + AT_BANNER([Regression Tests]) AT_SETUP([Glob patterns with character classes]) # Also checks closing brackets as part of the character set. -AT_CHECK([$SCITECO -e ":@EN/*.[[@:>@ch]]/foo.h/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK([[![! :@EN/*.[]ch]/foo.h/"F(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Glob patterns with unclosed trailing brackets]) -AT_CHECK([$SCITECO -e ":@EN/*.@<:@h/foo.@<:@h/\"F(0/0)'"], 0, ignore, ignore) +TE_CHECK([[:@EN/*.[h/foo.[h/"F(0/0)' !]]!]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Searching with large counts]) # Even though the search will be unsuccessful, it will not be considered # a proper error, so the process return code is still 0. -AT_CHECK([$SCITECO -e "2147483647@S/foo/"], 0, ignore, ignore) +TE_CHECK([[2147483647@S/foo/]], 0, ignore, ignore) # Will always break the memory limit which is considered an error. -AT_CHECK([$SCITECO -e "-2147483648@S/foo/"], 1, ignore, ignore) +TE_CHECK([[-2147483648@S/foo/]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Search on new empty document]) -AT_CHECK([$SCITECO -e ":@S/foo/\"S(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e ":@N/foo/\"S(0/0)'"], 0, ignore, ignore) +TE_CHECK([[:@S/foo/"S(0/0)']], 0, ignore, ignore) +TE_CHECK([[:@N/foo/"S(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Search for one of characters in uninitialized Q-Register]) # Register "a" exists, but it's string part is yet uninitialized. -AT_CHECK([$SCITECO -e ":@S/^EGa/\"S(0/0)'"], 0, ignore, ignore) +TE_CHECK([[:@S/^EGa/"S(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Search accesses wrong Q-Register table]) -AT_CHECK([$SCITECO -e '@^U.#xx/123/ @^Um{:@S/^EG.#xx/$} :Mm Mm'], 1, ignore, ignore) +TE_CHECK([[@^U.#xx/123/ @^Um{:@S/^EG.#xx/$} :Mm Mm]], 1, ignore, ignore) +AT_CLEANUP + +AT_SETUP([Invalid buffer ids]) +TE_CHECK([[42@EB//]], 1, ignore, ignore) +TE_CHECK([[23@EW//]], 1, ignore, ignore) +TE_CHECK([[11EF]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Memory limiting during spawning]) +# FIXME: Requires too much time and memory at least when running in a CI job on my fmsbw.de server +# (.fmsbw/10-freebsd14-sciteco). +AT_SKIP_IF([test $at_arg_valgrind != false]) # This might result in an OOM if memory limiting is not working -AT_CHECK([$SCITECO -e "50*1000*1000,2EJ 0,128ED @EC'dd if=/dev/zero'"], 1, ignore, ignore) +TE_CHECK([[50*1000*1000,2EJ 0,128ED @EC'dd if=/dev/zero']], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Memory limiting during file reading]) -AT_CHECK([dd if=/dev/zero of=big-file.txt bs=1000 count=50000], 0, ignore, ignore) -AT_CHECK([$SCITECO -8e "50*1000*1000,2EJ @EB'big-file.txt'"], 1, ignore, ignore) +AT_CHECK([[dd if=/dev/zero of=big-file.txt bs=1000 count=50000]], 0, ignore, ignore) +AT_CHECK([[$SCITECO -8e '50*1000*1000,2EJ @EB"big-file.txt"']], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Q-Register stack cleanup]) -AT_CHECK([$SCITECO -e '@<:@a'], 0, ignore, ignore) +TE_CHECK([[ [a !]!]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Uninitialized "_"-register]) -AT_CHECK([$SCITECO -e ":@S//\"S(0/0)'"], 0, ignore, ignore) -AT_CHECK([$SCITECO -e ":@EN///\"S(0/0)'"], 0, ignore, ignore) +TE_CHECK([[:@S//"S(0/0)']], 0, ignore, ignore) +TE_CHECK([[:@EN///"S(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Uninitialized Q-Register in string building]) -AT_CHECK([$SCITECO -e '@I/^E@a/'], 0, ignore, ignore) -AT_CHECK([$SCITECO -e '@I/^ENa/'], 0, ignore, ignore) +TE_CHECK([[@I/^E@a/]], 0, ignore, ignore) +TE_CHECK([[@I/^ENa/]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Setting special Q-Registers with EU]) @@ -305,36 +538,40 @@ AT_SETUP([Setting special Q-Registers with EU]) # should not influence the clipboard (and it's not in Curses anyway). # # Should fail, but not crash -AT_CHECK([$SCITECO -e '@EU*""'], 1, ignore, ignore) -AT_CHECK([$SCITECO -e '@EU$"."'], 0, ignore, ignore) +TE_CHECK([[@EU*""]], 1, ignore, ignore) +TE_CHECK([[@EU$"."]], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Empty help topic]) -AT_CHECK([$SCITECO -e '@?//'], 1, ignore, ignore) +# FIXME: Produces a false positive under Valgrind +# due to the value of $SCITECOPATH. +TE_CHECK([[@?//]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Empty lexer name]) -AT_CHECK([$SCITECO -e '@ES/SETILEXER//'], 1, ignore, ignore) +TE_CHECK([[@ES/SETILEXER//]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Empty command string]) -AT_CHECK([$SCITECO -e '@EC//'], 1, ignore, ignore) -AT_CHECK([$SCITECO -e '@EGa//'], 1, ignore, ignore) +TE_CHECK([[@EC//]], 1, ignore, ignore) +TE_CHECK([[@EGa//]], 1, ignore, ignore) AT_CLEANUP AT_SETUP([Jump to beginning of macro]) -AT_CHECK([$SCITECO -e "%a-2\"< F< ' Qa-2\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[%a-2"< F< ' Qa-2"N(0/0)']], 0, ignore, ignore) AT_CLEANUP AT_SETUP([Gotos and labels]) # Not a label redefinition, there must not even be a warning. -AT_CHECK([$SCITECO -e '2<!foo!>'], 0, ignore, stderr) +TE_CHECK([[2<!foo!>]], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Warning:" stderr]) # Will print a warning about label redefinition, though... -AT_CHECK([$SCITECO -e "!foo! Qa\"S^C' !foo! Qa\"S(0/0)' -Ua @O/foo/"], 0, ignore, ignore) +TE_CHECK([[!foo! Qa"S^C' !foo! Qa"S(0/0)' -Ua @O/foo/]], 0, ignore, ignore) # This should not leak memory or cause memory corruptions when running under # Valgrind or Asan: -AT_CHECK([$SCITECO_CMDLINE "!foo!{-5D}"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[!foo!{-5D}]], 0, ignore, stderr) +# FIXME: Could leak memory, but we cannot detect that easily. +#TE_CHECK([[!foo]], 1, ignore, ignore) AT_CLEANUP # @@ -342,32 +579,49 @@ AT_CLEANUP # See above for rules. # AT_SETUP([Rub out string append]) -AT_CHECK([$SCITECO_CMDLINE "@I/XXX/ H:Xa{-4D} :Qa-0\"N(0/0)'"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[@I/XXX/ H:Xa{-4D} :Qa-0"N(0/0)']], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) AT_CLEANUP AT_SETUP([Rub out of empty forward kill]) -AT_CHECK([$SCITECO_CMDLINE "@I/F/ J @I/X/ @FK/F/{-6D} Z-2\"N(0/0)'"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[@I/F/ J @I/X/ @FK/F/{-6D} Z-2"N(0/0)']], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) AT_CLEANUP AT_SETUP([Rub out Q-Register specifications]) # This was causing memory corruptions, that would at least show up under Valgrind. -AT_CHECK([$SCITECO_CMDLINE "GaGb{-4D}"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[GaGb{-4D}]], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) AT_CLEANUP AT_SETUP([Restore flags after rub out]) # Must throw an error if the @ flag is restored properly. -AT_CHECK([$SCITECO_CMDLINE '0@W{-D}C'], 0, ignore, stderr) +TE_CHECK_CMDLINE([[0@W{-D}C]], 0, ignore, stderr) AT_FAIL_IF([! $GREP "^Error:" stderr]) AT_CLEANUP +AT_SETUP([Rub out stack operations in macro calls]) +# This was causing memory corruptions, that would at least show up under Valgrind. +TE_CHECK_CMDLINE([[@^Um{[.a].b}Mm{-2D}]], 0, ignore, stderr) +AT_FAIL_IF([$GREP "^Error:" stderr]) +TE_CHECK_CMDLINE([[[.a@^Um{].b}Mm{-2D}]], 0, ignore, stderr) +AT_FAIL_IF([$GREP "^Error:" stderr]) +AT_CLEANUP + AT_SETUP([Searches from macro calls]) -AT_CHECK([$SCITECO_CMDLINE "@^Um{:@S/XXX/} :Mm\"S(0/0)' Mm\"S(0/0)'"], 0, ignore, stderr) +TE_CHECK_CMDLINE([[@^Um{:@S/XXX/} :Mm"S(0/0)' Mm"S(0/0)']], 0, ignore, stderr) AT_FAIL_IF([$GREP "^Error:" stderr]) AT_CLEANUP +AT_SETUP([Overwriting builtin registers]) +# Initializes registers in batch mode, which are later replaced during startup +# of interactive mode. +# This was causing assertion errors. +AT_DATA([test.tec], [[23U^[ !]! 23U~ +]]) +AT_CHECK([[$SCITECO --fake-cmdline '' --mung test.tec]], 0, ignore, ignore) +AT_CLEANUP + AT_BANNER([Known Bugs]) AT_SETUP([Number stack]) @@ -375,14 +629,14 @@ AT_SETUP([Number stack]) # will be replaced with proper number parser states, which will also allow for # floating point constants. # With the current parser, it is hard to even interpret the following code correctly... -AT_CHECK([$SCITECO -e "(12)3 - 3\"N(0/0)'"], 0, ignore, ignore) +TE_CHECK([[(12)3 - 3"N(0/0)']], 0, ignore, ignore) AT_XFAIL_IF(true) AT_CLEANUP AT_SETUP([Dangling Else/End-If]) # Should throw syntax errors. -AT_CHECK([$SCITECO -e "'"], 1, ignore, ignore) -AT_CHECK([$SCITECO -e "| (0/0) '"], 1, ignore, ignore) +TE_CHECK([[']], 1, ignore, ignore) +TE_CHECK([[| (0/0) ']], 1, ignore, ignore) AT_XFAIL_IF(true) AT_CLEANUP @@ -393,7 +647,7 @@ AT_CLEANUP ## Should no longer dump core. ## It could fail because the memory limit is exceeed, ## but not in this case since the match string isn't too large. -#AT_CHECK([$SCITECO -e '100000<@I"X">J @S"^EM^X"'], 0, ignore, ignore) +#TE_CHECK([[100000<@I"X">J @S"^EM^X"]], 0, ignore, ignore) #AT_XFAIL_IF(true) #AT_CLEANUP @@ -401,13 +655,22 @@ AT_SETUP([Recursion overflow]) # Should no longer dump core. # It could fail because the memory limit is exceeed, # but not in this case since we limit the recursion. -AT_CHECK([$SCITECO -e "@^Um{U.a Q.a-100000\"<%.aMm'} 0Mm"], 0, ignore, ignore) +TE_CHECK([[@^Um{U.a Q.a-100000"<%.aMm'} 0Mm]], 0, ignore, ignore) AT_XFAIL_IF(true) AT_CLEANUP AT_SETUP([Rub out from empty string argument]) # Should rub out the modifiers as well. -AT_CHECK([$SCITECO_CMDLINE ":@^Ua/${RUBOUT_WORD}{Z\"N(0/0)'}"], 0, ignore, stderr) -AT_CHECK([$GREP "^Error:" stderr], 0, ignore ignore) +# Will currently fail because it tries to execute `:@{`. +TE_CHECK_CMDLINE([[:@^Ua/]]TE_RUBOUT_WORD[[{Z"N(0/0)'}]], 0, ignore, stderr) +AT_CHECK([[! $GREP "^Error:" stderr]], 0, ignore, ignore) +AT_XFAIL_IF(true) +AT_CLEANUP + +AT_SETUP([Command-line termination]) +# Everything after the $$ should be preserved. +AT_DATA([expout], [[1234 +]]) +TE_CHECK_CMDLINE([[{@I/$$1234=/}]], 0, expout, ignore) AT_XFAIL_IF(true) AT_CLEANUP diff --git a/www/build.tes b/www/build.tes index 84f5521..848a63c 100755 --- a/www/build.tes +++ b/www/build.tes @@ -1,12 +1,13 @@ #!/usr/local/bin/sciteco -m !* - * Generate the website at https://rhaberkorn.github.io/sciteco + * Generate the website at https://sciteco.fmsbw.de * This reuses content from Markdown and grohtml-generated documents. * Everything else is cross-linked to Sourceforge. - * It must currently be run from the www/ subdirectory of an in-tree-build. - * The HTML manuals must be in ../doc. + * It must currently be run from the www/ subdirectory of the source tree. + * sciteco -m build.tes <builddir> * Required tools: lowdown *! +[[1]][builddir] !* * Perhaps everything should be white on black, like in a terminal? @@ -17,7 +18,7 @@ <html> <head> <title>SciTECO - <Website> Q[title]</title> - <link rel="icon" type="image/x-icon" href="https://raw.githubusercontent.com/rhaberkorn/sciteco/master/ico/sciteco.ico"> + <link rel="icon" type="image/x-icon" href="https://sciteco.fmsbw.de/graphics/sciteco.ico"> <meta name="description" content="Advanced TECO dialect and interactive screen editor based on Scintilla"> <style> @import "https://www.nerdfonts.com/assets/css/webfont.css"; @@ -39,11 +40,11 @@ SciTECO - <Website> <span class="nf nf-md-home"></span> <a href="index.html">Home</a> / <span class="nf nf-md-image"></span> <a href="screenshots.html">Screenshots</a> / - <span class="nf nf-md-floppy_variant"></span> <a href="https://github.com/rhaberkorn/sciteco/releases" target=_blank>Downloads</a> / + <span class="nf nf-md-floppy_variant"></span> <a href="https://sciteco.fmsbw.de/downloads" target=_blank>Downloads</a> / <span class="nf nf-fa-book_atlas"></span> <a href="sciteco.1.html"><b>sciteco</b>(1)</a> / <span class="nf nf-fa-book_bible"></span> <a href="sciteco.7.html"><b>sciteco</b>(7)</a> / - <span class="nf nf-md-alpha_w_box"></span> <a href="https://github.com/rhaberkorn/sciteco/wiki" target=_blank>Wiki</a> / - <span class="nf nf-fa-github_square"></span> <a href="https://github.com/rhaberkorn/sciteco" target=_blank>Github</a> + <span class="nf nf-fa-brain"></span> <a href="https://sciteco.fmsbw.de/knowledge" target=_blank>Knowledge Base</a> / + <span class="nf nf-dev-git"></span> <a href="https://git.fmsbw.de/sciteco" target=_blank>Git</a> </tt> <hr> } @@ -55,8 +56,8 @@ <table width="100%"><tr> <td width="1ch" valign=top><b>*</b></td> <td valign=top><marquee>IThis page was made with SciTECO.<span class=reverse>$</span>-EX<span class=reverse>$$</span></marquee></td> - <td width=56><a href="https://github.com/rhaberkorn/sciteco/issues" target=_blank> - <img src="https://sciteco.sf.net/graphics/notbug.gif" title="There are no bugs. Go away."> + <td width=56><a href="mailto:hackers@fmsbw.de" target=_blank> + <img src="https://sciteco.fmsbw.de/graphics/notbug.gif" title="There are no bugs. Go away."> </a></td> </tr></table> </tt> @@ -82,14 +83,18 @@ EW <p class="nf nf-fa-warning"> This documents the project's HEAD revision.</p> <div class="grohtml"> -EB../doc/sciteco.1.html +!* + * FIXME: Support out-of-tree builds. + * Perhaps pass in the biuld directory. + *! +EBQ[builddir]/doc/sciteco.1.html S<body>S<h1 L 0,.K [title]sciteco(1) M[header] G[grohtml-header] FD<hr>S</body> .,ZK M[footer] EWsciteco.1.html -EB../doc/sciteco.7.html +EBQ[builddir]/doc/sciteco.7.html S<body>S<h1 L 0,.K [title]sciteco(7) M[header] G[grohtml-header] FD<hr>S</body> .,ZK @@ -101,21 +106,21 @@ EWsciteco.7.html * but still postprocessed for consinstency. *! -EB../doc/grosciteco.tes.1.html +EBQ[builddir]/doc/grosciteco.tes.1.html S<body>S<h1 L 0,.K [title]grosciteco.tes(1) M[header] G[grohtml-header] FD<hr>S</body> .,ZK M[footer] EWgrosciteco.tes.1.html -EB../doc/tedoc.tes.1.html +EBQ[builddir]/doc/tedoc.tes.1.html S<body>S<h1 L 0,.K [title]tedoc.tes(1) M[header] G[grohtml-header] FD<hr>S</body> .,ZK M[footer] EWtedoc.tes.1.html -EB../doc/tutorial.html +EBQ[builddir]/doc/tutorial.html S<body>S<h1 L 0,.K [title]Tutorial M[header] G[grohtml-header] FD<hr>S</body> .,ZK diff --git a/www/screenshots.md b/www/screenshots.md index d0a3651..df8925a 100644 --- a/www/screenshots.md +++ b/www/screenshots.md @@ -2,30 +2,30 @@ ## v2.4.0 - + ## v2.3.0 - + ## v2.1.0 - + -<img src="https://sciteco.sf.net/screenshots/v2.1.0-freebsd-ncurses.png" width="921" alt="FreeBSD/ncurses, Unicode icons" title="FreeBSD/ncurses, Unicode icons"/> +<img src="https://sciteco.fmsbw.de/screenshots/v2.1.0-freebsd-ncurses.png" width="921" alt="FreeBSD/ncurses, Unicode icons" title="FreeBSD/ncurses, Unicode icons"/> ## v2.1 (dev) - + ## v0.7 (dev) - + - + - + ## v0.5 - + |
