12 KiB
Agent Guide for Telegram Desktop
This guide defines repository-wide instructions for coding agents working with the Telegram Desktop codebase.
Build System Structure
The build system expects this directory layout:
L:\Telegram\ # BuildPath
L:\Telegram\tdesktop\ # Repository (you work here)
L:\Telegram\Libraries\ # 32-bit dependencies (Linux/macOS)
L:\Telegram\win64\Libraries\ # 64-bit dependencies (Windows)
L:\Telegram\ThirdParty\ # Build tools (NuGet, Python, etc.)
Dependencies are located relative to the repository: ../Libraries, ../win64/Libraries, or ../ThirdParty.
Build Configuration
Build Commands
From repository root, run:
cmake --build out --config Debug --target Telegram
That's it. The out/ directory is already configured. The executable will be at out/Debug/Telegram.exe.
Important: When running cmake from a shell that doesn't support cd, use quoted absolute paths:
cmake --build "l:\Telegram\tx64\out" --config Debug --target Telegram
Never build Release - it's extremely heavy and not needed for testing changes.
Platform-Specific Requirements
Windows
- Requires Visual Studio 2022
- Must run from appropriate Native Tools Command Prompt:
- "x64 Native Tools Command Prompt" for
win64 - "x86 Native Tools Command Prompt" for
win - "ARM64 Native Tools Command Prompt" for
winarm
- "x64 Native Tools Command Prompt" for
- Dependencies:
../win64/Libraries(64-bit) or../Libraries(32-bit)
macOS
- Requires Xcode
- Dependencies:
../Libraries/local/Qt-* - Set
QTenvironment variable:export QT=6.8
Linux
- Build dependencies in
../Libraries - Set
QTenvironment variable if needed
Key Files
Telegram/build/version- Version informationout/- Build output directory
Troubleshooting
"Libraries not found"
Ensure the repository is in L:\Telegram\tdesktop. The build system requires ../win64/Libraries to exist.
Build fails with "wrong command prompt"
On Windows, use the correct Visual Studio Native Tools Command Prompt matching your target (x64/x86/ARM64).
Build fails with PDB or EXE access errors
âš ï¸ CRITICAL: DO NOT RETRY THE BUILD. STOP AND WAIT FOR USER.
If the build fails with ANY of these errors:
fatal error C1041: cannot open program databasecannot open output file 'Telegram.exe'LNK1104: cannot open file- Any "access denied" or "file in use" error
STOP IMMEDIATELY. These errors mean files are locked by a running process (Telegram.exe or debugger).
What to do:
- Do NOT attempt another build - it will fail the same way
- Do NOT try to delete files - they are locked
- Do NOT try any workarounds or fixes
- IMMEDIATELY inform the user:
"Build failed - files are locked. Please close Telegram.exe (and any debugger) so I can rebuild."
Then WAIT for user confirmation before attempting any build.
Retrying builds wastes time and context. The ONLY fix is for the user to close the running process.
Best Practices
- Always use Debug builds - Release builds are extremely heavy
- Don't build Release configuration - it's too heavy for testing
Development Guidelines
Coding Style
Do NOT write comments in code:
This is important! Do not write single-line comments that describe what the next line does - they are bloat. Comments are allowed ONLY to describe complex algorithms in detail, when the explanation requires at least 4-5 lines. Self-documenting code with clear variable and function names is preferred.
// BAD - don't do this:
// Get the user's name
auto name = user->name();
// Check if premium
if (user->isPremium()) {
// GOOD - no comments needed, code is self-explanatory:
auto name = user->name();
if (user->isPremium()) {
// ACCEPTABLE - complex algorithm explanation (4+ lines):
// The algorithm works by first collecting all visible messages
// in the viewport, then calculating their intersection with
// the clip rectangle. Messages are grouped by date headers,
// and we need to account for sticky headers that may overlap
// with the first message in each group.
Empty line before closing brace:
Always add an empty line before the closing brace of a class (after all private fields):
// GOOD:
class MyClass {
public:
void foo();
private:
int _value = 0;
};
// BAD:
class MyClass {
public:
void foo();
private:
int _value = 0;
};
Multi-line expressions — operators at the start of continuation lines:
When splitting an expression across multiple lines, place operators (like &&, ||, ;, +, etc.) at the beginning of continuation lines, not at the end of the previous line. This makes it immediately obvious from the left edge whether a line is a continuation or new code.
// BAD - continuation looks like scope code:
if (const auto &lottie = animation->lottie;
lottie && lottie->valid() && lottie->framesCount() > 1) {
lottie->animate([=] {
// GOOD - semicolon at start signals continuation:
if (const auto &lottie = animation->lottie
; lottie && lottie->valid() && lottie->framesCount() > 1) {
lottie->animate([=] {
// BAD - trailing && makes next line look like independent code:
if (veryLongExpression() &&
anotherLongExpression() &&
anotherOne()) {
doSomething();
// GOOD - leading && clearly marks continuation:
if (veryLongExpression()
&& anotherLongExpression()
&& anotherOne()) {
doSomething();
Use auto for type deduction:
Prefer auto (or const auto, const auto &) instead of explicit types:
// Prefer this:
auto currentTitle = tr::lng_settings_title(tr::now);
auto nameProducer = GetNameProducer();
// Instead of this:
QString currentTitle = tr::lng_settings_title(tr::now);
rpl::producer<QString> nameProducer = GetNameProducer();
API Usage
API Schema Files
API definitions use TL Language:
Telegram/SourceFiles/mtproto/scheme/mtproto.tl- MTProto protocol (encryption, auth, etc.)Telegram/SourceFiles/mtproto/scheme/api.tl- Telegram API (messages, users, chats, etc.)
Making API Requests
Standard pattern using api(), generated MTP... types, and callbacks:
api().request(MTPnamespace_MethodName(
MTP_flags(flags_value),
MTP_inputPeer(peer),
MTP_string(messageText),
MTP_long(randomId),
MTP_vector<MTPMessageEntity>()
)).done([=](const MTPResponseType &result) {
// Handle successful response
// Multiple constructors - use .match() or check type:
result.match([&](const MTPDuser &data) {
// use data.vfirst_name().v
}, [&](const MTPDuserEmpty &data) {
// handle empty user
});
// Single constructor - use .data() shortcut:
const auto &data = result.data();
// use data.vmessages().v
}).fail([=](const MTP::Error &error) {
// Handle API error
if (error.type() == u"FLOOD_WAIT_X"_q) {
// Handle flood wait
}
}).handleFloodErrors().send();
Key points:
- Always refer to
api.tlfor method signatures and return types - Use generated
MTP...types for parameters (MTP_int,MTP_string, etc.) - For multiple constructors, use
.match()or check.type()then.c_constructor() - For single constructors, use
.data()shortcut - Include
.handleFloodErrors()before.send()in rare cases where you want special case flood error handling
UI Styling
Style Files
UI styles are defined in .style files using custom syntax:
using "ui/basic.style";
using "ui/widgets/widgets.style";
MyButtonStyle {
textPadding: margins;
icon: icon;
height: pixels;
}
defaultButton: MyButtonStyle {
textPadding: margins(10px, 15px, 10px, 15px);
icon: icon{{ "gui/icons/search", iconColor }};
height: 30px;
}
primaryButton: MyButtonStyle(defaultButton) {
icon: icon{{ "gui/icons/check", iconColor }};
}
Built-in types:
int- Integer numberspixels- Pixel values withpxsuffix (e.g.,10px)color- Named colors fromui/colors.paletteicon- Inline icon definition:icon{{ "path/stem", color }}margins- Four values:margins(top, right, bottom, left)size- Two values:size(width, height)point- Two values:point(x, y)align- Alignment:align(center),align(left)font- Font:font(14px semibold)double- Floating point
Never hardcode sizes in code:
The app supports different interface scale options. Style px values are automatically scaled at runtime, but raw integer constants in code are not. Never use hardcoded numbers for margins, paddings, spacing, sizes, coordinates, or any other dimensional values. Always define them in .style files and reference via st::.
// BAD - breaks at non-100% interface scale:
p.drawText(10, 20, text);
widget->setFixedHeight(48);
auto margin = 8;
auto iconSize = QSize(24, 24);
// GOOD - define in .style file and reference:
p.drawText(st::myWidgetTextLeft, st::myWidgetTextTop, text);
widget->setFixedHeight(st::myWidgetHeight);
auto margin = st::myWidgetMargin;
auto iconSize = st::myWidgetIconSize;
Usage in Code
#include "styles/style_widgets.h"
// Access style members
int height = st::primaryButton.height;
const style::icon &icon = st::primaryButton.icon;
style::margins padding = st::primaryButton.textPadding;
// Use in painting
void MyWidget::paintEvent(QPaintEvent *e) {
Painter p(this);
p.fillRect(rect(), st::chatInput.backgroundColor);
}
Localization
String Definitions
Strings are defined in Telegram/Resources/langs/lang.strings:
"lng_settings_title" = "Settings";
"lng_confirm_delete_item" = "Are you sure you want to delete {item_name}?";
"lng_files_selected#one" = "{count} file selected";
"lng_files_selected#other" = "{count} files selected";
Usage in Code
Immediate (current value):
auto currentTitle = tr::lng_settings_title(tr::now);
auto currentConfirmation = tr::lng_confirm_delete_item(
tr::now,
lt_item_name, currentItemName);
auto filesText = tr::lng_files_selected(tr::now, lt_count, count);
Reactive (rpl::producer):
auto titleProducer = tr::lng_settings_title();
auto confirmationProducer = tr::lng_confirm_delete_item(
lt_item_name,
std::move(itemNameProducer));
auto filesTextProducer = tr::lng_files_selected(
lt_count,
countProducer | tr::to_count());
Key points:
- Pass
tr::nowas first argument for immediateQString - Omit
tr::nowfor reactiverpl::producer<QString> - Placeholders use
lt_tag_name, valuepattern - For
{count}: immediate usesint, reactive usesrpl::producer<float64>with| tr::to_count() - Move producers with
std::movewhen passing to placeholders
RPL (Reactive Programming Library)
Core Concepts
Producers represent streams of values over time:
auto intProducer = rpl::single(123); // Emits single value
auto lifetime = rpl::lifetime(); // Manages subscription lifetime
Starting Pipelines
std::move(counter) | rpl::on_next([=](int value) {
qDebug() << "Received: " << value;
}, lifetime);
// Without lifetime parameter - MUST store returned lifetime:
auto subscriptionLifetime = std::move(counter) | rpl::on_next([=](int value) {
// process value
});
Transforming Producers
auto strings = std::move(ints) | rpl::map([](int value) {
return QString::number(value * 2);
});
auto evenInts = std::move(ints) | rpl::filter([](int value) {
return (value % 2 == 0);
});
Combining Producers
rpl::combine - combines latest values (lambdas receive unpacked arguments):
auto combined = rpl::combine(countProducer, textProducer);
std::move(combined) | rpl::on_next([=](int count, const QString &text) {
qDebug() << "Count=" << count << ", Text=" << text;
}, lifetime);
rpl::merge - merges producers of same type:
auto merged = rpl::merge(sourceA, sourceB);
std::move(merged) | rpl::on_next([=](QString &&value) {
qDebug() << "Merged value: " << value;
}, lifetime);
Key points:
- Explicitly
std::moveproducers when starting pipelines - Pass
rpl::lifetimetoon_...methods or store returned lifetime - Use
rpl::duplicate(producer)to reuse a producer multiple times - Combined producers automatically unpack tuples in lambdas