Compare commits
15 commits
2601d78453
...
f48641bb58
Author | SHA1 | Date | |
---|---|---|---|
Christian Burger | f48641bb58 | ||
Christian Burger | 3be5336ca0 | ||
Christian Burger | 0a086ff604 | ||
Christian Burger | 9ddb769cb4 | ||
Christian Burger | 4fb2e6a976 | ||
Christian Burger | 7e7372ee52 | ||
Christian Burger | a801752620 | ||
Christian Burger | 792f12c96c | ||
Christian Burger | 1357a7f6bf | ||
Christian Burger | b723aa5f33 | ||
Christian Burger | 320f5ba63a | ||
Christian Burger | e42711f123 | ||
Christian Burger | b9e32941fb | ||
Christian Burger | 0462a68c54 | ||
Christian Burger | f8db9dc660 |
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
|
@ -5,10 +5,10 @@
|
|||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "(gdb) start NCursesPtyApp",
|
||||
"name": "(gdb) start kNCursesDemoApp",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/NCursesPtyApp",
|
||||
"program": "${workspaceFolder}/build/kNCursesDemoApp",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
|
@ -29,10 +29,10 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"name": "(gdb) attach to running NCursesPtyApp",
|
||||
"name": "(gdb) attach to running kNCursesDemoApp",
|
||||
"type": "cppdbg",
|
||||
"request": "attach",
|
||||
"program": "${workspaceFolder}/build/NCursesPtyApp",
|
||||
"program": "${workspaceFolder}/build/kNCursesDemoApp",
|
||||
"processId": "${command:pickProcess}",
|
||||
"MIMode": "gdb",
|
||||
"setupCommands": [
|
||||
|
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -53,7 +53,10 @@
|
|||
"iostream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"pointers": "cpp"
|
||||
"pointers": "cpp",
|
||||
"list": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"mutex": "cpp"
|
||||
},
|
||||
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
|
||||
"C_Cpp.default.includePath": [
|
||||
|
|
64
App.cpp
64
App.cpp
|
@ -1,64 +0,0 @@
|
|||
/**
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include "App.hpp"
|
||||
#include "Debug.hpp"
|
||||
#include <NCursesPtyWindow/PtyWindow.hpp>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <utmp.h>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
{
|
||||
App::App() : NCursesApplication(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
int App::run()
|
||||
{
|
||||
std::mutex writeMutex;
|
||||
|
||||
PtyWindow *ptyWindow = new PtyWindow(&writeMutex
|
||||
, Root_Window->lines()
|
||||
, Root_Window->cols()
|
||||
, 0
|
||||
, 0);
|
||||
|
||||
if(fork() == 0)
|
||||
{
|
||||
const char *shellPath = getenv("SHELL");
|
||||
if(!shellPath)
|
||||
shellPath = "/bin/bash";
|
||||
|
||||
login_tty(ptyWindow->getFdPtyClient());
|
||||
const int sizeArg = 2;
|
||||
const char *arg[sizeArg] = {shellPath, NULL};
|
||||
fprintf(stderr, "<CTRL>+C exits shell.\n\n");
|
||||
execv(shellPath, const_cast<char * const *>(arg));
|
||||
fprintf(stderr, "Well, well, well … could not start a shell. We "
|
||||
"tried `%s`. Maybe set `SHELL` environment variable"
|
||||
" to a working value?", shellPath);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
while(true)
|
||||
{
|
||||
SingleUserInput input = ptyWindow->readSingleUserInput();
|
||||
if(input.isNormalKey())
|
||||
ptyWindow->writeUnicodeCharToClient(input.getRawInput());
|
||||
if(input.isFunctionKey())
|
||||
{
|
||||
if(input.getRawInput() == KEY_RESIZE)
|
||||
ptyWindow->wresize(Root_Window->lines(), Root_Window->cols());
|
||||
else
|
||||
ptyWindow->writeKeyToClient(input.mapKeyNcursesToVTerm());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
23
App.hpp
23
App.hpp
|
@ -1,23 +0,0 @@
|
|||
/**
|
||||
* @brief demo application for the library
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#ifndef A3B2AE4E_0A39_468C_8CCA_E6508166702A
|
||||
#define A3B2AE4E_0A39_468C_8CCA_E6508166702A
|
||||
|
||||
#include <cursesapp.h>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
{
|
||||
class App : public NCursesApplication
|
||||
{
|
||||
public:
|
||||
App();
|
||||
|
||||
private:
|
||||
int run() override;
|
||||
};
|
||||
}
|
||||
|
||||
#endif /* A3B2AE4E_0A39_468C_8CCA_E6508166702A */
|
|
@ -4,8 +4,8 @@
|
|||
cmake_minimum_required(VERSION 3.16.3)
|
||||
|
||||
include("cmake/version.cmake")
|
||||
project(NCursesPtyWindow
|
||||
HOMEPAGE_URL "https://gitea.xndr.de/christian/NCursesPtyWindow"
|
||||
project(kNCurses
|
||||
HOMEPAGE_URL "https://gitea.xndr.de/christian/kNCurses"
|
||||
VERSION ${SEMANTIC_VERSION})
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
@ -13,7 +13,9 @@ set(CMAKE_CXX_STANDARD 17)
|
|||
include(CTest)
|
||||
enable_testing()
|
||||
|
||||
add_library(NCursesPtyWindow Window.cpp PtyWindow.cpp SingleUserInput.cpp Debug.cpp)
|
||||
add_library(kNCurses Window.cpp PtyWindow.cpp SingleUserInput.cpp Debug.cpp
|
||||
TilingWindowManager.cpp VerticalTilingWindowManager.cpp
|
||||
HorizontalTilingWindowManager.cpp)
|
||||
|
||||
### path to own system includes
|
||||
include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/include")
|
||||
|
@ -21,34 +23,41 @@ include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/include")
|
|||
### libraries
|
||||
|
||||
include("cmake/ncurses.cmake")
|
||||
target_link_libraries(NCursesPtyWindow ${CURSES_LIBRARIES})
|
||||
target_link_libraries(kNCurses ${CURSES_LIBRARIES})
|
||||
|
||||
include("cmake/gsl.cmake")
|
||||
|
||||
include("cmake/libvterm.cmake")
|
||||
if(EXISTS libvtermProject)
|
||||
add_dependencies(NCursesPtyWindow libvtermProject)
|
||||
add_dependencies(kNCurses libvtermProject)
|
||||
endif()
|
||||
target_link_libraries(NCursesPtyWindow ${LIBVTERM_LIBRARY})
|
||||
target_link_libraries(kNCurses ${LIBVTERM_LIBRARY})
|
||||
|
||||
find_library(UTIL_LIBRARY util)
|
||||
target_link_libraries(NCursesPtyWindow ${UTIL_LIBRARY})
|
||||
target_link_libraries(kNCurses ${UTIL_LIBRARY})
|
||||
|
||||
set(THREADS_PREFER_PTHREAD_FLAG true)
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(NCursesPtyWindow Threads::Threads)
|
||||
target_link_libraries(kNCurses Threads::Threads)
|
||||
|
||||
### demo application
|
||||
add_executable(NCursesPtyApp main.cpp App.cpp)
|
||||
target_link_libraries(NCursesPtyApp NCursesPtyWindow)
|
||||
add_executable(kNCursesDemoApp main.cpp DemoApp.cpp)
|
||||
target_link_libraries(kNCursesDemoApp kNCurses)
|
||||
|
||||
### installation and packaging
|
||||
set(NCURSES_PTY_WINDOW_SYSTEM_INCLUDE "include/NCursesPtyWindow")
|
||||
set_target_properties(NCursesPtyWindow PROPERTIES PUBLIC_HEADER "${NCURSES_PTY_WINDOW_SYSTEM_INCLUDE}/Window.hpp;${NCURSES_PTY_WINDOW_SYSTEM_INCLUDE}/SingleUserInput.hpp;${NCURSES_PTY_WINDOW_SYSTEM_INCLUDE}/PtyWindow.hpp"
|
||||
set(NCURSES_SYSTEM_INCLUDE "include/kNCurses")
|
||||
set_target_properties(kNCurses PROPERTIES
|
||||
PUBLIC_HEADER "${NCURSES_SYSTEM_INCLUDE}/Window.hpp;
|
||||
${NCURSES_SYSTEM_INCLUDE}/SingleUserInput.hpp;
|
||||
${NCURSES_SYSTEM_INCLUDE}/PtyWindow.hpp;
|
||||
${NCURSES_SYSTEM_INCLUDE}/TilingWindowManager.hpp;
|
||||
${NCURSES_SYSTEM_INCLUDE}/VerticalTilingWindowManager.hpp;
|
||||
${NCURSES_SYSTEM_INCLUDE}/HorizontalTilingWindowManager.hpp;
|
||||
"
|
||||
VERSION "${CMAKE_PROJECT_VERSION}")
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS NCursesPtyWindow ARCHIVE
|
||||
PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/NCursesPtyWindow/")
|
||||
install(TARGETS kNCurses ARCHIVE
|
||||
PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/kNCurses/")
|
||||
|
||||
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
|
||||
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
|
||||
|
|
142
DemoApp.cpp
Normal file
142
DemoApp.cpp
Normal file
|
@ -0,0 +1,142 @@
|
|||
/**
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include "DemoApp.hpp"
|
||||
#include "Debug.hpp"
|
||||
#include <kNCurses/VerticalTilingWindowManager.hpp>
|
||||
#include <kNCurses/HorizontalTilingWindowManager.hpp>
|
||||
#include <kNCurses/Window.hpp>
|
||||
#include <kNCurses/PtyWindow.hpp>
|
||||
|
||||
#include <unistd.h>
|
||||
#include <utmp.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
using std::scoped_lock;
|
||||
|
||||
DemoApp::DemoApp() : NCursesApplication(false)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
int DemoApp::run()
|
||||
{
|
||||
setUpWindows();
|
||||
forkShell();
|
||||
mainLoop();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DemoApp::setUpWindows()
|
||||
{
|
||||
windowManager
|
||||
= new VerticalTilingWindowManager(Root_Window, &ncursesMutex);
|
||||
topWindowManager
|
||||
= new HorizontalTilingWindowManager(new Window(), &ncursesMutex);
|
||||
bottomWindowManager
|
||||
= new HorizontalTilingWindowManager(new Window(), &ncursesMutex);
|
||||
|
||||
windowManager->addWindow(topWindowManager);
|
||||
ptyWindow = new PtyWindow(&ncursesMutex, 0, 0, 0, 0);
|
||||
windowManager->addWindow(ptyWindow);
|
||||
windowManager->addWindow(bottomWindowManager);
|
||||
windowManager->updateLayout();
|
||||
windowManager->refresh();
|
||||
|
||||
dummyWindowTopLeft = new Window();
|
||||
topWindowManager->addWindow(dummyWindowTopLeft, 1);
|
||||
dummyWindowTopLeft->addstr("t\no\np\n \nl\ne\nf\nt\n w\ni\nn\nd\no\nw");
|
||||
dummyWindowTopRight = new Window(topWindowManager);
|
||||
dummyWindowTopRight->addstr("top right window");
|
||||
|
||||
topWindowManager->updateLayout();
|
||||
topWindowManager->refresh();
|
||||
|
||||
dummyWindowBottomLeft = new Window(bottomWindowManager);
|
||||
dummyWindowBottomLeft->addstr("bottom left window");
|
||||
dummyWindowBottomRight = new Window(bottomWindowManager);
|
||||
dummyWindowBottomRight->addstr("bottom right window");
|
||||
|
||||
bottomWindowManager->updateLayout();
|
||||
bottomWindowManager->refresh();
|
||||
}
|
||||
|
||||
void DemoApp::forkShell()
|
||||
{
|
||||
if(fork() == 0)
|
||||
{
|
||||
const char *shellPath = getenv("SHELL");
|
||||
if(!shellPath)
|
||||
shellPath = "/bin/bash";
|
||||
|
||||
login_tty(ptyWindow->getFdPtyClient());
|
||||
const int sizeArg = 2;
|
||||
const char *arg[sizeArg] = {shellPath, NULL};
|
||||
fprintf(stderr, "<CTRL>+C exits shell.\n\n");
|
||||
execv(shellPath, const_cast<char * const *>(arg));
|
||||
fprintf(stderr, "Well, well, well … could not start a shell. We "
|
||||
"tried `%s`. Maybe set `SHELL` environment variable"
|
||||
" to a working value?", shellPath);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void DemoApp::mainLoop()
|
||||
{
|
||||
while(true)
|
||||
{
|
||||
SingleUserInput input = windowManager->readSingleUserInput();
|
||||
if(input.isNormalKey())
|
||||
{
|
||||
ptyWindow->writeUnicodeCharToClient(input.getRawInput());
|
||||
{
|
||||
scoped_lock lock(ncursesMutex);
|
||||
dummyWindowTopRight->addch(input.getRawInput());
|
||||
dummyWindowTopRight->refresh();
|
||||
}
|
||||
}
|
||||
if(input.isFunctionKey())
|
||||
{
|
||||
switch(input.getRawInput())
|
||||
{
|
||||
case KEY_RESIZE:
|
||||
windowManager->updateLayout();
|
||||
break;
|
||||
case KEY_F(1):
|
||||
if(topWindowManager->isHidden())
|
||||
windowManager->showWindow(topWindowManager);
|
||||
else
|
||||
windowManager->hideWindow(topWindowManager);
|
||||
windowManager->updateLayout();
|
||||
windowManager->refresh();
|
||||
break;
|
||||
case KEY_F(2):
|
||||
if(ptyWindow->isHidden())
|
||||
windowManager->showWindow(ptyWindow);
|
||||
else
|
||||
windowManager->hideWindow(ptyWindow);
|
||||
windowManager->updateLayout();
|
||||
windowManager->refresh();
|
||||
break;
|
||||
case KEY_F(3):
|
||||
if(bottomWindowManager->isHidden())
|
||||
windowManager->showWindow(bottomWindowManager);
|
||||
else
|
||||
windowManager->hideWindow(bottomWindowManager);
|
||||
windowManager->updateLayout();
|
||||
windowManager->refresh();
|
||||
break;
|
||||
case KEY_F(5):
|
||||
windowManager->refresh();
|
||||
break;
|
||||
default:
|
||||
ptyWindow->writeFunctionKeyToClient(input.mapKeyNcursesToVTerm());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
DemoApp.hpp
Normal file
39
DemoApp.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @brief demo application for the library
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#ifndef A3B2AE4E_0A39_468C_8CCA_E6508166702A
|
||||
#define A3B2AE4E_0A39_468C_8CCA_E6508166702A
|
||||
|
||||
#include <cursesapp.h>
|
||||
#include <mutex>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class VerticalTilingWindowManager;
|
||||
class HorizontalTilingWindowManager;
|
||||
class Window;
|
||||
class PtyWindow;
|
||||
|
||||
class DemoApp : public NCursesApplication
|
||||
{
|
||||
public:
|
||||
DemoApp();
|
||||
|
||||
private:
|
||||
VerticalTilingWindowManager *windowManager;
|
||||
HorizontalTilingWindowManager *topWindowManager, *bottomWindowManager;
|
||||
Window *dummyWindowTopLeft, *dummyWindowTopRight
|
||||
, *dummyWindowBottomLeft, *dummyWindowBottomRight;
|
||||
PtyWindow *ptyWindow;
|
||||
std::recursive_mutex ncursesMutex;
|
||||
|
||||
int run() override;
|
||||
void setUpWindows();
|
||||
void forkShell();
|
||||
void mainLoop();
|
||||
};
|
||||
}
|
||||
|
||||
#endif /* A3B2AE4E_0A39_468C_8CCA_E6508166702A */
|
40
HorizontalTilingWindowManager.cpp
Normal file
40
HorizontalTilingWindowManager.cpp
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include "Debug.hpp"
|
||||
#include <kNCurses/HorizontalTilingWindowManager.hpp>
|
||||
#include <kNCurses/Window.hpp>
|
||||
#include <ncursesw/ncurses.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
using std::recursive_mutex;
|
||||
|
||||
HorizontalTilingWindowManager::HorizontalTilingWindowManager(NCursesWindow *rootWindow, std::recursive_mutex *ncursesMutex)
|
||||
: TilingWindowManager(rootWindow, ncursesMutex)
|
||||
{}
|
||||
|
||||
void HorizontalTilingWindowManager::resizeAndMoveWindow(Window *window, windowDimension dimension, windowPosition position)
|
||||
{
|
||||
__debug_log("(" + std::to_string(position + begx()) + ", " + std::to_string(0 + begy()) + ", " + std::to_string(dimension) + ", " + std::to_string(height()) + ")");
|
||||
window->resize(height(), dimension);
|
||||
window->mvwin(0 + begy(), position + begx());
|
||||
}
|
||||
|
||||
TilingWindowManager::windowDimension HorizontalTilingWindowManager::getAvailableSpace()
|
||||
{
|
||||
return width();
|
||||
}
|
||||
|
||||
void HorizontalTilingWindowManager::windowBorder()
|
||||
{
|
||||
vline(height());
|
||||
}
|
||||
|
||||
void HorizontalTilingWindowManager::moveCursor(TilingWindowManager::windowPosition position)
|
||||
{
|
||||
move(0, position);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include <NCursesPtyWindow/PtyWindow.hpp>
|
||||
#include <kNCurses/PtyWindow.hpp>
|
||||
#include "Debug.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
|
@ -13,13 +13,14 @@
|
|||
#include <termios.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
using gsl::narrow;
|
||||
using std::lock_guard;
|
||||
using std::scoped_lock;
|
||||
using std::recursive_mutex;
|
||||
|
||||
PtyWindow::PtyWindow(std::mutex *writeToNCursesMutex, int lines, int columns, int y, int x)
|
||||
: writeToNCursesMutex(writeToNCursesMutex), Window(lines, columns, y, x)
|
||||
PtyWindow::PtyWindow(recursive_mutex *ncursesMutex, int lines, int columns, int y, int x)
|
||||
: ncursesMutex(ncursesMutex), Window(lines, columns, y, x)
|
||||
{
|
||||
// to get the original terminal we need to shutdown ncurses for a moment
|
||||
/// @todo maybe try `reset_prog_mode()` and `reset_shell_mode()` instead
|
||||
|
@ -43,9 +44,10 @@ namespace krikkel::NCursesPtyWindow
|
|||
vterm_screen_enable_altscreen(pseudoTerminalScreen, true);
|
||||
|
||||
//raw(); // — cbreak might suffice
|
||||
//noecho(); — already set
|
||||
//nodelay(true); — @todo needs some reprogramming
|
||||
keypad(true);
|
||||
//noecho(); // — already set by NCursesWindow
|
||||
keypad(true); /// — already set by NCursesWindow
|
||||
//meta(true); // — already set by NCursesWindow
|
||||
//nodelay(true); // — @todo would need some programming, not sure if useful
|
||||
nonl();
|
||||
|
||||
/// @todo block all signals, this thread does not handle any
|
||||
|
@ -65,21 +67,22 @@ namespace krikkel::NCursesPtyWindow
|
|||
return fdPtyClient;
|
||||
}
|
||||
|
||||
void PtyWindow::writeToClient(const char *output, size_t length)
|
||||
void PtyWindow::writeToPtyClient(const char *output, size_t length)
|
||||
{
|
||||
lock_guard writeLock(writeToPseudoTerminalMutex);
|
||||
write(fdPtyHost, output, length);
|
||||
__debug_log("written to PTY client: '" + __debug_make_bytes_printable(std::string(output, length)) + '\'');
|
||||
//__debug_log("written to PTY client: '" + __debug_make_bytes_printable(std::string(output, length)) + '\'');
|
||||
}
|
||||
|
||||
void PtyWindow::writeUnicodeCharToClient(wint_t character)
|
||||
{
|
||||
scoped_lock lock(ptyMutex);
|
||||
vterm_keyboard_unichar(pseudoTerminal, character, VTERM_MOD_NONE);
|
||||
}
|
||||
|
||||
void PtyWindow::writeKeyToClient(VTermKey key)
|
||||
void PtyWindow::writeFunctionKeyToClient(VTermKey key)
|
||||
{
|
||||
__debug_log("writing key: " + std::to_string(key));
|
||||
scoped_lock lock(ptyMutex);
|
||||
//__debug_log("writing key: " + std::to_string(key));
|
||||
vterm_keyboard_key(pseudoTerminal, key, VTERM_MOD_NONE);
|
||||
}
|
||||
|
||||
|
@ -105,10 +108,10 @@ namespace krikkel::NCursesPtyWindow
|
|||
size_t bytesRead = read(fdPtyHost, ptyClientOutputBuffer, PTY_CLIENT_OUTPUT_BUFFER_SIZE);
|
||||
if(bytesRead != -1 && bytesRead != 0)
|
||||
{
|
||||
lock_guard writeLock(writeToPseudoTerminalMutex);
|
||||
scoped_lock lock(ptyMutex, *ncursesMutex);
|
||||
vterm_input_write(pseudoTerminal, ptyClientOutputBuffer, bytesRead);
|
||||
}
|
||||
__debug_log("read from PTY client: '" + __debug_make_bytes_printable(std::string(ptyClientOutputBuffer, bytesRead)) + '\'');
|
||||
//__debug_log("read from PTY client: '" + __debug_make_bytes_printable(std::string(ptyClientOutputBuffer, bytesRead)) + '\'');
|
||||
}
|
||||
|
||||
VTermScreenCallbacks PtyWindow::screenCallbacks =
|
||||
|
@ -125,6 +128,8 @@ namespace krikkel::NCursesPtyWindow
|
|||
|
||||
int PtyWindow::handlerDamage(VTermRect rect)
|
||||
{
|
||||
scoped_lock lock(ptyMutex, *ncursesMutex);
|
||||
|
||||
for(int x = rect.start_col; x < rect.end_col; ++x)
|
||||
for(int y = rect.start_row; y < rect.end_row; ++y)
|
||||
copyPtyCellToNCursesWindow(x, y);
|
||||
|
@ -153,7 +158,7 @@ namespace krikkel::NCursesPtyWindow
|
|||
|
||||
int PtyWindow::handlerMoveCursor(VTermPos pos, VTermPos oldpos, int visible)
|
||||
{
|
||||
/// @todo maybe use `mvcur()` instead?
|
||||
scoped_lock lock(*ncursesMutex);
|
||||
cursorX = pos.col;
|
||||
cursorY = pos.row;
|
||||
refresh();
|
||||
|
@ -238,9 +243,9 @@ namespace krikkel::NCursesPtyWindow
|
|||
else
|
||||
character = *vTermCell.chars;
|
||||
|
||||
{
|
||||
lock_guard nCursesLock(*writeToNCursesMutex);
|
||||
//__debug_log(std::string("written '") + (char) character + std::string("' ") + std::to_string(x) + ", " + std::to_string(y));
|
||||
setcchar(&converted, &character, formatting, colorPair, NULL);
|
||||
{
|
||||
move(cellPosition.row, cellPosition.col);
|
||||
chgat(1, formatting, colorPair, NULL);
|
||||
add_wch(&converted);
|
||||
|
@ -249,17 +254,18 @@ namespace krikkel::NCursesPtyWindow
|
|||
|
||||
int PtyWindow::refresh()
|
||||
{
|
||||
lock_guard nCursesLock(*writeToNCursesMutex);
|
||||
//__debug_log("refreshing");
|
||||
scoped_lock lock(*ncursesMutex);
|
||||
move(cursorY, cursorX);
|
||||
return NCursesWindow::refresh();
|
||||
return Window::refresh();
|
||||
}
|
||||
|
||||
/// @todo potential racing condition where drawing into terminal while
|
||||
/// resizing?
|
||||
int PtyWindow::wresize(int rows, int cols)
|
||||
int PtyWindow::resize(int rows, int cols)
|
||||
{
|
||||
{
|
||||
lock_guard writeLock(writeToPseudoTerminalMutex);
|
||||
scoped_lock lock(ptyMutex, *ncursesMutex);
|
||||
|
||||
winsize windowSize =
|
||||
{
|
||||
.ws_row = narrow<unsigned short>(rows)
|
||||
|
@ -267,11 +273,8 @@ namespace krikkel::NCursesPtyWindow
|
|||
};
|
||||
ioctl(fdPtyHost, TIOCSWINSZ, &windowSize);
|
||||
vterm_set_size(pseudoTerminal, rows, cols);
|
||||
}
|
||||
{
|
||||
lock_guard nCursesLock(*writeToNCursesMutex);
|
||||
return NCursesWindow::wresize(rows, cols);
|
||||
}
|
||||
|
||||
return Window::resize(rows, cols);
|
||||
}
|
||||
|
||||
int PtyWindow::staticHandlerDamage(VTermRect rect, void *user)
|
||||
|
@ -325,7 +328,6 @@ namespace krikkel::NCursesPtyWindow
|
|||
void PtyWindow::staticHandlerOutput(const char *s, size_t len, void *user)
|
||||
{
|
||||
PtyWindow *instance = static_cast<PtyWindow *>(user);
|
||||
__debug_log("output handler writing to client: '" + __debug_make_bytes_printable(std::string(s, len)) + "'");
|
||||
instance->writeToClient(s, len);
|
||||
instance->writeToPtyClient(s, len);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include <NCursesPtyWindow/SingleUserInput.hpp>
|
||||
#include <kNCurses/SingleUserInput.hpp>
|
||||
#include "Debug.hpp"
|
||||
|
||||
#include <vterm.h>
|
||||
|
@ -13,7 +13,7 @@
|
|||
#define KEY_ALT_BACKSPACE 127
|
||||
#define KEY_ALT_ALT_BACKSPACE '\b'
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
SingleUserInput::SingleUserInput(int _resultGetWchCall, wint_t _input)
|
||||
: input(_input), resultGetWchCall(_resultGetWchCall)
|
||||
|
@ -42,7 +42,7 @@ namespace krikkel::NCursesPtyWindow
|
|||
// it is only processing user input, it is not time critical enough for
|
||||
// the wasted space
|
||||
|
||||
// @tode unmapped keys: keys erase
|
||||
// @todo unmapped keys: keys erase
|
||||
if(resultGetWchCall == OK)
|
||||
switch(input)
|
||||
{
|
||||
|
|
145
TilingWindowManager.cpp
Normal file
145
TilingWindowManager.cpp
Normal file
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include <kNCurses/TilingWindowManager.hpp>
|
||||
#include <kNCurses/Window.hpp>
|
||||
#include "Debug.hpp"
|
||||
|
||||
#include <ncursesw/ncurses.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
using std::list;
|
||||
using std::pair;
|
||||
using std::recursive_mutex;
|
||||
using std::scoped_lock;
|
||||
|
||||
TilingWindowManager::TilingWindowManager(NCursesWindow *rootWindow, recursive_mutex *ncursesMutex)
|
||||
: Window(*rootWindow), ncursesMutex(ncursesMutex)
|
||||
{}
|
||||
|
||||
void TilingWindowManager::addWindow(Window *window, windowDimension size)
|
||||
{
|
||||
WindowStackElement stackElement(window, size);
|
||||
stack.push_back(stackElement);
|
||||
visibleStack.push_back(stackElement);
|
||||
}
|
||||
|
||||
void TilingWindowManager::hideWindow(Window *window)
|
||||
{
|
||||
if(window->hidden)
|
||||
return;
|
||||
|
||||
visibleStack.remove_if(
|
||||
[window](WindowStackElement element)
|
||||
{
|
||||
return element.first == window;
|
||||
});
|
||||
window->hidden = true;
|
||||
}
|
||||
|
||||
void TilingWindowManager::showWindow(Window *window)
|
||||
{
|
||||
if(!window->hidden)
|
||||
return;
|
||||
|
||||
list<WindowStackElement>::iterator currentVisibleWindowElement = visibleStack.begin();
|
||||
for(WindowStackElement currentWindowElement : stack)
|
||||
{
|
||||
if(currentWindowElement.first == window)
|
||||
{
|
||||
visibleStack.insert(currentVisibleWindowElement, currentWindowElement);
|
||||
window->hidden = false;
|
||||
break;
|
||||
}
|
||||
if(currentWindowElement != (*currentVisibleWindowElement))
|
||||
continue;
|
||||
++currentVisibleWindowElement;
|
||||
}
|
||||
}
|
||||
|
||||
int TilingWindowManager::resize(int rows, int cols)
|
||||
{
|
||||
int result = Window::resize(rows, cols);
|
||||
updateLayout();
|
||||
return result;
|
||||
}
|
||||
|
||||
int TilingWindowManager::refresh()
|
||||
{
|
||||
scoped_lock lock(*ncursesMutex);
|
||||
|
||||
int result = Window::refresh();
|
||||
|
||||
for(WindowStackElement stackElement : visibleStack)
|
||||
// @todo there are return values; compound them?
|
||||
stackElement.first->refresh();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void TilingWindowManager::updateLayout()
|
||||
{
|
||||
scoped_lock lock(*ncursesMutex);
|
||||
|
||||
size_t stackSize = visibleStack.size();
|
||||
if(stackSize == 0)
|
||||
{
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
int unitSize = getRelativeUnitSizeForVisibleWindows(getAvailableSpace()) - 1;
|
||||
windowPosition position = 0;
|
||||
auto lastButNotLeast = prev(visibleStack.end());
|
||||
for(auto stackElementIterator = visibleStack.begin()
|
||||
; stackElementIterator != lastButNotLeast
|
||||
; ++stackElementIterator)
|
||||
{
|
||||
WindowStackElement stackElement = *stackElementIterator;
|
||||
Window *window = stackElement.first;
|
||||
if(!window->isHidden())
|
||||
{
|
||||
int windowShift;
|
||||
int windowSize = stackElement.second;
|
||||
if(windowSize < 0)
|
||||
windowShift = unitSize * (-windowSize);
|
||||
else
|
||||
windowShift = windowSize;
|
||||
|
||||
resizeAndMoveWindow(window, windowShift, position);
|
||||
position += windowShift;
|
||||
__debug_log("drawing line at " + std::to_string(position));
|
||||
moveCursor(position++);
|
||||
windowBorder();
|
||||
}
|
||||
}
|
||||
resizeAndMoveWindow((*lastButNotLeast).first, getAvailableSpace() - position, position);
|
||||
}
|
||||
|
||||
|
||||
TilingWindowManager::windowDimension TilingWindowManager::getRelativeUnitSizeForVisibleWindows(windowDimension spaceAvailable)
|
||||
{
|
||||
uint16_t numberOfRelativeUnits = 0;
|
||||
windowDimension absoluteSum = 0;
|
||||
list<WindowStackElement>::iterator windowIterator = stack.begin();
|
||||
|
||||
for(WindowStackElement stackElement : visibleStack)
|
||||
{
|
||||
windowDimension size = stackElement.second;
|
||||
if(size < 0)
|
||||
numberOfRelativeUnits -= size;
|
||||
else
|
||||
absoluteSum += size;
|
||||
}
|
||||
|
||||
windowDimension relativeUnitSize = (spaceAvailable - absoluteSum) / numberOfRelativeUnits;
|
||||
windowDimension remainder = (spaceAvailable - absoluteSum - relativeUnitSize * numberOfRelativeUnits);
|
||||
if(remainder > 0 && remainder * 2 < numberOfRelativeUnits)
|
||||
++relativeUnitSize;
|
||||
|
||||
return relativeUnitSize;
|
||||
}
|
||||
}
|
41
VerticalTilingWindowManager.cpp
Normal file
41
VerticalTilingWindowManager.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include "Debug.hpp"
|
||||
#include <kNCurses/VerticalTilingWindowManager.hpp>
|
||||
#include <kNCurses/Window.hpp>
|
||||
#include <ncursesw/ncurses.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
using std::recursive_mutex;
|
||||
|
||||
VerticalTilingWindowManager::VerticalTilingWindowManager(NCursesWindow *rootWindow, std::recursive_mutex *ncursesMutex)
|
||||
: TilingWindowManager(rootWindow, ncursesMutex)
|
||||
{}
|
||||
|
||||
void VerticalTilingWindowManager::resizeAndMoveWindow(Window *window, windowDimension dimension, windowPosition position)
|
||||
{
|
||||
__debug_log("(" + std::to_string(0 + begx()) + ", " + std::to_string(position + begy()) + ", " + std::to_string(width()) + ", " + std::to_string(dimension) + ")");
|
||||
window->resize(dimension, width());
|
||||
window->mvwin(position + begy(), 0 + begx());
|
||||
}
|
||||
|
||||
TilingWindowManager::windowDimension VerticalTilingWindowManager::getAvailableSpace()
|
||||
{
|
||||
return height();
|
||||
}
|
||||
|
||||
void VerticalTilingWindowManager::windowBorder()
|
||||
{
|
||||
hline(width());
|
||||
}
|
||||
|
||||
void VerticalTilingWindowManager::moveCursor(TilingWindowManager::windowPosition position)
|
||||
{
|
||||
move(position, 0);
|
||||
}
|
||||
|
||||
}
|
35
Window.cpp
35
Window.cpp
|
@ -2,10 +2,25 @@
|
|||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#include <NCursesPtyWindow/Window.hpp>
|
||||
#include <kNCurses/Window.hpp>
|
||||
#include <kNCurses/VerticalTilingWindowManager.hpp>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
Window::Window() : NCursesWindow(0, 0, 0, 0)
|
||||
{}
|
||||
|
||||
Window::Window(TilingWindowManager *windowManager)
|
||||
: NCursesWindow(0, 0, 0, 0)
|
||||
{
|
||||
windowManager->addWindow(this);
|
||||
windowManager->updateLayout();
|
||||
}
|
||||
|
||||
Window::Window(const NCursesWindow &window)
|
||||
: NCursesWindow(window)
|
||||
{}
|
||||
|
||||
Window::Window(int lines, int columns, int y, int x)
|
||||
: NCursesWindow(lines, columns, y, x)
|
||||
{}
|
||||
|
@ -30,6 +45,11 @@ namespace krikkel::NCursesPtyWindow
|
|||
return ::wget_wch(w, character);
|
||||
}
|
||||
|
||||
bool Window::isHidden()
|
||||
{
|
||||
return hidden;
|
||||
}
|
||||
|
||||
SingleUserInput Window::readSingleUserInput()
|
||||
{
|
||||
wint_t input;
|
||||
|
@ -37,4 +57,15 @@ namespace krikkel::NCursesPtyWindow
|
|||
return SingleUserInput(result, input);
|
||||
}
|
||||
|
||||
int Window::resize(int rows, int cols)
|
||||
{
|
||||
return NCursesWindow::wresize(rows, cols);
|
||||
}
|
||||
|
||||
int Window::refresh()
|
||||
{
|
||||
if(!hidden)
|
||||
return NCursesWindow::refresh();
|
||||
return 0;
|
||||
}
|
||||
}
|
31
include/kNCurses/HorizontalTilingWindowManager.hpp
Normal file
31
include/kNCurses/HorizontalTilingWindowManager.hpp
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @brief Window manager
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#ifndef B7BCF793_2FAB_49CC_9E00_CDEA370D38F9
|
||||
#define B7BCF793_2FAB_49CC_9E00_CDEA370D38F9
|
||||
|
||||
#include "TilingWindowManager.hpp"
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class Window;
|
||||
class SingleUserInput;
|
||||
|
||||
class HorizontalTilingWindowManager : public TilingWindowManager
|
||||
{
|
||||
public:
|
||||
HorizontalTilingWindowManager(NCursesWindow *rootWindow, std::recursive_mutex *ncursesMutex);
|
||||
|
||||
void resizeAndMoveWindow(Window *window
|
||||
, windowDimension dimension
|
||||
, windowPosition position) override;
|
||||
windowDimension getAvailableSpace() override;
|
||||
void windowBorder() override;
|
||||
void moveCursor(windowPosition position) override;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#endif /* B7BCF793_2FAB_49CC_9E00_CDEA370D38F9 */
|
|
@ -1,12 +1,12 @@
|
|||
/**
|
||||
* @brief `ncurses` window displaying contents of a pseudo terminal
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
* @todo remove all invalid constructors
|
||||
*/
|
||||
|
||||
#ifndef __WINDOW_H__
|
||||
#define __WINDOW_H__
|
||||
|
||||
#include "SingleUserInput.hpp"
|
||||
#include "Window.hpp"
|
||||
|
||||
#include <cursesw.h>
|
||||
|
@ -16,28 +16,27 @@
|
|||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class PtyWindow : public Window
|
||||
{
|
||||
public:
|
||||
PtyWindow(std::mutex *writeToNCursesMutex, int lines, int columns, int y, int x);
|
||||
PtyWindow(std::recursive_mutex *ncursesMutex, int lines, int columns, int y, int x);
|
||||
~PtyWindow();
|
||||
|
||||
int getFdPtyClient() const;
|
||||
void writeToClient(const char * string, size_t length);
|
||||
void writeUnicodeCharToClient(wint_t character);
|
||||
void writeKeyToClient(VTermKey key);
|
||||
void writeFunctionKeyToClient(VTermKey key);
|
||||
|
||||
int refresh() override;
|
||||
int wresize(int rows, int cols);
|
||||
int resize(int rows, int cols) override;
|
||||
|
||||
private:
|
||||
int fdPtyHost, fdPtyClient;
|
||||
struct termios terminalParameters;
|
||||
VTerm *pseudoTerminal;
|
||||
std::mutex writeToPseudoTerminalMutex;
|
||||
std::mutex *writeToNCursesMutex;
|
||||
std::recursive_mutex ptyMutex;
|
||||
std::recursive_mutex *ncursesMutex;
|
||||
VTermScreen *pseudoTerminalScreen;
|
||||
static VTermScreenCallbacks screenCallbacks;
|
||||
/// @todo one line is at most 4096 chars long
|
||||
|
@ -48,6 +47,7 @@ namespace krikkel::NCursesPtyWindow
|
|||
std::thread readPtyClientThread;
|
||||
void readFromPtyClientThreadMethod();
|
||||
void readFromPtyClient();
|
||||
void writeToPtyClient(const char * string, size_t length);
|
||||
|
||||
int handlerDamage(VTermRect rect);
|
||||
int handlerMoveRect(VTermRect dest, VTermRect src);
|
|
@ -9,7 +9,7 @@
|
|||
#include <cursesw.h>
|
||||
#include <vterm_keycodes.h>
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class SingleUserInput
|
||||
{
|
61
include/kNCurses/TilingWindowManager.hpp
Normal file
61
include/kNCurses/TilingWindowManager.hpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @brief Window manager for tiling (template pattern, abstract parent)
|
||||
*
|
||||
* A window manager contains windows of a given size, lays them out and can hide
|
||||
* or show them. There is no concrete window manager for tiling horizontally and
|
||||
* vertically at the same time. Instead nesting the window managers is adviced,
|
||||
* since the window manager classes are derived from windows in the end.
|
||||
*
|
||||
* This is an abstract class used by the concrete classes
|
||||
* `VerticalTilingWindowManager` and `HorizontalTilingWindowManager`. Both
|
||||
* specify the virtual and protected, declared but undefined methods used by the
|
||||
* public interface to facilitate the function depending on the orientation
|
||||
* (horizontally or vertically).
|
||||
*
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#ifndef C51BA18F_0915_43B9_BD5D_129F0CDBC1CD
|
||||
#define C51BA18F_0915_43B9_BD5D_129F0CDBC1CD
|
||||
|
||||
#include "Window.hpp"
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class SingleUserInput;
|
||||
|
||||
class TilingWindowManager : public Window
|
||||
{
|
||||
public:
|
||||
/// @todo figure out more appropiate names …
|
||||
typedef int16_t windowDimension;
|
||||
typedef uint16_t windowPosition;
|
||||
typedef std::pair<Window *, windowDimension> WindowStackElement;
|
||||
|
||||
TilingWindowManager(NCursesWindow *rootWindow, std::recursive_mutex *ncursesMutex);
|
||||
void addWindow(Window *window, windowDimension size = -1);
|
||||
int resize(int rows, int cols) override;
|
||||
int refresh() override;
|
||||
void updateLayout();
|
||||
|
||||
void hideWindow(Window *window);
|
||||
void showWindow(Window *window);
|
||||
|
||||
protected:
|
||||
std::recursive_mutex *ncursesMutex;
|
||||
std::list<WindowStackElement> stack, visibleStack;
|
||||
|
||||
windowDimension getRelativeUnitSizeForVisibleWindows(windowDimension spaceAvailable);
|
||||
virtual void resizeAndMoveWindow(Window *window
|
||||
, windowDimension dimension
|
||||
, windowPosition position) = 0;
|
||||
virtual void windowBorder() = 0;
|
||||
virtual windowDimension getAvailableSpace() = 0;
|
||||
virtual void moveCursor(windowPosition position) = 0;
|
||||
};
|
||||
}
|
||||
|
||||
#endif /* C51BA18F_0915_43B9_BD5D_129F0CDBC1CD */
|
31
include/kNCurses/VerticalTilingWindowManager.hpp
Normal file
31
include/kNCurses/VerticalTilingWindowManager.hpp
Normal file
|
@ -0,0 +1,31 @@
|
|||
/**
|
||||
* @brief Window manager
|
||||
* @author Christian Burger (christian@krikkel.de)
|
||||
*/
|
||||
|
||||
#ifndef C10F5DF3_1DB4_4714_A84D_115F492F5CDC
|
||||
#define C10F5DF3_1DB4_4714_A84D_115F492F5CDC
|
||||
|
||||
#include "TilingWindowManager.hpp"
|
||||
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class Window;
|
||||
class SingleUserInput;
|
||||
|
||||
class VerticalTilingWindowManager : public TilingWindowManager
|
||||
{
|
||||
public:
|
||||
VerticalTilingWindowManager(NCursesWindow *rootWindow, std::recursive_mutex *ncursesMutex);
|
||||
|
||||
void resizeAndMoveWindow(Window *window
|
||||
, windowDimension dimension
|
||||
, windowPosition position) override;
|
||||
windowDimension getAvailableSpace() override;
|
||||
void windowBorder() override;
|
||||
void moveCursor(windowPosition position) override;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
#endif /* C10F5DF3_1DB4_4714_A84D_115F492F5CDC */
|
|
@ -34,18 +34,31 @@ inline int UNDEF(get_wch)(wint_t *character) { get_wch(character); }
|
|||
#define get_wch UNDEF(get_wch)
|
||||
#endif
|
||||
|
||||
namespace krikkel::NCursesPtyWindow
|
||||
namespace krikkel::NCurses
|
||||
{
|
||||
class TilingWindowManager;
|
||||
|
||||
class Window : public NCursesWindow
|
||||
{
|
||||
friend class TilingWindowManager;
|
||||
|
||||
public:
|
||||
Window();
|
||||
Window(TilingWindowManager *windowManager);
|
||||
Window(const NCursesWindow &window);
|
||||
Window(int lines, int columns, int y, int x);
|
||||
int addnwstr(const wchar_t *wstr, int n);
|
||||
int add_wch(const cchar_t *character);
|
||||
int ins_wch(const cchar_t *character);
|
||||
int get_wch(wint_t *character);
|
||||
|
||||
bool isHidden();
|
||||
SingleUserInput readSingleUserInput();
|
||||
virtual int resize(int rows, int cols);
|
||||
virtual int refresh() override;
|
||||
|
||||
protected:
|
||||
bool hidden = false;
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in a new issue