Compare commits

...

13 Commits

Author SHA1 Message Date
Dima Krasner b2fac23783 fix failure when first chunk arrives last 2023-11-09 08:20:12 +01:00
Dima Krasner b84f4ba5dd coding style cleanup 2023-11-09 08:20:12 +01:00
Dima Krasner e18f9e4ecb coding style cleanup 2023-11-09 08:20:12 +01:00
Dima Krasner 64fd9d7484 fix build with cmake 2023-11-09 08:20:12 +01:00
Dima Krasner 1a45619461 add ugly guppy:// v0.4 support 2023-11-09 08:20:12 +01:00
Felix Yan 40448d458c Correct a typo in BUILDING.md 2023-11-07 17:57:05 +01:00
Xavier Del Campo Romero 6d97b7f198 Replace QInputDialog with custom dialog for queries
Recent commits allowed multi-line input while reusing the QInputDialog
object already defined by Kristall. However, QInputDialog lacks a way to
access its QPlainTextEdit directly, and therefore set the wrap mode.

Since QInputDialog does no wrapping, it is inconvenient for writing a
long text (think of social media sites such as BBS or Station).
Therefore, a custom QDialog-derived class, namely QueryDialog, has been
provided.
2023-10-13 13:14:00 +02:00
Xavier Del Campo Romero 8cb79ee671 build.yml: Test CMake builds 2023-10-02 16:36:29 +02:00
Xavier Del Campo Romero 42022bdfd2 Add CMake-based build system 2023-10-02 16:36:29 +02:00
Xavier Del Campo Romero 317bdbad5b settingsdialog.ui: Move "Generic" widgets to QScrollArea
This would allow smaller screens (think of mobile devices such as the
PinePhone or Librem 5) to be able to fit the settings dialog, and
therefore use it.
2023-09-29 14:23:09 +02:00
Xavier Del Campo Romero 505723e9df browsertab.cpp: Use multi-line input when required
Some Gemini sites such as Station or BBS allow multi-line posts.
2023-09-29 14:22:22 +02:00
Michael Steenbeek 3709c3d6bd Use startsWith() instead of left(4) 2023-08-30 08:43:16 +02:00
Michael Steenbeek 10a9c4ccfa Support URLs in Gophermaps
Paths in Gophermaps that start with “URL:” should be interpreted as direct
URLs, rather than references to files or directories on the Gopher server
itself.

An excerpt from the standard document:
```

Links to URLs from a gopher directory shall be defined as follows:

 Type -- the appropriate character corresponding to the type of the
 document on the remote end; h if HTML.

 Path -- the full URL, preceeded by "URL:".  For instance:
         URL:http://www.complete.org/

 Host, Port -- pointing back to the gopher server that provided
 the directory for compatibility reasons.

 Name -- as usual for a Gopher directory entry.

```
Source: gopher://quux.org/0/Archives/Mailing Lists/gopher/gopher.2002-02?/MBOX-MESSAGE/34

An example of this in the wild can be seen at gopher://gopher.floodgap.com ,
at the bottom of the page.

Note that above link carries a fallback for clients that do not support it,
as described by the Bucktooth server software:
```
[...]  most people will want to add web links to their
gophers anyway. In 0.1-pr4 and up, this is supported in a protocol independent
fashion; simply specify any URL and an 'h' item type, like so:

hYour Web Link<TAB>URL:http://www.floodgap.com/

Note that the URL must be preceded by a literal "URL:" and that the itemtype
is h. Smart clients will automatically take the URL portion and use it, but
even if they do not, Bucktooth will generate an HTML page with a Refresh:
header and forward them on automatically.
```

Other clients supporting this standard include the OverbiteWX extension.
(Most likely, there will be others, but I haven’t tested them all.)
2023-08-30 08:43:16 +02:00
16 changed files with 990 additions and 306 deletions

View File

@ -33,7 +33,27 @@ jobs:
- name: make
run: make build/kristall
build_cmake:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: jurplel/install-qt-action@v2
with:
version: "5.12.8"
- name: Install OpenSSL
run: sudo apt install -y libssl-dev
- name: Configure the project
uses: threeal/cmake-action@v1.3.0
with:
build-dir: build
- name: Build the project
run: cmake --build build
# Disabled until both aqinstall and install-qt-action support the fixes…
# build_windows:
# runs-on: windows-latest

View File

@ -113,7 +113,7 @@ Install Qt via the Qt installer. Install the following components:
If you didn't istall Qt to `C:\Qt`, you have to adjust the paths in `src/kristall.pro` for the `win32-g++` adjustments to the path you used.
Then open `src/kristall.pro` with Qt creator to compile the project. Alternativly you can use `ci\build-and-deploy.bat`, but note that this script tries to deploy the file to `random-projects.net`, so this will fail in the end.
Then open `src/kristall.pro` with Qt creator to compile the project. Alternatively you can use `ci\build-and-deploy.bat`, but note that this script tries to deploy the file to `random-projects.net`, so this will fail in the end.
**Troubleshouting:**
If you get an error message that `MSVCR100.dll` is missing, you may need to install the [Microsoft C++ Runtime](https://www.microsoft.com/en-us/download/details.aspx?id=14632).

189
CMakeLists.txt Normal file
View File

@ -0,0 +1,189 @@
cmake_minimum_required(VERSION 3.13)
if(NOT KRISTALL_VERSION)
execute_process(COMMAND git describe --tags --abbrev=0
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE KRISTALL_CMAKE_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT KRISTALL_CMAKE_VERSION)
set(KRISTALL_CMAKE_VERSION "0.0.0")
endif()
execute_process(COMMAND git describe --tags
WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
OUTPUT_VARIABLE KRISTALL_VERSION
OUTPUT_STRIP_TRAILING_WHITESPACE
)
if(NOT KRISTALL_VERSION)
set(KRISTALL_VERSION "development")
endif()
endif()
# CMake requires the following format for versions:
# <major>[.<minor>[.<patch>[.<tweak>]]]]
string(REPLACE "V" "" KRISTALL_CMAKE_VERSION ${KRISTALL_CMAKE_VERSION})
project(kristall VERSION ${KRISTALL_CMAKE_VERSION} LANGUAGES CXX)
set(QT_VERSION_MAJOR 5)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
# QXmpp requires C++17.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS
Multimedia MultimediaWidgets Network Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS
Multimedia MultimediaWidgets Network Widgets REQUIRED)
set(PROJECT_SOURCES
src/browsertab.cpp
src/certificatehelper.cpp
src/cryptoidentity.cpp
src/dialogs/certificateiodialog.cpp
src/dialogs/certificatemanagementdialog.cpp
src/dialogs/certificateselectiondialog.cpp
src/dialogs/newidentitiydialog.cpp
src/dialogs/settingsdialog.cpp
src/documentoutlinemodel.cpp
src/documentstyle.cpp
src/favouritecollection.cpp
src/identitycollection.cpp
src/ioutil.cpp
src/localization.cpp
src/main.cpp
src/mainwindow.cpp
src/renderers/htmlrenderer.cpp
src/renderers/markdownrenderer.cpp
src/renderers/renderhelpers.cpp
src/renderers/textstyleinstance.cpp
src/widgets/browsertabbar.cpp
src/widgets/browsertabwidget.cpp
src/widgets/kristalltextbrowser.cpp
src/widgets/mediaplayer.cpp
src/mimeparser.cpp
src/protocolhandler.cpp
src/protocols/abouthandler.cpp
src/protocols/filehandler.cpp
src/protocols/fingerclient.cpp
src/protocols/geminiclient.cpp
src/protocols/gopherclient.cpp
src/protocols/guppyclient.cpp
src/protocols/webclient.cpp
src/protocolsetup.cpp
src/renderers/geminirenderer.cpp
src/renderers/gophermaprenderer.cpp
src/renderers/plaintextrenderer.cpp
src/ssltrust.cpp
src/tabbrowsinghistory.cpp
src/trustedhost.cpp
src/trustedhostcollection.cpp
src/widgets/elidelabel.cpp
src/widgets/searchbar.cpp
src/widgets/ssltrusteditor.cpp
src/widgets/favouritepopup.cpp
src/widgets/favouritebutton.cpp
src/widgets/querydialog.ui
src/widgets/querydialog.cpp
src/cachehandler.cpp
src/widgets/searchbox.cpp
src/browsertab.ui
src/dialogs/certificateiodialog.ui
src/dialogs/certificatemanagementdialog.ui
src/dialogs/certificateselectiondialog.ui
src/dialogs/newidentitiydialog.ui
src/dialogs/settingsdialog.ui
src/mainwindow.ui
src/widgets/mediaplayer.ui
src/widgets/ssltrusteditor.ui
src/fonts.qrc
lib/BreezeStyleSheets/breeze.qrc
src/builtins.qrc
src/icons.qrc
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(${PROJECT_NAME}
MANUAL_FINALIZATION
${PROJECT_SOURCES}
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
add_library(${PROJECT_NAME} SHARED
${PROJECT_SOURCES}
)
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
add_executable(${PROJECT_NAME}
${PROJECT_SOURCES}
)
endif()
endif()
message(STATUS "Building Kristall ${KRISTALL_VERSION}")
target_compile_definitions(${PROJECT_NAME} PUBLIC
KRISTALL_VERSION=${KRISTALL_VERSION})
add_subdirectory(lib/luis-l-gist)
target_link_libraries(${PROJECT_NAME} PRIVATE luis-l-gist)
find_package(PkgConfig REQUIRED)
pkg_check_modules(cmark IMPORTED_TARGET cmark)
if(cmark_FOUND)
target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::cmark)
else()
message(STATUS "Using in-tree cmark")
add_subdirectory(lib/cmark)
target_link_libraries(${PROJECT_NAME} PRIVATE cmark)
endif()
pkg_check_modules(gumbo-parser IMPORTED_TARGET gumbo)
if(gumbo_FOUND)
target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::gumbo-parser)
else()
message(STATUS "Using in-tree gumbo-parser")
add_subdirectory(lib/gumbo-parser)
target_link_libraries(${PROJECT_NAME} PRIVATE gumbo-parser)
endif()
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt${QT_VERSION_MAJOR}::Multimedia
Qt${QT_VERSION_MAJOR}::MultimediaWidgets
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Widgets)
target_include_directories(${PROJECT_NAME} PRIVATE src)
set_target_properties(${PROJECT_NAME} PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)
if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(${PROJECT_NAME})
endif()
find_package(OpenSSL 1.1 REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenSSL::SSL)
if(NOT UNIX)
target_link_libraries(${PROJECT_NAME} PRIVATE iconv)
endif()

24
lib/cmark/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.13)
project(cmark C)
set(sources
src/blocks.c
src/buffer.c
src/cmark.c
src/cmark_ctype.c
src/commonmark.c
src/houdini_href_e.c
src/houdini_html_e.c
src/houdini_html_u.c
src/inlines.c
src/iterator.c
src/node.c
src/references.c
src/render.c
src/scanners.c
src/utf8.c
src/html.c
src/xml.c)
add_library(${PROJECT_NAME} ${sources})
target_include_directories(${PROJECT_NAME} PUBLIC src)

17
lib/gumbo-parser/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.13)
project(gumbo-parser C)
set(sources
attribute.c
char_ref.c
error.c
gumbo-utf8.c
parser.c
string_buffer.c
string_piece.c
tag.c
tokenizer.c
util.c
vector.c
)
add_library(${PROJECT_NAME} ${sources})
target_include_directories(${PROJECT_NAME} PUBLIC include PRIVATE .)

7
lib/luis-l-gist/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.13)
project(luis-l-gist CXX)
add_library(${PROJECT_NAME} interactiveview.cpp)
target_include_directories(${PROJECT_NAME} PUBLIC .)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)
target_link_libraries(${PROJECT_NAME} PUBLIC Qt${QT_VERSION_MAJOR}::Widgets)

View File

@ -17,6 +17,7 @@
#include "protocols/geminiclient.hpp"
#include "protocols/webclient.hpp"
#include "protocols/gopherclient.hpp"
#include "protocols/guppyclient.hpp"
#include "protocols/fingerclient.hpp"
#include "protocols/abouthandler.hpp"
#include "protocols/filehandler.hpp"
@ -25,6 +26,7 @@
#include "kristall.hpp"
#include "widgets/favouritepopup.hpp"
#include "widgets/searchbox.hpp"
#include "widgets/querydialog.hpp"
#include <cassert>
#include <QTabWidget>
@ -76,6 +78,7 @@ BrowserTab::BrowserTab(MainWindow *mainWindow) : QWidget(nullptr),
addProtocolHandler<GeminiClient>();
addProtocolHandler<FingerClient>();
addProtocolHandler<GopherClient>();
addProtocolHandler<GuppyClient>();
addProtocolHandler<WebClient>();
addProtocolHandler<AboutHandler>();
addProtocolHandler<FileHandler>();
@ -867,9 +870,8 @@ void BrowserTab::on_inputRequired(const QString &query, const bool is_sensitive)
{
this->network_timeout_timer.stop();
QInputDialog dialog{this};
QueryDialog dialog(this);
dialog.setInputMode(QInputDialog::TextInput);
dialog.setLabelText(query);
if (is_sensitive) dialog.setTextEchoMode(QLineEdit::Password);

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>1100</width>
<height>737</height>
<width>496</width>
<height>404</height>
</rect>
</property>
<property name="windowTitle">
@ -32,281 +32,313 @@
<string>Generic</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Start Page:</string>
<item row="0" column="0" colspan="2">
<widget class="QScrollArea" name="scrollArea">
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOn</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="start_page">
<property name="placeholderText">
<string>about://blank</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_40">
<property name="text">
<string>Search engine:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="search_engine">
<property name="editable">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>585</width>
<height>490</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_44">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="selected_language"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Start Page:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="start_page">
<property name="placeholderText">
<string>about://blank</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_40">
<property name="text">
<string>Search engine:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="search_engine">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_29">
<property name="text">
<string>Additional toolbar buttons</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_99">
<item>
<widget class="QCheckBox" name="enable_home_btn">
<property name="text">
<string>Home</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_newtab_btn">
<property name="text">
<string>New tab</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_root_btn">
<property name="text">
<string>Root (/)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_parent_btn">
<property name="text">
<string>Parent (..)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Startup Behaviour</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="session_restore_behaviour"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>UI Theme</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="ui_theme"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>Icon Theme</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="icon_theme"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_1">
<property name="text">
<string>UI Density</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="ui_density"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Enabled Protocols</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="enable_gemini">
<property name="text">
<string>Gemini</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_gopher">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Gopher</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_guppy">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Guppy</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_finger">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Finger</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_http">
<property name="text">
<string>HTTP</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_https">
<property name="text">
<string>HTTPS</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Unknown Scheme</string>
</property>
</widget>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QRadioButton" name="scheme_os_default">
<property name="text">
<string>Use OS default handler</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="scheme_error">
<property name="text">
<string>Display error message</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Max. Number of Redirections</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QSpinBox" name="max_redirects">
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Redirection Handling</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QComboBox" name="redirection_mode"/>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_28">
<property name="text">
<string>Network Timeout</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QSpinBox" name="network_timeout">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>90000</number>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="label_45">
<property name="text">
<string>Tab close behaviour</string>
</property>
</widget>
</item>
<item row="13" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QRadioButton" name="tab_keep_window">
<property name="text">
<string>Keep window open</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_10</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="tab_close_window">
<property name="text">
<string>Close window</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_10</string>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_29">
<property name="text">
<string>Additional toolbar buttons</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_99">
<item>
<widget class="QCheckBox" name="enable_home_btn">
<property name="text">
<string>Home</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_newtab_btn">
<property name="text">
<string>New tab</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_root_btn">
<property name="text">
<string>Root (/)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_parent_btn">
<property name="text">
<string>Parent (..)</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_19">
<property name="text">
<string>Startup Behaviour</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="session_restore_behaviour"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>UI Theme</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="ui_theme"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_34">
<property name="text">
<string>Icon Theme</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QComboBox" name="icon_theme"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_1">
<property name="text">
<string>UI Density</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="ui_density"/>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Enabled Protocols</string>
</property>
</widget>
</item>
<item row="8" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="enable_gemini">
<property name="text">
<string>Gemini</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_gopher">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Gopher</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_finger">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Finger</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_http">
<property name="text">
<string>HTTP</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="enable_https">
<property name="text">
<string>HTTPS</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Unknown Scheme</string>
</property>
</widget>
</item>
<item row="9" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QRadioButton" name="scheme_os_default">
<property name="text">
<string>Use OS default handler</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="scheme_error">
<property name="text">
<string>Display error message</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Max. Number of Redirections</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QSpinBox" name="max_redirects">
<property name="value">
<number>5</number>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Redirection Handling</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QComboBox" name="redirection_mode"/>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_28">
<property name="text">
<string>Network Timeout</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QSpinBox" name="network_timeout">
<property name="suffix">
<string> ms</string>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="maximum">
<number>90000</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_44">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="selected_language"/>
</item>
<item row="13" column="0">
<widget class="QLabel" name="label_45">
<property name="text">
<string>Tab close behaviour</string>
</property>
</widget>
</item>
<item row="13" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_12">
<item>
<widget class="QRadioButton" name="tab_keep_window">
<property name="text">
<string>Keep window open</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_10</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="tab_close_window">
<property name="text">
<string>Close window</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_10</string>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="display_tab">
@ -705,9 +737,9 @@
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>530</width>
<height>712</height>
<y>-351</y>
<width>570</width>
<height>783</height>
</rect>
</property>
<layout class="QFormLayout" name="style_scroll_layout">
@ -1619,19 +1651,14 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>ui_theme</tabstop>
<tabstop>icon_theme</tabstop>
<tabstop>ui_density</tabstop>
<tabstop>enable_gemini</tabstop>
<tabstop>enable_gopher</tabstop>
<tabstop>enable_guppy</tabstop>
<tabstop>enable_finger</tabstop>
<tabstop>enable_http</tabstop>
<tabstop>enable_https</tabstop>
<tabstop>scheme_os_default</tabstop>
<tabstop>scheme_error</tabstop>
<tabstop>max_redirects</tabstop>
<tabstop>redirection_mode</tabstop>
<tabstop>network_timeout</tabstop>
<tabstop>enable_home_btn</tabstop>
<tabstop>enable_newtab_btn</tabstop>
<tabstop>enable_root_btn</tabstop>
@ -1714,15 +1741,15 @@
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup_2"/>
<buttongroup name="buttonGroup_7"/>
<buttongroup name="buttonGroup_5"/>
<buttongroup name="buttonGroup"/>
<buttongroup name="buttonGroup_6"/>
<buttongroup name="buttonGroup_8"/>
<buttongroup name="buttonGroup_3"/>
<buttongroup name="buttonGroup_7"/>
<buttongroup name="buttonGroup_4"/>
<buttongroup name="buttonGroup_9"/>
<buttongroup name="buttonGroup_10"/>
<buttongroup name="buttonGroup_8"/>
<buttongroup name="buttonGroup_2"/>
<buttongroup name="buttonGroup_6"/>
<buttongroup name="buttonGroup_9"/>
</buttongroups>
</ui>

View File

@ -143,6 +143,7 @@ SOURCES += \
protocols/fingerclient.cpp \
protocols/geminiclient.cpp \
protocols/gopherclient.cpp \
protocols/guppyclient.cpp \
protocols/webclient.cpp \
protocolsetup.cpp \
renderers/geminirenderer.cpp \
@ -158,7 +159,8 @@ SOURCES += \
widgets/favouritepopup.cpp \
widgets/favouritebutton.cpp \
cachehandler.cpp \
widgets/searchbox.cpp
widgets/searchbox.cpp \
widgets/querydialog.cpp
HEADERS += \
../lib/luis-l-gist/interactiveview.hpp \
@ -192,6 +194,7 @@ HEADERS += \
protocols/fingerclient.hpp \
protocols/geminiclient.hpp \
protocols/gopherclient.hpp \
protocols/guppyclient.hpp \
protocols/webclient.hpp \
protocolsetup.hpp \
renderers/geminirenderer.hpp \
@ -206,6 +209,7 @@ HEADERS += \
widgets/ssltrusteditor.hpp \
widgets/favouritepopup.hpp \
widgets/favouritebutton.hpp \
widgets/querydialog.hpp \
cachehandler.hpp \
widgets/searchbox.hpp
@ -218,7 +222,8 @@ FORMS += \
dialogs/settingsdialog.ui \
mainwindow.ui \
widgets/mediaplayer.ui \
widgets/ssltrusteditor.ui
widgets/ssltrusteditor.ui \
widgets/querydialog.ui
CONFIG += lrelease embed_translations

View File

@ -0,0 +1,207 @@
#include "guppyclient.hpp"
#include "ioutil.hpp"
#include "kristall.hpp"
GuppyClient::GuppyClient(QObject *parent) : ProtocolHandler(parent)
{
connect(&socket, &QUdpSocket::connected, this, &GuppyClient::on_connected);
connect(&socket, &QUdpSocket::readyRead, this, &GuppyClient::on_readRead);
connect(&timer, &QTimer::timeout, this, &GuppyClient::on_timerTick);
connect(&socket, &QAbstractSocket::hostFound, this, [this]() {
emit this->requestStateChange(RequestState::HostFound);
});
emit this->requestStateChange(RequestState::None);
}
GuppyClient::~GuppyClient()
{
}
bool GuppyClient::supportsScheme(const QString &scheme) const
{
return (scheme == "guppy");
}
bool GuppyClient::startRequest(const QUrl &url, RequestOptions options)
{
Q_UNUSED(options)
if(this->isInProgress())
return false;
if(url.scheme() != "guppy")
return false;
emit this->requestStateChange(RequestState::Started);
this->requested_url = url;
this->was_cancelled = false;
this->prev_seq = 0;
this->first_seq = 0;
this->last_seq = 0;
this->socket.connectToHost(url.host(), url.port(6775));
return true;
}
bool GuppyClient::isInProgress() const
{
return this->socket.isOpen();
}
bool GuppyClient::cancelRequest()
{
was_cancelled = true;
this->socket.close();
this->timer.stop();
this->body.clear();
this->chunks.clear();
return true;
}
void GuppyClient::on_connected()
{
request = (this->requested_url.toString(QUrl::FormattingOptions(QUrl::FullyEncoded)) + "\r\n").toUtf8();
if(this->socket.write(request.constData(), request.size()) <= 0)
{
this->socket.close();
return;
}
this->timer.start(2000);
emit this->requestStateChange(RequestState::Connected);
}
void GuppyClient::on_readRead()
{
QByteArray chunk = this->socket.read(65535);
if(int crlf = chunk.indexOf("\r\n"); crlf > 0) {
QByteArray header = chunk.left(crlf);
// first response packet (success, error or redirect) header should contain a space
int seq = -1;
if(int space = header.indexOf(' '); space > 0) {
QByteArray meta = header.mid(space + 1);
seq = chunk.left(space).toInt();
if(seq < 6 || seq > 2147483647) {
this->timer.stop();
this->body.clear();
this->chunks.clear();
emit this->requestStateChange(RequestState::None);
if(seq == 4) {
emit networkError(UnknownError, meta); // error
}
else if(seq == 3) { // redirect
QUrl new_url(meta);
if(new_url.isRelative()) new_url = this->requested_url.resolved(new_url);
assert(not new_url.isRelative());
this->socket.close();
this->timer.stop();
this->body.clear();
this->chunks.clear();
emit this->requestStateChange(RequestState::None);
emit redirected(new_url, false);
}
else if(seq == 1) { // input prompt
this->socket.close();
this->timer.stop();
this->body.clear();
this->chunks.clear();
emit this->requestStateChange(RequestState::None);
emit inputRequired(meta, false);
}
else {
emit networkError(ProtocolViolation, QObject::tr("invalid seq"));
}
return;
}
this->first_seq = seq; // success
this->mime = meta;
}
else {
seq = header.toInt();
}
if(seq < this->first_seq) {
return;
}
if(chunk.size() == crlf + 2) { // eof
last_seq = seq;
}
else if(seq >= first_seq) { // success or continuation
if(!this->prev_seq || seq >= this->prev_seq) {
this->chunks[seq] = chunk.mid(crlf + 2, chunk.size() - crlf - 2);
}
// postpone the timer when we receive the next packet
if(seq == this->prev_seq + 1) {
this->timer.start();
}
}
// acknowledge every valid packet we receive
QByteArray ack = QString("%1\r\n").arg(seq).toUtf8();
socket.write(ack.constData(), ack.size());
}
else {
emitNetworkError(this->socket.error(), this->socket.errorString());
return;
}
// append all consequent chunks we have
int next_seq = this->prev_seq ? this->prev_seq + 1 : this->first_seq;
while(next_seq != last_seq) {
QByteArray next = this->chunks.take(next_seq);
if(next.isNull()) {
break;
}
body.append(next.constData(), next.size());
this->prev_seq = next_seq++;
}
if(not this->was_cancelled) {
emit this->requestProgress(body.size());
}
// we're done when the last appended chunk is the one before the eof chunk
if(this->last_seq && next_seq == this->last_seq) {
if(not this->was_cancelled) {
emit this->requestComplete(this->body, this->mime);
this->was_cancelled = true;
}
this->socket.close();
this->timer.stop();
this->body.clear();
this->chunks.clear();
emit this->requestStateChange(RequestState::None);
}
}
void GuppyClient::on_timerTick()
{
QByteArray pkt;
if(this->prev_seq) { // resend the last ack
pkt = QString("%1\r\n").arg(this->prev_seq).toUtf8();
}
else if(this->chunks.empty()) { // resend the request
pkt = request;
}
else {
return;
}
this->socket.write(pkt.constData(), pkt.size());
}

View File

@ -0,0 +1,47 @@
#ifndef GUPPYCLIENT_HPP
#define GUPPYCLIENT_HPP
#include <QObject>
#include <QUdpSocket>
#include <QUrl>
#include <QTimer>
#include "protocolhandler.hpp"
class GuppyClient : public ProtocolHandler
{
Q_OBJECT
public:
explicit GuppyClient(QObject *parent = nullptr);
~GuppyClient() override;
bool supportsScheme(QString const & scheme) const override;
bool startRequest(QUrl const & url, RequestOptions options) override;
bool isInProgress() const override;
bool cancelRequest() override;
private: // slots
void on_connected();
void on_readRead();
void on_finished();
void on_timerTick();
void on_socketError(QAbstractSocket::SocketError errorCode);
private:
QUdpSocket socket;
QHash<long, QByteArray> chunks;
QByteArray body;
QUrl requested_url;
QByteArray request;
bool was_cancelled;
int prev_seq, first_seq, last_seq;
QString mime;
QTimer timer;
};
#endif // GUPPYCLIENT_HPP

View File

@ -7,6 +7,7 @@
MAC(http) \
MAC(https) \
MAC(gopher) \
MAC(guppy) \
MAC(gemini) \
MAC(finger)

View File

@ -156,21 +156,32 @@ std::unique_ptr<QTextDocument> GophermapRenderer::render(const QByteArray &input
else
{
QString dst_url;
switch (items.size())
// If a resources path starts with “URL:”, it is a direct link (to HTTP or another protocol), rather than a file or directory on this server.
if (items.size() >= 2 && items.at(1).startsWith("URL:"))
{
case 0:
assert(false);
case 1:
assert(false);
case 2:
dst_url = root_url.resolved(QUrl(items.at(1))).toString();
break;
case 3:
dst_url = scheme + "://" + items.at(2) + "/" + QString(type) + items.at(1);
break;
default:
dst_url = scheme + "://" + items.at(2) + ":" + items.at(3) + "/" + QString(type) + items.at(1);
break;
auto item1 = QString(items.at(1));
item1.remove(0, 4);
dst_url = item1;
}
else
{
switch (items.size())
{
case 0:
assert(false);
case 1:
assert(false);
case 2:
dst_url = root_url.resolved(QUrl(items.at(1))).toString();
break;
case 3:
dst_url = scheme + "://" + items.at(2) + "/" + QString(type) + items.at(1);
break;
default:
dst_url = scheme + "://" + items.at(2) + ":" + items.at(3) + "/" + QString(type) + items.at(1);
break;
}
}
if (not QUrl(dst_url).isValid())

View File

@ -0,0 +1,30 @@
#include "widgets/querydialog.hpp"
QueryDialog::QueryDialog(QWidget *parent) :
QDialog(parent),
mode(QLineEdit::Normal)
{
ui.setupUi(this);
ui.lineEdit->setVisible(false);
}
void QueryDialog::setLabelText(const QString &text)
{
ui.query->setText(text);
}
void QueryDialog::setTextEchoMode(QLineEdit::EchoMode mode)
{
ui.text->setVisible(mode == QLineEdit::Normal);
ui.lineEdit->setVisible(mode != QLineEdit::Normal);
ui.lineEdit->setEchoMode(mode);
this->mode = mode;
}
QString QueryDialog::textValue()
{
if (mode == QLineEdit::Normal)
return ui.text->toPlainText();
return ui.lineEdit->text();
}

View File

@ -0,0 +1,20 @@
#ifndef QUERYDIALOG_H
#define QUERYDIALOG_H
#include "ui_querydialog.h"
#include <QDialog>
#include <QLineEdit>
#include <QString>
class QueryDialog : public QDialog {
public:
QueryDialog(QWidget *parent);
void setLabelText(const QString &text);
void setTextEchoMode(QLineEdit::EchoMode mode);
QString textValue();
private:
Ui_QueryDialog ui;
QLineEdit::EchoMode mode;
};
#endif

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QueryDialog</class>
<widget class="QDialog" name="QueryDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>240</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="query">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="text"/>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QueryDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QueryDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>