7 Commits

Author SHA1 Message Date
1a5e32a580 Restore TDLib workflow assets
All checks were successful
Release App / release-app (push) Successful in 1m42s
2026-04-24 15:06:21 +03:00
2e79f2b00b Fix update notice for versioned releases
Some checks failed
Release App / release-app (push) Has been cancelled
2026-04-24 15:06:07 +03:00
891fc73e25 Use versioned app releases for binary package
All checks were successful
Release App / release-app (push) Successful in 1m31s
2026-04-24 15:00:42 +03:00
5a5677a994 Add update notice and require release build secrets
All checks were successful
Release App / release-app (push) Successful in 58s
2026-04-24 14:55:36 +03:00
94fc240086 Show build version in app header
All checks were successful
Release App / release-app (push) Successful in 1m12s
2026-04-24 14:47:01 +03:00
c39071b61a Use versioned TDLib releases from shinoa-tdlib
All checks were successful
Release App / release-app (push) Successful in 1m1s
2026-04-24 14:42:41 +03:00
7b35201799 Fetch nlohmann_json for prebuilt TDLib builds
All checks were successful
Release App / release-app (push) Successful in 56s
2026-04-24 14:33:11 +03:00
10 changed files with 246 additions and 30 deletions

View File

@@ -13,10 +13,10 @@ jobs:
release-app:
runs-on: ubuntu-latest
env:
RELEASE_TAG: latest
ARCHIVE_NAME: shinoa-linux-x86_64.tar.gz
CHECKSUM_NAME: shinoa-linux-x86_64.tar.gz.sha256
TDLIB_ARCHIVE_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/releases/download/tdlib/tdlib-linux-x86_64.tar.gz
TDLIB_RELEASE_TAG: v1.8.63
TDLIB_ARCHIVE_URL: https://git.mshq.dev/AxiFisk/shinoa-tdlib/releases/download/${{ env.TDLIB_RELEASE_TAG }}/tdlib-linux-x86_64.tar.gz
steps:
- name: Check out repository
uses: https://github.com/actions/checkout@v4
@@ -45,6 +45,20 @@ jobs:
pkg-config \
zlib1g-dev
- name: Resolve app version
id: app_version
run: |
set -euo pipefail
version="$(sed -n 's/^project(shinoa VERSION \([^ ]*\) LANGUAGES CXX)$/\1/p' CMakeLists.txt)"
if [ -z "$version" ]; then
echo "Failed to resolve app version from CMakeLists.txt" >&2
exit 1
fi
{
echo "version=$version"
echo "release_tag=v$version"
} >> "$GITHUB_OUTPUT"
- name: Download prebuilt TDLib
run: |
set -euo pipefail
@@ -54,12 +68,19 @@ jobs:
test -f tdlib/lib/libtdjson.so
- name: Build release bundle
env:
TELEGRAM_TUI_BUILD_API_ID: ${{ secrets.TELEGRAM_API_ID }}
TELEGRAM_TUI_BUILD_API_HASH: ${{ secrets.TELEGRAM_API_HASH }}
run: |
set -euo pipefail
rm -rf build dist
test -n "${TELEGRAM_TUI_BUILD_API_ID}"
test -n "${TELEGRAM_TUI_BUILD_API_HASH}"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DTELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS=ON \
-DTELEGRAM_TUI_TDLIB_ROOT="$PWD/tdlib"
cmake --build build -j"$(nproc)"
@@ -80,18 +101,20 @@ jobs:
set -euo pipefail
api="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
commit_sha="${{ github.sha }}"
release_tag="${{ steps.app_version.outputs.release_tag }}"
version="${{ steps.app_version.outputs.version }}"
release_json="$(mktemp)"
body_file="$(mktemp)"
cat > "$body_file" <<EOF
Automated rolling release for commit \`${commit_sha}\`.
Release ${version} for commit \`${commit_sha}\`.
EOF
status="$(curl --silent --show-error \
--output "$release_json" \
--write-out "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
"$api/releases/tags/$RELEASE_TAG")"
"$api/releases/tags/$release_tag")"
if [ "$status" = "200" ]; then
release_id="$(jq -r '.id' "$release_json")"
@@ -105,7 +128,7 @@ jobs:
curl --fail-with-body \
-H "Authorization: token ${GITEA_TOKEN}" \
-X DELETE \
"$api/releases/tags/$RELEASE_TAG"
"$api/releases/tags/$release_tag"
elif [ "$status" != "404" ]; then
echo "Failed to query release, HTTP $status" >&2
cat "$release_json" >&2
@@ -117,11 +140,11 @@ jobs:
--write-out "%{http_code}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-X DELETE \
"$api/tags/$RELEASE_TAG")"
"$api/tags/$release_tag")"
case "$status" in
204|404) ;;
*)
echo "Failed to delete tag $RELEASE_TAG, HTTP $status" >&2
echo "Failed to delete tag $release_tag, HTTP $status" >&2
exit 1
;;
esac
@@ -135,9 +158,9 @@ jobs:
{
"body": $(jq -Rs . < "$body_file"),
"draft": false,
"name": "latest",
"prerelease": true,
"tag_name": "$RELEASE_TAG",
"name": "v${version}",
"prerelease": false,
"tag_name": "$release_tag",
"target_commitish": "$commit_sha"
}
EOF

View File

@@ -7,15 +7,30 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_program(CLANG_FORMAT_BIN clang-format)
find_package(Git QUIET)
include(FetchContent)
option(TELEGRAM_TUI_USE_SYSTEM_TDLIB "Use an installed TDLib package instead of fetching it." OFF)
option(TELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS
"Fail configure if build credentials are not provided." OFF)
set(TELEGRAM_TUI_TDLIB_ROOT ""
CACHE PATH "Path to a prebuilt TDLib root containing include/ and lib/ directories.")
set(CURSES_NEED_WIDE TRUE)
find_package(Curses REQUIRED)
find_package(Threads REQUIRED)
find_package(nlohmann_json QUIET)
if(NOT nlohmann_json_FOUND)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.3
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(nlohmann_json)
endif()
set(TELEGRAM_TUI_APP_CONFIG_PATH "")
if(DEFINED ENV{XDG_DATA_HOME} AND NOT "$ENV{XDG_DATA_HOME}" STREQUAL "")
@@ -26,22 +41,67 @@ endif()
set(TELEGRAM_TUI_BUILD_API_ID "")
set(TELEGRAM_TUI_BUILD_API_HASH "")
if(TELEGRAM_TUI_APP_CONFIG_PATH AND EXISTS "${TELEGRAM_TUI_APP_CONFIG_PATH}")
set(TELEGRAM_TUI_BUILD_VERSION "${PROJECT_VERSION}")
set(TELEGRAM_TUI_BUILD_COMMIT "")
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
execute_process(
COMMAND "${GIT_EXECUTABLE}" rev-parse --short HEAD
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE TELEGRAM_TUI_BUILD_COMMIT
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
execute_process(
COMMAND "${GIT_EXECUTABLE}" describe --tags --always --dirty
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
OUTPUT_VARIABLE TELEGRAM_TUI_GIT_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
if(TELEGRAM_TUI_GIT_DESCRIBE)
set(TELEGRAM_TUI_BUILD_VERSION "${PROJECT_VERSION}+${TELEGRAM_TUI_GIT_DESCRIBE}")
endif()
endif()
if(DEFINED ENV{TELEGRAM_TUI_BUILD_API_ID} AND NOT "$ENV{TELEGRAM_TUI_BUILD_API_ID}" STREQUAL "")
set(TELEGRAM_TUI_BUILD_API_ID "$ENV{TELEGRAM_TUI_BUILD_API_ID}")
elseif(DEFINED ENV{TELEGRAM_API_ID} AND NOT "$ENV{TELEGRAM_API_ID}" STREQUAL "")
set(TELEGRAM_TUI_BUILD_API_ID "$ENV{TELEGRAM_API_ID}")
endif()
if(DEFINED ENV{TELEGRAM_TUI_BUILD_API_HASH} AND NOT "$ENV{TELEGRAM_TUI_BUILD_API_HASH}" STREQUAL "")
set(TELEGRAM_TUI_BUILD_API_HASH "$ENV{TELEGRAM_TUI_BUILD_API_HASH}")
elseif(DEFINED ENV{TELEGRAM_API_HASH} AND NOT "$ENV{TELEGRAM_API_HASH}" STREQUAL "")
set(TELEGRAM_TUI_BUILD_API_HASH "$ENV{TELEGRAM_API_HASH}")
endif()
if((TELEGRAM_TUI_BUILD_API_ID STREQUAL "" OR TELEGRAM_TUI_BUILD_API_HASH STREQUAL "") AND
TELEGRAM_TUI_APP_CONFIG_PATH AND EXISTS "${TELEGRAM_TUI_APP_CONFIG_PATH}")
file(READ "${TELEGRAM_TUI_APP_CONFIG_PATH}" TELEGRAM_TUI_APP_CONFIG_JSON)
string(REGEX MATCH "\"api_id\"[ \t\r\n]*:[ \t\r\n]*\"?([0-9]+)\"?"
TELEGRAM_TUI_APP_CONFIG_API_ID_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}")
if(CMAKE_MATCH_1)
set(TELEGRAM_TUI_BUILD_API_ID "${CMAKE_MATCH_1}")
if(TELEGRAM_TUI_BUILD_API_ID STREQUAL "")
string(REGEX MATCH "\"api_id\"[ \t\r\n]*:[ \t\r\n]*\"?([0-9]+)\"?"
TELEGRAM_TUI_APP_CONFIG_API_ID_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}")
if(CMAKE_MATCH_1)
set(TELEGRAM_TUI_BUILD_API_ID "${CMAKE_MATCH_1}")
endif()
endif()
string(REGEX MATCH "\"api_hash\"[ \t\r\n]*:[ \t\r\n]*\"([^\"]+)\""
TELEGRAM_TUI_APP_CONFIG_API_HASH_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}")
if(CMAKE_MATCH_1)
set(TELEGRAM_TUI_BUILD_API_HASH "${CMAKE_MATCH_1}")
if(TELEGRAM_TUI_BUILD_API_HASH STREQUAL "")
string(REGEX MATCH "\"api_hash\"[ \t\r\n]*:[ \t\r\n]*\"([^\"]+)\""
TELEGRAM_TUI_APP_CONFIG_API_HASH_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}")
if(CMAKE_MATCH_1)
set(TELEGRAM_TUI_BUILD_API_HASH "${CMAKE_MATCH_1}")
endif()
endif()
endif()
if(TELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS AND
(TELEGRAM_TUI_BUILD_API_ID STREQUAL "" OR TELEGRAM_TUI_BUILD_API_HASH STREQUAL ""))
message(FATAL_ERROR
"Build credentials are required. Set TELEGRAM_TUI_BUILD_API_ID and "
"TELEGRAM_TUI_BUILD_API_HASH (or TELEGRAM_API_ID/TELEGRAM_API_HASH).")
endif()
configure_file(
${CMAKE_SOURCE_DIR}/src/build_config.h.in
${CMAKE_CURRENT_BINARY_DIR}/build_config.h
@@ -119,7 +179,8 @@ if(CLANG_FORMAT_BIN)
endif()
target_include_directories(shinoa PRIVATE ${CURSES_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(shinoa PRIVATE ${CURSES_LIBRARIES})
target_link_libraries(shinoa PRIVATE ${CURSES_LIBRARIES} Threads::Threads
nlohmann_json::nlohmann_json)
if(TELEGRAM_TUI_USE_SYSTEM_TDLIB)
target_link_libraries(shinoa PRIVATE Td::TdJson)

View File

@@ -1,15 +1,16 @@
pkgname=shinoa-bin
pkgver=latest
pkgver=0.1.0
pkgrel=1
pkgdesc='Minimal Telegram terminal client built with ncurses and bundled TDLib'
arch=('x86_64')
url='https://git.mshq.dev/AxiFisk/shinoa'
license=('custom:unknown')
depends=('gcc-libs' 'glibc' 'ncurses' 'openssl' 'zlib')
options=(!debug)
provides=('shinoa')
conflicts=('shinoa')
source=(
"shinoa-linux-x86_64.tar.gz::${url}/releases/download/latest/shinoa-linux-x86_64.tar.gz"
"shinoa-linux-x86_64.tar.gz::${url}/releases/download/v${pkgver}/shinoa-linux-x86_64.tar.gz"
)
sha256sums=('SKIP')

View File

@@ -31,6 +31,9 @@ cmake --build build -j
During configure, CMake also checks the app config at
`$XDG_DATA_HOME/telegram-tui/config.json` or `~/.local/share/telegram-tui/config.json`.
If that file contains `api_id` and `api_hash`, they are embedded into the build.
For CI or release builds, prefer setting `TELEGRAM_TUI_BUILD_API_ID` and
`TELEGRAM_TUI_BUILD_API_HASH` in the environment so credentials come from secrets instead of
local config.
## Run
@@ -64,10 +67,12 @@ archive plus checksum to the Gitea Generic Package Registry under the `tdlib` pa
It also refreshes a `latest/tdlib-latest.json` manifest with the newest published version.
The repository also includes [`.gitea/workflows/release-app.yaml`](.gitea/workflows/release-app.yaml),
which downloads a prebuilt TDLib bundle from this repository's `tdlib` release tag, builds a
rolling `latest` app release, and publishes an archive containing `usr/bin/shinoa` plus the
which downloads a prebuilt TDLib bundle from the `shinoa-tdlib` repository using a pinned
version tag such as `v1.8.63`, builds a
versioned app release tag such as `v0.1.0`, and publishes an archive containing `usr/bin/shinoa` plus the
bundled `usr/lib/libtdjson.so*`. The root `PKGBUILD` installs that prebuilt release as
`shinoa-bin`.
`shinoa-bin`. The package disables debug splitting with `options=(!debug)`. That workflow expects Gitea secrets named `TELEGRAM_API_ID` and
`TELEGRAM_API_HASH`. Release builds are configured to fail if those secrets are missing.
To prepare the TDLib bundle on your own machine:
@@ -78,8 +83,10 @@ cmake --install td-build
./scripts/package-tdlib.sh td-install tdlib-linux-x86_64.tar.gz
```
Upload `tdlib-linux-x86_64.tar.gz` to a release tagged `tdlib` in this repository. After that,
the `Release App` workflow can consume it and publish the app bundle.
Publish `tdlib-linux-x86_64.tar.gz` to the `shinoa-tdlib` repository under a versioned tag such
as `v1.8.63`. Then update `TDLIB_RELEASE_TAG` in
[`.gitea/workflows/release-app.yaml`](.gitea/workflows/release-app.yaml) and run the
`Release App` workflow.
## Keys

View File

@@ -1,5 +1,9 @@
#include "app.h"
#include <array>
#include <cstdio>
#include <future>
#include <curses.h>
#include "build_config.h"
@@ -9,11 +13,83 @@ namespace telegram_tui {
namespace {
std::string trim_release_tag_prefix(const std::string &value) {
if (!value.empty() && (value[0] == 'v' || value[0] == 'V')) {
return value.substr(1);
}
return value;
}
bool is_truthy_env_value(const std::string &value) {
return value == "1" || value == "true" || value == "TRUE" || value == "yes" ||
value == "YES" || value == "on" || value == "ON";
}
std::string shell_quote(const std::string &value) {
std::string quoted = "'";
for (char ch : value) {
if (ch == '\'') {
quoted += "'\\''";
} else {
quoted.push_back(ch);
}
}
quoted.push_back('\'');
return quoted;
}
std::string run_command_capture(const std::string &command) {
FILE *pipe = popen(command.c_str(), "r");
if (pipe == nullptr) {
return {};
}
std::string output;
std::array<char, 4096> buffer{};
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
output += buffer.data();
}
if (pclose(pipe) != 0) {
return {};
}
return output;
}
std::optional<std::string> fetch_update_notice() {
static constexpr const char *kLatestReleaseApiUrl =
"https://git.mshq.dev/api/v1/repos/AxiFisk/shinoa/releases/latest";
if (std::string(TELEGRAM_TUI_BUILD_COMMIT).empty()) {
return std::nullopt;
}
const std::string response =
run_command_capture("curl -fsSL " + shell_quote(kLatestReleaseApiUrl) + " 2>/dev/null");
if (response.empty()) {
return std::nullopt;
}
try {
const json release = json::parse(response, nullptr, true, true);
const std::string latest_tag = trim_release_tag_prefix(safe_string(release, "tag_name"));
if (!latest_tag.empty() && latest_tag == TELEGRAM_TUI_PROJECT_VERSION) {
return std::nullopt;
}
const std::string target_commit = safe_string(release, "target_commitish");
if (target_commit.empty()) {
return std::nullopt;
}
const std::string current_commit = TELEGRAM_TUI_BUILD_COMMIT;
if (target_commit == current_commit || target_commit.rfind(current_commit, 0) == 0) {
return std::nullopt;
}
return std::string("Update available");
} catch (const json::exception &) {
return std::nullopt;
}
}
} // namespace
App::App() {
@@ -49,6 +125,8 @@ App::App() {
if (use_test_dc_) {
status_line_ = "Starting TDLib in test DC mode...";
}
start_update_check();
}
int App::run() {
@@ -80,6 +158,27 @@ int App::run() {
return 0;
}
void App::start_update_check() {
update_check_future_ =
std::async(std::launch::async, []() { return fetch_update_notice(); });
}
bool App::refresh_update_check_result() {
if (!update_check_future_.valid()) {
return false;
}
if (update_check_future_.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
return false;
}
const auto notice = update_check_future_.get();
if (notice == update_notice_) {
return false;
}
update_notice_ = notice.value_or(std::string());
return true;
}
std::optional<std::int64_t> App::highlighted_chat_id() const {
if (sorted_chat_ids_.empty()) {
return std::nullopt;

View File

@@ -3,6 +3,7 @@
#include <cstdint>
#include <chrono>
#include <filesystem>
#include <future>
#include <map>
#include <optional>
#include <tuple>
@@ -38,6 +39,8 @@ class App {
void init_curses();
void shutdown_curses();
void start_update_check();
bool refresh_update_check_result();
bool process_updates();
void handle_td_object(const json &object);
void handle_authorization_state();
@@ -197,6 +200,7 @@ class App {
std::string input_prompt_;
std::string input_buffer_;
std::string status_line_ = "Starting TDLib...";
std::string update_notice_;
std::string attachment_preview_graphics_data_;
std::string attachment_viewer_title_;
std::string attachment_preview_signature_;
@@ -217,6 +221,7 @@ class App {
std::size_t attachment_viewer_frame_index_ = 0;
std::string attachment_viewer_send_caption_;
std::vector<std::int64_t> forward_message_ids_;
std::future<std::optional<std::string>> update_check_future_;
};
} // namespace telegram_tui

View File

@@ -9,6 +9,7 @@
#include <curses.h>
#include "build_config.h"
#include "util.h"
namespace telegram_tui {
@@ -242,7 +243,7 @@ void App::send_tdlib_parameters() {
{"system_language_code", "en"},
{"device_model", "shinoa"},
{"system_version", "Linux"},
{"application_version", "0.1.0"},
{"application_version", TELEGRAM_TUI_BUILD_VERSION},
{"enable_storage_optimizer", true},
{"ignore_file_names", false},
});

View File

@@ -6,6 +6,7 @@
#include <curses.h>
#include "app_ui.h"
#include "build_config.h"
#include "util.h"
namespace telegram_tui {
@@ -88,10 +89,25 @@ void App::draw() {
attron(A_REVERSE);
mvhline(header_y, 0, ' ', width);
const std::string header_label = use_test_dc_ ? "shinoa [TEST DC]" : "shinoa";
std::string header_label = std::string("shinoa ") + TELEGRAM_TUI_BUILD_VERSION;
if (use_test_dc_) {
header_label += " [TEST DC]";
}
mvprintw(header_y, 1, "%s", header_label.c_str());
const std::string auth_label = authorized_ ? "ready" : current_auth_label();
const int auth_x = std::max(1, width - static_cast<int>(auth_label.size()) - 2);
if (!update_notice_.empty()) {
const std::string centered_notice =
truncate_to_width(update_notice_, std::max(1, width - 4));
const int notice_x =
std::max(1, (width - static_cast<int>(centered_notice.size())) / 2);
if (notice_x > 1 + static_cast<int>(header_label.size()) &&
notice_x + static_cast<int>(centered_notice.size()) < auth_x - 1) {
attron(A_BOLD);
mvprintw(header_y, notice_x, "%s", centered_notice.c_str());
attroff(A_BOLD);
}
}
mvprintw(header_y, auth_x, "%s", auth_label.c_str());
attroff(A_REVERSE);

View File

@@ -25,7 +25,7 @@ std::string format_download_progress(std::int64_t downloaded_size, std::int64_t
} // namespace
bool App::process_updates() {
bool changed = false;
bool changed = refresh_update_check_result();
while (true) {
auto update = td_.receive(0.0);
if (!update.has_value()) {

View File

@@ -1,4 +1,7 @@
#pragma once
#define TELEGRAM_TUI_PROJECT_VERSION "@PROJECT_VERSION@"
#define TELEGRAM_TUI_BUILD_COMMIT "@TELEGRAM_TUI_BUILD_COMMIT@"
#define TELEGRAM_TUI_BUILD_VERSION "@TELEGRAM_TUI_BUILD_VERSION@"
#define TELEGRAM_TUI_BUILD_API_ID "@TELEGRAM_TUI_BUILD_API_ID@"
#define TELEGRAM_TUI_BUILD_API_HASH "@TELEGRAM_TUI_BUILD_API_HASH@"