80 Commits

Author SHA1 Message Date
p b0e9d60ff7 Bump version, update NEWS
Arch Linux AUR Success
OpenBSD 7.8 Success
Alpine 3.24 Success
2026-09-04 15:26:50 +02:00
p 95076ff6ab Bump liberty
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-09-04 15:00:26 +02:00
p f26ab854c9 xF: tiniest AppKit steps
It's not quite possible to port the editor, so not finishing this.
2026-09-02 13:01:53 +02:00
p 6e84d0c48b Bump liberty
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-31 12:12:39 +02:00
p 1b0e46d33a xF: optionally colour manage X11
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Bump liberty.
2026-08-29 07:16:03 +02:00
p ae6519687a xF: state true shortcut origin 2026-08-29 05:10:44 +02:00
p 0caab02933 xF: cleanup
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-11 11:03:00 +02:00
p 074b378f03 xC/fancy-prompt.lua: remove old Readline hacks
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-11 10:25:59 +02:00
p 6a5533f783 Update NEWS
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-11 07:53:09 +02:00
p bb48e185d0 xF: support pasting from clipboard
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Bump liberty.  Most of the change happens in liberty-xui.
2026-08-11 05:33:17 +02:00
p 83128dc7bb xF: bind C-t to transpose characters
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Bump liberty.
2026-08-10 13:10:47 +02:00
p 780e9c35bf xF: improve layouting
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-10 10:42:30 +02:00
p 3aaf9de5f2 xF: improve the X11 IM experience
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Bump liberty.  We're not yet achieving what we want to.
2026-08-10 10:13:23 +02:00
p a1c7d2c983 xC/xF: make it possible to insert newlines directly
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
That should do for us to stop claiming that the interface needs polish.
2026-08-10 08:22:33 +02:00
p 63d54df66a xC: add a basic C-t binding 2026-08-10 08:02:29 +02:00
p 959fb15828 xF: play highlight sounds through libcanberra
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-10 07:30:38 +02:00
p 36037cb3e5 Update documentation
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-10 06:28:25 +02:00
p 800a4e354c xF: push huge logs directly to external editors
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
At around 10 megabytes, processing starts taking quite long.

It might be a good idea to remove the log view altogether now;
it's only there because we copied other frontends.
2026-08-10 04:41:40 +02:00
p 9f1ef95f46 xF: menu autocomplete
I'm not sure if this is simpler or more complex than listing candidates.

xC already has its old libedit hack, so I suppose I'm not going to
bother with it.
2026-08-10 04:36:27 +02:00
p 064102b786 xF: simplify action code
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-09 15:20:50 +02:00
p 621f76c797 xC: enable relay binding on multiple addresses
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-09 15:04:09 +02:00
p 2d1df86a46 xC + frontends: merge M-! into M-a + reversed M-A
Both WeeChat and irssi merge these two by prioritising highlights
when you press M-a.
2026-08-09 14:58:11 +02:00
p 6ba94ba664 xF: don't call into Xlib when running as a TUI 2026-08-09 14:58:07 +02:00
p f1170bc161 xF: maintain an index of buffer by name
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Imperceptibly improves performance, shortens source code.
2026-08-08 15:43:01 +02:00
p dbb47ec2ee Adjust key bindings
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Now they should actually be more friendly to WeeChat users.
2026-08-08 14:13:10 +02:00
p c413f6d4b2 xF: add a binding to open the last link
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Better than nothing.
2026-08-08 08:58:22 +02:00
p 8ce60cdd2f xF: add bindings to scroll to the top and bottom 2026-08-08 08:22:53 +02:00
p d9af9c1057 xA: mildly improve the experience
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Fyne got a bit faster at layouting, but showing the log still kills it.
2026-08-08 06:56:07 +02:00
p e816ddb32a xF: adjust layouting to be more like nncmpp
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-08 06:08:20 +02:00
p 57e2163bac xF: simplify variable naming 2026-08-08 06:08:20 +02:00
p 60e40f4fe2 Bump liberty
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-08 04:16:55 +02:00
p 7c76e60656 xF: eliminate two compiler warnings 2026-08-08 03:20:47 +02:00
p 6fe5f7de4c Update NEWS 2026-08-08 03:02:18 +02:00
p 8e2e7be56f xF: add basic configuration
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-08 01:47:21 +02:00
p 3275cd1fa2 xF: reuse a function 2026-08-08 01:47:21 +02:00
p 087091577c xF: deduplicate transcript logic
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
After the last commit it got a bit ridiculous.
2026-08-07 15:17:37 +02:00
p f6a41d7c51 xF: add date change marker
Also rename and reorganise attributes to match xC better.
2026-08-07 15:16:00 +02:00
p 7538dfad2d xF: make highlights request attention from the WM
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-07 12:02:26 +02:00
p 2d8867f6fa Clean up the build around xF
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
And make sure it gets built at all, if possible.

It's actually pretty good now, reaching feature parity with xP and xC.
2026-08-07 11:12:55 +02:00
p 4f5d4cb64b xF: make sure the caret is visible in X11 editor
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-07 10:30:58 +02:00
p 1704f90934 xF: close buffers on middle click 2026-08-07 09:05:06 +02:00
p 41be32692c xF: enable running external editors on input
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
And also on the transcript.

This is an easy way of resolving text selection,
and the mediocre input editor.
2026-08-07 08:33:23 +02:00
p 65a98dfd7c xF: make xC nick colors just work
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Colour mapping still suffers, just not as heavily.
2026-08-07 01:26:17 +02:00
p ce848bff0f xF: add word wrapping where it matters the most
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Plus some necessary cleanup.

This is a bit simplistic in that it is very inefficient.
It runs fast enough, though.
2026-08-06 01:44:31 +02:00
p 6859e9abc9 xF: improve caret visibility 2026-08-06 01:41:17 +02:00
p 82f19cc144 Update README.adoc
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-03 02:41:09 +02:00
p e4ce659174 xF: add link detection and opening 2026-08-03 02:41:08 +02:00
p 8f3df19786 xF: add a scrollbar to the buffer list
And clean this area up.
2026-08-03 02:41:08 +02:00
p f8ecf7d0f7 xP: fix channel title layout
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-08-02 19:39:44 +02:00
p 5b8335654f Bump version, update NEWS
Arch Linux AUR Success
OpenBSD 7.8 Success
Alpine 3.24 Success
2026-07-31 02:23:48 +02:00
p 2a0ce32bdd CMakeLists.txt: rename option to match README.adoc 2026-07-31 02:13:38 +02:00
p 13e1b220ff Miscellaneous fixes
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-31 01:55:17 +02:00
p 8827d1d473 xF: render formatting
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-30 23:29:07 +02:00
p 70fa559752 xF: finish the frontend using liberty-xui
This has long been the missing piece in our frontend collection.

After including the termo library in a recent rewrite, we've kind of
received an option to use liberty-xui, the terminal/X11/AppKit library
spawned off from nncmpp.

The frontend for the most part resembles xP, with some missing
xC features on top.

We do not care about text layouting or X11 text selection,
seeing as that requires rather extensive changes,
and I'm not planning to use this frontend other than for testing.
General formatting support will arrive in a following commit.

It should build for macOS as well with minor changes,
but we already have xM in our collection, so this is not finished.

Bump liberty.
2026-07-30 23:23:32 +02:00
p bcfbd40003 xC/xP: implement session resumption
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Mobile clients pretty much need this, because they keep interrupting
their connections to the relay.
2026-07-28 18:28:19 +02:00
p 38188632e0 xP: fix inverted condition 2026-07-28 18:28:19 +02:00
p 682f3de097 xD/xS: fix KICKs in quiet channels
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-21 17:33:31 +02:00
p 5295733629 xD/xS: NICK without USER would block nicks forever 2026-07-21 17:33:31 +02:00
p 49e76b49de xS: fix multiple old porting errors 2026-07-21 17:33:31 +02:00
p 60a14b6683 xW: acknowledge a harmless possible underflow 2026-07-21 16:36:08 +02:00
p d2a3719b78 xA/xT/xW: handle server rename events better 2026-07-21 16:36:08 +02:00
p d453b64d3f xA: make reset items also reset inverse text 2026-07-21 16:36:08 +02:00
p 09c3980c50 xA: show relay error messages rather than crash 2026-07-21 16:36:08 +02:00
p ed6ad5981a xC: transcode input editor output back
Alpine 3.24 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-21 15:38:47 +02:00
p 6c749be5f7 xC: parse RPL_CHANNELMODEIS properly
The mode processor used to start with the channel name,
but it must start past it.
2026-07-21 15:38:47 +02:00
p 8f21138f26 xC: more precise capability whitelisting
It could theoretically match on more entries than intended.
2026-07-21 15:38:47 +02:00
p 542f2bca6b xC: validate server TLS certificate hostname
I haven't actually checked if this takes effect.
2026-07-21 15:38:47 +02:00
p b9097f40f6 xA: bump Fyne to 2.8.0
Alpine 3.23 Scripts failed
Arch Linux AUR Success
OpenBSD 7.8 Success
Alpine 3.24 Success
Now supporting strike-through formatting.

Overall, this frontend is much more usable now.
2026-07-20 07:49:50 +02:00
p 4102e3c02c xC: fix the integration test
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-20 03:03:31 +02:00
p 474a5973a3 xC: don't crash on non-terminal output 2026-07-20 02:43:24 +02:00
p a65389ec62 xC: show the prompt even with empty configuration
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-20 02:27:06 +02:00
p 0c256943fd xC: fix a segmentation fault on startup
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
A last-minute "harmless" change turns out to have side-effects.
2026-07-20 02:17:36 +02:00
p da2c8de56f Bump liberty 2026-07-20 01:58:16 +02:00
p eb02e8eee6 Try to fix OpenBSD build
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-20 01:52:56 +02:00
p 24c8108f35 Fix some compiler warnings 2026-07-20 01:50:33 +02:00
p 67d044a426 xC: nuke Readline and libedit
Arch Linux AUR Success
Alpine 3.23 Success
OpenBSD 7.8 Scripts failed
Replace them with a custom frontend making use of the termo library.
It's a bit rough at the edges, yet thoroughly functional.

The original libraries have carried us far, but they've become more
trouble than what they're worth:

 - It's much easier to make this change than to keep fixing
   both frontends.  That has become particularly difficult as of late.

 - It's also a much smaller undertaking than rewriting the client
   in more radical ways, such as:
    - Going for headless operation (also note that we currently lack
      a terminal relay client).
    - Rewriting the entire user interface to use curses.

 - Abandoning GNU Readline clears up licencing.

 - The old interface code was full of odd hacks and wild adapters.
   While this commit adds some fresh ugly areas, in sum it's a cleanup.

 - Purging the dependencies simplifies an eventual automated rewrite
   in another language.

Some freshly invalidated issues:

 - Readline 8.3 has reworked history, and our method of circumventing
   a memory leak in history handling stopped working reliably.
   Very tough to trace.

 - Editline could block waiting on input.

 - Editline refresh.c revision 1.61 seems to have completely broken
   the prompt hiding mechanism, with no obvious means of circumventing.
2026-07-20 00:46:08 +02:00
p e52501a1df prime.lua: prevent DoS on infinity
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-07-16 23:35:07 +02:00
p f57e8c59d6 xC: document the Lua API, bump liberty
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
asciiman.awk has needed a minor change, and it's forced us into
writing the document in a certain way, but it remains strong.

Looking at the API more closely makes one realise how lacking it is,
but the async functionality is still cool.
2026-07-15 05:47:12 +02:00
p 5ba2c825a1 xC: fix Lua 5.5 build
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
Everything appears to work, not inspecting this in depth.
2026-03-28 03:50:44 +01:00
p d11bd2c54e xT: produce an Android icon
Alpine 3.23 Success
Arch Linux AUR Success
OpenBSD 7.8 Success
2026-01-26 18:31:16 +01:00
33 changed files with 7156 additions and 2189 deletions
+3
View File
@@ -1,3 +1,6 @@
[submodule "liberty"]
path = liberty
url = https://git.janouch.name/p/liberty.git
[submodule "termo"]
path = termo
url = https://git.janouch.name/p/termo.git
+113 -42
View File
@@ -8,11 +8,6 @@ string (STRIP "${project_version}" project_version)
project (xK VERSION "${project_version}"
DESCRIPTION "IRC daemon, bot, TUI client and its web frontend" LANGUAGES C)
# Options
option (WANT_READLINE "Use GNU Readline for the UI (better)" ON)
option (WANT_LIBEDIT "Use BSD libedit for the UI" OFF)
option (WANT_XF "Build xF" OFF)
# Moar warnings
set (CMAKE_C_STANDARD 99)
set (CMAKE_C_STANDARD_REQUIRED ON)
@@ -72,6 +67,8 @@ endif ()
# -lrt is only for glibc < 2.17
# -liconv may or may not be a part of libc
# -lm may or may not be a part of libc
find_path (iconv_INCLUDE_DIRS iconv.h)
include_directories (${iconv_INCLUDE_DIRS})
foreach (extra iconv rt m)
find_library (extra_lib_${extra} ${extra})
if (extra_lib_${extra})
@@ -88,13 +85,32 @@ CHECK_C_SOURCE_RUNS ("#include <iconv.h>
== (iconv_t) -1; }" ICONV_ACCEPTS_TRANSLIT)
# Dependencies for xC
pkg_check_modules (libffi REQUIRED libffi)
list (APPEND xC_libraries ${libffi_LIBRARIES})
include_directories (${libffi_INCLUDE_DIRS})
link_directories (${libffi_LIBRARY_DIRS})
find_package (Termo QUIET NO_MODULE)
option (USE_SYSTEM_TERMO
"Don't compile our own termo library, use the system one" "${Termo_FOUND}")
if (USE_SYSTEM_TERMO)
if (NOT Termo_FOUND)
message (FATAL_ERROR "System termo library not found")
endif ()
else ()
# We don't want the library to install, but EXCLUDE_FROM_ALL ignores tests
add_subdirectory (termo EXCLUDE_FROM_ALL)
file (WRITE ${PROJECT_BINARY_DIR}/CTestCustom.cmake
"execute_process (COMMAND ${CMAKE_COMMAND} --build termo)")
# We don't have many good choices; this is a relatively clean approach
# (other possibilities: setting a variable in the parent scope, using
# a cache variable, writing a special config file with build paths in it
# and including it here, or setting a custom property on the targets)
get_directory_property (Termo_INCLUDE_DIRS
DIRECTORY termo INCLUDE_DIRECTORIES)
set (Termo_LIBRARIES termo-static)
endif ()
list (APPEND xC_libraries ${Termo_LIBRARIES})
# XXX: other Lua versions may be acceptable, don't know yet
pkg_search_module (lua lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua>=5.3)
pkg_search_module (lua
lua53 lua5.3 lua-5.3 lua54 lua5.4 lua-5.4 lua55 lua5.5 lua-5.5 lua>=5.3)
option (WITH_LUA "Enable support for Lua plugins" ${lua_FOUND})
if (WITH_LUA)
@@ -119,32 +135,45 @@ else ()
message (SEND_ERROR "Curses not found")
endif ()
if ((WANT_READLINE AND WANT_LIBEDIT) OR (NOT WANT_READLINE AND NOT WANT_LIBEDIT))
message (SEND_ERROR "You have to choose either GNU Readline or libedit")
elseif (WANT_READLINE)
pkg_check_modules (readline readline)
# Dependencies for xF
find_package (Unistring)
# OpenBSD's default readline is too old
if ("${CMAKE_SYSTEM_NAME}" MATCHES "OpenBSD")
include_directories (${OPENBSD_LOCALBASE}/include/ereadline)
list (APPEND xC_libraries ereadline)
elseif (readline_FOUND)
list (APPEND xC_libraries ${readline_LIBRARIES})
include_directories (${readline_INCLUDE_DIRS})
link_directories (${readline_LIBRARY_DIRS})
else ()
list (APPEND xC_libraries readline)
endif ()
elseif (WANT_LIBEDIT)
pkg_check_modules (libedit REQUIRED libedit)
list (APPEND xC_libraries ${libedit_LIBRARIES})
include_directories (${libedit_INCLUDE_DIRS})
set (xF_SATISFIED FALSE)
if (ncursesw_FOUND AND Unistring_FOUND)
set (xF_SATISFIED TRUE)
endif ()
option (WITH_XF "Build the xF frontend" "${xF_SATISFIED}")
if (WITH_XF AND NOT xF_SATISFIED)
message (FATAL_ERROR "Some xF dependencies were not found")
endif ()
pkg_check_modules (x11 x11 xrender xft fontconfig libpng)
option (WITH_X11 "Build xF with X11 support" "${x11_FOUND}")
if (WITH_X11 AND NOT x11_FOUND)
message (FATAL_ERROR "Some X11 libraries were not found")
endif ()
pkg_check_modules (lcms2 lcms2)
option (WITH_LCMS2 "Build xF with X11 colour management" "${lcms2_FOUND}")
pkg_check_modules (canberra libcanberra)
set (canberra_SATISFIED FALSE)
if (WITH_X11 AND canberra_FOUND)
set (canberra_SATISFIED TRUE)
endif ()
option (WITH_CANBERRA "Build xF with libcanberra sound support"
"${canberra_SATISFIED}")
if (WITH_CANBERRA AND NOT WITH_X11)
message (FATAL_ERROR "libcanberra support requires X11 support")
elseif (WITH_CANBERRA AND NOT canberra_FOUND)
message (FATAL_ERROR "libcanberra not found")
endif ()
# Generate a configuration file
set (HAVE_READLINE "${WANT_READLINE}")
set (HAVE_EDITLINE "${WANT_LIBEDIT}")
set (HAVE_LUA "${WITH_LUA}")
set (HAVE_LUA "${WITH_LUA}")
set (HAVE_X11 "${WITH_X11}")
set (HAVE_LCMS2 "${WITH_LCMS2}")
set (HAVE_CANBERRA "${WITH_CANBERRA}")
include (GNUInstallDirs)
set (project_config ${PROJECT_BINARY_DIR}/config.h)
@@ -177,23 +206,42 @@ add_custom_target (xC-proto DEPENDS ${PROJECT_BINARY_DIR}/xC-proto.c)
# Build
foreach (name xB xC xD)
list (APPEND project_binaries ${name})
add_executable (${name} ${name}.c ${project_config})
target_link_libraries (${name} ${project_libraries})
target_link_libraries (${name} PRIVATE ${project_libraries})
add_threads (${name})
endforeach ()
add_dependencies (xD replies)
add_dependencies (xC replies xC-proto)
target_link_libraries (xC ${xC_libraries})
target_link_libraries (xC PRIVATE ${xC_libraries})
target_include_directories (xC PRIVATE ${Termo_INCLUDE_DIRS})
if (WANT_XF)
pkg_check_modules (x11 REQUIRED x11 xrender xft fontconfig)
include_directories (${x11_INCLUDE_DIRS})
link_directories (${x11_LIBRARY_DIRS})
if (WITH_XF)
list (APPEND project_binaries xF)
add_executable (xF xF.c ${project_config})
add_dependencies (xF xC-proto)
target_link_libraries (xF ${x11_LIBRARIES} ${project_libraries})
target_include_directories (xF PRIVATE
${Unistring_INCLUDE_DIRS} ${Termo_INCLUDE_DIRS})
target_link_libraries (xF PRIVATE
${Unistring_LIBRARIES} ${Termo_LIBRARIES}
${ncursesw_LIBRARIES} ${project_libraries})
if (WITH_X11)
target_include_directories (xF PRIVATE ${x11_INCLUDE_DIRS})
target_link_directories (xF PRIVATE ${x11_LIBRARY_DIRS})
target_link_libraries (xF PRIVATE ${x11_LIBRARIES})
endif ()
if (WITH_LCMS2)
target_include_directories (xF PRIVATE ${lcms2_INCLUDE_DIRS})
target_link_directories (xF PRIVATE ${lcms2_LIBRARY_DIRS})
target_link_libraries (xF PRIVATE ${lcms2_LIBRARIES})
endif ()
if (WITH_CANBERRA)
target_include_directories (xF PRIVATE ${canberra_INCLUDE_DIRS})
target_link_directories (xF PRIVATE ${canberra_LIBRARY_DIRS})
target_link_libraries (xF PRIVATE ${canberra_LIBRARIES})
endif ()
add_threads (xF)
endif ()
@@ -202,7 +250,10 @@ include (CTest)
if (BUILD_TESTING)
add_executable (test-xC $<TARGET_PROPERTY:xC,SOURCES>)
set_target_properties (test-xC PROPERTIES COMPILE_DEFINITIONS TESTING)
target_link_libraries (test-xC $<TARGET_PROPERTY:xC,LINK_LIBRARIES>)
target_include_directories (test-xC
PUBLIC $<TARGET_PROPERTY:xC,INCLUDE_DIRECTORIES>)
target_link_libraries (test-xC
PUBLIC $<TARGET_PROPERTY:xC,LINK_LIBRARIES>)
add_threads (test-xC)
add_dependencies (test-xC replies)
@@ -231,13 +282,33 @@ add_custom_target (clang-tidy
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
# Installation
install (TARGETS xB xC xD DESTINATION ${CMAKE_INSTALL_BINDIR})
install (TARGETS ${project_binaries} DESTINATION ${CMAKE_INSTALL_BINDIR})
install (FILES LICENSE DESTINATION ${CMAKE_INSTALL_DOCDIR})
install (DIRECTORY plugins/xB/
DESTINATION ${CMAKE_INSTALL_DATADIR}/xB/plugins USE_SOURCE_PERMISSIONS)
install (DIRECTORY plugins/xC/
DESTINATION ${CMAKE_INSTALL_DATADIR}/xC/plugins)
# Rasterize and install SVG icons
if (WITH_XF AND WITH_X11)
include (IconUtils)
set (icon_base ${PROJECT_BINARY_DIR}/icons)
set (icon_png_list)
foreach (icon_size 16 32 48)
icon_to_png (xF ${PROJECT_SOURCE_DIR}/xF.svg
${icon_size} ${icon_base} icon_png)
list (APPEND icon_png_list ${icon_png})
endforeach ()
add_custom_target (icons ALL DEPENDS ${icon_png_list})
install (FILES xF.svg
DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps)
install (DIRECTORY ${icon_base}
DESTINATION ${CMAKE_INSTALL_DATADIR})
endif ()
# Generate documentation from text markup
find_program (ASCIIDOCTOR_EXECUTABLE asciidoctor)
find_program (A2X_EXECUTABLE a2x)
@@ -246,7 +317,7 @@ if (NOT ASCIIDOCTOR_EXECUTABLE AND NOT A2X_EXECUTABLE)
"falling back to a substandard manual page generator")
endif ()
foreach (page xB xC xD)
foreach (page ${project_binaries})
set (page_output "${PROJECT_BINARY_DIR}/${page}.1")
list (APPEND project_MAN_PAGES "${page_output}")
if (ASCIIDOCTOR_EXECUTABLE)
+51 -2
View File
@@ -1,3 +1,52 @@
2.4.0 (2026-09-04) "Complete"
* xC: general.relay_bind is now a string array
* xC: after the last release, added back a C-t binding to transpose characters,
and M-Enter to insert newlines
* xC: removed Readline hacks from the fancy-prompt.lua plugin
* xC + all frontends: adjusted key bindings, most notably:
- changed M-a to prioritize highlights when looking for activity;
- added M-A to look for buffers in reverse order;
- removed the old M-!;
- replaced M-PageUp/M-PageDown with M-Up/M-Down;
- added M-/ to supplement M-TAB.
* xF: reached rough feature parity with xC and xP
* xF: X11: now optionally colour-managed using lcms2, improved IME integration
* xP: fixed channel title layout for narrow browser windows
2.3.0 (2026-07-31) "Clean"
* xC: nuked broken Readline and editline support, added a custom interface
* xC: added a /bindings command
* xC: relay sessions can now be resumed after a dropped connection (adjustable
with general.relay_resume_timeout and general.relay_resume_log_limit);
dead links are detected with periodic pings
* xC: server TLS certificate hostnames are now properly verified
* xC: added support for Lua 5.5
* xP: added a button to reconnect and resume the session if possible;
page reloads start a new session as before
* prime.lua: prevented a DoS opportunity on excessively large numbers
* Added a terminal/X11 frontend for xC called xF
* Bumped relay protocol version
* Various bugfixes
2.2.0 (2026-01-10) "Just Doing the Needful"
* xC: added more characters as nickname delimiters,
@@ -253,7 +302,7 @@
* xC: don't show remembered channel modes on parted channels
* xC: fix highlight detection in colored text
* xC: fix highlight detection in coloured text
* xC: fix CTCP handling for the real world and don't decode X-QUOTEs
@@ -356,7 +405,7 @@
* xC: nicknames in /names are now ordered
* xC: nicknames now use the 256-color terminal palette if available
* xC: nicknames now use the 256-colour terminal palette if available
* xC: now we skip entries in the "addresses" list that can't be resolved
to an address, along with displaying a more helpful message
+25 -36
View File
@@ -10,9 +10,8 @@ They're all lean on dependencies, and offer a maximally permissive licence.
xC
--
The IRC client, and the core of 'xK'. It is largely defined by building on top
of GNU Readline or BSD Editline that have been hacked to death. Its interface
should feel somewhat familiar for weechat or irssi users.
The IRC client, and the core of 'xK'. Its interface should feel somewhat
familiar for WeeChat or irssi users.
image::xC.webp[align="center"]
@@ -33,10 +32,18 @@ including link:xC.adoc#_key_bindings[keyboard shortcuts].
image::xP.webp[align="center"]
xF
--
The hybrid terminal/X11 frontend for 'xC'. It has simplified word wrapping
and limited colour support. Furthermore, X11 has simplified text shaping
and doesn't support text selection (though in 'xC' tradition, it will happily
launch an embedded editor on both the input and the log).
In general, 'xF' mimics 'xP' and adds back a bunch of lost 'xC' UI features.
xA, xT, xW, xM
--------------
Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
Using them is not recommended.
Experimental Fyne, Qt Widgets, Win32, Cocoa frontends for 'xC'.
xD
--
@@ -86,16 +93,17 @@ or as a https://git.janouch.name/p/nixexprs[Nix derivation].
Building
--------
Build-only dependencies: CMake, pkg-config, awk, liberty (included),
asciidoctor or asciidoc (recommended but optional) +
asciidoctor or asciidoc (recommended but optional), rsvg-convert (for 'xF') +
Common runtime dependencies: openssl +
Additionally for 'xC': curses, libffi, readline >= 6.0 or libedit >= 2013-07-12,
lua >= 5.3 (optional) +
Additionally for 'xC': any curses, lua >= 5.3 (optional), termo (included) +
Additionally for 'xF': ncursesw, libunistring, termo (included),
x11 + xft + libpng (X11), lcms2 (X11, optional), libcanberra (X11, optional)
$ git clone --recursive https://git.janouch.name/p/xK.git
$ mkdir xK/build
$ cd xK/build
$ cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DWANT_READLINE=ON -DWANT_LIBEDIT=OFF -DWITH_LUA=ON
-DWITH_LUA=ON -DWITH_XF=ON
$ make
To install the application, you can do either the usual:
@@ -116,12 +124,14 @@ For the rest you might want to generate a configuration file:
$ xB --write-default-config
$ xD --write-default-config
$ xF --write-default-config
After making any necessary edits to the file (there are comments to aid you in
doing that), simply run the appropriate program with no arguments:
$ xB
$ xD
$ xF
'xB' stays running in the foreground, therefore I recommend launching it inside
a Screen or tmux session.
@@ -130,12 +140,14 @@ a Screen or tmux session.
file or something like `killall` if you want to terminate it. You can run it
as a `forking` type systemd user service.
xP
~~
Frontends
~~~~~~~~~
The precondition for running 'xC' frontends is enabling its relay interface:
/set general.relay_bind = "127.0.0.1:9000"
xP
~~
To build the web server, install the Go compiler, and run `make`
from the _xP_ directory. Then start the resulting binary, and navigate to
the adress you give it as its first argument--in the following example,
@@ -156,8 +168,8 @@ running `make generate` first.
xT
~~
The Qt Widgets frontend is a separate CMake subproject. It generally supports
all desktop operating systems. To avoid having to specify the relay address
each time you run it, pass it on the command line.
all desktop operating systems, Android is technically usable. To avoid having
to specify the relay address each time you run it, pass it on the command line.
xW
~~
@@ -191,26 +203,6 @@ in the required form, use:
$ openssl x509 -in public.pem -outform DER | sha256sum
Custom Key Bindings in xC
-------------------------
The default and preferred frontend used in 'xC' is GNU Readline. This means
that you can change your bindings by editing '~/.inputrc'. For example:
....
# Preload with system-wide settings
$include /etc/inputrc
# Make M-left and M-right reorder buffers
$if xC
"\e\e[C": move-buffer-right
"\e\e[D": move-buffer-left
$endif
....
Consult the source code and the GNU Readline manual for a list of available
functions. Also refer to the latter for the exact syntax of this file.
Beware that you can easily break the program if you're not careful.
How do I make xC look like the screenshot?
------------------------------------------
With the defaults, 'xC' doesn't look too fancy because I don't want to have
@@ -255,6 +247,3 @@ License
-------
This software is released under the terms of the 0BSD license, the text of which
is included within the package along with the list of authors.
Note that 'xC' becomes GPL-licensed when you link it against GNU Readline,
but that is not a concern of this source package. The licenses are compatible.
+60
View File
@@ -31,6 +31,7 @@
#include "liberty/liberty.c"
#include <arpa/inet.h>
#include <math.h>
#include <netinet/tcp.h>
static void
@@ -108,8 +109,42 @@ tokenize_host_port (char *address, const char **port)
return address;
}
static int *
filter_color_cube_for_acceptable_nick_colors (size_t *len)
{
// This is a pure function and we don't use threads, static storage is fine
static int table[6 * 6 * 6];
size_t len_counter = 0;
for (int x = 0; x < (int) N_ELEMENTS (table); x++)
{
int r = x / 36;
int g = (x / 6) % 6;
int b = (x % 6);
// The first step is 95/255, the rest are 40/255,
// as an approximation we can double the first step
double linear_R = pow ((r + !!r) / 6., 2.2);
double linear_G = pow ((g + !!g) / 6., 2.2);
double linear_B = pow ((b + !!b) / 6., 2.2);
// Use the relative luminance of colours within the cube to filter
// colours that look okay-ish on terminals with both black and white
// backgrounds (use the test-nick-colors script to calibrate)
double Y = 0.2126 * linear_R + 0.7152 * linear_G + 0.0722 * linear_B;
if (Y >= .25 && Y <= .4)
table[len_counter++] = 16 + x;
}
*len = len_counter;
return table;
}
// --- To be moved to liberty --------------------------------------------------
#define LIST_FOR_EACH_REVERSED(type, iter, tail) \
for (type *iter = (tail), *prev; \
(iter && (prev = iter->prev)) || iter; \
iter = prev)
// FIXME: in xssl_get_error() we rely on error reasons never being NULL (i.e.,
// all loaded), which isn't very robust.
// TODO: check all places where this is used and see if we couldn't gain better
@@ -134,6 +169,31 @@ xerr_describe_error (void)
return reason;
}
static bool
str_append_c_utf8 (struct str *s, uint32_t ucs4)
{
if (ucs4 < 0x80)
str_append_c (s, ucs4);
else if (ucs4 < 0x7FF)
str_append_data (s, (char[]) {
0xc0 | (ucs4 >> 6),
0x80 | (ucs4 & 0x3f) }, 2);
else if (ucs4 < 0xFFFF)
str_append_data (s, (char[]) {
0xe0 | (ucs4 >> 12),
0x80 | ((ucs4 >> 6) & 0x3f),
0x80 | (ucs4 & 0x3f) }, 3);
else if (ucs4 < 0x10FFFF)
str_append_data (s, (char[]) {
0xf0 | (ucs4 >> 18),
0x80 | ((ucs4 >> 12) & 0x3f),
0x80 | ((ucs4 >> 6) & 0x3f),
0x80 | (ucs4 & 0x3f) }, 4);
else
return false;
return true;
}
static struct str
str_from_cstr (const char *cstr)
{
+3 -2
View File
@@ -6,9 +6,10 @@
// We use the XDG Base Directory Specification, but may be installed anywhere.
#define PROJECT_DATADIR "${CMAKE_INSTALL_FULL_DATADIR}"
#cmakedefine HAVE_READLINE
#cmakedefine HAVE_EDITLINE
#cmakedefine HAVE_LUA
#cmakedefine HAVE_X11
#cmakedefine HAVE_LCMS2
#cmakedefine HAVE_CANBERRA
#cmakedefine01 ICONV_ACCEPTS_TRANSLIT
+1 -1
Submodule liberty updated: 31ae400852...022cce71fa
+5 -25
View File
@@ -14,19 +14,7 @@
-- OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Beware that it is a hack and only goes about 90% of the way, which is why
-- this functionality is only available as a plugin in the first place
-- (well, and also for customizability).
--
-- The biggest problem is that the way we work with Readline is incompatible
-- with multiline prompts, and normal newlines just don't work. This is being
-- circumvented by using an overflowing single-line prompt with a specially
-- crafted character in the rightmost column that prevents the bar's background
-- from spilling all over the last line.
--
-- There is also a problem with C-r search rendering not clearing out the
-- background but to really fix that mode, we'd have to fully reimplement it
-- since its alternative prompt very often gets overriden by accident anyway.
-- This might go directly to xC, though it's more customizable as a plugin.
xC.hook_prompt (function (hook)
local current = xC.current_buffer
@@ -67,11 +55,7 @@ xC.hook_prompt (function (hook)
x = x .. " (" .. active:sub (2) .. ")"
end
-- Readline 7.0.003 seems to be broken and completely corrupts the prompt.
-- However 8.0.004 seems to be fine with these, as is libedit 20191231-3.1.
--x = x:gsub("[\128-\255]", "?")
-- Align to the terminal's width and apply formatting, including the hack.
-- Align to the terminal's width and apply formatting.
local lines, cols = xC.get_screen_size ()
local trailing, width = " ", xC.measure (x)
while cols > 0 and width >= cols do
@@ -79,10 +63,10 @@ xC.hook_prompt (function (hook)
trailing, width = ">", xC.measure (x)
end
-- xC automatically smartly wraps the line.
x = "\x01\x1b[0;4;1;38;5;16m\x1b[48;5;" .. bg_color .. "m\x02" ..
x .. string.rep (" ", cols - width - 1) ..
"\x01\x1b[0;4;1;7;38;5;" .. bg_color .. "m\x02" ..
trailing .. "\x01\x1b[0;1m\x02"
x .. string.rep (" ", cols - width - 1) .. trailing ..
"\x01\x1b[0;1m\x02"
local user_prefix = function (chan, user)
for i, chan_user in ipairs (chan.users) do
@@ -104,10 +88,6 @@ xC.hook_prompt (function (hook)
if modes ~= "" then x = x .. "(" .. modes .. ")" end
end
x = x .. "] "
else
-- There needs to be at least one character so that the cursor
-- doesn't get damaged by our hack in that last column
x = x .. "> "
end
return x
end)
+2 -1
View File
@@ -1,7 +1,7 @@
--
-- prime.lua: highlight prime numbers in messages
--
-- Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
-- Copyright (c) 2020, 2026 Přemysl Eric Janouch <p@janouch.name>
--
-- Permission to use, copy, modify, and/or distribute this software for any
-- purpose with or without fee is hereby granted.
@@ -34,6 +34,7 @@ xC.setup_config {
-- The prime test is actually very fast, so there is no DoS concern
local do_intercolour = function (text)
return tostring (text:gsub ("%f[%w_]%d+", function (n)
if tonumber (n) > math.maxinteger then return nil end
if tonumber (n) < smallest then return nil end
for i = 2, n ^ (1 / 2) do if (n % i) == 0 then return nil end end
return highlight .. n .. highlight
Submodule
+1
Submodule termo added at a8f3f6eca3
+1 -1
View File
@@ -9,7 +9,7 @@ export example=$(
#define N_ELEMENTS(a) (sizeof (a) / sizeof ((a)[0]))
$(perl -0777 -ne 'print $& if /^.*?\nfilter_color(?s:.*?)^}$/m' \
"$(dirname "$0")"/xC.c)
"$(dirname "$0")"/common.c)
void main () {
size_t len = 0;
+2
View File
@@ -33,6 +33,8 @@ function expect (string)
xC:eof {function (p) error "xC exited prematurely" end})
end
expect "]"
-- Connect to the daemon
send "/server add localhost\n"
expect "]"
+4
View File
@@ -18,6 +18,10 @@ FyneApp.toml: ../xK-version
Version = '$$(cat ../xK-version)'\n\
Build = 1\n\
\n\
[Migrations]\n\
# This is temporary, for Fyne 2.8\n\
fyneDo = true\n\
\n\
[LinuxAndBSD]\n\
GenericName = 'IRC Client'\n\
Categories = ['Network', 'Chat', 'IRCClient']\n" > $@
+20 -17
View File
@@ -1,33 +1,36 @@
module janouch.name/xK/xA
go 1.24.0
go 1.26
require (
fyne.io/fyne/v2 v2.7.2
fyne.io/fyne/v2 v2.8.0
github.com/ebitengine/oto/v3 v3.4.0
)
require (
fyne.io/systray v1.12.0 // indirect
fyne.io/systray v1.12.2 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/FyshOS/fancyfs v0.0.1 // indirect
github.com/anthonynsimon/bild v0.16.1 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/ebitengine/purego v0.10.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 // indirect
github.com/fyne-io/glfw-js v0.4.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.3.2 // indirect
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a // indirect
github.com/go-text/render v0.2.1 // indirect
github.com/go-text/typesetting v0.3.4 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -35,10 +38,10 @@ require (
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
golang.org/x/image v0.34.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
github.com/yuin/goldmark v1.8.4 // indirect
golang.org/x/image v0.44.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+40 -33
View File
@@ -1,40 +1,45 @@
fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw=
fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/fyne/v2 v2.8.0 h1:KNUdIk1eKsXSPy/wU6MdiR1hppAPvyzbjPbtJ8h6EUQ=
fyne.io/fyne/v2 v2.8.0/go.mod h1:tLJK7CVtUBOnMiSDR+J88t/quiGuEhwGs09tIVM1RXg=
fyne.io/systray v1.12.2 h1:Y8DZxgLHsVQt6rY9Zrkkg+j67S7vv/1F2viOWKPpVeA=
fyne.io/systray v1.12.2/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/FyshOS/fancyfs v0.0.1 h1:kgvm7VvwOMLkYTqSflplp62SlMVWQ2uAoHw9CXwXHYg=
github.com/FyshOS/fancyfs v0.0.1/go.mod h1:S5SHVz/5R72iCXOxCqdcyTPSlg3JxNd0gaHyGBSrY8A=
github.com/anthonynsimon/bild v0.16.1 h1:ECqtLkQ15kqfHdRtzUfNvQniJtHNpzdVU/7feMYAm0o=
github.com/anthonynsimon/bild v0.16.1/go.mod h1:hYAxurnswTQ9dexoiK922MepdXLC1lBzG35n6ypk//g=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/oto/v3 v3.4.0 h1:br0PgASsEWaoWn38b2Goe7m1GKFYfNgnsjSd5Gg+/bQ=
github.com/ebitengine/oto/v3 v3.4.0/go.mod h1:IOleLVD0m+CMak3mRVwsYY8vTctQgOM0iiL6S7Ar7eI=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.1 h1:dewVBCBT2GaMu1SrNTYxQhgQBethzfhiwvZiLGP/qyY=
github.com/ebitengine/purego v0.10.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 h1:0kdPD/GEntpWmZEK5Zu/xE6Tr37jYCVDf9QP8lA/QK8=
github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.4.0 h1:I9hREBeFyI10cNIqbMKYb1PRidyPDgwob8o2la9SfQo=
github.com/fyne-io/glfw-js v0.4.0/go.mod h1:SDchsFZh4n7nVuBoiowOhOgIBdz+qUQVeC1w9fe2yVU=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.2 h1:OUOFxp9Rx5PiO0/rh2IY+5gmyXjXsVG8+LfEyk9NMcE=
github.com/go-text/typesetting v0.3.2/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI=
github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a h1:HWK0MBggT/T6YH7VffE10xBIhqeTq8JzIUPJXrRy87g=
github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8=
github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
@@ -49,6 +54,8 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe9
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
@@ -67,18 +74,18 @@ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqd
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+97 -55
View File
@@ -393,10 +393,10 @@ func bufferScrollToBottom() {
func bufferPushLine(b *buffer, line bufferLine) {
b.lines = append(b.lines, line)
// Fyne's text layouting is extremely slow.
// This also noticeably improves performance.
// The limit could be made configurable,
// and we could use a ring buffer approach to storing the lines.
if len(b.lines) > 100 {
if len(b.lines) > 1000 {
b.lines = slices.Delete(b.lines, 0, 1)
}
}
@@ -529,6 +529,10 @@ func bufferToggleLog() {
relaySend(RelayCommandData{Variant: &RelayCommandDataBufferLog{
BufferName: name,
}}, func(err string, response *RelayResponseData) {
if response == nil {
showErrorMessage(err)
return
}
if bufferCurrent == name {
bufferToggleLogFinish(
err, response.Variant.(*RelayResponseDataBufferLog))
@@ -545,6 +549,7 @@ func convertItemFormatting(
switch data := item.Variant.(type) {
case *RelayItemDataReset:
*cf = defaultBufferLineItem()
*inverse = false
case *RelayItemDataFlipBold:
cf.format.Bold = !cf.format.Bold
case *RelayItemDataFlipItalic:
@@ -552,7 +557,7 @@ func convertItemFormatting(
case *RelayItemDataFlipUnderline:
cf.format.Underline = !cf.format.Underline
case *RelayItemDataFlipCrossedOut:
// https://github.com/fyne-io/fyne/issues/1084
cf.format.Strikethrough = !cf.format.Strikethrough
case *RelayItemDataFlipInverse:
*inverse = !*inverse
case *RelayItemDataFlipMonospace:
@@ -1049,6 +1054,11 @@ func relayProcessMessage(m *RelayEventMessage) {
case *RelayEventDataServerRename:
servers[data.New] = servers[data.ServerName]
delete(servers, data.ServerName)
for i := range buffers {
if buffers[i].serverName == data.ServerName {
buffers[i].serverName = data.New
}
}
case *RelayEventDataServerRemove:
delete(servers, data.ServerName)
}
@@ -1097,10 +1107,11 @@ func relayResetState() {
}
func relayRun() {
fyne.CurrentApp().Preferences().SetString(preferenceAddress, backendAddress)
backendLock.Lock()
fyne.DoAndWait(func() {
fyne.CurrentApp().Preferences().
SetString(preferenceAddress, backendAddress)
relayResetState()
})
@@ -1127,17 +1138,10 @@ func relayRun() {
}, nil)
relayMessages := relayMakeReceiver(backendContext, backendConn)
Loop:
for {
select {
case m, ok := <-relayMessages:
if !ok {
break Loop
}
fyne.DoAndWait(func() {
relayProcessMessage(&m)
})
}
for m := range relayMessages {
fyne.DoAndWait(func() {
relayProcessMessage(&m)
})
}
fyne.DoAndWait(func() {
wConnect.Show()
@@ -1215,6 +1219,10 @@ func inputComplete() bool {
Text: state.input,
Position: uint32(len(state.input)),
}}, func(err string, response *RelayResponseData) {
if response == nil {
showErrorMessage(err)
return
}
if stamp := inputGetStamp(); state == stamp {
inputCompleteFinish(state,
err, response.Variant.(*RelayResponseDataBufferComplete))
@@ -1289,11 +1297,11 @@ func (e *inputEntry) KeyDown(key *fyne.KeyEvent) {
}
switch {
case modifiedKey.Modifier == fyne.KeyModifierControl &&
case modifiedKey.Modifier == fyne.KeyModifierAlt &&
modifiedKey.KeyName == fyne.KeyP:
inputUp()
return
case modifiedKey.Modifier == fyne.KeyModifierControl &&
case modifiedKey.Modifier == fyne.KeyModifierAlt &&
modifiedKey.KeyName == fyne.KeyN:
inputDown()
return
@@ -1412,7 +1420,7 @@ func (l *customLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// rotatedBuffers returns buffer indexes starting with the current buffer.
// rotatedBuffers returns buffer indexes starting after the current buffer.
func rotatedBuffers() []int {
r, start := make([]int, len(buffers)), 0
for i := range buffers {
@@ -1428,57 +1436,91 @@ func rotatedBuffers() []int {
return r
}
func actionBufferPrevious() {
if r := rotatedBuffers(); len(r) <= 0 {
} else if i := r[len(r)-1]; i == 0 {
bufferActivate(buffers[len(buffers)-1].bufferName)
} else {
bufferActivate(buffers[i-1].bufferName)
}
}
func actionBufferNext() {
if r := rotatedBuffers(); len(r) <= 0 {
} else {
bufferActivate(buffers[r[0]].bufferName)
}
}
func actionBufferLast() {
if bufferLast != "" {
bufferActivate(bufferLast)
}
}
func actionBufferActivity(backwards bool) {
r := rotatedBuffers()
if len(r) == 0 {
return
}
r = r[:len(r)-1]
if backwards {
slices.Reverse(r)
}
for _, i := range r {
if buffers[i].highlighted {
bufferActivate(buffers[i].bufferName)
return
}
}
for _, i := range r {
if buffers[i].newMessages != 0 {
bufferActivate(buffers[i].bufferName)
return
}
}
}
var shortcuts = map[desktop.CustomShortcut]func(){
{
KeyName: fyne.KeyPageUp,
Modifier: fyne.KeyModifierControl,
}: func() {
if r := rotatedBuffers(); len(r) <= 0 {
} else if i := r[len(r)-1]; i == 0 {
bufferActivate(buffers[len(buffers)-1].bufferName)
} else {
bufferActivate(buffers[i-1].bufferName)
}
},
}: actionBufferPrevious,
{
KeyName: fyne.KeyUp,
Modifier: fyne.KeyModifierAlt,
}: actionBufferPrevious,
{
KeyName: fyne.KeyF5,
Modifier: 0,
}: actionBufferPrevious,
{
KeyName: fyne.KeyPageDown,
Modifier: fyne.KeyModifierControl,
}: func() {
if r := rotatedBuffers(); len(r) <= 0 {
} else {
bufferActivate(buffers[r[0]].bufferName)
}
},
}: actionBufferNext,
{
KeyName: fyne.KeyDown,
Modifier: fyne.KeyModifierAlt,
}: actionBufferNext,
{
KeyName: fyne.KeyF6,
Modifier: 0,
}: actionBufferNext,
{
KeyName: fyne.KeyTab,
Modifier: fyne.KeyModifierAlt,
}: func() {
if bufferLast != "" {
bufferActivate(bufferLast)
}
},
{
// XXX: This makes an assumption on the keyboard layout (we want '!').
KeyName: fyne.Key1,
Modifier: fyne.KeyModifierAlt | fyne.KeyModifierShift,
}: func() {
for _, i := range rotatedBuffers() {
if buffers[i].highlighted {
bufferActivate(buffers[i].bufferName)
break
}
}
},
}: actionBufferLast,
{
KeyName: fyne.KeyA,
Modifier: fyne.KeyModifierAlt,
}: func() {
for _, i := range rotatedBuffers() {
if buffers[i].newMessages != 0 {
bufferActivate(buffers[i].bufferName)
break
}
}
actionBufferActivity(false)
},
{
KeyName: fyne.KeyA,
Modifier: fyne.KeyModifierAlt | fyne.KeyModifierShift,
}: func() {
actionBufferActivity(true)
},
{
KeyName: fyne.KeyH,
+1 -1
View File
@@ -630,7 +630,7 @@ recovery_handler (int signum, siginfo_t *info, void *context)
const char *signal_name = NULL, *reason = NULL;
translate_signal_info (signum, &signal_name, info->si_code, &reason);
char buf[128], numbuf[8];
char buf[128], numbuf[16];
if (!signal_name)
{
snprintf (numbuf, sizeof numbuf, "%d", signum);
+223 -21
View File
@@ -39,28 +39,27 @@ This feature may be used to preview server MOTD files.
Key bindings
------------
Most key bindings are inherited from the frontend in use, which is either GNU
Readline or BSD editline. A few of them, however, are special to the IRC client
or assume a different function. This is a list of all local overrides and
their respective function names:
The basic key bindings are inspired by GNU Readline. On top of that, there are
some which are special to the IRC client or assume a different function.
This is a list of all local specialties and their respective function names:
*M-p*::
Go up in history for this buffer (normally mapped to *C-p*).
*M-n*::
Go down in history for this buffer (normally mapped to *C-n*).
*C-p*, *F5*: *previous-buffer*::
*C-p*, *F5*, *M-Up*: *previous-buffer*::
Switch to the previous buffer in order.
*C-n*, *F6*: *next-buffer*::
*C-n*, *F6*, *M-Down*: *next-buffer*::
Switch to the next buffer in order.
*M-TAB*: *switch-buffer*::
*M-/*, *M-TAB*: *switch-buffer*::
Switch to the last buffer, i.e., the one you were in before.
*M-0*, *M-1*, ..., *M-9*: *goto-buffer*::
Go to the N-th buffer (normally sets a repeat counter).
Since there is no buffer number zero, *M-0* goes to the tenth one.
*M-!*: *goto-highlight*::
Go to the first following buffer with an unseen highlight.
*M-a*: *goto-activity*::
Go to the first following buffer with unseen activity.
*M-A*: *goto-backtivity*::
Go to the first following/preceding buffer with an unseen highlight,
or, if there are none, with unseen activity.
*PageUp*: *display-backlog*::
Show the in-memory backlog for this buffer using *general.pager*,
which is almost certainly the *less*(1) program.
@@ -83,13 +82,220 @@ their respective function names:
Should there be any issues with the display, this will clear the terminal
screen and redraw all information.
Additionally, *C-w* and *C-u* in editline behave the same as they would in
Readline or the "vi" command mode, even though the "emacs" mode is enabled
by default.
Custom bindings
---------------
Edit the configuration file manually. For example, to make buffer reordering
functions accessible:
Bindings can be customized in your _.inputrc_ or _.editrc_ file. Both libraries
support conditional execution based on the program name. Beware that it is easy
to make breaking changes.
....
bindings = {
"M-Left" = "move-buffer-left"
"M-Right" = "move-buffer-right"
}
....
You may bind any function you see in the */bindings* listing, though beware that
you can easily break the program if you're not careful.
Lua plugin API
--------------
The API closely reflects the internal structure of the client.
For code examples, please consult preinstalled plugins.
All references to internal objects are weak, and may become invalid.
Methods named like __:get_PROPERTY__ can also be accessed as __.PROPERTY__.
As a rule, properties are read-only, and strings are encoded in UTF-8.
*xC.buffers* ({Buffer, ...})::
All buffers in order.
*xC.current_buffer* (Buffer)::
The active buffer.
*xC.global_buffer* (Buffer)::
The global buffer.
*xC.servers* ({[string = Server, ...]})::
Configured servers, indexed by their name.
*xC.get_screen_size*() -> integer, integer::
Return the number of terminal lines and columns.
*xC.hook_completion*(_callback_[, _priority_]) -> Hook::
Register a generator _callback_ for Tab completions: +
Hook, {buffer, line, words, location}, string -> {[string, ...]}?
+
* *buffer* (Buffer): Where the completion occurs.
+
* *line* (string): The whole input line.
+
* *words* ({[string, ...]}): The input line split into words.
+
* *location* (integer): Which word is being completed, zero-based.
*xC.hook_input*(_callback_[, _priority_]) -> Hook::
Register a filter _callback_ for user-typed input: +
Hook, Buffer, string -> string?
*xC.hook_irc*(_callback_[, _priority_]) -> Hook::
Register a filter _callback_ for incoming raw IRC messages: +
Hook, Server, string -> string?
*xC.hook_prompt*(_callback_[, _priority_]) -> Hook::
Register a generator _callback_ for the TUI input line prompt: +
Hook -> string?
+
So as to not break the display, escape any terminal control sequences
by enclosing them between \x01 and \x02.
*xC.measure*(_string_) -> integer::
Determine the width of a _string_ in terms of terminal cells.
*xC.parse*(_message_) -> {tags, prefix, command, params}::
Parse a raw IRC _message_ into its components:
+
* *tags* ({[string = string, ...]})
+
* *prefix* (string?)
+
* *command* (string?)
+
* *params* ({[string, ...]})
*xC.setup_config*(_options_)::
Declare persistent plugin configuration under _plugins.NAME_, with the name
derived from the script's basename without any extensions.
The _options_ index configuration items by name, as further tables with:
+
* *type* (string): The configuration type of the value: one of _null_,
_object_, _boolean_, _integer_, _string_, __string_array__.
+
* *comment* (string?): The description of the value.
+
* *default* (string?): The default value, expressed in terms of
the configuration language. Items with a default may not become _null_.
+
* *on_change* (function?): Called when the value is initialized,
first read, or later changed.
+
* *validate* (function?): Called to validate values after they pass a type
check, returning _true_ if a change should be allowed.
Async Library
~~~~~~~~~~~~~
Within this concurency model, use Lua's *coroutine.yield* like *await*.
*xC.async.dial*(_host_, _service_) -> WaitChannel::
Connect to the server at _host_, port _service_. Make sure
to set the appropriate callbacks on the returned Connection object.
*xC.async.go*(_fn_, ...) -> Task::
Run _fn_ as a coroutine. These coroutines may only yield WaitChannels.
When yielding multiple wait channel values, the operation will return
the index of the completed channel, followed by all its return values packed
into a single table.
*xC.async.timer_ms*(_milliseconds_) -> WaitChannel::
Resume the calling task after the given number of _milliseconds_.
Buffer
~~~~~~
*.channel* (Channel?)::
The associated channel, if any.
*.hide_unimportant* (boolean)::
Whether unimportant messages are hidden in the user interface.
*.highlighted* (boolean)::
Whether we've been highlighted since last seeing the buffer.
*.name* (string)::
The full name of the buffer.
*.new_messages_count* (integer)::
The number of new messages since last seeing the buffer.
*.new_unimportant_count* (integer)::
How many of those messages are unimportant.
*.server* (Server?)::
The associated server, if any.
*.user* (User?)::
The associated user, if any.
*:execute*(_input_)::
Process a line of _input_ within the buffer, as if the user typed it in.
*:log*(_message_)::
Log a status _message_.
Channel
~~~~~~~
*.left_manually* (boolean)::
Whether we've left the channel manually, not to rejoin on reconnect.
*.name* (string)::
The channel's literal name, which may not be in UTF-8.
*.no_param_modes* (string)::
Active no-parameter channel modes.
*.param_modes* ({[string = string, ...]})::
Active parametrized channel modes.
*.topic* (string)::
The channel topic, which may not be in UTF-8.
This string includes any in-line formatting.
*.users_len* (integer)::
The number of users on the channel.
*:get_users*() -> {[{prefixes = string, user = User}, ...]}::
Return all users on the channel, together with their prefixes.
Connection
~~~~~~~~~~
*.on_data*(_data_)::
Called whenever _data_ is received from the server.
*.on_eof*()::
Called when the connection is closed by the server.
*.on_error*(_error_)::
Called when the connection sees an _error_.
*:close*()::
Close the connection.
*:send*(_data_)::
Send a string to the server.
Hook
~~~~
*:unhook*()::
Forget about the hook.
Server
~~~~~~
*.autoaway_active* (boolean)::
Whether we've currently automatically marked ourselves away.
*.buffer* (Buffer)::
The server's buffer.
*.cap_echo_message* (boolean)::
Whether the echo-message capability is active.
*.irc_user_host* (string)::
Our own user's user@host as we know it, used for message splitting.
*.manual_disconnect* (boolean)::
A flag to avoid reconnecting automatically after a manual disconnect request
causes a connection interrupt.
*.name* (string)::
The name of the server.
*.reconnect_attempt* (integer)::
Attempt counter while establishing a connection.
//*.state* (integer)::
// The connection state as an integer constant. Overriden.
*.user* (User?)::
Our own user, once we're registered.
*.user_mode* (string)::
Our own user's modes.
*:get_state*() -> string::
The connection state: one of _disconnected_, _connecting_, _connected_,
_registered_, _closing_, _half-closed_.
*:send*(_message_)::
Send an IRC message verbatim, after appending newline characters.
Task
~~~~
*:cancel*()::
Cancel the task, interrupting any *yield*.
User
~~~~
*.away* (boolean)::
Whether the user is marked away.
*.nickname* (string)::
The user's literal nickname, which may not be in UTF-8.
*:get_channels*() -> {[Channel, ...]}::
Return all channels the user shares with us.
Environment
-----------
@@ -113,10 +319,6 @@ _/usr/local/share/xC/plugins/_::
_/usr/share/xC/plugins/_::
Plugins are searched for in these directories, in order.
Bugs
----
The editline (libedit) frontend may exhibit some unexpected behaviour.
Reporting bugs
--------------
Use https://git.janouch.name/p/xK to report bugs, request features,
@@ -124,4 +326,4 @@ or submit pull requests.
See also
--------
*less*(1), *readline*(3) or *editline*(7)
*less*(1), *xF*(1)
+1611 -1753
View File
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -1,5 +1,5 @@
// Backwards-compatible protocol version.
const VERSION = 2;
const VERSION = 3;
// From the frontend to the relay.
// All commands receive either an Event.RESPONSE, or an Event.ERROR.
@@ -13,16 +13,20 @@ struct CommandMessage {
BUFFER_ACTIVATE,
BUFFER_INPUT,
BUFFER_TOGGLE_UNIMPORTANT,
PING_RESPONSE,
PING,
PING_RESPONSE,
BUFFER_COMPLETE,
BUFFER_LOG,
} command) {
// If the version check succeeds, the client will receive
// an initial stream of SERVER_UPDATE, BUFFER_UPDATE, BUFFER_STATS,
// BUFFER_LINE, and finally a BUFFER_ACTIVATE message.
// When a session is resumed, the RESPONSE is preceded by an event log
// replay starting at event_seq.
case HELLO:
u32 version;
// Empty to start a new session, otherwise the session to resume.
u8 session_id<>;
// The event_seq to continue from;
// a new session is started when that point is no longer available.
u32 event_seq;
case ACTIVE:
void;
case BUFFER_ACTIVATE:
@@ -74,6 +78,12 @@ struct EventMessage {
case RESPONSE:
u32 command_seq;
union ResponseData switch (Command command) {
case HELLO:
// The session to present on the next connection. If it differs
// from the command, the session is new, and a full resync follows:
// an initial stream of SERVER_UPDATE, BUFFER_UPDATE, BUFFER_STATS,
// BUFFER_LINE, and finally a BUFFER_ACTIVATE message.
u8 session_id<>;
case BUFFER_COMPLETE:
u32 start;
string completions<>;
+11
View File
@@ -887,7 +887,14 @@ static void
client_unregister (struct client *c, const char *reason)
{
if (!c->registered)
{
if (c->nickname)
{
str_map_set (&c->ctx->users, c->nickname, NULL);
cstr_set (&c->nickname, NULL);
}
return;
}
char *message = xstrdup_printf (":%s!%s@%s QUIT :%s",
c->nickname, c->username, c->hostname, reason);
@@ -2630,7 +2637,11 @@ irc_try_kick (struct client *c, const char *channel_name, const char *nick,
if (!(chan->modes & IRC_CHAN_MODE_QUIET))
irc_channel_multicast (chan, message, NULL);
else
{
client_send (c, "%s", message);
if (c != client)
client_send (client, "%s", message);
}
free (message);
channel_remove_user (chan, user);
+63
View File
@@ -0,0 +1,63 @@
xF(1)
=====
:doctype: manpage
:manmanual: xK Manual
:mansource: xK {release-version}
Name
----
xF - Terminal/X11 frontend for xC
Synopsis
--------
*xF* [_OPTION_]... [HOST:PORT]
Description
-----------
*xF* is a frontend for the relay interface of the *xC* IRC client.
It normally runs in the terminal, and can also use a graphical interface
when built with support for one.
When an explicit relay address is given, it takes priority over configuration.
Options
-------
*-g*, *--gui*::
Use the graphical interface even when running from a terminal.
*-h*, *--help*::
Display a help message and exit.
*-V*, *--version*::
Output version information and exit.
*--write-default-cfg*[**=**__PATH__]::
Write a configuration file with defaults, show its path and exit.
+
The file will be appropriately commented.
Key bindings
------------
As in *xC*(1), with a bunch of notable additions:
*C-q*::
Quit the frontend.
*C-F5*::
Forcefully reconnect to the relay.
*C-o*::
Open the last link in the transcript using *xdg-open*(1).
*M-E*::
Run an external editor on transcript contents, enabling, e.g., search.
*F1*, *F2*, *M-F1*, *M-F2*::
Scroll the buffer list: up, down, top, bottom.
*PageUp*, *PageUp*, *M-Home*, *M-End*, *C-Home*, *C-End*::
Scroll the transcript: up, down, top, bottom, top, bottom.
Reporting bugs
--------------
Use https://git.janouch.name/p/xK to report bugs, request features,
or submit pull requests.
See also
--------
*xC*(1), *xdg-open*(1)
+4554 -85
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
2.2.0
2.4.0
+24 -13
View File
@@ -697,7 +697,8 @@ func refreshBuffer(b: Buffer) {
// --- Event processing --------------------------------------------------------
relayRPC.onConnected = {
let hello = RelayCommandDataHello(version: UInt32(relayVersion))
let hello = RelayCommandDataHello(
version: UInt32(relayVersion), sessionId: [], eventSeq: 0)
relayRPC.send(data: hello)
}
@@ -1209,7 +1210,7 @@ class WindowDelegate: NSObject, NSWindowDelegate {
refreshBufferList()
}
// Buffer indexes rotated to start after the current buffer.
// Buffers rotated to start after the current buffer.
func rotatedBuffers() -> Array<Buffer> {
guard let i = relayBuffers.firstIndex(
where: { $0.bufferName == relayBufferCurrent }) else {
@@ -1238,22 +1239,32 @@ class WindowDelegate: NSObject, NSWindowDelegate {
}
}
@objc func actionGotoHighlight() {
for b in rotatedBuffers() {
func gotoActivity(backwards: Bool) {
var candidates = rotatedBuffers()
candidates.popLast()
if backwards {
candidates.reverse()
}
for b in candidates {
if b.highlighted {
bufferActivate(name: b.bufferName)
return
}
}
for b in candidates {
if b.newMessages != 0 {
bufferActivate(name: b.bufferName)
return
}
}
}
@objc func actionGotoActivity() {
for b in rotatedBuffers() {
if b.newMessages != 0 {
bufferActivate(name: b.bufferName)
return
}
}
gotoActivity(backwards: false)
}
@objc func actionGotoBacktivity() {
gotoActivity(backwards: true)
}
@objc func actionToggleUnimportant() {
@@ -1313,12 +1324,12 @@ pushAccelerator("Switch buffer",
// TODO(p): Remove .command, and ignore these with the right Option key.
bufferMenu.addItem(NSMenuItem.separator())
pushAccelerator("Go to highlight",
#selector(WindowDelegate.actionGotoHighlight),
"!", [.command, .option])
pushAccelerator("Go to activity",
#selector(WindowDelegate.actionGotoActivity),
"a", [.command, .option])
pushAccelerator("Go to activity backwards",
#selector(WindowDelegate.actionGotoBacktivity),
"a", [.shift, .command, .option])
bufferMenu.addItem(NSMenuItem.separator())
pushAccelerator("Toggle unimportant",
+105 -44
View File
@@ -9,14 +9,40 @@ class RelayRPC extends EventTarget {
super()
this.url = url
this.commandSeq = 0
this.promised = {}
// Session to resume after a lost connection, and our progress in it.
this.sessionId = ''
this.lastEventSeq = 0
this.connecting = false
}
// FIXME: The m.redraws are contamination for properties
// that should have an event: this.connecting, this.busy
// When we can't resume our session, a 'reset' event will ask for the model
// to be reinitialized.
connect() {
// We can't close the connection immediately, as that queues a task.
if (this.connecting)
return
this.connecting = true
m.redraw()
// We can't reopen the socket immediately, as closing queues a task.
if (this.ws === undefined)
this._connect()
else {
this.ws.addEventListener('close',
() => this._connect(), {once: true})
this.ws.close()
}
}
_connect() {
if (this.ws !== undefined)
throw "Already connecting or connected"
return new Promise((resolve, reject) => {
new Promise((resolve, reject) => {
let ws = this.ws = new WebSocket(this.url)
ws.onopen = event => {
this._initialize()
@@ -27,6 +53,20 @@ class RelayRPC extends EventTarget {
this.ws = undefined
reject()
}
}).then(() => {
return this.send({
command: 'Hello',
version: Relay.version,
sessionId: this.sessionId,
// Continue right after the last event we have processed.
eventSeq: this.sessionId ? (this.lastEventSeq + 1) >>> 0 : 0,
})
}).catch(error => {
// Failures manifest as a closed connection.
console.error(error)
}).finally(() => {
this.connecting = false
m.redraw()
})
}
@@ -42,8 +82,6 @@ class RelayRPC extends EventTarget {
this.ws.onclose = event => {
let message = "Connection closed: " +
event.reason + " (" + event.code + ")"
for (const seq in this.promised)
this.promised[seq].reject(message)
this.ws = undefined
this.dispatchEvent(new CustomEvent('close', {
@@ -52,8 +90,6 @@ class RelayRPC extends EventTarget {
// Now connect() can be called again.
}
this.promised = {}
}
_process(data) {
@@ -66,6 +102,8 @@ class RelayRPC extends EventTarget {
}
_processOne(message) {
this.lastEventSeq = message.eventSeq
let e = message.data
let p
switch (e.event) {
@@ -76,6 +114,9 @@ class RelayRPC extends EventTarget {
p.reject(e.error)
break
case Relay.Event.Response:
if (e.data.command === Relay.Command.Hello)
this._adoptSession(base64Encode(e.data.sessionId), e.commandSeq)
if ((p = this.promised[e.commandSeq]) === undefined)
console.error("Unawaited response")
else if (p !== true)
@@ -90,15 +131,33 @@ class RelayRPC extends EventTarget {
delete this.promised[e.commandSeq]
for (const seq in this.promised) {
// We don't particularly care about wraparound issues.
if (seq >= e.commandSeq)
continue
this.promised[seq].reject("No response")
delete this.promised[seq]
if (seq < e.commandSeq)
this._abandon(seq, "No response")
}
m.redraw()
}
_adoptSession(sessionId, seq) {
if (sessionId === this.sessionId)
return
// Nothing else can be answered from within the new session.
for (const pending in this.promised)
if (pending != seq)
this._abandon(pending, "Session lost")
this.sessionId = sessionId
this.dispatchEvent(new CustomEvent('reset'))
}
// Stop waiting for a reply that is never going to arrive.
_abandon(seq, message) {
const p = this.promised[seq]
if (p !== true)
p.reject(message)
delete this.promised[seq]
}
get busy() {
for (const seq in this.promised)
return true
@@ -138,6 +197,8 @@ class RelayRPC extends EventTarget {
function utf8Encode(s) { return new TextEncoder().encode(s) }
function utf8Decode(s) { return new TextDecoder().decode(s) }
function base64Encode(bytes) { return btoa(String.fromCharCode(...bytes)) }
function hasShortcutModifiers(event) {
return (event.altKey || event.escapePrefix) &&
!event.metaKey && !event.ctrlKey
@@ -202,6 +263,16 @@ let bufferAutoscroll = true
let servers = new Map()
rpc.addEventListener('reset', event => {
buffers.clear()
bufferLast = undefined
bufferCurrent = undefined
bufferLog = undefined
bufferAutoscroll = true
servers.clear()
})
let lastActive = undefined
function notifyActive() {
@@ -259,23 +330,7 @@ function bufferToggleLog() {
})
}
let connecting = true
rpc.connect().then(result => {
buffers.clear()
bufferLast = undefined
bufferCurrent = undefined
bufferLog = undefined
bufferAutoscroll = true
servers.clear()
rpc.send({command: 'Hello', version: Relay.version})
connecting = false
m.redraw()
}).catch(error => {
connecting = false
m.redraw()
})
rpc.connect()
rpc.addEventListener('close', event => {
m.redraw()
@@ -1115,7 +1170,7 @@ let Input = {
let Main = {
view: vnode => {
let overlay = undefined
if (connecting)
if (rpc.connecting)
overlay = m('.overlay', {}, "Connecting...")
else if (rpc.ws === undefined)
overlay = m('.overlay', {}, [
@@ -1126,11 +1181,12 @@ let Main = {
return m('.xP', {}, [
overlay,
m('.title', {}, [
m('span', [
m('.toolbar', [
// Midline Horizontal Ellipsis, No-Break Space
m('span', {class: rpc.busy ? undefined : 'invisible'},
`\u22EF\u00A0`),
m('b', {}, `xP`),
m('button', {onclick: () => rpc.connect()}, '⟳'),
]),
m(Topic),
]),
@@ -1178,10 +1234,22 @@ document.addEventListener('keydown', event => {
return
// Rotate names so that the current buffer comes first.
let names = [...buffers.keys()]
const names = [...buffers.keys()]
names.push.apply(names,
names.splice(0, names.findIndex(name => name == bufferCurrent)))
const gotoActivity = backwards => {
const candidates = names.slice(1)
if (backwards)
candidates.reverse()
for (const name of candidates)
if (buffers.get(name).highlighted)
return bufferActivate(name)
for (const name of candidates)
if (buffers.get(name).newMessages)
return bufferActivate(name)
}
switch (event.key) {
case 'h':
bufferToggleLog()
@@ -1191,28 +1259,21 @@ document.addEventListener('keydown', event => {
bufferToggleUnimportant(bufferCurrent)
break
case 'a':
for (const name of names.slice(1))
if (buffers.get(name).newMessages) {
bufferActivate(name)
break
}
gotoActivity(false)
break
case '!':
for (const name of names.slice(1))
if (buffers.get(name).highlighted) {
bufferActivate(name)
break
}
case 'A':
gotoActivity(true)
break
case 'Tab':
case '/':
if (bufferLast !== undefined)
bufferActivate(bufferLast)
break
case 'PageUp':
case 'ArrowUp':
if (names.length > 1)
bufferActivate(names.at(-1))
break
case 'PageDown':
case 'ArrowDown':
if (names.length > 1)
bufferActivate(names.at(+1))
break
+1 -1
View File
@@ -160,7 +160,7 @@ func clientWriteError(ctx context.Context, ws *websocket.Conn, err error) bool {
},
},
}).AppendTo(nil)
if ok {
if !ok {
log.Println("Event serialization failed")
return false
}
+20 -10
View File
@@ -910,6 +910,10 @@ func (c *client) nicknameOrStar() string {
func (c *client) unregister(reason string) {
if !c.registered {
if c.nickname != "" {
delete(users, ircToCanon(c.nickname))
c.nickname = ""
}
return
}
@@ -1126,7 +1130,9 @@ func (c *client) sendLUSERS() {
nChannels := 0
for _, ch := range channels {
if 0 != ch.modes&ircChanModeSecret {
if 0 == ch.modes&ircChanModeSecret {
nChannels++
} else if _, present := ch.userModes[c]; present {
nChannels++
}
}
@@ -1440,7 +1446,7 @@ func ircHandleUSER(msg *message, c *client) {
c.realname = realname
c.mode = 0
if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
if m, err := strconv.ParseUint(mode, 10, 32); err == nil {
if 0 != m&4 {
c.mode |= ircUserModeRxWallops
}
@@ -1494,7 +1500,7 @@ func ircHandleLUSERS(msg *message, c *client) {
func ircHandleMOTD(msg *message, c *client) {
if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
return
}
c.sendMOTD()
@@ -1868,9 +1874,9 @@ func (mp *modeProcessor) step(modeChar byte) bool {
case 'b':
mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST)
case 'e':
mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
mp.doList(&mp.ch.exceptionList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
case 'I':
mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST)
mp.doList(&mp.ch.inviteList, RPL_INVITELIST, RPL_ENDOFINVITELIST)
case 'k':
mp.doKey()
@@ -2373,7 +2379,7 @@ func ircHandleTOPIC(msg *message, c *client) {
}
ch.topic = msg.params[1]
ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
ch.topicWho = fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
ch.topicTime = time.Now()
message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
@@ -2397,7 +2403,7 @@ func ircTryPart(c *client, channelName string, reason string) {
return
}
message := fmt.Sprintf(":%s@%s@%s PART %s :%s",
message := fmt.Sprintf(":%s!%s@%s PART %s :%s",
c.nickname, c.username, c.hostname, channelName, reason)
if 0 == ch.modes&ircChanModeQuiet {
ircChannelMulticast(ch, message, nil)
@@ -2454,12 +2460,15 @@ func ircTryKick(c *client, channelName, nick, reason string) {
return
}
message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s",
message := fmt.Sprintf(":%s!%s@%s KICK %s %s :%s",
c.nickname, c.username, c.hostname, channelName, nick, reason)
if 0 == ch.modes&ircChanModeQuiet {
ircChannelMulticast(ch, message, nil)
} else {
c.send(message)
if c != client {
client.send(message)
}
}
delete(ch.userModes, client)
@@ -2485,7 +2494,7 @@ func ircHandleKICK(msg *message, c *client) {
ircTryKick(c, targetChannels[0], targetUsers[i], reason)
}
} else {
for i := 0; i < len(channels) && i < len(targetUsers); i++ {
for i := 0; i < len(targetChannels) && i < len(targetUsers); i++ {
ircTryKick(c, targetChannels[i], targetUsers[i], reason)
}
}
@@ -2560,7 +2569,7 @@ func ircTryJoin(c *client, channelName, key string) {
}
_, invitedByChanop := c.invites[ircToCanon(channelName)]
if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) &&
if 0 != ch.modes&ircChanModeInviteOnly && !c.inMaskList(ch.inviteList) &&
!invitedByChanop {
c.sendReply(ERR_INVITEONLYCHAN, channelName)
return
@@ -3113,6 +3122,7 @@ func processOneEvent() {
capVersion: 301,
opened: time.Now(),
lastActive: time.Now(),
invites: map[string]bool{},
// TODO: Make this configurable and more fine-grained.
antiflood: newFloodDetector(10*time.Second, 20),
}
+38 -4
View File
@@ -48,7 +48,32 @@ foreach (icon xT xT-highlighted)
list (APPEND icon_rsrc_list "${icon_png}")
endforeach ()
if (APPLE)
function (android_mipmap_icon svg density size output_dir output)
set (_mipmap_dir "${output_dir}/res/mipmap-${density}")
set (_png "${_mipmap_dir}/ic_launcher.png")
set (${output} "${_png}" PARENT_SCOPE)
find_program (rsvg_convert_EXECUTABLE rsvg-convert REQUIRED)
add_custom_command (OUTPUT "${_png}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${_mipmap_dir}"
COMMAND ${rsvg_convert_EXECUTABLE} "--output=${_png}"
"--width=${size}" "--height=${size}" -- "${svg}"
DEPENDS "${svg}"
COMMENT "Generating Android ${density} application icon" VERBATIM)
endfunction ()
if (ANDROID)
set (android_base "${PROJECT_BINARY_DIR}/android")
set (android_icon_list)
foreach (density_size
ldpi:36 mdpi:48 hdpi:72 xhdpi:96 xxhdpi:144 xxxhdpi:192)
string (REPLACE ":" ";" density_size "${density_size}")
android_mipmap_icon ("${PROJECT_SOURCE_DIR}/xT.svg"
${density_size} "${android_base}" android_icon)
list (APPEND android_icon_list "${android_icon}")
endforeach ()
add_custom_target (android-icons ALL DEPENDS ${android_icon_list})
elseif (APPLE)
set (MACOSX_BUNDLE_ICON_FILE xT.icns)
icon_to_icns ("${PROJECT_SOURCE_DIR}/xT.svg"
"${MACOSX_BUNDLE_ICON_FILE}" icon_icns)
@@ -79,7 +104,7 @@ else ()
endif ()
# Build the main executable and link it
find_program (awk_EXECUTABLE awk ${find_program_REQUIRE})
find_program (awk_EXECUTABLE awk REQUIRED)
add_custom_command (OUTPUT xC-proto.cpp
COMMAND ${CMAKE_COMMAND} -E env LC_ALL=C ${awk_EXECUTABLE}
-f ${root}/liberty/tools/lxdrgen.awk
@@ -99,8 +124,17 @@ qt_add_executable (xT
add_dependencies (xT xC-proto)
qt_add_resources (xT "rsrc" PREFIX / FILES "${beep}" ${icon_rsrc_list})
target_link_libraries (xT PRIVATE Qt6::Widgets Qt6::Network Qt6::Multimedia)
set_target_properties (xT PROPERTIES WIN32_EXECUTABLE ON MACOSX_BUNDLE ON
MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.xT)
set_target_properties (xT PROPERTIES
WIN32_EXECUTABLE ON
MACOSX_BUNDLE ON
MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.xT
QT_ANDROID_APP_NAME xT
QT_ANDROID_APP_ICON "@mipmap/ic_launcher"
QT_ANDROID_PACKAGE_NAME name.janouch.xT
QT_ANDROID_PACKAGE_SOURCE_DIR "${android_base}")
if (ANDROID)
add_dependencies (xT android-icons)
endif ()
# https://stackoverflow.com/questions/79079161 and resolved in Qt Creator 16.
set (QT_QML_GENERATE_QMLLS_INI ON)
+35 -20
View File
@@ -19,6 +19,7 @@
#include "xC-proto.cpp"
#include "config.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include <map>
@@ -1120,9 +1121,12 @@ relay_process_message(const Relay::EventMessage &m)
{
auto &data = dynamic_cast<Relay::EventData_ServerRename &>(*m.data);
auto original = QString::fromStdWString(data.server_name);
g.servers.insert_or_assign(
QString::fromStdWString(data.new_), g.servers.at(original));
auto new_ = QString::fromStdWString(data.new_);
g.servers.insert_or_assign(new_, g.servers.at(original));
g.servers.erase(original);
for (auto &b : g.buffers)
if (b.server_name == original)
b.server_name = new_;
break;
}
case Relay::Event::SERVER_REMOVE:
@@ -1619,6 +1623,22 @@ rotated_buffers()
return rotated;
}
static void
goto_activity_bidirectional(bool backwards)
{
auto candidates = rotated_buffers();
if (!candidates.empty())
candidates.pop_back();
if (backwards)
std::reverse(candidates.begin(), candidates.end());
for (auto i : candidates)
if (auto &b = g.buffers[i]; b.highlighted)
return buffer_activate(b.buffer_name);
for (auto i : candidates)
if (auto &b = g.buffers[i]; b.new_messages)
return buffer_activate(b.buffer_name);
}
static void
bind_shortcuts()
{
@@ -1638,47 +1658,42 @@ bind_shortcuts()
if (auto b = buffer_by_name(g.buffer_last))
buffer_activate(b->buffer_name);
};
auto goto_highlight = [] {
for (auto i : rotated_buffers())
if (g.buffers[i].highlighted) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
};
auto goto_activity = [] {
for (auto i : rotated_buffers())
if (g.buffers[i].new_messages) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
};
auto toggle_unimportant = [] {
if (auto b = buffer_by_name(g.buffer_current))
buffer_toggle_unimportant(b->buffer_name);
};
auto goto_activity = [] {
goto_activity_bidirectional(false);
};
auto goto_backtivity = [] {
goto_activity_bidirectional(true);
};
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_Tab),
g.wMain, switch_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Tab),
g.wMain, switch_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Slash),
g.wMain, switch_buffer);
new QShortcut(QKeyCombination(Qt::NoModifier, Qt::Key_F5),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_PageUp),
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Up),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_PageUp),
g.wMain, previous_buffer);
new QShortcut(QKeyCombination(Qt::NoModifier, Qt::Key_F6),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_PageDown),
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Down),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::ControlModifier, Qt::Key_PageDown),
g.wMain, next_buffer);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_A),
g.wMain, goto_activity);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_Exclam),
g.wMain, goto_highlight);
new QShortcut(
QKeyCombination(Qt::AltModifier | Qt::ShiftModifier, Qt::Key_A),
g.wMain, goto_backtivity);
new QShortcut(QKeyCombination(Qt::AltModifier, Qt::Key_H),
g.wMain, toggle_unimportant);
}
+2 -2
View File
@@ -7,8 +7,8 @@
#define ID_PREVIOUS_BUFFER 11
#define ID_NEXT_BUFFER 12
#define ID_SWITCH_BUFFER 13
#define ID_GOTO_HIGHLIGHT 14
#define ID_GOTO_ACTIVITY 15
#define ID_GOTO_ACTIVITY 14
#define ID_GOTO_BACKTIVITY 15
#define ID_TOGGLE_UNIMPORTANT 16
#define ID_DISPLAY_FULL_LOG 17
+23 -13
View File
@@ -741,7 +741,7 @@ refresh_buffer(const Buffer &b)
// PFM_BORDER is not implemented, at most we can try to construct
// an OLE object the width of the screen and see how it clips
// (this is a lot of code).
size_t i = 0, mark_before = b.lines.size() -
int64_t i = 0, mark_before = int64_t(b.lines.size()) -
b.new_messages - b.new_unimportant_messages;
for (auto line = b.lines.begin(); line != b.lines.end(); ++line) {
if (i == mark_before)
@@ -1057,6 +1057,9 @@ relay_process_message(const Relay::EventMessage &m)
auto &data = dynamic_cast<Relay::EventData_ServerRename &>(*m.data);
g.servers.insert_or_assign(data.new_, g.servers.at(data.server_name));
g.servers.erase(data.server_name);
for (auto &b : g.buffers)
if (b.server_name == data.server_name)
b.server_name = data.new_;
break;
}
case Relay::Event::SERVER_REMOVE:
@@ -1612,6 +1615,21 @@ process_bufferlist_notification(WORD code)
}
}
static void
goto_activity(std::vector<size_t> candidates, bool backwards)
{
if (!candidates.empty())
candidates.pop_back();
if (backwards)
std::reverse(candidates.begin(), candidates.end());
for (auto i : candidates)
if (auto &b = g.buffers[i]; b.highlighted)
return buffer_activate(b.buffer_name);
for (auto i : candidates)
if (auto &b = g.buffers[i]; b.new_messages)
return buffer_activate(b.buffer_name);
}
static void
process_accelerator(WORD id)
{
@@ -1642,19 +1660,11 @@ process_accelerator(WORD id)
if (!g.buffer_last.empty())
buffer_activate(g.buffer_last);
return;
case ID_GOTO_HIGHLIGHT:
for (auto i : rotated)
if (g.buffers[i].highlighted) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
return;
case ID_GOTO_ACTIVITY:
for (auto i : rotated)
if (g.buffers[i].new_messages) {
buffer_activate(g.buffers[i].buffer_name);
break;
}
goto_activity(rotated, false);
return;
case ID_GOTO_BACKTIVITY:
goto_activity(rotated, true);
return;
case ID_TOGGLE_UNIMPORTANT:
if (b)
+1 -1
View File
@@ -25,8 +25,8 @@ BEGIN
VK_TAB, ID_SWITCH_BUFFER, CONTROL, VIRTKEY
// These are proper, but llvm-rc won't accept them (GitHub #64002).
#ifndef __clang__
"!", ID_GOTO_HIGHLIGHT, ALT
"a", ID_GOTO_ACTIVITY, ALT
"A", ID_GOTO_BACKTIVITY, ALT
"H", ID_TOGGLE_UNIMPORTANT, ALT
"h", ID_DISPLAY_FULL_LOG, ALT
#endif