├── .github └── workflows │ └── cmake.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── COPYRIGHT ├── README.md ├── deps ├── CMakeLists.txt └── vterm │ └── CMakeLists.txt ├── include ├── pch.h └── tvterm │ ├── array.h │ ├── consts.h │ ├── debug.h │ ├── mutex.h │ ├── pty.h │ ├── termctrl.h │ ├── termemu.h │ ├── termframe.h │ ├── termview.h │ ├── termwnd.h │ └── vtermemu.h └── source ├── tvterm-core ├── debug.cc ├── pty.cc ├── termctrl.cc ├── termframe.cc ├── termview.cc ├── termwnd.cc ├── util.h └── vtermemu.cc └── tvterm ├── app.cc ├── app.h ├── apputil.h ├── cmds.h ├── desk.cc ├── desk.h ├── wnd.cc └── wnd.h /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-linux-gcc7: 7 | name: Linux (GCC 7) 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | submodules: 'recursive' 13 | 14 | - name: Install Dependencies 15 | run: | 16 | # Add legacy repositories required by g++-7 17 | sudo tee --append /etc/apt/sources.list << EOF 18 | deb http://us.archive.ubuntu.com/ubuntu/ bionic universe 19 | deb http://us.archive.ubuntu.com/ubuntu/ bionic main 20 | EOF 21 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 22 | sudo apt-get update 23 | sudo apt-get -y install g++-7 24 | 25 | - name: Configure CMake 26 | shell: bash 27 | env: 28 | CC: gcc-7 29 | CXX: g++-7 30 | run: cmake . -DCMAKE_BUILD_TYPE=Release 31 | 32 | - name: Build 33 | shell: bash 34 | run: cmake --build . -j$(nproc) 35 | 36 | build-linux-clang: 37 | name: Linux (Clang) 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | with: 42 | submodules: 'recursive' 43 | 44 | - name: Configure CMake 45 | shell: bash 46 | env: 47 | CC: clang 48 | CXX: clang++ 49 | run: cmake . -DCMAKE_BUILD_TYPE=Release -DTVTERM_OPTIMIZE_BUILD=OFF 50 | 51 | - name: Build 52 | shell: bash 53 | run: cmake --build . -j$(nproc) 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | # Manually include root-level directories 4 | !deps/ 5 | !deps/**/ 6 | !include/ 7 | !include/**/ 8 | !source/ 9 | !source/**/ 10 | !.github/ 11 | !.github/**/ 12 | 13 | !*.* 14 | *~/ 15 | .* 16 | !.git* 17 | *.~* 18 | *.o 19 | *.bkp 20 | *.[bB][aA][kK] 21 | *.[dD][sS][wW] 22 | *.[cC][sS][mM] 23 | *.[eE][xX][eE] 24 | *.[lL][iI][bB] 25 | *.[oO][bB][jJ] 26 | *.[dD][sS][tT] 27 | *.[dD][sS][kK] 28 | *.[pP][rR][jJ] 29 | *.[mM][aA][pP] 30 | *.[cC][fF][gG] 31 | *.[iI][nN][cC] 32 | *.[cC]32 33 | *.[tT][dD] 34 | *.[tT][rR] 35 | *.[tT][rR]2 36 | *.[fhFH]16 37 | *.[fhFH]32 38 | *.kate-swp 39 | *.a 40 | *.ilk 41 | *.pdb 42 | *.ninja 43 | *.lwi 44 | *.log 45 | perf.data* 46 | callgrind.out* 47 | 48 | *.kdev4 49 | *.vcxproj* 50 | *.sln 51 | 52 | CMakeCache.txt 53 | CMakeFiles 54 | CMakeScripts 55 | Makefile 56 | cmake_install.cmake 57 | compile_commands.json 58 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tvision"] 2 | path = deps/tvision 3 | url = https://github.com/magiblot/tvision.git 4 | [submodule "deps/vterm/libvterm"] 5 | path = deps/vterm/libvterm 6 | url = https://github.com/magiblot/libvterm.git 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | option(TVTERM_BUILD_APP "Build main application" ON) 4 | option(TVTERM_USE_SYSTEM_TVISION "Use system-wide Turbo Vision instead of the submodule" OFF) 5 | option(TVTERM_USE_SYSTEM_LIBVTERM "Use system-wide libvterm instead of the submodule" OFF) 6 | option(TVTERM_OPTIMIZE_BUILD "Enable build optimizations (Unity Build, Precompiled Headers)" ON) 7 | 8 | project(tvterm) 9 | 10 | function(tvterm_set_warnings t) 11 | if (NOT ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC")) 12 | target_compile_options(${t} PRIVATE 13 | -Wall 14 | ) 15 | endif() 16 | endfunction() 17 | 18 | # Dependencies 19 | 20 | include(deps/CMakeLists.txt) 21 | 22 | # Target 'tvterm-core' 23 | 24 | file(GLOB_RECURSE TVTERM_CORE_SRC "${CMAKE_CURRENT_LIST_DIR}/source/tvterm-core/*.cc") 25 | add_library(tvterm-core ${TVTERM_CORE_SRC}) 26 | 27 | tvterm_set_warnings(tvterm-core) 28 | if (HAVE_VTERMSTRINGFRAGMENT) 29 | target_compile_definitions(tvterm-core PRIVATE HAVE_VTERMSTRINGFRAGMENT) 30 | endif() 31 | target_include_directories(tvterm-core PUBLIC 32 | "$" 33 | "$" 34 | ) 35 | target_include_directories(tvterm-core PRIVATE 36 | "${CMAKE_CURRENT_LIST_DIR}/source/tvterm-core" 37 | ) 38 | target_link_libraries(tvterm-core PUBLIC 39 | tvision 40 | vterm 41 | ${SYSTEM_DEPS} 42 | ) 43 | install(TARGETS vterm ${SYSTEM_DEPS} EXPORT tvterm-config) 44 | install(TARGETS tvterm-core 45 | EXPORT tvterm-config 46 | ARCHIVE DESTINATION lib 47 | COMPONENT library 48 | ) 49 | install(EXPORT tvterm-config 50 | DESTINATION lib/cmake/tvterm 51 | NAMESPACE tvterm:: 52 | FILE tvterm-config.cmake 53 | COMPONENT library 54 | ) 55 | install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include/tvterm" DESTINATION include) 56 | 57 | # Target 'tvterm' 58 | 59 | if (TVTERM_BUILD_APP) 60 | file(GLOB_RECURSE TVTERM_SRC "${CMAKE_CURRENT_LIST_DIR}/source/tvterm/*.cc") 61 | add_executable(tvterm ${TVTERM_SRC}) 62 | tvterm_set_warnings(tvterm) 63 | target_link_libraries(tvterm PRIVATE 64 | tvterm-core 65 | ) 66 | install(TARGETS tvterm RUNTIME DESTINATION bin) 67 | endif() 68 | 69 | # Build optimization 70 | 71 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") 72 | if (TVTERM_OPTIMIZE_BUILD) 73 | foreach (t tvterm tvterm-core) 74 | set_target_properties(${t} PROPERTIES UNITY_BUILD ON) 75 | target_precompile_headers(${t} PRIVATE 76 | "${CMAKE_CURRENT_LIST_DIR}/include/pch.h" 77 | ) 78 | endforeach() 79 | endif() 80 | endif() 81 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 magiblot . 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tvterm 2 | 3 | A terminal emulator that runs in your terminal. Powered by Turbo Vision. 4 | 5 | ![htop, turbo and notcurses-demo running in tvterm](https://user-images.githubusercontent.com/20713561/137407902-27538f99-dc0e-47a8-9bf2-0705733d8a8c.png) 6 | 7 | `tvterm` is an experimental terminal emulator widget and application based on the [Turbo Vision](https://github.com/magiblot/tvision) framework. It was created for the purpose of demonstrating new features in Turbo Vision such as 24-bit color support. 8 | 9 | `tvterm` relies on Paul Evan's [libvterm](http://www.leonerd.org.uk/code/libvterm/) terminal emulator, also used by [Neovim](https://github.com/neovim/libvterm) and [Emacs](https://github.com/akermu/emacs-libvterm). 10 | 11 | Additionally, `tvterm` supports both Unix and Windows (Windows 10 1809 or later). 12 | 13 | The original location of this project is https://github.com/magiblot/tvterm. 14 | 15 | # Building 16 | 17 | First of all, you should clone this repository along its submodules with the `--recursive` option of `git clone` (or run `git submodule init && git submodule update` if you have already cloned it). 18 | 19 | Then, make sure the following dependencies are installed: 20 | 21 | * CMake. 22 | * A compiler supporting C++14. 23 | * `libvterm`: 24 | * If you initialized the submodules, you can build `libvterm` as part of `tvterm`. Perl is needed for building `libvterm`. 25 | * Otherwise, a system-provided `libvterm` (e.g. `libvterm-dev` in Ubuntu) can be used if enabling the CMake option `-DTVTERM_USE_SYSTEM_LIBVTERM=ON`. 26 | * [Turbo Vision](https://github.com/magiblot/tvision#build-environment)'s dependencies: 27 | * `libncursesw` (Unix only) (e.g. `libncursesw5-dev` in Ubuntu). 28 | * `libgpm` (optional, Linux only) (e.g. `libgpm-dev` in Ubuntu). 29 | * Turbo Vision itself: 30 | * If you initialized the submodules, you can build Turbo Vision as part of `tvterm`. 31 | * Otherwise, clone [Turbo Vision](https://github.com/magiblot/tvision) separately and follow its [build](https://github.com/magiblot/tvision#build-environment) and [install](https://github.com/magiblot/tvision#build-cmake) instructions. Make sure you don't use a version of Turbo Vision older than the one required by `tvterm` (specified in the [`tvision` submodule](https://github.com/magiblot/tvterm/tree/master/deps)). When building `tvterm`, enable the CMake option `-DTVTERM_USE_SYSTEM_TVISION=ON`. 32 | 33 | `tvterm` can be built with the following commands: 34 | 35 | ```sh 36 | cmake . -B ./build -DCMAKE_BUILD_TYPE=Release && # Could also be 'Debug', 'MinSizeRel' or 'RelWithDebInfo'. 37 | cmake --build ./build 38 | ``` 39 | 40 | CMake versions older than 3.13 may not support the `-B` option. You can try the following instead: 41 | 42 | ```sh 43 | mkdir -p build; cd build 44 | cmake .. -DCMAKE_BUILD_TYPE=Release && 45 | cmake --build . 46 | ``` 47 | 48 | # Features 49 | 50 | This project is still WIP. Some features it may achieve at some point are: 51 | 52 | - [x] UTF-8 support. 53 | - [x] fullwidth and zero-width character support. 54 | - [x] 24-bit color support. 55 | - [x] Windows support. 56 | - [ ] Scrollback. 57 | - [ ] Text selection. 58 | - [ ] Find text. 59 | - [ ] Send signal to child process. 60 | - [ ] Text reflow on resize. 61 | - [ ] Having other terminal emulator implementations to choose from. 62 | - [ ] Better dependency management. 63 | -------------------------------------------------------------------------------- /deps/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | function(tvterm_find_library name) 2 | find_library(${name}_LIB ${name}) 3 | if (NOT ${name}_LIB) 4 | message(FATAL_ERROR "Library '${name}' not found") 5 | else() 6 | message(STATUS "Found '${name}': ${${name}_LIB}") 7 | endif() 8 | add_library(${name} INTERFACE) 9 | target_link_libraries(${name} INTERFACE ${${name}_LIB}) 10 | endfunction() 11 | function(tvterm_find_library_and_header name header) 12 | tvterm_find_library(${name}) 13 | find_path(${name}_INCLUDE ${header}) 14 | if (NOT ${name}_INCLUDE) 15 | message(FATAL_ERROR "'${name}' development headers not found: missing '${header}'") 16 | endif() 17 | target_include_directories(${name} INTERFACE ${${name}_INCLUDE}) 18 | endfunction() 19 | 20 | set(SYSTEM_DEPS) 21 | 22 | if (NOT WIN32) 23 | tvterm_find_library(util) 24 | tvterm_find_library(pthread) 25 | set(SYSTEM_DEPS util pthread) 26 | endif() 27 | 28 | if (TVTERM_USE_SYSTEM_LIBVTERM) 29 | tvterm_find_library_and_header(vterm "vterm.h") 30 | else() 31 | add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/vterm" EXCLUDE_FROM_ALL) 32 | endif() 33 | 34 | set(TV_OPTIMIZE_BUILD ${TVTERM_OPTIMIZE_BUILD}) 35 | if (TVTERM_USE_SYSTEM_TVISION) 36 | find_package(tvision CONFIG REQUIRED) 37 | add_library(tvision ALIAS tvision::tvision) 38 | get_target_property(_TVISION tvision LOCATION) 39 | message(STATUS "Found 'tvision': ${_TVISION}") 40 | else() 41 | add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/tvision" EXCLUDE_FROM_ALL) 42 | endif() 43 | -------------------------------------------------------------------------------- /deps/vterm/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMake script for libvterm originally created by Rui Abreu Ferreira 2 | # (https://github.com/equalsraf/libvterm-win). 3 | 4 | cmake_minimum_required(VERSION 3.5) 5 | project(libvterm LANGUAGES C) 6 | 7 | include(GNUInstallDirs) 8 | find_package(Perl REQUIRED) 9 | 10 | set(VTERM_DIR ${CMAKE_CURRENT_LIST_DIR}/libvterm) 11 | 12 | # Generate includes from tables 13 | file(GLOB TBL_FILES ${VTERM_DIR}/src/encoding/*.tbl) 14 | set(TBL_FILES_HEADERS) 15 | foreach(file ${TBL_FILES}) 16 | get_filename_component(basename ${file} NAME_WE) 17 | set(tname encoding/${basename}.inc) 18 | add_custom_command(OUTPUT 19 | COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/encoding/ 20 | COMMAND ${PERL_EXECUTABLE} -CSD ${VTERM_DIR}/tbl2inc_c.pl ${file} > ${CMAKE_CURRENT_BINARY_DIR}/${tname} 21 | COMMENT "Generating ${tname}" 22 | OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${tname} 23 | ) 24 | list(APPEND TBL_FILES_HEADERS ${CMAKE_CURRENT_BINARY_DIR}/${tname}) 25 | endforeach() 26 | 27 | file(GLOB VTERM_SOURCES ${VTERM_DIR}/src/*.c) 28 | add_library(vterm ${VTERM_SOURCES} ${TBL_FILES_HEADERS}) 29 | target_include_directories(vterm PUBLIC 30 | "$" 31 | "$" 32 | ) 33 | target_include_directories(vterm PRIVATE 34 | ${CMAKE_CURRENT_BINARY_DIR} 35 | ) 36 | 37 | if(MSVC) 38 | target_compile_definitions(vterm PRIVATE 39 | _CRT_SECURE_NO_WARNINGS 40 | _CRT_NONSTDC_NO_DEPRECATE 41 | ) 42 | else() 43 | set_property(TARGET vterm PROPERTY C_STANDARD 99) 44 | set_property(TARGET vterm PROPERTY C_STANDARD_REQUIRED ON) 45 | endif() 46 | -------------------------------------------------------------------------------- /include/pch.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | -------------------------------------------------------------------------------- /include/tvterm/array.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_ARRAY_H 2 | #define TVTERM_ARRAY_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace tvterm 10 | { 11 | 12 | class GrowArray 13 | { 14 | struct 15 | { 16 | char *data; 17 | size_t size; 18 | size_t capacity; 19 | } p {}; 20 | 21 | void grow(size_t minCapacity) noexcept; 22 | 23 | public: 24 | 25 | GrowArray() noexcept = default; 26 | GrowArray(GrowArray &&other) noexcept; 27 | ~GrowArray(); 28 | 29 | GrowArray &operator=(GrowArray other) noexcept; 30 | 31 | char *data() noexcept; 32 | size_t size() noexcept; 33 | void push(const char *aData, size_t aSize) noexcept; 34 | void clear() noexcept; 35 | }; 36 | 37 | inline GrowArray::GrowArray(GrowArray &&other) noexcept 38 | { 39 | p = other.p; 40 | other.p = {}; 41 | } 42 | 43 | inline GrowArray::~GrowArray() 44 | { 45 | free(p.data); 46 | } 47 | 48 | inline GrowArray &GrowArray::operator=(GrowArray other) noexcept 49 | { 50 | auto aux = p; 51 | p = other.p; 52 | other.p = aux; 53 | return *this; 54 | } 55 | 56 | inline char *GrowArray::data() noexcept 57 | { 58 | return p.data; 59 | } 60 | 61 | inline size_t GrowArray::size() noexcept 62 | { 63 | return p.size; 64 | } 65 | 66 | inline void GrowArray::push(const char *aData, size_t aSize) noexcept 67 | { 68 | size_t newSize = p.size + aSize; 69 | if (newSize > p.capacity) 70 | grow(newSize); 71 | memcpy(&p.data[p.size], aData, aSize); 72 | p.size = newSize; 73 | } 74 | 75 | inline void GrowArray::clear() noexcept 76 | { 77 | p.size = 0; 78 | } 79 | 80 | inline void GrowArray::grow(size_t minCapacity) noexcept 81 | { 82 | size_t newCapacity = max(minCapacity, 2*p.capacity); 83 | if (!(p.data = (char *) realloc(p.data, newCapacity))) 84 | abort(); 85 | p.capacity = newCapacity; 86 | } 87 | 88 | } // namespace tvterm 89 | #endif 90 | -------------------------------------------------------------------------------- /include/tvterm/consts.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_CONSTS_H 2 | #define TVTERM_CONSTS_H 3 | 4 | namespace tvterm 5 | { 6 | 7 | struct TVTermConstants 8 | { 9 | ushort cmCheckTerminalUpdates; 10 | ushort cmTerminalUpdated; 11 | // Focused commands 12 | ushort cmGrabInput; 13 | ushort cmReleaseInput; 14 | // Help contexts 15 | ushort hcInputGrabbed; 16 | 17 | TSpan focusedCmds() const 18 | { 19 | return {&cmGrabInput, size_t(&cmReleaseInput + 1 - &cmGrabInput)}; 20 | } 21 | }; 22 | 23 | class TerminalView; 24 | struct TerminalState; 25 | 26 | struct TerminalUpdatedMsg 27 | { 28 | TerminalView &view; 29 | TerminalState &state; 30 | }; 31 | 32 | } // namespace tvterm 33 | 34 | #endif // TVTERM_CONSTS_H 35 | -------------------------------------------------------------------------------- /include/tvterm/debug.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_DEBUG_H 2 | #define TVTERM_DEBUG_H 3 | 4 | #include 5 | 6 | namespace tvterm 7 | { 8 | namespace debug 9 | { 10 | 11 | // Set a breakpoint in debug::breakable and call debug_breakable() 12 | // to trigger it. 13 | void breakable(); 14 | 15 | // Call through volatile pointer is never inlined. 16 | extern void (* volatile const breakable_ptr)(); 17 | 18 | } // namespace debug 19 | 20 | inline void debug_breakable() 21 | { 22 | (*debug::breakable_ptr)(); 23 | } 24 | 25 | class DebugCout 26 | { 27 | 28 | struct NullStreambuf : public std::streambuf 29 | { 30 | int_type overflow(int_type ch) override { return ch; } 31 | }; 32 | 33 | bool enabled; 34 | NullStreambuf nbuf; 35 | std::ostream nout; 36 | 37 | DebugCout(); 38 | 39 | public: 40 | 41 | static DebugCout instance; 42 | 43 | operator std::ostream&(); 44 | 45 | template 46 | std::ostream& operator<<(const T &t); 47 | 48 | }; 49 | 50 | static DebugCout &dout = DebugCout::instance; 51 | 52 | inline DebugCout::operator std::ostream&() 53 | { 54 | if (enabled) 55 | return std::cerr; 56 | return nout; 57 | } 58 | 59 | template 60 | inline std::ostream& DebugCout::operator<<(const T &t) 61 | { 62 | return operator std::ostream&() << t; 63 | } 64 | 65 | } // namespace tvterm 66 | 67 | #include 68 | 69 | namespace tvterm 70 | { 71 | 72 | inline std::ostream &operator<<(std::ostream &os, const TerminalSurface::RowDamage &r) 73 | { 74 | return os << "{" << r.begin << ", " << r.end << "}"; 75 | } 76 | 77 | } // namespace tvterm 78 | 79 | #define Uses_TRect 80 | #include 81 | 82 | namespace tvterm 83 | { 84 | 85 | inline std::ostream &operator<<(std::ostream &os, TPoint p) 86 | { 87 | return os << "{" << p.x << ", " << p.y << "}"; 88 | } 89 | 90 | inline std::ostream &operator<<(std::ostream &os, const TRect &r) 91 | { 92 | return os << "TRect {" << r.a.x << ", " << r.a.y << ", " 93 | << r.b.x << ", " << r.b.y << "}"; 94 | } 95 | 96 | } // namespace tvterm 97 | 98 | #include 99 | 100 | namespace tvterm 101 | { 102 | 103 | inline std::ostream &operator<<(std::ostream &os, const VTermPos &a) 104 | { 105 | return os << "VTermPos { .row = " << a.row << ", .col = " << a.col << " }"; 106 | } 107 | 108 | inline std::ostream &operator<<(std::ostream &os, const VTermRect &a) 109 | { 110 | return os << "VTermRect {" 111 | ".start_col = " << a.start_col << ", .end_col = " << a.end_col << ", " 112 | ".start_row = " << a.start_row << ", .end_row = " << a.end_row << " }"; 113 | } 114 | 115 | inline std::ostream &operator<<(std::ostream &os, const VTermProp &a) 116 | { 117 | const char *s = ""; 118 | switch (a) 119 | { 120 | case VTERM_PROP_CURSORVISIBLE: s = "VTERM_PROP_CURSORVISIBLE"; break; 121 | case VTERM_PROP_CURSORBLINK: s = "VTERM_PROP_CURSORBLINK"; break; 122 | case VTERM_PROP_ALTSCREEN: s = "VTERM_PROP_ALTSCREEN"; break; 123 | case VTERM_PROP_TITLE: s = "VTERM_PROP_TITLE"; break; 124 | case VTERM_PROP_ICONNAME: s = "VTERM_PROP_ICONNAME"; break; 125 | case VTERM_PROP_REVERSE: s = "VTERM_PROP_REVERSE"; break; 126 | case VTERM_PROP_CURSORSHAPE: s = "VTERM_PROP_CURSORSHAPE"; break; 127 | case VTERM_PROP_MOUSE: s = "VTERM_PROP_MOUSE"; break; 128 | case VTERM_N_PROPS: s = "VTERM_N_PROPS"; break; 129 | } 130 | return os << s; 131 | } 132 | 133 | } // namespace tvterm 134 | 135 | #endif // TVTERM_DEBUG_H 136 | -------------------------------------------------------------------------------- /include/tvterm/mutex.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_MUTEX_H 2 | #define TVTERM_MUTEX_H 3 | 4 | #include 5 | 6 | namespace tvterm 7 | { 8 | 9 | template 10 | class Mutex 11 | { 12 | std::mutex m; 13 | T item; 14 | 15 | public: 16 | 17 | template 18 | Mutex(Args&&... args) : 19 | item(static_cast(args)...) 20 | { 21 | } 22 | 23 | template 24 | auto lock(Func &&func) 25 | { 26 | std::lock_guard lk {m}; 27 | return func(item); 28 | } 29 | }; 30 | 31 | } // namespace tvterm 32 | 33 | #endif // TVTERM_MUTEX_H 34 | -------------------------------------------------------------------------------- /include/tvterm/pty.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_PTY_H 2 | #define TVTERM_PTY_H 3 | 4 | #include 5 | 6 | #if !defined(_WIN32) 7 | #include 8 | #else 9 | // Use Turbo Vision's windows.h, which omits several unneeded or unwanted 10 | // definitions. 11 | #include 12 | #endif 13 | 14 | template 15 | class TSpan; 16 | class TPoint; 17 | 18 | namespace tvterm 19 | { 20 | 21 | struct PtyDescriptor 22 | { 23 | #if !defined(_WIN32) 24 | int masterFd; 25 | pid_t clientPid; 26 | #else 27 | HANDLE hMasterRead; 28 | HANDLE hMasterWrite; 29 | HPCON hPseudoConsole; 30 | HANDLE hClientProcess; 31 | #endif 32 | }; 33 | 34 | struct EnvironmentVar 35 | { 36 | const char *name; 37 | const char *value; 38 | }; 39 | 40 | bool createPty( PtyDescriptor &ptyDescriptor, 41 | TPoint size, 42 | TSpan environment, 43 | void (&onError)(const char *reason) ) noexcept; 44 | 45 | class PtyMaster 46 | { 47 | PtyDescriptor d; 48 | 49 | public: 50 | 51 | PtyMaster(PtyDescriptor ptyDescriptor) noexcept; 52 | 53 | bool readFromClient(TSpan data, size_t &bytesRead) noexcept; 54 | bool writeToClient(TSpan data) noexcept; 55 | void resizeClient(TPoint size) noexcept; 56 | void disconnect() noexcept; 57 | }; 58 | 59 | inline PtyMaster::PtyMaster(PtyDescriptor ptyDescriptor) noexcept : 60 | d(ptyDescriptor) 61 | { 62 | } 63 | 64 | } // namespace tvterm 65 | 66 | #endif // TVTERM_PTY_H 67 | -------------------------------------------------------------------------------- /include/tvterm/termctrl.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_TERMCTRL_H 2 | #define TVTERM_TERMCTRL_H 3 | 4 | #define Uses_TPoint 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace tvterm 13 | { 14 | 15 | class TerminalController 16 | { 17 | public: 18 | 19 | // Returns a new-allocated TerminalController. 20 | // On error, invokes the 'onError' callback and returns null. 21 | static TerminalController *create( TPoint size, 22 | TerminalEmulatorFactory &terminalEmulatorFactory, 23 | void (&onError)(const char *reason) ) noexcept; 24 | // Takes ownership over 'this'. 25 | void shutDown() noexcept; 26 | 27 | void sendEvent(const TerminalEvent &event) noexcept; 28 | 29 | bool stateHasBeenUpdated() noexcept; 30 | bool clientIsDisconnected() noexcept; 31 | 32 | template 33 | // This method locks a mutex, so reentrance will lead to a deadlock. 34 | // * 'func' takes a 'TerminalState &' by parameter. 35 | auto lockState(Func &&func); 36 | 37 | private: 38 | 39 | struct TerminalEventLoop; 40 | 41 | PtyMaster ptyMaster; 42 | Mutex terminalState; 43 | TerminalEventLoop &eventLoop; 44 | TerminalEmulator &terminalEmulator; 45 | 46 | std::atomic updated {false}; 47 | std::atomic disconnected {false}; 48 | 49 | std::shared_ptr selfOwningPtr; 50 | 51 | TerminalController(TPoint, TerminalEmulatorFactory &, PtyDescriptor) noexcept; 52 | ~TerminalController(); 53 | }; 54 | 55 | inline bool TerminalController::stateHasBeenUpdated() noexcept 56 | { 57 | return updated.exchange(false) == true; 58 | } 59 | 60 | inline bool TerminalController::clientIsDisconnected() noexcept 61 | { 62 | return disconnected; 63 | } 64 | 65 | template 66 | inline auto TerminalController::lockState(Func &&func) 67 | { 68 | return terminalState.lock([&] (auto &state) { 69 | return func(state); 70 | }); 71 | } 72 | 73 | } // namespace tvterm 74 | 75 | #endif // TVTERM_TERMCTRL_H 76 | -------------------------------------------------------------------------------- /include/tvterm/termemu.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_TERMEMU_H 2 | #define TVTERM_TERMEMU_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define Uses_TDrawSurface 10 | #define Uses_TEvent 11 | #include 12 | 13 | namespace tvterm 14 | { 15 | 16 | class TerminalSurface : private TDrawSurface 17 | { 18 | // A TDrawSurface that can keep track of the areas that were modified. 19 | // Otherwise, the view displaying this surface would have to copy all of 20 | // it every time, which doesn't scale well when using big resolutions. 21 | 22 | public: 23 | 24 | struct RowDamage 25 | { 26 | int begin {INT_MAX}; 27 | int end {INT_MIN}; 28 | }; 29 | 30 | using TDrawSurface::size; 31 | 32 | void resize(TPoint aSize); 33 | void clearDamage(); 34 | using TDrawSurface::at; 35 | RowDamage &damageAtRow(size_t y); 36 | const RowDamage &damageAtRow(size_t y) const; 37 | void addDamageAtRow(size_t y, int begin, int end); 38 | 39 | private: 40 | 41 | std::vector damageByRow; 42 | }; 43 | 44 | inline void TerminalSurface::resize(TPoint aSize) 45 | { 46 | if (aSize != size) 47 | { 48 | TDrawSurface::resize(aSize); 49 | // The surface's contents are not relevant after the resize. 50 | clearDamage(); 51 | } 52 | } 53 | 54 | inline void TerminalSurface::clearDamage() 55 | { 56 | damageByRow.resize(0); 57 | damageByRow.resize(max(0, size.y)); 58 | } 59 | 60 | inline TerminalSurface::RowDamage &TerminalSurface::damageAtRow(size_t y) 61 | { 62 | return damageByRow[y]; 63 | } 64 | 65 | inline const TerminalSurface::RowDamage &TerminalSurface::damageAtRow(size_t y) const 66 | { 67 | return damageByRow[y]; 68 | } 69 | 70 | inline void TerminalSurface::addDamageAtRow(size_t y, int begin, int end) 71 | { 72 | auto &damage = damageAtRow(y); 73 | damage = { 74 | min(begin, damage.begin), 75 | max(end, damage.end), 76 | }; 77 | } 78 | 79 | struct TerminalState 80 | { 81 | TerminalSurface surface; 82 | 83 | bool cursorChanged {false}; 84 | TPoint cursorPos {0, 0}; 85 | bool cursorVisible {false}; 86 | bool cursorBlink {false}; 87 | 88 | bool titleChanged {false}; 89 | GrowArray title; 90 | }; 91 | 92 | enum class TerminalEventType 93 | { 94 | KeyDown, 95 | Mouse, 96 | ClientDataRead, 97 | ViewportResize, 98 | FocusChange, 99 | }; 100 | 101 | struct MouseEvent 102 | { 103 | ushort what; 104 | MouseEventType mouse; 105 | }; 106 | 107 | struct ClientDataReadEvent 108 | { 109 | const char *data; 110 | size_t size; 111 | }; 112 | 113 | struct ViewportResizeEvent 114 | { 115 | int x, y; 116 | }; 117 | 118 | struct FocusChangeEvent 119 | { 120 | bool focusEnabled; 121 | }; 122 | 123 | struct TerminalEvent 124 | { 125 | TerminalEventType type; 126 | union 127 | { 128 | ::KeyDownEvent keyDown; 129 | MouseEvent mouse; 130 | ClientDataReadEvent clientDataRead; 131 | ViewportResizeEvent viewportResize; 132 | FocusChangeEvent focusChange; 133 | }; 134 | }; 135 | 136 | class TerminalEmulator 137 | { 138 | public: 139 | 140 | virtual ~TerminalEmulator() = default; 141 | 142 | virtual void handleEvent(const TerminalEvent &event) noexcept = 0; 143 | virtual void updateState(TerminalState &state) noexcept = 0; 144 | }; 145 | 146 | class Writer 147 | { 148 | public: 149 | 150 | virtual void write(TSpan data) noexcept = 0; 151 | }; 152 | 153 | class TerminalEmulatorFactory 154 | { 155 | public: 156 | 157 | // Function returning a new-allocated TerminalEmulator. 158 | // 'clientDataWriter' is a non-owning reference and exceeds the lifetime 159 | // of the returned TerminalEmulator. 160 | virtual TerminalEmulator &create( TPoint size, 161 | Writer &clientDataWriter ) noexcept = 0; 162 | 163 | // Returns environment variables that the TerminalEmulator requires the 164 | // client process to have. 165 | // The lifetime of the returned value must not be shorter than that of 'this'. 166 | virtual TSpan getCustomEnvironment() noexcept = 0; 167 | }; 168 | 169 | } // namespace tvterm 170 | 171 | #endif // TVTERM_TERMEMU_H 172 | -------------------------------------------------------------------------------- /include/tvterm/termframe.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_TERMFRAME_H 2 | #define TVTERM_TERMFRAME_H 3 | 4 | #define Uses_TFrame 5 | #include 6 | 7 | namespace tvterm 8 | { 9 | 10 | // A TFrame that displays the terminal dimensions while being dragged. 11 | // It assumes that the terminal fills the inner area of the frame. 12 | 13 | class BasicTerminalFrame : public TFrame 14 | { 15 | public: 16 | 17 | BasicTerminalFrame(const TRect &bounds) noexcept; 18 | 19 | void draw() override; 20 | }; 21 | 22 | } // namespace tvterm 23 | 24 | #endif // TVTERM_TERMFRAME_H 25 | -------------------------------------------------------------------------------- /include/tvterm/termview.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_TERMVIEW_H 2 | #define TVTERM_TERMVIEW_H 3 | 4 | #define Uses_TView 5 | #define Uses_TGroup 6 | #include 7 | 8 | struct MouseEventType; 9 | 10 | namespace tvterm 11 | { 12 | 13 | class TerminalController; 14 | class TerminalSurface; 15 | struct TerminalState; 16 | struct TVTermConstants; 17 | 18 | class TerminalView : public TView 19 | { 20 | const TVTermConstants &consts; 21 | bool ownerBufferChanged {false}; 22 | 23 | void handleMouse(ushort what, MouseEventType mouse) noexcept; 24 | void updateCursor(TerminalState &state) noexcept; 25 | void updateDisplay(TerminalSurface &surface) noexcept; 26 | bool canReuseOwnerBuffer() noexcept; 27 | 28 | public: 29 | 30 | TerminalController &termCtrl; 31 | 32 | // Takes ownership over 'termCtrl'. 33 | // The lifetime of 'consts' must exceed that of 'this'. 34 | TerminalView( const TRect &bounds, TerminalController &termCtrl, 35 | const TVTermConstants &consts ) noexcept; 36 | ~TerminalView(); 37 | 38 | void changeBounds(const TRect& bounds) override; 39 | void setState(ushort aState, bool enable) override; 40 | void handleEvent(TEvent &ev) override; 41 | void draw() override; 42 | }; 43 | 44 | } // namespace tvterm 45 | 46 | #endif // TVTERM_TERMVIEW_H 47 | -------------------------------------------------------------------------------- /include/tvterm/termwnd.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_TERMWND_H 2 | #define TVTERM_TERMWND_H 3 | 4 | #include 5 | 6 | #define Uses_TWindow 7 | #define Uses_TCommandSet 8 | #include 9 | 10 | namespace tvterm 11 | { 12 | 13 | class TerminalView; 14 | class TerminalController; 15 | struct TerminalState; 16 | struct TVTermConstants; 17 | struct TerminalUpdatedMsg; 18 | 19 | class BasicTerminalWindow : public TWindow 20 | { 21 | const TVTermConstants &consts; 22 | TerminalView *view {nullptr}; 23 | size_t titleCapacity {0}; 24 | GrowArray termTitle; 25 | 26 | void checkChanges(TerminalUpdatedMsg &) noexcept; 27 | void resizeTitle(size_t); 28 | bool updateTitle(TerminalController &, TerminalState &) noexcept; 29 | 30 | protected: 31 | 32 | bool isDisconnected() const noexcept; 33 | 34 | public: 35 | 36 | static TFrame *initFrame(TRect); 37 | 38 | // Takes ownership over 'termCtrl'. 39 | // The lifetime of 'consts' must exceed that of 'this'. 40 | // Assumes 'this->TWindow::frame' to be a BasicTerminalFrame. 41 | BasicTerminalWindow( const TRect &bounds, TerminalController &termCtrl, 42 | const TVTermConstants &consts ) noexcept; 43 | 44 | void shutDown() override; 45 | const char *getTitle(short) override; 46 | 47 | void handleEvent(TEvent &ev) override; 48 | void setState(ushort aState, Boolean enable) override; 49 | ushort execute() override; 50 | 51 | static TRect viewBounds(const TRect &windowBounds); 52 | static TPoint viewSize(const TRect &windowBounds); 53 | }; 54 | 55 | inline TRect BasicTerminalWindow::viewBounds(const TRect &windowBounds) 56 | { 57 | return TRect(windowBounds).grow(-1, -1); 58 | } 59 | 60 | inline TPoint BasicTerminalWindow::viewSize(const TRect &windowBounds) 61 | { 62 | TRect r = viewBounds(windowBounds); 63 | return r.b - r.a; 64 | } 65 | 66 | } // namespace tvterm 67 | 68 | #endif // TVTERM_TERMWND_H 69 | -------------------------------------------------------------------------------- /include/tvterm/vtermemu.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_VTERMEMU_H 2 | #define TVTERM_VTERMEMU_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | namespace tvterm 11 | { 12 | 13 | class VTermEmulatorFactory final : public TerminalEmulatorFactory 14 | { 15 | public: 16 | 17 | TerminalEmulator &create(TPoint size, Writer &clientDataWriter) noexcept override; 18 | TSpan getCustomEnvironment() noexcept override; 19 | 20 | }; 21 | 22 | class VTermEmulator final : public TerminalEmulator 23 | { 24 | public: 25 | 26 | VTermEmulator(TPoint size, Writer &aClientDataWriter) noexcept; 27 | ~VTermEmulator(); 28 | 29 | void handleEvent(const TerminalEvent &event) noexcept override; 30 | void updateState(TerminalState &state) noexcept override; 31 | 32 | private: 33 | 34 | struct LineStack 35 | { 36 | enum { maxSize = 10000 }; 37 | 38 | std::vector, size_t>> stack; 39 | void push(size_t cols, const VTermScreenCell *cells); 40 | bool pop(const VTermEmulator &vterm, size_t cols, VTermScreenCell *cells); 41 | TSpan top() const; 42 | }; 43 | 44 | struct LocalState 45 | { 46 | bool cursorChanged {false}; 47 | TPoint cursorPos {0, 0}; 48 | bool cursorVisible {false}; 49 | bool cursorBlink {false}; 50 | 51 | bool titleChanged {false}; 52 | GrowArray title; 53 | 54 | bool mouseEnabled {false}; 55 | bool altScreenEnabled {false}; 56 | }; 57 | 58 | struct VTerm *vt; 59 | struct VTermState *vtState; 60 | struct VTermScreen *vtScreen; 61 | Writer &clientDataWriter; 62 | std::vector damageByRow; 63 | GrowArray strFragBuf; 64 | LineStack linestack; 65 | LocalState localState; 66 | 67 | static const VTermScreenCallbacks callbacks; 68 | 69 | TPoint getSize() noexcept; 70 | void setSize(TPoint size) noexcept; 71 | void drawDamagedArea(TerminalSurface &surface) noexcept; 72 | 73 | void writeOutput(const char *data, size_t size); 74 | int damage(VTermRect rect); 75 | int moverect(VTermRect dest, VTermRect src); 76 | int movecursor(VTermPos pos, VTermPos oldpos, int visible); 77 | int settermprop(VTermProp prop, VTermValue *val); 78 | int bell(); 79 | int resize(int rows, int cols); 80 | int sb_pushline(int cols, const VTermScreenCell *cells); 81 | int sb_popline(int cols, VTermScreenCell *cells); 82 | 83 | VTermScreenCell getDefaultCell() const; 84 | }; 85 | 86 | inline TSpan VTermEmulator::LineStack::top() const 87 | { 88 | auto &pair = stack.back(); 89 | return {pair.first.get(), pair.second}; 90 | } 91 | 92 | } // namespace tvterm 93 | 94 | #endif // TVTERM_VTERMEMU_H 95 | -------------------------------------------------------------------------------- /source/tvterm-core/debug.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | namespace tvterm 6 | { 7 | namespace debug 8 | { 9 | 10 | void breakable() 11 | { 12 | } 13 | 14 | void (* volatile const breakable_ptr)() = &debug::breakable; 15 | 16 | } // namespace debug 17 | 18 | DebugCout DebugCout::instance; 19 | 20 | DebugCout::DebugCout() : 21 | nbuf(), 22 | nout(&nbuf) 23 | { 24 | const char *env = getenv("TVTERM_DEBUG"); 25 | enabled = env && *env; 26 | } 27 | 28 | } // namespace tvterm 29 | 30 | -------------------------------------------------------------------------------- /source/tvterm-core/pty.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define Uses_TPoint 4 | #include 5 | 6 | #if !defined(_WIN32) 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #if __has_include() 16 | # include 17 | #elif __has_include() 18 | # include 19 | #elif __has_include() 20 | # include 21 | #endif 22 | 23 | namespace tvterm 24 | { 25 | 26 | static struct termios createTermios() noexcept; 27 | static struct winsize createWinsize(TPoint size) noexcept; 28 | 29 | bool createPty( PtyDescriptor &ptyDescriptor, TPoint size, 30 | TSpan customEnvironment, 31 | void (&onError)(const char *) ) noexcept 32 | { 33 | auto termios = createTermios(); 34 | auto winsize = createWinsize(size); 35 | int masterFd; 36 | pid_t clientPid = forkpty(&masterFd, nullptr, &termios, &winsize); 37 | if (clientPid == 0) 38 | { 39 | // Use the default ISIG signal handlers. 40 | signal(SIGINT, SIG_DFL); 41 | signal(SIGQUIT, SIG_DFL); 42 | signal(SIGSTOP, SIG_DFL); 43 | signal(SIGCONT, SIG_DFL); 44 | 45 | for (const auto &envVar : customEnvironment) 46 | setenv(envVar.name, envVar.value, 1); 47 | 48 | char *shell = getenv("SHELL"); 49 | char *args[] = {shell, nullptr}; 50 | execvp(shell, args); 51 | 52 | setbuf(stderr, nullptr); // Ensure 'stderr' is unbuffered. 53 | fprintf( 54 | stderr, 55 | "\x1B[1;31m" // Color attributes: bold and red. 56 | "Error: Failed to execute the program specified by the environment variable SHELL ('%s'): %s" 57 | "\x1B[0m", // Reset color attributes. 58 | (shell ? shell : ""), 59 | strerror(errno) 60 | ); 61 | 62 | // Exit the subprocess without cleaning up resources. 63 | _Exit(EXIT_FAILURE); 64 | } 65 | else if (clientPid == -1) 66 | { 67 | char *str = fmtStr("forkpty failed: %s", strerror(errno)); 68 | onError(str); 69 | delete[] str; 70 | return false; 71 | } 72 | ptyDescriptor = {masterFd, clientPid}; 73 | return true; 74 | } 75 | 76 | bool PtyMaster::readFromClient(TSpan data, size_t &bytesRead) noexcept 77 | { 78 | bytesRead = 0; 79 | if (data.size() > 1) 80 | { 81 | ssize_t r = read(d.masterFd, &data[0], 1); 82 | if (r < 0) 83 | return false; 84 | else if (r > 0) 85 | { 86 | bytesRead += r; 87 | int availableBytes = 0; 88 | if ( ioctl(d.masterFd, FIONREAD, &availableBytes) != -1 && 89 | availableBytes > 0 ) 90 | { 91 | size_t bytesToRead = min(availableBytes, data.size() - 1); 92 | r = read(d.masterFd, &data[1], bytesToRead); 93 | if (r < 0) 94 | return false; 95 | bytesRead += r; 96 | } 97 | } 98 | } 99 | return true; 100 | } 101 | 102 | bool PtyMaster::writeToClient(TSpan data) noexcept 103 | { 104 | size_t written = 0; 105 | while (written < data.size()) 106 | { 107 | size_t bytesToWrite = data.size() - written; 108 | ssize_t r = write(d.masterFd, &data[written], bytesToWrite); 109 | if (r < 0) 110 | return false; 111 | written += r; 112 | } 113 | return true; 114 | } 115 | 116 | void PtyMaster::resizeClient(TPoint size) noexcept 117 | { 118 | struct winsize w = {}; 119 | w.ws_row = size.y; 120 | w.ws_col = size.x; 121 | int rr = ioctl(d.masterFd, TIOCSWINSZ, &w); 122 | (void) rr; 123 | } 124 | 125 | void PtyMaster::disconnect() noexcept 126 | { 127 | close(d.masterFd); 128 | // Send a SIGHUP, then a SIGKILL after a while if the process is not yet 129 | // terminated, like most terminal emulators do. 130 | kill(d.clientPid, SIGHUP); 131 | sleep(1); 132 | if (waitpid(d.clientPid, nullptr, WNOHANG) != d.clientPid) 133 | { 134 | kill(d.clientPid, SIGKILL); 135 | while( waitpid(d.clientPid, nullptr, 0) != d.clientPid && 136 | errno == EINTR ); 137 | } 138 | } 139 | 140 | static struct winsize createWinsize(TPoint size) noexcept 141 | { 142 | struct winsize w = {}; 143 | w.ws_row = size.y; 144 | w.ws_col = size.x; 145 | return w; 146 | } 147 | 148 | static struct termios createTermios() noexcept 149 | { 150 | // Initialization like in pangoterm. 151 | struct termios t = {}; 152 | 153 | t.c_iflag = ICRNL | IXON; 154 | t.c_oflag = OPOST | ONLCR 155 | #ifdef TAB0 156 | | TAB0 157 | #endif 158 | ; 159 | t.c_cflag = CS8 | CREAD; 160 | t.c_lflag = ISIG | ICANON | IEXTEN | ECHO | ECHOE | ECHOK; 161 | 162 | #ifdef IUTF8 163 | t.c_iflag |= IUTF8; 164 | #endif 165 | #ifdef NL0 166 | t.c_oflag |= NL0; 167 | #endif 168 | #ifdef CR0 169 | t.c_oflag |= CR0; 170 | #endif 171 | #ifdef BS0 172 | t.c_oflag |= BS0; 173 | #endif 174 | #ifdef VT0 175 | t.c_oflag |= VT0; 176 | #endif 177 | #ifdef FF0 178 | t.c_oflag |= FF0; 179 | #endif 180 | #ifdef ECHOCTL 181 | t.c_lflag |= ECHOCTL; 182 | #endif 183 | #ifdef ECHOKE 184 | t.c_lflag |= ECHOKE; 185 | #endif 186 | 187 | t.c_cc[VINTR] = 0x1f & 'C'; 188 | t.c_cc[VQUIT] = 0x1f & '\\'; 189 | t.c_cc[VERASE] = 0x7f; 190 | t.c_cc[VKILL] = 0x1f & 'U'; 191 | t.c_cc[VEOF] = 0x1f & 'D'; 192 | t.c_cc[VEOL] = _POSIX_VDISABLE; 193 | t.c_cc[VEOL2] = _POSIX_VDISABLE; 194 | t.c_cc[VSTART] = 0x1f & 'Q'; 195 | t.c_cc[VSTOP] = 0x1f & 'S'; 196 | t.c_cc[VSUSP] = 0x1f & 'Z'; 197 | t.c_cc[VREPRINT] = 0x1f & 'R'; 198 | t.c_cc[VWERASE] = 0x1f & 'W'; 199 | t.c_cc[VLNEXT] = 0x1f & 'V'; 200 | t.c_cc[VMIN] = 1; 201 | t.c_cc[VTIME] = 0; 202 | 203 | cfsetispeed(&t, B38400); 204 | cfsetospeed(&t, B38400); 205 | 206 | return t; 207 | } 208 | 209 | } // namespace tvterm 210 | 211 | #else 212 | 213 | #include 214 | 215 | namespace tvterm 216 | { 217 | 218 | // The OS-provided ConPTY is usually outdated. Try to overcome this by loading 219 | // a user-installed conpty.dll. 220 | 221 | struct ConPtyApi 222 | { 223 | decltype(::CreatePseudoConsole) *CreatePseudoConsole {nullptr}; 224 | decltype(::ResizePseudoConsole) *ResizePseudoConsole {nullptr}; 225 | decltype(::ClosePseudoConsole) *ClosePseudoConsole {nullptr}; 226 | 227 | void init() noexcept; 228 | }; 229 | 230 | static ConPtyApi conPty; 231 | 232 | void ConPtyApi::init() noexcept 233 | { 234 | HMODULE mod = LoadLibraryA("conpty"); 235 | if (!mod) 236 | mod = GetModuleHandleA("kernel32"); 237 | 238 | CreatePseudoConsole = 239 | (decltype(CreatePseudoConsole)) GetProcAddress(mod, "CreatePseudoConsole"); 240 | ResizePseudoConsole = 241 | (decltype(ResizePseudoConsole)) GetProcAddress(mod, "ResizePseudoConsole"); 242 | ClosePseudoConsole = 243 | (decltype(ClosePseudoConsole)) GetProcAddress(mod, "ClosePseudoConsole"); 244 | } 245 | 246 | static COORD toCoord(TPoint point) noexcept 247 | { 248 | return { 249 | (short) point.x, 250 | (short) point.y, 251 | }; 252 | } 253 | 254 | static std::vector constructEnvironmentBlock(TSpan customEnvironment) noexcept 255 | { 256 | std::vector result; 257 | 258 | if (const char *currentEnvironment = GetEnvironmentStrings()) 259 | { 260 | size_t len = 1; 261 | // The environment block is terminated by two null characters. 262 | while (currentEnvironment[len - 1] != '\0' || currentEnvironment[len] != '\0') 263 | ++len; 264 | result.insert(result.end(), ¤tEnvironment[0], ¤tEnvironment[len]); 265 | } 266 | 267 | for (const auto &var : customEnvironment) 268 | { 269 | TStringView name(var.name), 270 | value(var.value); 271 | result.insert(result.end(), name.begin(), name.end()); 272 | result.push_back('='); 273 | result.insert(result.end(), value.begin(), value.end()); 274 | result.push_back('\0'); 275 | } 276 | 277 | result.push_back('\0'); 278 | 279 | return result; 280 | } 281 | 282 | struct ProcessWaiter 283 | { 284 | HANDLE hClientWrite; 285 | HANDLE hWait; 286 | 287 | ~ProcessWaiter() 288 | { 289 | CloseHandle(hClientWrite); 290 | } 291 | 292 | static void CALLBACK callback(PVOID ctx, BOOLEAN) 293 | { 294 | auto &self = *(ProcessWaiter *) ctx; 295 | // Wake up the thread waiting for client data. 296 | DWORD r = 0; 297 | WriteFile(self.hClientWrite, "", 1, &r, nullptr); 298 | UnregisterWait(self.hWait); 299 | delete &self; 300 | } 301 | }; 302 | 303 | bool createPty( PtyDescriptor &ptyDescriptor, 304 | TPoint size, 305 | TSpan customEnvironment, 306 | void (&onError)(const char *) ) noexcept 307 | { 308 | static bool conPtyAvailable = [] () 309 | { 310 | conPty.init(); 311 | return conPty.CreatePseudoConsole && 312 | conPty.ResizePseudoConsole && 313 | conPty.ClosePseudoConsole; 314 | }(); 315 | 316 | HANDLE hMasterRead {}, 317 | hClientWrite {}; 318 | HANDLE hMasterWrite {}, 319 | hClientRead {}; 320 | HPCON hPseudoConsole {}; 321 | STARTUPINFOEXA siClient {}; 322 | PROCESS_INFORMATION piClient {}; 323 | 324 | size_t attrListSize = 0; 325 | InitializeProcThreadAttributeList(nullptr, 1, 0, &attrListSize); 326 | std::vector attributeList(attrListSize); 327 | 328 | const char *failedAction {}; 329 | 330 | do 331 | { 332 | if (!conPtyAvailable) 333 | { 334 | failedAction = "Loading ConPTY"; 335 | break; 336 | } 337 | 338 | if (!CreatePipe(&hMasterRead, &hClientWrite, nullptr, 0)) 339 | { 340 | failedAction = "CreatePipe"; 341 | break; 342 | } 343 | 344 | if (!CreatePipe(&hClientRead, &hMasterWrite, nullptr, 0)) 345 | { 346 | failedAction = "CreatePipe"; 347 | break; 348 | } 349 | 350 | if (FAILED(conPty.CreatePseudoConsole(toCoord(size), hClientRead, hClientWrite, 0, &hPseudoConsole))) 351 | { 352 | failedAction = "CreatePseudoConsole"; 353 | break; 354 | } 355 | 356 | siClient.StartupInfo.cb = sizeof(STARTUPINFOEX); 357 | siClient.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST) &attributeList[0]; 358 | if (!InitializeProcThreadAttributeList(siClient.lpAttributeList, 1, 0, &attrListSize)) 359 | { 360 | failedAction = "InitializeProcThreadAttributeList"; 361 | break; 362 | } 363 | 364 | if ( !UpdateProcThreadAttribute( siClient.lpAttributeList, 365 | 0, 366 | PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 367 | hPseudoConsole, 368 | sizeof(HPCON), 369 | nullptr, 370 | nullptr ) ) 371 | { 372 | failedAction = "UpdateProcThreadAttribute"; 373 | break; 374 | } 375 | 376 | char *comspec = getenv("COMSPEC"); 377 | if (!comspec) 378 | { 379 | failedAction = "Retrieving 'COMSPEC' environment variable"; 380 | break; 381 | } 382 | 383 | std::vector environmentBlock = constructEnvironmentBlock(customEnvironment); 384 | if ( !CreateProcessA( nullptr, 385 | comspec, 386 | nullptr, 387 | nullptr, 388 | false, 389 | EXTENDED_STARTUPINFO_PRESENT, 390 | &environmentBlock[0], 391 | nullptr, 392 | (LPSTARTUPINFOA) &siClient.StartupInfo, 393 | &piClient ) ) 394 | { 395 | failedAction = "CreateProcessA"; 396 | break; 397 | } 398 | 399 | ProcessWaiter &procWaiter = *new ProcessWaiter {hClientWrite}; 400 | if ( !RegisterWaitForSingleObject( &procWaiter.hWait, 401 | piClient.hProcess, 402 | &ProcessWaiter::callback, 403 | &procWaiter, 404 | INFINITE, 405 | WT_EXECUTEINWAITTHREAD | WT_EXECUTEONLYONCE ) ) 406 | { 407 | failedAction = "RegisterWaitForSingleObject"; 408 | delete &procWaiter; 409 | break; 410 | } 411 | } while (false); 412 | 413 | if (!failedAction) 414 | { 415 | ptyDescriptor = { 416 | hMasterRead, 417 | hMasterWrite, 418 | hPseudoConsole, 419 | piClient.hProcess, 420 | }; 421 | } 422 | else 423 | { 424 | char *msg = fmtStr("%s failed with error code %d", failedAction, (int) GetLastError()); 425 | onError(msg); 426 | delete[] msg; 427 | 428 | for (HANDLE handle : {hMasterRead, hMasterWrite, hClientWrite, piClient.hProcess}) 429 | if (handle) 430 | CloseHandle(handle); 431 | if (hPseudoConsole) 432 | conPty.ClosePseudoConsole(hPseudoConsole); 433 | } 434 | 435 | for (HANDLE handle : {hClientRead, piClient.hThread}) 436 | if (handle) 437 | CloseHandle(handle); 438 | if (siClient.lpAttributeList) 439 | DeleteProcThreadAttributeList(siClient.lpAttributeList); 440 | 441 | return !failedAction; 442 | } 443 | 444 | static bool processIsNotRunning(HANDLE hProcess) 445 | { 446 | DWORD exitCode; 447 | return !GetExitCodeProcess(hProcess, &exitCode) || 448 | exitCode != STILL_ACTIVE; 449 | } 450 | 451 | bool PtyMaster::readFromClient(TSpan data, size_t &bytesRead) noexcept 452 | { 453 | bytesRead = 0; 454 | 455 | if (processIsNotRunning(d.hClientProcess)) 456 | return false; 457 | 458 | if (data.size() > 1) 459 | { 460 | DWORD r; 461 | if (!ReadFile(d.hMasterRead, &data[0], 1, &r, nullptr)) 462 | return false; 463 | else if (r > 0) 464 | { 465 | bytesRead += r; 466 | DWORD availableBytes = 0; 467 | if ( PeekNamedPipe(d.hMasterRead, nullptr, 0, nullptr, &availableBytes, nullptr) && 468 | availableBytes > 0 ) 469 | { 470 | DWORD bytesToRead = min(availableBytes, data.size() - 1); 471 | if (!ReadFile(d.hMasterRead, &data[1], bytesToRead, &r, nullptr)) 472 | return false; 473 | bytesRead += r; 474 | } 475 | } 476 | } 477 | return true; 478 | } 479 | 480 | bool PtyMaster::writeToClient(TSpan data) noexcept 481 | { 482 | if (processIsNotRunning(d.hClientProcess)) 483 | return false; 484 | 485 | size_t written = 0; 486 | while (written < data.size()) 487 | { 488 | DWORD bytesToWrite = data.size() - written; 489 | DWORD r; 490 | if (!WriteFile(d.hMasterWrite, &data[written], bytesToWrite, &r, nullptr)) 491 | return false; 492 | written += r; 493 | } 494 | return true; 495 | } 496 | 497 | void PtyMaster::resizeClient(TPoint size) noexcept 498 | { 499 | conPty.ResizePseudoConsole(d.hPseudoConsole, toCoord(size)); 500 | } 501 | 502 | void PtyMaster::disconnect() noexcept 503 | { 504 | conPty.ClosePseudoConsole(d.hPseudoConsole); 505 | CloseHandle(d.hMasterRead); 506 | CloseHandle(d.hMasterWrite); 507 | CloseHandle(d.hClientProcess); 508 | } 509 | 510 | } // namespace tvterm 511 | 512 | #endif // _WIN32 513 | -------------------------------------------------------------------------------- /source/tvterm-core/termctrl.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define Uses_TEventQueue 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace tvterm 13 | { 14 | 15 | class GrowArrayWriter final : public Writer 16 | { 17 | public: 18 | 19 | GrowArray buffer; 20 | 21 | void write(TSpan data) noexcept override 22 | { 23 | buffer.push(&data[0], data.size()); 24 | } 25 | }; 26 | 27 | // In order to support Windows pseudoconsoles, we need different threads for 28 | // reading and writing client data, since we are expected to use blocking pipes. 29 | // In Unix, on the other hand, the most straightforward implementation is 30 | // probably to use something like 'select' to coordinate reads and writes in a 31 | // single thread. But the model required by Windows also works on Unix, so we 32 | // just use it everywhere. 33 | 34 | struct TerminalController::TerminalEventLoop 35 | { 36 | using Clock = std::chrono::steady_clock; 37 | using TimePoint = Clock::time_point; 38 | 39 | enum { maxReadTimeMs = 20, readWaitStepMs = 5 }; 40 | enum { readBufSize = 4096 }; 41 | 42 | TerminalController &ctrl; 43 | 44 | // Used for granting exclusive access to TerminalEventLoop's fields and the 45 | // TerminalEmulator. 46 | std::mutex mutex; 47 | 48 | // Used for indicating whether event processing should be stopped because 49 | // the terminal was shut down by the application. 50 | bool terminated {false}; 51 | 52 | // Used for sending events from the main thread to the TerminalEventLoop's 53 | // threads. Has its own mutex to avoid blocking the main thread for long. 54 | Mutex> eventQueue; 55 | 56 | // Used for waking up the WriterLoop a short time after data was received 57 | // by the ReaderLoop thread. 58 | TimePoint currentTimeout {}; 59 | TimePoint maxReadTimeout {}; 60 | 61 | // Used for waking up the WriterLoop thread on demand, e.g. when there are 62 | // pending events or when the timeouts change. 63 | std::condition_variable condVar; 64 | 65 | // Used for storing data to be sent to the client. 66 | GrowArrayWriter clientDataWriter; 67 | 68 | // Used for handling ViewportResize events properly. 69 | bool viewportResized {false}; 70 | TPoint viewportSize {}; 71 | 72 | void runWriterLoop() noexcept; 73 | void runReaderLoop() noexcept; 74 | void processEvents() noexcept; 75 | void updateState(bool &) noexcept; 76 | void updateTimeouts() noexcept; 77 | 78 | void writePendingData(GrowArray &, bool &) noexcept; 79 | void notifyMainThread() noexcept; 80 | }; 81 | 82 | TerminalController *TerminalController::create( TPoint size, 83 | TerminalEmulatorFactory &terminalEmulatorFactory, 84 | void (&onError)(const char *) ) noexcept 85 | { 86 | PtyDescriptor ptyDescriptor; 87 | if ( !createPty( ptyDescriptor, size, 88 | terminalEmulatorFactory.getCustomEnvironment(), onError ) ) 89 | return nullptr; 90 | 91 | auto &terminalController = *new TerminalController( size, 92 | terminalEmulatorFactory, 93 | ptyDescriptor ); 94 | 95 | // 'this' will be deleted when: 96 | // 1. 'shutDown()' is invoked from the main thread. 97 | // 2. Both the WriterLoop and ReaderLoop threads exit. 98 | auto deleter = [] (TerminalController *ctrl) { 99 | delete ctrl; 100 | }; 101 | terminalController.selfOwningPtr.reset(&terminalController, deleter); 102 | 103 | std::thread([owningPtr = terminalController.selfOwningPtr] { 104 | owningPtr->eventLoop.runWriterLoop(); 105 | }).detach(); 106 | 107 | std::thread([owningPtr = terminalController.selfOwningPtr] { 108 | owningPtr->eventLoop.runReaderLoop(); 109 | }).detach(); 110 | 111 | return &terminalController; 112 | } 113 | 114 | void TerminalController::shutDown() noexcept 115 | { 116 | { 117 | std::lock_guard lock(eventLoop.mutex); 118 | eventLoop.terminated = true; 119 | } 120 | eventLoop.condVar.notify_one(); 121 | selfOwningPtr.reset(); // May delete 'this'. 122 | } 123 | 124 | TerminalController::TerminalController( TPoint size, 125 | TerminalEmulatorFactory &terminalEmulatorFactory, 126 | PtyDescriptor ptyDescriptor ) noexcept : 127 | ptyMaster(ptyDescriptor), 128 | eventLoop(*new TerminalEventLoop {*this}), 129 | terminalEmulator(terminalEmulatorFactory.create(size, eventLoop.clientDataWriter)) 130 | { 131 | } 132 | 133 | TerminalController::~TerminalController() 134 | { 135 | delete &terminalEmulator; 136 | delete &eventLoop; 137 | } 138 | 139 | void TerminalController::sendEvent(const TerminalEvent &event) noexcept 140 | { 141 | eventLoop.eventQueue.lock([&] (auto &eventQueue) { 142 | eventQueue.push(event); 143 | }); 144 | eventLoop.condVar.notify_one(); 145 | } 146 | 147 | void TerminalController::TerminalEventLoop::runWriterLoop() noexcept 148 | { 149 | GrowArray outputBuffer; 150 | while (true) 151 | { 152 | bool updated = false; 153 | { 154 | std::unique_lock lock(mutex); 155 | if (currentTimeout != TimePoint()) 156 | condVar.wait_until(lock, currentTimeout); 157 | else 158 | // Waiting until 'TimePoint()' is not always supported, 159 | // so use a regular 'wait'. 160 | condVar.wait(lock); 161 | 162 | if (terminated) 163 | { 164 | ctrl.ptyMaster.disconnect(); 165 | break; 166 | } 167 | 168 | processEvents(); 169 | updateState(updated); 170 | 171 | outputBuffer = std::move(clientDataWriter.buffer); 172 | } 173 | 174 | writePendingData(outputBuffer, updated); 175 | 176 | if (updated) 177 | notifyMainThread(); 178 | } 179 | } 180 | 181 | void TerminalController::TerminalEventLoop::runReaderLoop() noexcept 182 | { 183 | static thread_local char inputBuffer alignas(4096) [readBufSize]; 184 | while (true) 185 | { 186 | size_t bytesRead; 187 | bool readOk = ctrl.ptyMaster.readFromClient(inputBuffer, bytesRead); 188 | 189 | if (!readOk || bytesRead == 0) 190 | { 191 | ctrl.disconnected = true; 192 | notifyMainThread(); 193 | break; 194 | } 195 | 196 | if (terminated) 197 | // We are expected to consume all of the client's data, so keep 198 | // reading while we can. 199 | continue; 200 | 201 | bool updated = false; 202 | { 203 | std::lock_guard lock(mutex); 204 | 205 | TerminalEvent event; 206 | event.type = TerminalEventType::ClientDataRead; 207 | event.clientDataRead = {inputBuffer, bytesRead}; 208 | ctrl.terminalEmulator.handleEvent(event); 209 | 210 | updateTimeouts(); 211 | 212 | // We also do these in the ReaderLoop thread because locking may be 213 | // unfair. In a situation where we continously receive data from the 214 | // client, the ReaderLoop thread may be able to acquire the lock 215 | // several times before the WriterLoop wakes up. 216 | processEvents(); 217 | updateState(updated); 218 | } 219 | 220 | if (updated) 221 | notifyMainThread(); 222 | // Notify the WriterLoop so that it can use the new timeouts and write 223 | // any pending data. 224 | condVar.notify_one(); 225 | } 226 | } 227 | 228 | void TerminalController::TerminalEventLoop::processEvents() noexcept 229 | // Pre: 'this->mutex' is locked. 230 | { 231 | while (true) 232 | { 233 | bool hasEvent; 234 | TerminalEvent event; 235 | 236 | eventQueue.lock([&] (auto &eventQueue) { 237 | if ((hasEvent = !eventQueue.empty())) 238 | { 239 | event = eventQueue.front(); 240 | eventQueue.pop(); 241 | } 242 | }); 243 | 244 | if (!hasEvent) 245 | break; 246 | 247 | switch (event.type) 248 | { 249 | case TerminalEventType::ViewportResize: 250 | // Do not resize the client yet. We will handle this later. 251 | viewportSize = {event.viewportResize.x, event.viewportResize.y}; 252 | viewportResized = true; 253 | break; 254 | 255 | default: 256 | ctrl.terminalEmulator.handleEvent(event); 257 | break; 258 | } 259 | } 260 | } 261 | 262 | void TerminalController::TerminalEventLoop::updateState(bool &updated) noexcept 263 | // Pre: 'this->mutex' is locked. 264 | { 265 | if (Clock::now() > currentTimeout) 266 | { 267 | updated = true; 268 | currentTimeout = TimePoint(); 269 | maxReadTimeout = TimePoint(); 270 | 271 | ctrl.lockState([&] (auto &state) { 272 | ctrl.terminalEmulator.updateState(state); 273 | }); 274 | } 275 | 276 | if (currentTimeout == TimePoint() && viewportResized) 277 | { 278 | // Handle the resize only once we reach a timeout and after updating 279 | // the TerminalState, because the client is now likely drawn properly. 280 | viewportResized = false; 281 | 282 | TerminalEvent event; 283 | event.type = TerminalEventType::ViewportResize; 284 | event.viewportResize = {viewportSize.x, viewportSize.y}; 285 | 286 | ctrl.terminalEmulator.handleEvent(event); 287 | ctrl.ptyMaster.resizeClient(viewportSize); 288 | } 289 | } 290 | 291 | void TerminalController::TerminalEventLoop::updateTimeouts() noexcept 292 | // Pre: 'this->mutex' is locked. 293 | { 294 | // When receiving data, we want to flush updates either: 295 | // - 'readWaitStepMs' after data was last received. 296 | // - 'maxReadTimeMs' after the first time data was received. 297 | auto now = Clock::now(); 298 | if (maxReadTimeout == TimePoint()) 299 | maxReadTimeout = now + std::chrono::milliseconds(maxReadTimeMs); 300 | 301 | currentTimeout = ::min( now + std::chrono::milliseconds(readWaitStepMs), 302 | maxReadTimeout ); 303 | } 304 | 305 | void TerminalController::TerminalEventLoop::writePendingData(GrowArray &outputBuffer, bool &updated) noexcept 306 | // Pre: 'this->mutex' needs not be locked. 307 | { 308 | if (outputBuffer.size() > 0) 309 | { 310 | if ( !ctrl.disconnected && 311 | !ctrl.ptyMaster.writeToClient({outputBuffer.data(), outputBuffer.size()}) ) 312 | { 313 | ctrl.disconnected = true; 314 | updated = true; 315 | } 316 | 317 | outputBuffer.clear(); 318 | } 319 | } 320 | 321 | void TerminalController::TerminalEventLoop::notifyMainThread() noexcept 322 | // Pre: 'this->mutex' needs not be locked. 323 | { 324 | ctrl.updated = true; 325 | TEventQueue::wakeUp(); 326 | } 327 | 328 | } // namespace tvterm 329 | -------------------------------------------------------------------------------- /source/tvterm-core/termframe.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define Uses_TRect 4 | #include 5 | 6 | namespace tvterm 7 | { 8 | 9 | BasicTerminalFrame::BasicTerminalFrame(const TRect &bounds) noexcept : 10 | TFrame(bounds) 11 | { 12 | } 13 | 14 | void BasicTerminalFrame::draw() 15 | { 16 | TFrame::draw(); 17 | 18 | if (state & sfDragging) 19 | { 20 | TRect r(4, size.y - 1, min(size.x - 4, 14), size.y); 21 | if (r.a.x < r.b.x && r.a.y < r.b.y) 22 | { 23 | TDrawBuffer b; 24 | char str[256]; 25 | TPoint termSize = {max(size.x - 2, 0), max(size.y - 2, 0)}; 26 | snprintf(str, sizeof(str), " %dx%d ", termSize.x, termSize.y); 27 | uchar color = mapColor(5); 28 | ushort width = b.moveStr(0, str, color, r.b.x - r.a.x); 29 | writeLine(r.a.x, r.a.y, width, r.b.y - r.a.y, b); 30 | } 31 | } 32 | } 33 | 34 | } // namespace tvterm 35 | -------------------------------------------------------------------------------- /source/tvterm-core/termview.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define Uses_TKeys 6 | #define Uses_TEvent 7 | #include 8 | 9 | namespace tvterm 10 | { 11 | 12 | TerminalView::TerminalView( const TRect &bounds, TerminalController &aTermCtrl, 13 | const TVTermConstants &aConsts ) noexcept : 14 | TView(bounds), 15 | consts(aConsts), 16 | termCtrl(aTermCtrl) 17 | { 18 | growMode = gfGrowHiX | gfGrowHiY; 19 | options |= ofSelectable | ofFirstClick; 20 | eventMask |= evMouseMove | evMouseAuto | evMouseWheel | evBroadcast; 21 | showCursor(); 22 | } 23 | 24 | TerminalView::~TerminalView() 25 | { 26 | termCtrl.shutDown(); 27 | } 28 | 29 | void TerminalView::changeBounds(const TRect& bounds) 30 | { 31 | setBounds(bounds); 32 | ownerBufferChanged = true; 33 | drawView(); 34 | 35 | TerminalEvent termEvent; 36 | termEvent.type = TerminalEventType::ViewportResize; 37 | termEvent.viewportResize = {size.x, size.y}; 38 | termCtrl.sendEvent(termEvent); 39 | } 40 | 41 | void TerminalView::setState(ushort aState, bool enable) 42 | { 43 | if (aState == sfExposed && enable != getState(sfExposed)) 44 | ownerBufferChanged = true; 45 | 46 | TView::setState(aState, enable); 47 | 48 | if (aState == sfFocused) 49 | { 50 | TerminalEvent termEvent; 51 | termEvent.type = TerminalEventType::FocusChange; 52 | termEvent.focusChange = {enable}; 53 | termCtrl.sendEvent(termEvent); 54 | } 55 | } 56 | 57 | void TerminalView::handleEvent(TEvent &ev) 58 | { 59 | TView::handleEvent(ev); 60 | 61 | switch (ev.what) 62 | { 63 | case evBroadcast: 64 | if ( ev.message.command == consts.cmCheckTerminalUpdates && 65 | termCtrl.stateHasBeenUpdated() ) 66 | drawView(); 67 | break; 68 | 69 | case evKeyDown: 70 | { 71 | TerminalEvent termEvent; 72 | termEvent.type = TerminalEventType::KeyDown; 73 | termEvent.keyDown = ev.keyDown; 74 | termCtrl.sendEvent(termEvent); 75 | 76 | clearEvent(ev); 77 | break; 78 | } 79 | 80 | case evMouseDown: 81 | do { 82 | handleMouse(ev.what, ev.mouse); 83 | } while (mouseEvent(ev, evMouse)); 84 | if (ev.what == evMouseUp) 85 | handleMouse(ev.what, ev.mouse); 86 | clearEvent(ev); 87 | break; 88 | 89 | case evMouseWheel: 90 | case evMouseMove: 91 | case evMouseAuto: 92 | case evMouseUp: 93 | handleMouse(ev.what, ev.mouse); 94 | clearEvent(ev); 95 | break; 96 | } 97 | } 98 | 99 | void TerminalView::handleMouse(ushort what, MouseEventType mouse) noexcept 100 | { 101 | mouse.where = makeLocal(mouse.where); 102 | 103 | TerminalEvent termEvent; 104 | termEvent.type = TerminalEventType::Mouse; 105 | termEvent.mouse = {what, mouse}; 106 | termCtrl.sendEvent(termEvent); 107 | } 108 | 109 | void TerminalView::draw() 110 | { 111 | termCtrl.lockState([&] (auto &state) { 112 | updateCursor(state); 113 | updateDisplay(state.surface); 114 | 115 | TerminalUpdatedMsg upd {*this, state}; 116 | message(owner, evCommand, consts.cmTerminalUpdated, &upd); 117 | }); 118 | } 119 | 120 | void TerminalView::updateCursor(TerminalState &state) noexcept 121 | { 122 | if (state.cursorChanged) 123 | { 124 | state.cursorChanged = false; 125 | setState(sfCursorVis, state.cursorVisible); 126 | setState(sfCursorIns, state.cursorBlink); 127 | setCursor(state.cursorPos.x, state.cursorPos.y); 128 | } 129 | } 130 | 131 | static TerminalSurface::RowDamage rangeToCopy(int y, const TRect &r, const TerminalSurface &surface, bool reuseBuffer) 132 | { 133 | auto &damage = surface.damageAtRow(y); 134 | if (reuseBuffer) 135 | return { 136 | max(r.a.x, damage.begin), 137 | min(r.b.x, damage.end), 138 | }; 139 | else 140 | return {r.a.x, r.b.x}; 141 | } 142 | 143 | void TerminalView::updateDisplay(TerminalSurface &surface) noexcept 144 | { 145 | bool reuseBuffer = canReuseOwnerBuffer(); 146 | TRect r = getExtent().intersect({{0, 0}, surface.size}); 147 | if (0 <= r.a.x && r.a.x < r.b.x && 0 <= r.a.y && r.a.y < r.b.y) 148 | { 149 | for (int y = r.a.y; y < r.b.y; ++y) 150 | { 151 | auto c = rangeToCopy(y, r, surface, reuseBuffer); 152 | writeLine(c.begin, y, c.end - c.begin, 1, &surface.at(y, c.begin)); 153 | } 154 | surface.clearDamage(); 155 | // We don't need to draw the area that is not filled by the surface. 156 | // It will be blank. 157 | } 158 | } 159 | 160 | bool TerminalView::canReuseOwnerBuffer() noexcept 161 | { 162 | if (ownerBufferChanged) 163 | { 164 | ownerBufferChanged = false; 165 | return false; 166 | } 167 | return owner && owner->buffer; 168 | } 169 | 170 | } // namespace tvterm 171 | -------------------------------------------------------------------------------- /source/tvterm-core/termwnd.cc: -------------------------------------------------------------------------------- 1 | #define Uses_TFrame 2 | #define Uses_TEvent 3 | #define Uses_TStaticText 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace tvterm 13 | { 14 | 15 | TFrame *BasicTerminalWindow::initFrame(TRect bounds) 16 | { 17 | return new BasicTerminalFrame(bounds); 18 | } 19 | 20 | BasicTerminalWindow::BasicTerminalWindow( const TRect &bounds, 21 | TerminalController &termCtrl, 22 | const TVTermConstants &aConsts 23 | ) noexcept : 24 | TWindowInit(&BasicTerminalWindow::initFrame), 25 | TWindow(bounds, nullptr, wnNoNumber), 26 | consts(aConsts) 27 | { 28 | options |= ofTileable; 29 | eventMask |= evBroadcast; 30 | setState(sfShadow, False); 31 | view = new TerminalView(getExtent().grow(-1, -1), termCtrl, aConsts); 32 | insert(view); 33 | } 34 | 35 | void BasicTerminalWindow::shutDown() 36 | { 37 | view = nullptr; 38 | TWindow::shutDown(); 39 | } 40 | 41 | void BasicTerminalWindow::checkChanges(TerminalUpdatedMsg &upd) noexcept 42 | { 43 | if (frame && updateTitle(upd.view.termCtrl, upd.state)) 44 | frame->drawView(); 45 | } 46 | 47 | bool BasicTerminalWindow::updateTitle( TerminalController &term, 48 | TerminalState &state ) noexcept 49 | { 50 | if (state.titleChanged) 51 | { 52 | state.titleChanged = false; 53 | termTitle = std::move(state.title); 54 | return true; 55 | } 56 | // When the terminal is closed for the first time, 'state.title' does not 57 | // change but we still need to redraw the title. 58 | return term.clientIsDisconnected(); 59 | } 60 | 61 | void BasicTerminalWindow::resizeTitle(size_t aCapacity) 62 | { 63 | if (titleCapacity < aCapacity) 64 | { 65 | if (title) 66 | delete[] title; 67 | title = new char[aCapacity]; 68 | titleCapacity = aCapacity; 69 | } 70 | } 71 | 72 | bool BasicTerminalWindow::isDisconnected() const noexcept 73 | { 74 | return !view || view->termCtrl.clientIsDisconnected(); 75 | } 76 | 77 | const char *BasicTerminalWindow::getTitle(short) 78 | { 79 | TStringView tail = isDisconnected() ? " (Disconnected)" 80 | : helpCtx == consts.hcInputGrabbed ? " (Input Grab)" 81 | : ""; 82 | TStringView text = {termTitle.data(), termTitle.size()}; 83 | if (size_t length = text.size() + tail.size()) 84 | { 85 | resizeTitle(length + 1); 86 | auto *title = (char *) this->title; 87 | memcpy(title, text.data(), text.size()); 88 | memcpy(title + text.size(), tail.data(), tail.size()); 89 | title[length] = '\0'; 90 | return title; 91 | } 92 | return nullptr; 93 | } 94 | 95 | void BasicTerminalWindow::handleEvent(TEvent &ev) 96 | { 97 | switch (ev.what) 98 | { 99 | case evCommand: 100 | if ( ev.message.command == consts.cmGrabInput && 101 | helpCtx != consts.hcInputGrabbed && owner ) 102 | { 103 | owner->execView(this); 104 | clearEvent(ev); 105 | } 106 | else if ( (ev.message.command == cmClose || 107 | ev.message.command == consts.cmReleaseInput) && 108 | (state & sfModal) ) 109 | { 110 | endModal(cmCancel); 111 | if (ev.message.command == cmClose) 112 | putEvent(ev); 113 | clearEvent(ev); 114 | } 115 | else if ( ev.message.command == consts.cmTerminalUpdated && 116 | ev.message.infoPtr ) 117 | checkChanges(*(TerminalUpdatedMsg *) ev.message.infoPtr); 118 | break; 119 | 120 | case evMouseDown: 121 | if ((state & sfModal) && !mouseInView(ev.mouse.where)) 122 | { 123 | endModal(cmCancel); 124 | putEvent(ev); 125 | clearEvent(ev); 126 | } 127 | break; 128 | } 129 | if (isDisconnected() && (state & sfModal)) 130 | endModal(cmCancel); 131 | TWindow::handleEvent(ev); 132 | } 133 | 134 | void BasicTerminalWindow::setState(ushort aState, Boolean enable) 135 | { 136 | TWindow::setState(aState, enable); 137 | if (aState == sfActive) 138 | { 139 | for (ushort cmd : consts.focusedCmds()) 140 | if (enable) 141 | enableCommand(cmd); 142 | else 143 | disableCommand(cmd); 144 | } 145 | } 146 | 147 | ushort BasicTerminalWindow::execute() 148 | { 149 | auto lastHelpCtx = helpCtx; 150 | helpCtx = consts.hcInputGrabbed; 151 | if (frame) frame->drawView(); 152 | 153 | TWindow::execute(); 154 | 155 | helpCtx = lastHelpCtx; 156 | if (frame) frame->drawView(); 157 | return 0; 158 | } 159 | 160 | } // namespace tvterm 161 | -------------------------------------------------------------------------------- /source/tvterm-core/util.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_UTIL_H 2 | #define TVTERM_UTIL_H 3 | 4 | // https://stackoverflow.com/a/60166119 5 | 6 | template 7 | struct static_wrapper {}; 8 | 9 | template 10 | struct static_wrapper 11 | { 12 | static R invoke(Args... args, void *user) 13 | { 14 | return (((T*) user)->*func)(static_cast(args)...); 15 | } 16 | }; 17 | 18 | #define _static_wrap(func) (&static_wrapper::invoke) 19 | 20 | inline constexpr uint32_t utf8To32(TStringView s) 21 | { 22 | constexpr uint32_t error = 0xFFFD; // "�". 23 | switch (s.size()) 24 | { 25 | case 1: 26 | if (((uchar) s[0] & 0b1000'0000) == 0) 27 | return s[0]; 28 | return error; 29 | case 2: 30 | if ((((uchar) s[0] & 0b1110'0000) == 0b1100'0000) & (((uchar) s[1] & 0b1100'0000) == 0b1000'0000)) 31 | return ((s[0] & 0b0001'1111) << 6) | (s[1] & 0b0011'1111); 32 | return error; 33 | case 3: 34 | if ( (((uchar) s[0] & 0b1111'0000) == 0b1110'0000) & (((uchar) s[1] & 0b1100'0000) == 0b1000'0000) & 35 | (((uchar) s[2] & 0b1100'0000) == 0b1000'0000) 36 | ) 37 | return ((s[0] & 0b0000'1111) << 12) | ((s[1] & 0b0011'1111) << 6) | (s[2] & 0b0011'1111); 38 | return error; 39 | case 4: 40 | if ( (((uchar) s[0] & 0b1111'1000) == 0b1111'0000) & (((uchar) s[1] & 0b1100'0000) == 0b1000'0000) & 41 | (((uchar) s[2] & 0b1100'0000) == 0b1000'0000) & (((uchar) s[3] & 0b1100'0000) == 0b1000'0000) 42 | ) 43 | return ((s[0] & 0b0000'1111) << 18) | ((s[1] & 0b0011'1111) << 12) | ((s[2] & 0b0011'1111) << 6) | (s[3] & 0b0011'1111); 44 | return error; 45 | } 46 | return 0; 47 | } 48 | 49 | #endif // TVTERM_UTIL_H 50 | -------------------------------------------------------------------------------- /source/tvterm-core/vtermemu.cc: -------------------------------------------------------------------------------- 1 | #define Uses_TText 2 | #define Uses_TKeys 3 | #define Uses_TEvent 4 | #include 5 | 6 | #include "util.h" 7 | #include 8 | #include 9 | #include 10 | 11 | namespace tvterm 12 | { 13 | 14 | const VTermScreenCallbacks VTermEmulator::callbacks = 15 | { 16 | _static_wrap(&VTermEmulator::damage), 17 | _static_wrap(&VTermEmulator::moverect), 18 | _static_wrap(&VTermEmulator::movecursor), 19 | _static_wrap(&VTermEmulator::settermprop), 20 | _static_wrap(&VTermEmulator::bell), 21 | _static_wrap(&VTermEmulator::resize), 22 | _static_wrap(&VTermEmulator::sb_pushline), 23 | _static_wrap(&VTermEmulator::sb_popline), 24 | }; 25 | 26 | namespace vtermemu 27 | { 28 | 29 | // Input conversion. 30 | 31 | static const std::unordered_map keys = 32 | { 33 | { kbEnter, VTERM_KEY_ENTER }, 34 | { kbTab, VTERM_KEY_TAB }, 35 | { kbBack, VTERM_KEY_BACKSPACE }, 36 | { kbEsc, VTERM_KEY_ESCAPE }, 37 | { kbUp, VTERM_KEY_UP }, 38 | { kbDown, VTERM_KEY_DOWN }, 39 | { kbLeft, VTERM_KEY_LEFT }, 40 | { kbRight, VTERM_KEY_RIGHT }, 41 | { kbIns, VTERM_KEY_INS }, 42 | { kbDel, VTERM_KEY_DEL }, 43 | { kbHome, VTERM_KEY_HOME }, 44 | { kbEnd, VTERM_KEY_END }, 45 | { kbPgUp, VTERM_KEY_PAGEUP }, 46 | { kbPgDn, VTERM_KEY_PAGEDOWN }, 47 | { kbF1, VTERM_KEY_FUNCTION(1) }, 48 | { kbF2, VTERM_KEY_FUNCTION(2) }, 49 | { kbF3, VTERM_KEY_FUNCTION(3) }, 50 | { kbF4, VTERM_KEY_FUNCTION(4) }, 51 | { kbF5, VTERM_KEY_FUNCTION(5) }, 52 | { kbF6, VTERM_KEY_FUNCTION(6) }, 53 | { kbF7, VTERM_KEY_FUNCTION(7) }, 54 | { kbF8, VTERM_KEY_FUNCTION(8) }, 55 | { kbF9, VTERM_KEY_FUNCTION(9) }, 56 | { kbF10, VTERM_KEY_FUNCTION(10) }, 57 | { kbF11, VTERM_KEY_FUNCTION(11) }, 58 | { kbF12, VTERM_KEY_FUNCTION(12) }, 59 | }; 60 | 61 | static constexpr struct { ushort tv; VTermModifier vt; } modifiers[] = 62 | { 63 | { kbShift, VTERM_MOD_SHIFT }, 64 | { kbLeftAlt, VTERM_MOD_ALT }, 65 | { kbCtrlShift, VTERM_MOD_CTRL }, 66 | }; 67 | 68 | static VTermKey convKey(ushort keyCode) 69 | { 70 | auto it = keys.find(keyCode); 71 | if (it != keys.end()) 72 | return VTermKey(it->second); 73 | return VTERM_KEY_NONE; 74 | } 75 | 76 | static VTermModifier convMod(ushort controlKeyState) 77 | { 78 | VTermModifier mod = VTERM_MOD_NONE; 79 | for (const auto &m : modifiers) 80 | if (controlKeyState & m.tv) 81 | mod = VTermModifier(mod | m.vt); 82 | return mod; 83 | } 84 | 85 | static void convMouse( const MouseEventType &mouse, 86 | VTermModifier &mod, int &button ) 87 | { 88 | mod = convMod(mouse.controlKeyState); 89 | button = (mouse.buttons & mbLeftButton) ? 1 : 90 | (mouse.buttons & mbMiddleButton) ? 2 : 91 | (mouse.buttons & mbRightButton) ? 3 : 92 | (mouse.wheel & mwUp) ? 4 : 93 | (mouse.wheel & mwDown) ? 5 : 94 | 0 ; 95 | } 96 | 97 | static void processKey(VTerm *vt, KeyDownEvent keyDown) 98 | { 99 | TKey tvKey(keyDown); 100 | VTermModifier vtMod = convMod(tvKey.mods); 101 | // Pass control characters directly, with no modifiers. 102 | if ( tvKey.mods == kbCtrlShift 103 | && 'A' <= tvKey.code && tvKey.code <= 'Z' ) 104 | { 105 | keyDown.text[0] = keyDown.charScan.charCode; 106 | keyDown.textLength = 1; 107 | vtMod = VTERM_MOD_NONE; 108 | } 109 | // Pass other legacy 'letter+mod' combinations as text. 110 | else if ( keyDown.textLength == 0 111 | && ' ' <= tvKey.code && tvKey.code < '\x7F' ) 112 | { 113 | keyDown.text[0] = (char) tvKey.code; 114 | keyDown.textLength = 1; 115 | // On Windows, ConPTY unfortunately adds the Shift modifier on an 116 | // uppercase Alt+Key, so make it lowercase. 117 | if ( (keyDown.controlKeyState & (kbShift | kbCtrlShift | kbAltShift)) == kbLeftAlt 118 | && ('A' <= tvKey.code && tvKey.code <= 'Z') ) 119 | keyDown.text[0] += 'a' - 'A'; 120 | } 121 | 122 | if (keyDown.textLength != 0) 123 | vterm_keyboard_unichar(vt, utf8To32(keyDown.getText()), vtMod); 124 | else if (VTermKey vtKey = convKey(tvKey.code)) 125 | vterm_keyboard_key(vt, vtKey, vtMod); 126 | } 127 | 128 | static void processMouse(VTerm *vt, ushort what, const MouseEventType &mouse) 129 | { 130 | VTermModifier mod; int button; 131 | convMouse(mouse, mod, button); 132 | vterm_mouse_move(vt, mouse.where.y, mouse.where.x, mod); 133 | if (what & (evMouseDown | evMouseUp | evMouseWheel)) 134 | vterm_mouse_button(vt, button, what != evMouseUp, mod); 135 | } 136 | 137 | static void wheelToArrow(VTerm *vt, uchar wheel) 138 | { 139 | VTermKey key = VTERM_KEY_NONE; 140 | switch (wheel) 141 | { 142 | case mwUp: key = VTERM_KEY_UP; break; 143 | case mwDown: key = VTERM_KEY_DOWN; break; 144 | case mwLeft: key = VTERM_KEY_LEFT; break; 145 | case mwRight: key = VTERM_KEY_RIGHT; break; 146 | } 147 | for (int i = 0; i < 3; ++i) 148 | vterm_keyboard_key(vt, key, VTERM_MOD_NONE); 149 | } 150 | 151 | // Output conversion. 152 | 153 | static TColorRGB VTermRGBtoRGB(VTermColor c) 154 | { 155 | return {c.rgb.red, c.rgb.green, c.rgb.blue}; 156 | } 157 | 158 | static TColorAttr convAttr(const VTermScreenCell &cell) 159 | { 160 | auto &vt_fg = cell.fg, 161 | &vt_bg = cell.bg; 162 | auto &vt_attr = cell.attrs; 163 | TColorDesired fg, bg; 164 | // I prefer '{}', but GCC doesn't optimize it very well. 165 | memset(&fg, 0, sizeof(fg)); 166 | memset(&bg, 0, sizeof(bg)); 167 | 168 | if (!VTERM_COLOR_IS_DEFAULT_FG(&vt_fg)) 169 | { 170 | if (VTERM_COLOR_IS_INDEXED(&vt_fg)) 171 | fg = TColorXTerm(vt_fg.indexed.idx); 172 | else if (VTERM_COLOR_IS_RGB(&vt_fg)) 173 | fg = VTermRGBtoRGB(vt_fg); 174 | } 175 | 176 | if (!VTERM_COLOR_IS_DEFAULT_BG(&vt_bg)) 177 | { 178 | if (VTERM_COLOR_IS_INDEXED(&vt_bg)) 179 | bg = TColorXTerm(vt_bg.indexed.idx); 180 | else if (VTERM_COLOR_IS_RGB(&vt_bg)) 181 | bg = VTermRGBtoRGB(vt_bg); 182 | } 183 | 184 | ushort style = 185 | (slBold & -!!vt_attr.bold) 186 | | (slItalic & -!!vt_attr.italic) 187 | | (slUnderline & -!!vt_attr.underline) 188 | | (slBlink & -!!vt_attr.blink) 189 | | (slReverse & -!!vt_attr.reverse) 190 | | (slStrike & -!!vt_attr.strike) 191 | ; 192 | return {fg, bg, style}; 193 | } 194 | 195 | static void convCell( TSpan cells, int x, 196 | const VTermScreenCell &vtCell ) 197 | { 198 | if (vtCell.chars[0] == (uint32_t) -1) // Wide char trail. 199 | { 200 | // Turbo Vision and libvterm may disagree on what characters 201 | // are double-width. If libvterm considers a character isn't 202 | // double-width but Turbo Vision does, it will manage to display it 203 | // properly anyway. But, in the opposite case, we need to place a 204 | // space after the double-width character. 205 | if (x > 0 && !cells[x - 1].isWide()) 206 | { 207 | ::setChar(cells[x], ' '); 208 | ::setAttr(cells[x], ::getAttr(cells[x - 1])); 209 | } 210 | } 211 | else 212 | { 213 | size_t length = 0; 214 | while (vtCell.chars[length]) 215 | ++length; 216 | TSpan text {vtCell.chars, max(1, length)}; 217 | TText::drawStr(cells, x, text, 0, convAttr(vtCell)); 218 | } 219 | } 220 | 221 | static void drawLine( TerminalSurface &surface, VTermScreen *vtScreen, 222 | int y, int begin, int end ) 223 | // Pre: the area must be within bounds. 224 | { 225 | dout << "drawLine(" << y << ", " << begin << ", " << end << ")" << std::endl; 226 | TSpan cells(&surface.at(y, 0), surface.size.x); 227 | for (int x = begin; x < end; ++x) 228 | { 229 | VTermScreenCell cell; 230 | if (vterm_screen_get_cell(vtScreen, {y, x}, &cell)) 231 | convCell(cells, x, cell); 232 | else 233 | cells[x] = {}; 234 | } 235 | surface.addDamageAtRow(y, begin, end); 236 | } 237 | 238 | } // namespace vtermemu 239 | 240 | TerminalEmulator &VTermEmulatorFactory::create(TPoint size, Writer &clientDataWriter) noexcept 241 | { 242 | return *new VTermEmulator(size, clientDataWriter); 243 | } 244 | 245 | TSpan VTermEmulatorFactory::getCustomEnvironment() noexcept 246 | { 247 | static constexpr EnvironmentVar customEnvironment[] = 248 | { 249 | {"TERM", "xterm-256color"}, 250 | {"COLORTERM", "truecolor"}, 251 | }; 252 | 253 | return customEnvironment; 254 | } 255 | 256 | VTermEmulator::VTermEmulator(TPoint size, Writer &aClientDataWriter) noexcept : 257 | clientDataWriter(aClientDataWriter) 258 | { 259 | // VTerm requires size to be at least 1. 260 | size.x = max(size.x, 1); 261 | size.y = max(size.y, 1); 262 | damageByRow.resize(size.y); 263 | 264 | vt = vterm_new(size.y, size.x); 265 | vterm_set_utf8(vt, 1); 266 | 267 | vtState = vterm_obtain_state(vt); 268 | vterm_state_reset(vtState, true); 269 | 270 | vtScreen = vterm_obtain_screen(vt); 271 | vterm_screen_enable_altscreen(vtScreen, true); 272 | vterm_screen_set_callbacks(vtScreen, &callbacks, this); 273 | vterm_screen_set_damage_merge(vtScreen, VTERM_DAMAGE_SCROLL); 274 | vterm_screen_reset(vtScreen, true); 275 | 276 | vterm_output_set_callback(vt, _static_wrap(&VTermEmulator::writeOutput), this); 277 | 278 | // VTerm's cursor blinks by default, but it shouldn't. 279 | VTermValue val {0}; 280 | vterm_state_set_termprop(vtState, VTERM_PROP_CURSORBLINK, &val); 281 | } 282 | 283 | VTermEmulator::~VTermEmulator() 284 | { 285 | vterm_free(vt); 286 | } 287 | 288 | void VTermEmulator::handleEvent(const TerminalEvent &event) noexcept 289 | { 290 | using namespace vtermemu; 291 | switch (event.type) 292 | { 293 | case TerminalEventType::KeyDown: 294 | processKey(vt, event.keyDown); 295 | break; 296 | 297 | case TerminalEventType::Mouse: 298 | if (localState.mouseEnabled) 299 | processMouse(vt, event.mouse.what, event.mouse.mouse); 300 | else if (localState.altScreenEnabled && event.mouse.what == evMouseWheel) 301 | wheelToArrow(vt, event.mouse.mouse.wheel); 302 | break; 303 | 304 | case TerminalEventType::ClientDataRead: 305 | { 306 | auto &clientData = event.clientDataRead; 307 | vterm_input_write(vt, clientData.data, clientData.size); 308 | break; 309 | } 310 | 311 | case TerminalEventType::ViewportResize: 312 | { 313 | TPoint size = {event.viewportResize.x, event.viewportResize.y}; 314 | setSize(size); 315 | break; 316 | } 317 | 318 | case TerminalEventType::FocusChange: 319 | if (event.focusChange.focusEnabled) 320 | vterm_state_focus_in(vtState); 321 | else 322 | vterm_state_focus_out(vtState); 323 | break; 324 | 325 | default: 326 | break; 327 | } 328 | } 329 | 330 | void VTermEmulator::updateState(TerminalState &state) noexcept 331 | { 332 | vterm_screen_flush_damage(vtScreen); 333 | drawDamagedArea(state.surface); 334 | if (localState.cursorChanged) 335 | { 336 | localState.cursorChanged = false; 337 | state.cursorChanged = true; 338 | state.cursorPos = localState.cursorPos; 339 | state.cursorVisible = localState.cursorVisible; 340 | state.cursorBlink = localState.cursorBlink; 341 | } 342 | if (localState.titleChanged) 343 | { 344 | localState.titleChanged = false; 345 | state.titleChanged = true; 346 | state.title = std::move(localState.title); 347 | } 348 | } 349 | 350 | TPoint VTermEmulator::getSize() noexcept 351 | { 352 | TPoint size; 353 | vterm_get_size(vt, &size.y, &size.x); 354 | return size; 355 | } 356 | 357 | void VTermEmulator::setSize(TPoint size) noexcept 358 | { 359 | size.x = max(size.x, 1); 360 | size.y = max(size.y, 1); 361 | 362 | if (size != getSize()) 363 | { 364 | vterm_set_size(vt, size.y, size.x); 365 | damageByRow.resize(0); 366 | damageByRow.resize(size.y); 367 | } 368 | } 369 | 370 | void VTermEmulator::drawDamagedArea(TerminalSurface &surface) noexcept 371 | { 372 | using namespace vtermemu; 373 | TPoint size = getSize(); 374 | if (surface.size != size) 375 | surface.resize(size); 376 | for (int y = 0; y < size.y; ++y) 377 | { 378 | auto &damage = damageByRow[y]; 379 | int begin = max(damage.begin, 0); 380 | int end = min(damage.end, size.x); 381 | if (begin < end) 382 | drawLine(surface, vtScreen, y, begin, end); 383 | damage = {}; 384 | } 385 | } 386 | 387 | void VTermEmulator::writeOutput(const char *data, size_t size) 388 | { 389 | clientDataWriter.write({data, size}); 390 | } 391 | 392 | int VTermEmulator::damage(VTermRect rect) 393 | { 394 | rect.start_row = min(max(rect.start_row, 0), damageByRow.size()); 395 | rect.end_row = min(max(rect.end_row, 0), damageByRow.size()); 396 | for (int y = rect.start_row; y < rect.end_row; ++y) 397 | { 398 | auto &damage = damageByRow[y]; 399 | damage.begin = min(rect.start_col, damage.begin); 400 | damage.end = max(rect.end_col, damage.end); 401 | } 402 | return true; 403 | } 404 | 405 | int VTermEmulator::moverect(VTermRect dest, VTermRect src) 406 | { 407 | dout << "moverect(" << dest << ", " << src << ")" << std::endl; 408 | return false; 409 | } 410 | 411 | int VTermEmulator::movecursor(VTermPos pos, VTermPos oldpos, int visible) 412 | { 413 | localState.cursorChanged = true; 414 | localState.cursorPos = {pos.col, pos.row}; 415 | return true; 416 | } 417 | 418 | int VTermEmulator::settermprop(VTermProp prop, VTermValue *val) 419 | { 420 | dout << "settermprop(" << prop << ", " << val << ")" << std::endl; 421 | if (vterm_get_prop_type(prop) == VTERM_VALUETYPE_STRING) 422 | { 423 | if (val->string.initial) 424 | strFragBuf.clear(); 425 | strFragBuf.push(val->string.str, val->string.len); 426 | if (!val->string.final) 427 | return true; 428 | } 429 | 430 | switch (prop) 431 | { 432 | case VTERM_PROP_TITLE: 433 | localState.titleChanged = true; 434 | localState.title = std::move(strFragBuf); 435 | break; 436 | case VTERM_PROP_CURSORVISIBLE: 437 | localState.cursorChanged = true; 438 | localState.cursorVisible = val->boolean; 439 | break; 440 | case VTERM_PROP_CURSORBLINK: 441 | localState.cursorChanged = true; 442 | localState.cursorBlink = val->boolean; 443 | break; 444 | case VTERM_PROP_MOUSE: 445 | localState.mouseEnabled = val->boolean; 446 | break; 447 | case VTERM_PROP_ALTSCREEN: 448 | localState.altScreenEnabled = val->boolean; 449 | break; 450 | default: 451 | return false; 452 | } 453 | return true; 454 | } 455 | 456 | int VTermEmulator::bell() 457 | { 458 | dout << "bell()" << std::endl; 459 | return false; 460 | } 461 | 462 | int VTermEmulator::resize(int rows, int cols) 463 | { 464 | return false; 465 | } 466 | 467 | int VTermEmulator::sb_pushline(int cols, const VTermScreenCell *cells) 468 | { 469 | linestack.push(std::max(cols, 0), cells); 470 | return true; 471 | } 472 | 473 | int VTermEmulator::sb_popline(int cols, VTermScreenCell *cells) 474 | { 475 | return linestack.pop(*this, std::max(cols, 0), cells); 476 | } 477 | 478 | inline VTermScreenCell VTermEmulator::getDefaultCell() const 479 | { 480 | VTermScreenCell cell {}; 481 | cell.width = 1; 482 | vterm_state_get_default_colors(vtState, &cell.fg, &cell.bg); 483 | return cell; 484 | } 485 | 486 | void VTermEmulator::LineStack::push(size_t cols, const VTermScreenCell *src) 487 | { 488 | if (stack.size() < maxSize) 489 | { 490 | auto *line = new VTermScreenCell[cols]; 491 | memcpy(line, src, sizeof(VTermScreenCell)*cols); 492 | stack.emplace_back(line, cols); 493 | } 494 | } 495 | 496 | bool VTermEmulator::LineStack::pop( const VTermEmulator &vterm, 497 | size_t cols, VTermScreenCell *dst ) 498 | { 499 | if (!stack.empty()) 500 | { 501 | auto line = top(); 502 | size_t dst_size = cols*sizeof(VTermScreenCell); 503 | size_t copy_bytes = std::min(line.size_bytes(), dst_size); 504 | memcpy(dst, line.data(), copy_bytes); 505 | auto cell = vterm.getDefaultCell(); 506 | for (size_t i = line.size(); i < cols; ++i) 507 | dst[i] = cell; 508 | stack.pop_back(); 509 | return true; 510 | } 511 | return false; 512 | } 513 | 514 | } // namespace tvterm 515 | -------------------------------------------------------------------------------- /source/tvterm/app.cc: -------------------------------------------------------------------------------- 1 | #define Uses_TMenuBar 2 | #define Uses_TSubMenu 3 | #define Uses_TMenu 4 | #define Uses_TMenuItem 5 | #define Uses_TStatusLine 6 | #define Uses_TStatusItem 7 | #define Uses_TStatusDef 8 | #define Uses_TKeys 9 | #define Uses_TEvent 10 | #define Uses_TChDirDialog 11 | #define Uses_TDeskTop 12 | #define Uses_MsgBox 13 | #include 14 | 15 | #include "app.h" 16 | #include "cmds.h" 17 | #include "desk.h" 18 | #include "wnd.h" 19 | #include "apputil.h" 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | 26 | TCommandSet TVTermApp::tileCmds = []() 27 | { 28 | TCommandSet ts; 29 | ts += cmTile; 30 | ts += cmTileCols; 31 | ts += cmTileRows; 32 | ts += cmCascade; 33 | return ts; 34 | }(); 35 | 36 | int main(int, char**) 37 | { 38 | TVTermApp app; 39 | app.run(); 40 | app.shutDown(); 41 | } 42 | 43 | TVTermApp::TVTermApp() : 44 | TProgInit( &TVTermApp::initStatusLine, 45 | nullptr, 46 | &TVTermApp::initDeskTop 47 | ) 48 | { 49 | disableCommands(tileCmds); 50 | for (ushort cmd : TerminalWindow::appConsts.focusedCmds()) 51 | disableCommand(cmd); 52 | newTerm(); 53 | } 54 | 55 | TStatusLine *TVTermApp::initStatusLine(TRect r) 56 | { 57 | r.b.y = r.a.y + 1; 58 | TStatusLine *statusLine = new TStatusLine(r, 59 | *new TStatusDef(hcDragging, hcDragging) + 60 | *new TStatusItem("~↑↓→←~ Move", kbNoKey, 0) + 61 | *new TStatusItem("~Shift-↑↓→←~ Resize", kbNoKey, 0) + 62 | *new TStatusItem("~Ctrl~ Move/Resize Faster", kbNoKey, 0) + 63 | *new TStatusItem("~Enter~ Done", kbNoKey, 0) + 64 | *new TStatusItem("~Esc~ Cancel", kbNoKey, 0) + 65 | *new TStatusDef(hcInputGrabbed, hcInputGrabbed) + 66 | *new TStatusItem("~Alt-End~ Release Input", kbAltEnd, cmReleaseInput) + 67 | *new TStatusDef(hcMenu, hcMenu) + 68 | *new TStatusItem("~Esc~ Close Menu", kbNoKey, 0) + 69 | *new TStatusDef(0, 0xFFFF) + 70 | *new TStatusItem("~Ctrl-B~ Open Menu" , kbCtrlB, cmMenu) 71 | ); 72 | statusLine->growMode = gfGrowHiX; 73 | return statusLine; 74 | } 75 | 76 | TDeskTop *TVTermApp::initDeskTop(TRect r) 77 | { 78 | r.a.y += 1; 79 | return new TVTermDesk(r); 80 | } 81 | 82 | void TVTermApp::handleEvent(TEvent &event) 83 | { 84 | TApplication::handleEvent(event); 85 | bool handled = true; 86 | switch (event.what) 87 | { 88 | case evCommand: 89 | switch (event.message.command) 90 | { 91 | case cmMenu: openMenu(); break; 92 | case cmNewTerm: newTerm(); break; 93 | case cmChangeDir: changeDir(); break; 94 | case cmTileCols: getDeskTop()->tileVertical(getTileRect()); break; 95 | case cmTileRows: getDeskTop()->tileHorizontal(getTileRect()); break; 96 | default: 97 | handled = false; 98 | break; 99 | } 100 | break; 101 | default: 102 | handled = false; 103 | break; 104 | } 105 | if (handled) 106 | clearEvent(event); 107 | } 108 | 109 | size_t TVTermApp::getOpenTermCount() 110 | { 111 | size_t count = 0; 112 | message(this, evBroadcast, cmGetOpenTerms, &count); 113 | return count; 114 | } 115 | 116 | Boolean TVTermApp::valid(ushort command) 117 | { 118 | if (command == cmQuit) 119 | { 120 | if (size_t count = getOpenTermCount()) 121 | { 122 | auto *format = (count == 1) 123 | ? "There is %zu open terminal window.\nDo you want to quit anyway?" 124 | : "There are %zu open terminal windows.\nDo you want to quit anyway?"; 125 | return messageBox( 126 | mfConfirmation | mfYesButton | mfNoButton, 127 | format, count 128 | ) == cmYes; 129 | } 130 | return True; 131 | } 132 | return TApplication::valid(command); 133 | } 134 | 135 | void TVTermApp::idle() 136 | { 137 | TApplication::idle(); 138 | { 139 | // Enable or disable the cmTile and cmCascade commands. 140 | auto isTileable = 141 | [] (TView *p, void *) -> Boolean { return p->options & ofTileable; }; 142 | if (deskTop->firstThat(isTileable, nullptr)) 143 | enableCommands(tileCmds); 144 | else 145 | disableCommands(tileCmds); 146 | } 147 | message(this, evBroadcast, cmCheckTerminalUpdates, nullptr); 148 | } 149 | 150 | void TVTermApp::openMenu() 151 | { 152 | TMenuItem &menuItems = 153 | *new TMenuItem("New Term", cmNewTerm, 'N', hcNoContext, "~N~") + 154 | *new TMenuItem("Close Term", cmClose, 'W', hcNoContext, "~W~") + 155 | newLine() + 156 | *new TMenuItem("Next Term", cmNext, kbTab, hcNoContext, "~Tab~") + 157 | *new TMenuItem("Previous Term", cmPrev, kbShiftTab, hcNoContext, "~Shift-Tab~") + 158 | *new TMenuItem("Tile (Columns First)", cmTileCols, 'V', hcNoContext, "~V~") + 159 | *new TMenuItem("Tile (Rows First)", cmTileRows, 'H', hcNoContext, "~H~") + 160 | *new TMenuItem("Resize/Move", cmResize, 'R', hcNoContext, "~R~") + 161 | *new TMenuItem("Maximize/Restore", cmZoom, 'F', hcNoContext, "~F~") + 162 | newLine() + 163 | ( *new TSubMenu("~M~ore...", kbNoKey, hcMenu) + 164 | *new TMenuItem("~C~hange working dir...", cmChangeDir, kbNoKey) + 165 | newLine() + 166 | *new TMenuItem("C~a~scade", cmCascade, kbNoKey) + 167 | *new TMenuItem("~G~rab Input", cmGrabInput, kbNoKey) 168 | ) + 169 | *new TMenuItem("Suspend", cmDosShell, 'U', hcNoContext, "~U~") + 170 | *new TMenuItem("Exit", cmQuit, 'Q', hcNoContext, "~Q~"); 171 | 172 | TMenu *menu = new TMenu(menuItems); 173 | TMenuPopup *menuPopup = new CenteredMenuPopup(menu); 174 | menuPopup->helpCtx = hcMenu; 175 | 176 | if (ushort cmd = execView(menuPopup)) 177 | { 178 | TEvent event = {}; 179 | event.what = evCommand; 180 | event.message.command = cmd; 181 | putEvent(event); 182 | } 183 | 184 | TObject::destroy(menuPopup); 185 | delete menu; 186 | } 187 | 188 | static void onTermError(const char *reason) 189 | { 190 | messageBox(mfError | mfOKButton, "Cannot create terminal: %s.", reason); 191 | }; 192 | 193 | void TVTermApp::newTerm() 194 | { 195 | using namespace tvterm; 196 | TRect r = deskTop->getExtent(); 197 | VTermEmulatorFactory factory; 198 | auto *termCtrl = TerminalController::create( TerminalWindow::viewSize(r), 199 | factory, onTermError ); 200 | if (termCtrl) 201 | insertWindow(new TerminalWindow(r, *termCtrl)); 202 | } 203 | 204 | void TVTermApp::changeDir() 205 | { 206 | execDialog(new TChDirDialog(cdNormal, 0)); 207 | } 208 | -------------------------------------------------------------------------------- /source/tvterm/app.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_APP_H 2 | #define TVTERM_APP_H 3 | 4 | #define Uses_TApplication 5 | #define Uses_TCommandSet 6 | #include 7 | 8 | class TVTermDesk; 9 | 10 | struct TVTermApp : public TApplication 11 | { 12 | static TCommandSet tileCmds; 13 | 14 | TVTermApp(); 15 | static TStatusLine* initStatusLine(TRect r); 16 | static TDeskTop* initDeskTop(TRect r); 17 | 18 | TVTermDesk* getDeskTop(); 19 | 20 | void handleEvent(TEvent &event) override; 21 | Boolean valid(ushort command) override; 22 | void idle() override; 23 | 24 | size_t getOpenTermCount(); 25 | 26 | // Command handlers 27 | 28 | void openMenu(); 29 | void newTerm(); 30 | void changeDir(); 31 | 32 | }; 33 | 34 | inline TVTermDesk* TVTermApp::getDeskTop() 35 | { 36 | return (TVTermDesk*) deskTop; 37 | } 38 | 39 | #endif // TVTERM_APP_H 40 | -------------------------------------------------------------------------------- /source/tvterm/apputil.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_APPUTIL_H 2 | #define TVTERM_APPUTIL_H 3 | 4 | #define Uses_TProgram 5 | #define Uses_TDeskTop 6 | #define Uses_TMenuPopup 7 | #include 8 | 9 | inline ushort execDialog(TDialog *d) 10 | { 11 | TView *p = TProgram::application->validView(d); 12 | if (p) 13 | { 14 | ushort result = TProgram::deskTop->execView(p); 15 | TObject::destroy(p); 16 | return result; 17 | } 18 | return cmCancel; 19 | } 20 | 21 | class CenteredMenuPopup : public TMenuPopup 22 | { 23 | public: 24 | 25 | CenteredMenuPopup(TMenu *aMenu) noexcept : 26 | TMenuPopup(TRect(0, 0, 0, 0), aMenu) 27 | { 28 | options |= ofCentered; 29 | } 30 | 31 | void calcBounds(TRect &bounds, TPoint delta) override 32 | { 33 | bounds.a.x = (owner->size.x - size.x)/2; 34 | bounds.a.y = (owner->size.y - size.y)/2; 35 | bounds.b.x = bounds.a.x + size.x; 36 | bounds.b.y = bounds.a.y + size.y; 37 | } 38 | }; 39 | 40 | #endif // TVTERM_APPUTIL_H 41 | -------------------------------------------------------------------------------- /source/tvterm/cmds.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_CMDS_H 2 | #define TVTERM_CMDS_H 3 | 4 | #include 5 | 6 | enum : ushort 7 | { 8 | cmGrabInput = 100, 9 | cmReleaseInput, 10 | cmTileCols, 11 | cmTileRows, 12 | // Commands that cannot be deactivated. 13 | cmNewTerm = 1000, 14 | cmCheckTerminalUpdates, 15 | cmTerminalUpdated, 16 | cmGetOpenTerms, 17 | }; 18 | 19 | enum : ushort 20 | { 21 | hcMenu = 1000, 22 | hcInputGrabbed, 23 | }; 24 | 25 | #endif // TVTERM_CMDS_H 26 | -------------------------------------------------------------------------------- /source/tvterm/desk.cc: -------------------------------------------------------------------------------- 1 | #include "desk.h" 2 | 3 | TVTermDesk::TVTermDesk(const TRect &bounds) : 4 | TDeskInit(&TDeskTop::initBackground), 5 | TDeskTop(bounds) 6 | { 7 | } 8 | 9 | void TVTermDesk::tileWithOrientation(bool columnsFirst, const TRect &bounds) 10 | { 11 | bool bak = tileColumnsFirst; 12 | tileColumnsFirst = columnsFirst; 13 | tile(bounds); 14 | tileColumnsFirst = bak; 15 | } 16 | 17 | void TVTermDesk::tileVertical(const TRect &bounds) 18 | { 19 | tileWithOrientation(true, bounds); 20 | } 21 | 22 | void TVTermDesk::tileHorizontal(const TRect &bounds) 23 | { 24 | tileWithOrientation(false, bounds); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /source/tvterm/desk.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_DESK_H 2 | #define TVTERM_DESK_H 3 | 4 | #define Uses_TDeskTop 5 | #include 6 | 7 | class TVTermDesk : public TDeskTop 8 | { 9 | 10 | void tileWithOrientation(bool columnsFirst, const TRect &bounds); 11 | 12 | public: 13 | 14 | TVTermDesk(const TRect &bounds); 15 | 16 | void tileVertical(const TRect &bounds); 17 | void tileHorizontal(const TRect &bounds); 18 | 19 | }; 20 | 21 | #endif // TVTERM_DESK_H 22 | -------------------------------------------------------------------------------- /source/tvterm/wnd.cc: -------------------------------------------------------------------------------- 1 | #include "wnd.h" 2 | #include "cmds.h" 3 | 4 | #define Uses_TEvent 5 | #include 6 | 7 | const tvterm::TVTermConstants TerminalWindow::appConsts = 8 | { 9 | cmCheckTerminalUpdates, 10 | cmTerminalUpdated, 11 | cmGrabInput, 12 | cmReleaseInput, 13 | hcInputGrabbed, 14 | }; 15 | 16 | void TerminalWindow::handleEvent(TEvent &ev) 17 | { 18 | if ( ev.what == evBroadcast && 19 | ev.message.command == cmGetOpenTerms && !isDisconnected() ) 20 | *(size_t *) ev.message.infoPtr += 1; 21 | else if( ev.what == evCommand && ev.message.command == cmZoom && 22 | (!ev.message.infoPtr || ev.message.infoPtr == this) ) 23 | { 24 | zoom(); 25 | clearEvent(ev); 26 | } 27 | else if ( ev.what == evKeyDown && isDisconnected() && 28 | !(state & (sfDragging | sfModal)) ) 29 | { 30 | close(); 31 | return; 32 | } 33 | Super::handleEvent(ev); 34 | } 35 | 36 | void TerminalWindow::sizeLimits(TPoint &min, TPoint &max) 37 | { 38 | Super::sizeLimits(min, max); 39 | if (owner) 40 | { 41 | max = owner->size; 42 | max.x += 2; 43 | max.y += 1; 44 | } 45 | } 46 | 47 | void TerminalWindow::zoom() noexcept 48 | { 49 | TPoint minSize, maxSize; 50 | sizeLimits(minSize, maxSize); 51 | if (size != maxSize) 52 | { 53 | zoomRect = getBounds(); 54 | 55 | // A maximized terminal shows just the title bar and cannot be dragged. 56 | TRect r(0, 0, maxSize.x, maxSize.y); 57 | r.move(-1, 0); 58 | growMode = gfGrowHiX | gfGrowHiY; 59 | flags &= ~(wfMove | wfGrow); 60 | 61 | locate(r); 62 | } 63 | else 64 | { 65 | growMode = gfGrowAll | gfGrowRel; 66 | flags |= wfMove | wfGrow; 67 | 68 | locate(zoomRect); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /source/tvterm/wnd.h: -------------------------------------------------------------------------------- 1 | #ifndef TVTERM_WND_H 2 | #define TVTERM_WND_H 3 | 4 | #include 5 | #include 6 | 7 | class TerminalWindow : public tvterm::BasicTerminalWindow 8 | { 9 | public: 10 | 11 | static const tvterm::TVTermConstants appConsts; 12 | 13 | TerminalWindow(const TRect &bounds, tvterm::TerminalController &aTerm) noexcept; 14 | 15 | void handleEvent(TEvent &ev) override; 16 | void sizeLimits(TPoint &min, TPoint &max) override; 17 | 18 | private: 19 | 20 | using Super = tvterm::BasicTerminalWindow; 21 | 22 | void zoom() noexcept; 23 | }; 24 | 25 | inline TerminalWindow::TerminalWindow( const TRect &bounds, 26 | tvterm::TerminalController &aTerm ) noexcept : 27 | TWindowInit(&initFrame), 28 | Super(bounds, aTerm, appConsts) 29 | { 30 | } 31 | 32 | #endif // TVTERM_WND_H 33 | --------------------------------------------------------------------------------