├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── ci-linux.yml │ ├── ci-macos.yml │ ├── ci-windows.yml │ ├── cleanup-caches.yml │ └── not-used-build-tdlib.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── build.rs ├── config ├── app.toml ├── first_theme.toml ├── keymap.toml ├── logger.toml ├── telegram.toml └── theme.toml ├── docs ├── configuration │ ├── README.md │ ├── app.toml.md │ ├── keymap.toml.md │ ├── logger.toml.md │ ├── telegram.toml.md │ ├── terminal-config.md │ └── theme.toml.md └── vim_keymap.toml ├── examples ├── example.rs ├── example_chat_list.rs ├── get_me.rs └── telegram.rs ├── flake.lock ├── flake.nix ├── imgs ├── example.png ├── example_movie.gif ├── example_movie.webm └── logo.png ├── patches └── 0001-check-filesystem-writability-before-operations.patch ├── rustfmt.toml └── src ├── action.rs ├── app_context.rs ├── app_error.rs ├── cli.rs ├── component_name.rs ├── components ├── chat_list_window.rs ├── chat_window.rs ├── component_traits.rs ├── core_window.rs ├── mod.rs ├── prompt_window.rs ├── reply_message.rs ├── status_bar.rs └── title_bar.rs ├── configs ├── config_file.rs ├── config_theme.rs ├── config_type.rs ├── custom │ ├── app_custom.rs │ ├── keymap_custom.rs │ ├── logger_custom.rs │ ├── mod.rs │ ├── palette_custom.rs │ ├── telegram_custom.rs │ └── theme_custom.rs ├── mod.rs └── raw │ ├── app_raw.rs │ ├── keymap_raw.rs │ ├── logger_raw.rs │ ├── mod.rs │ ├── palette_raw.rs │ ├── telegram_raw.rs │ └── theme_raw.rs ├── event.rs ├── logger.rs ├── main.rs ├── run.rs ├── tg ├── message_entry.rs ├── mod.rs ├── ordered_chat.rs ├── td_enums.rs ├── tg_backend.rs └── tg_context.rs ├── tui.rs ├── tui_backend.rs └── utils.rs /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report. 3 | title: "[bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | > [!WARNING] 10 | > Before submitting this issue, please ensure that a similar issue or pull request does not already exist. 11 | > You can search existing issues and pull requests [here](https://github.com/FedericoBruzzone/tgt/issues) and [here](https://github.com/FedericoBruzzone/tgt/pulls). 12 | > If you find a similar issue or pull request, please consider adding a comment to provide additional information or upvote it to indicate your interest. 13 | - type: input 14 | id: description 15 | attributes: 16 | label: Describe the bug 17 | description: A clear and concise description of what the bug is. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: reproduction 22 | attributes: 23 | label: Reproduction steps 24 | description: "How do you trigger this bug? Please walk us through it step by step." 25 | value: | 26 | 1. 27 | 2. 28 | 3. 29 | ... 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: expected 34 | attributes: 35 | label: Expected behavior 36 | placeholder: A clear and concise description of what you expected to happen. 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: logs 41 | attributes: 42 | label: Logs and screenshots 43 | placeholder: Add logs and screenshots to help explain your problem. 44 | validations: 45 | required: false 46 | - type: markdown 47 | attributes: 48 | value: Additional context 49 | - type: dropdown 50 | id: installation 51 | attributes: 52 | label: Method of Installation 53 | description: Please specify how you installed tgt 54 | options: 55 | - Cargo 56 | - Docker 57 | - Download from GitHub 58 | - Build from source 59 | validations: 60 | required: true 61 | - type: input 62 | id: version 63 | attributes: 64 | label: tgt version 65 | description: | 66 | Please specify the version of the application where the issue occurred. 67 | The version can be found by running the command `tgt --version`. 68 | placeholder: "1.0.0" 69 | validations: 70 | required: true 71 | - type: input 72 | id: os 73 | attributes: 74 | label: Operating System (including version and architecture) 75 | description: | 76 | Please provide the operating system you're using, including its version and architecture. 77 | If you are using tgt inside Docker, please also specify the Docker version you're using. 78 | placeholder: Windows 10 (64-bit) 79 | validations: 80 | required: true 81 | - type: input 82 | id: terminal 83 | attributes: 84 | label: Terminal and Command Line Interface (CLI) (including versions) 85 | description: Please specify the terminal emulator and the Command Line Interface (CLI) you're using, along with its versions. 86 | placeholder: Windows Terminal (1.19.10821.0) PowerShell (7.4.2) 87 | validations: 88 | required: false 89 | - type: input 90 | id: rust 91 | attributes: 92 | label: rustc and rustup target (including versions) 93 | description: | 94 | Please specify the rustc and rustup target you're using, along with its versions. 95 | The rustup target can be found by running the command `rustup target list` and looking for an enrty marked as installed. 96 | placeholder: rustc 1.74.1 (a28077b28 2023-12-04) x86_64-pc-windows-msvc 97 | validations: 98 | required: false 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[feature]: ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+" 7 | 8 | env: 9 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 10 | REPO: ${{ github.repository }} 11 | 12 | jobs: 13 | release_github: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Create release 17 | run: gh release create ${{ github.ref_name }} -R $REPO --generate-notes 18 | 19 | release_crates: 20 | runs-on: ubuntu-latest 21 | needs: release_github 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | - name: Publish to crates.io 26 | run: | 27 | cargo login ${{ secrets.CRATES_IO_TOKEN }} 28 | cargo publish --package tgt 29 | 30 | # --no-verify 31 | -------------------------------------------------------------------------------- /.github/workflows/ci-linux.yml: -------------------------------------------------------------------------------- 1 | name: CI Linux 2 | 3 | on: 4 | push: 5 | branches-ignore: [ 'dependabot/**' ] 6 | tags-ignore: [ '**' ] 7 | paths: 8 | - 'src/**' 9 | - 'Cargo.lock' 10 | - 'Cargo.toml' 11 | - 'rustfmt.toml' 12 | - 'config/**' 13 | - '.github/workflows/**' 14 | pull_request: 15 | branches: [ '**' ] 16 | paths: 17 | - 'src/**' 18 | - 'Cargo.lock' 19 | - 'Cargo.toml' 20 | - 'rustfmt.toml' 21 | - 'config/**' 22 | - '.github/workflows/**' 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | RUST_BACKTRACE: 1 27 | TDLIB_VERSION: af69dd4397b6dc1bf23ba0fd0bf429fcba6454f6 28 | API_HASH: a3406de8d171bb422bb6ddf3bbd800e2 29 | API_ID: 94575 30 | 31 | jobs: 32 | ci: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | feature: [local-tdlib, pkg-config, download-tdlib] 37 | os: [ubuntu-latest, ubuntu-24.04-arm] 38 | 39 | runs-on: ${{ matrix.os }} 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Install needed packages 45 | run: | 46 | sudo apt update 47 | sudo apt install libc++-dev libc++abi-dev 48 | - name: Restore cache TDLib 49 | if: matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config' 50 | id: cache-tdlib-restore 51 | uses: actions/cache/restore@v4 52 | with: 53 | path: td/ 54 | key: ${{ runner.os }}-${{ runner.arch }}-TDLib-${{ env.TDLIB_VERSION }} 55 | - name: Build TDLib 56 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 57 | # sudo apt-get -y install make git zlib1g-dev libssl-dev gperf php-cli cmake clang-14 libc++-dev libc++abi-dev 58 | # CXXFLAGS="-stdlib=libc++" CC=/usr/bin/clang-14 CXX=/usr/bin/clang++-14 cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. 59 | run: | 60 | sudo apt-get update 61 | sudo apt-get -y upgrade 62 | sudo apt-get install make git zlib1g-dev libssl-dev gperf php-cli cmake clang-18 libc++-18-dev libc++abi-18-dev 63 | git clone https://github.com/tdlib/td.git 64 | cd td 65 | git checkout $TDLIB_VERSION 66 | rm -rf build 67 | mkdir build 68 | cd build 69 | CXXFLAGS="-stdlib=libc++" CC=/usr/bin/clang-18 CXX=/usr/bin/clang++-18 cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. 70 | cmake --build . --target install 71 | - name: Save cache TDLib 72 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 73 | uses: actions/cache/save@v4 74 | with: 75 | path: td/ 76 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 77 | - name: Extract TDLib 78 | if: matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config' 79 | run: | 80 | cp -r ./td/tdlib ./ 81 | sudo cp ./tdlib/lib/libtdjson.so.* /usr/lib/ 82 | - name: Set PKG_CONFIG_PATH and LD_LIBRARY_PATH 83 | if: matrix.feature == 'pkg-config' 84 | run: | 85 | echo "PKG_CONFIG_PATH=$(pwd)/tdlib/lib/pkgconfig" >> $GITHUB_ENV 86 | echo "LD_LIBRARY_PATH=$(pwd)/tdlib/lib" >> $GITHUB_ENV 87 | - name: Set LOCAL_TDLIB_PATH 88 | if: matrix.feature == 'local-tdlib' 89 | run: echo "LOCAL_TDLIB_PATH=$(pwd)/tdlib" >> $GITHUB_ENV 90 | # - name: Cache cargo 91 | # uses: actions/cache@v4 92 | # with: 93 | # path: | 94 | # ~/.cargo/bin/ 95 | # ~/.cargo/registry/index/ 96 | # ~/.cargo/registry/cache/ 97 | # ~/.cargo/git/db/ 98 | # target/ 99 | # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 100 | # restore-keys: ${{ runner.os }}-cargo- 101 | - name: Run cargo build 102 | run: cargo build --no-default-features --verbose --features ${{ matrix.feature }} 103 | - name: Run cargo test 104 | run: cargo test --no-default-features --verbose --features ${{ matrix.feature }} -- --nocapture --test-threads=1 105 | - name: Run cargo clippy 106 | run: cargo clippy --no-default-features --all-targets --features ${{ matrix.feature }} -- -D warnings 107 | - name: Run cargo fmt 108 | run: cargo fmt --all -- --check 109 | # - name: Upload artifact 110 | # uses: actions/upload-artifact@v4 111 | # with: 112 | # name: ${{ runner.os }}-artifact 113 | # path: ./ 114 | # overwrite: true 115 | -------------------------------------------------------------------------------- /.github/workflows/ci-macos.yml: -------------------------------------------------------------------------------- 1 | name: CI macOS 2 | 3 | on: 4 | push: 5 | branches-ignore: [ 'dependabot/**' ] 6 | tags-ignore: [ '**' ] 7 | paths: 8 | - 'src/**' 9 | - 'Cargo.lock' 10 | - 'Cargo.toml' 11 | - 'rustfmt.toml' 12 | - 'config/**' 13 | - '.github/workflows/**' 14 | pull_request: 15 | branches: [ '**' ] 16 | paths: 17 | - 'src/**' 18 | - 'Cargo.lock' 19 | - 'Cargo.toml' 20 | - 'rustfmt.toml' 21 | - 'config/**' 22 | - '.github/workflows/**' 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | RUST_BACKTRACE: 1 27 | TDLIB_VERSION: af69dd4397b6dc1bf23ba0fd0bf429fcba6454f6 28 | API_HASH: a3406de8d171bb422bb6ddf3bbd800e2 29 | API_ID: 94575 30 | 31 | jobs: 32 | ci: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | os: [macos-13, macos-14] 37 | feature: [local-tdlib, download-tdlib, pkg-config] 38 | 39 | runs-on: ${{ matrix.os }} 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | - name: Restore cache TDLib 45 | if: matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config' 46 | id: cache-tdlib-restore 47 | uses: actions/cache/restore@v4 48 | with: 49 | path: td/ 50 | key: ${{ runner.os }}-${{ runner.arch }}-TDLib-${{ env.TDLIB_VERSION }} 51 | - name: Build TDLib 52 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 53 | run: | 54 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 55 | brew install gperf openssl 56 | git clone https://github.com/tdlib/td.git 57 | cd td 58 | git checkout $TDLIB_VERSION 59 | rm -rf build 60 | mkdir build 61 | cd build 62 | cmake -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR=${{ runner.arch == 'ARM64' && '/opt/homebrew/opt/openssl/' || '/usr/local/opt/openssl/' }} -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. 63 | cmake --build . --target install 64 | - name: Save cache TDLib 65 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 66 | uses: actions/cache/save@v4 67 | with: 68 | path: td/ 69 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 70 | - name: Extract TDLib x86_64 71 | if: runner.arch == 'x64' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 72 | run: | 73 | cp -r ./td/tdlib ./ 74 | # cp ./tdlib/lib/libtdjson.*.dylib /usr/local/lib/ 75 | - name: Extract TDLib ARM64 76 | if: runner.arch == 'ARM64' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 77 | run: | 78 | sudo mkdir -p /usr/local/lib 79 | sudo cp -r ./td/tdlib ./ 80 | # sudo cp ./tdlib/lib/libtdjson.*.dylib /usr/local/lib/ 81 | - name: Set PKG_CONFIG_PATH and DYLD_LIBRARY_PATH 82 | if: matrix.feature == 'pkg-config' 83 | run: | 84 | echo "PKG_CONFIG_PATH=$(pwd)/tdlib/lib/pkgconfig" >> $GITHUB_ENV 85 | echo "DYLD_LIBRARY_PATH=$(pwd)/tdlib/lib" >> $GITHUB_ENV 86 | - name: Set LOCAL_TDLIB_PATH 87 | if: matrix.feature == 'local-tdlib' 88 | run: | 89 | echo "LOCAL_TDLIB_PATH=$(pwd)/tdlib" >> $GITHUB_ENV 90 | # - name: Cache cargo 91 | # uses: actions/cache@v4 92 | # with: 93 | # path: | 94 | # ~/.cargo/bin/ 95 | # ~/.cargo/registry/index/ 96 | # ~/.cargo/registry/cache/ 97 | # ~/.cargo/git/db/ 98 | # target/ 99 | # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 100 | # restore-keys: ${{ runner.os }}-cargo- 101 | - name: Run cargo build 102 | run: cargo build --no-default-features --verbose --features ${{ matrix.feature }} 103 | - name: Run cargo test 104 | run: cargo test --no-default-features --verbose --features ${{ matrix.feature }} -- --nocapture --test-threads=1 105 | - name: Run cargo clippy 106 | run: cargo clippy --no-default-features --all-targets --features ${{ matrix.feature }} -- -D warnings 107 | - name: Run cargo fmt 108 | run: cargo fmt --all -- --check 109 | # - name: Upload artifact 110 | # uses: actions/upload-artifact@v4 111 | # with: 112 | # name: ${{ runner.os }}-artifact 113 | # path: ./ 114 | # overwrite: true 115 | -------------------------------------------------------------------------------- /.github/workflows/ci-windows.yml: -------------------------------------------------------------------------------- 1 | name: CI Windows 2 | 3 | on: 4 | push: 5 | branches-ignore: [ 'dependabot/**' ] 6 | tags-ignore: [ '**' ] 7 | paths: 8 | - 'src/**' 9 | - 'Cargo.lock' 10 | - 'Cargo.toml' 11 | - 'rustfmt.toml' 12 | - 'config/**' 13 | - '.github/workflows/**' 14 | pull_request: 15 | branches: [ '**' ] 16 | paths: 17 | - 'src/**' 18 | - 'Cargo.lock' 19 | - 'Cargo.toml' 20 | - 'rustfmt.toml' 21 | - 'config/**' 22 | - '.github/workflows/**' 23 | 24 | env: 25 | CARGO_TERM_COLOR: always 26 | RUST_BACKTRACE: 1 27 | TDLIB_VERSION: af69dd4397b6dc1bf23ba0fd0bf429fcba6454f6 28 | API_HASH: a3406de8d171bb422bb6ddf3bbd800e2 29 | API_ID: 94575 30 | 31 | jobs: 32 | ci: 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | feature: [local-tdlib, pkg-config, download-tdlib] 37 | 38 | runs-on: windows-latest 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@v4 42 | - name: Restore cache TDLib 43 | if: matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config' 44 | id: cache-tdlib-restore 45 | uses: actions/cache/restore@v4 46 | with: 47 | path: td/ 48 | key: ${{ runner.os }}-${{ runner.arch }}-TDLib-${{ env.TDLIB_VERSION }} 49 | - name: Build TDLib 50 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 51 | # git checkout cd5e746ec203c8c3c61647e0886a8df8c1e78e41 52 | run: | 53 | git clone https://github.com/tdlib/td.git 54 | cd td 55 | git checkout $TDLIB_VERSION 56 | git clone https://github.com/Microsoft/vcpkg.git 57 | cd vcpkg 58 | git checkout 07b30b49e5136a36100a2ce644476e60d7f3ddc1 59 | ./bootstrap-vcpkg.bat 60 | ./vcpkg.exe install gperf:x64-windows openssl:x64-windows zlib:x64-windows 61 | cd .. 62 | rm -rf build 63 | mkdir build 64 | cd build 65 | cmake -A x64 -DCMAKE_INSTALL_PREFIX:PATH=../tdlib -DCMAKE_TOOLCHAIN_FILE:FILEPATH=../vcpkg/scripts/buildsystems/vcpkg.cmake .. 66 | cmake --build . --target install --config Release 67 | shell: bash 68 | - name: Save cache TDLib 69 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' && (matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config') 70 | uses: actions/cache/save@v4 71 | with: 72 | path: td/ 73 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 74 | - name: Extract TDLib 75 | if: matrix.feature == 'local-tdlib' || matrix.feature == 'pkg-config' 76 | run: cp -r ./td/tdlib ./ 77 | shell: bash 78 | - name: Install pkg-config 79 | if: matrix.feature == 'pkg-config' 80 | run: | 81 | mkdir pkg-config 82 | cd pkg-config 83 | curl -kLSsO http://ftp.gnome.org/pub/gnome/binaries/win32/dependencies/pkg-config_0.26-1_win32.zip 84 | curl -kLSsO http://ftp.gnome.org/pub/gnome/binaries/win32/glib/2.28/glib_2.28.8-1_win32.zip 85 | curl -kLSsO http://ftp.gnome.org/pub/gnome/binaries/win32/dependencies/gettext-runtime_0.18.1.1-2_win32.zip 86 | unzip -q pkg-config_0.26-1_win32.zip -d pkg-config_0.26-1_win32 87 | unzip -q glib_2.28.8-1_win32.zip -d glib_2.28.8-1_win32 88 | unzip -q gettext-runtime_0.18.1.1-2_win32.zip -d gettext-runtime_0.18.1.1-2_win32 89 | cp ./gettext-runtime_0.18.1.1-2_win32/bin/intl.dll ./pkg-config_0.26-1_win32/bin/ 90 | cp ./glib_2.28.8-1_win32/bin/* ./pkg-config_0.26-1_win32/bin/ 91 | shell: bash 92 | - name: Set PKG_CONFIG_PATH and bin 93 | if: matrix.feature == 'pkg-config' 94 | run: | 95 | echo "$((Get-Item .).FullName)\pkg-config\pkg-config_0.26-1_win32\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 96 | echo "PKG_CONFIG_PATH=$((Get-Item .).FullName)\tdlib\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 97 | echo "$((Get-Item .).FullName)\tdlib\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 98 | shell: pwsh 99 | - name: Set LOCAL_TDLIB_PATH 100 | if: matrix.feature == 'local-tdlib' 101 | run: echo "LOCAL_TDLIB_PATH=$((Get-Item .).FullName)\tdlib" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append 102 | # - name: Cache cargo 103 | # uses: actions/cache@v4 104 | # with: 105 | # path: | 106 | # ~/.cargo/bin/ 107 | # ~/.cargo/registry/index/ 108 | # ~/.cargo/registry/cache/ 109 | # ~/.cargo/git/db/ 110 | # target/ 111 | # key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 112 | # restore-keys: ${{ runner.os }}-cargo- 113 | - name: Run cargo build 114 | run: cargo build --no-default-features --verbose --features ${{ matrix.feature }} 115 | - name: Run cargo test 116 | run: cargo test --no-default-features --verbose --features ${{ matrix.feature }} -- --nocapture --test-threads=1 117 | - name: Run cargo clippy 118 | run: cargo clippy --no-default-features --all-targets --features ${{ matrix.feature }} -- -D warnings 119 | - name: Run cargo fmt 120 | run: cargo fmt --all -- --check 121 | # - name: Upload artifact 122 | # uses: actions/upload-artifact@v4 123 | # with: 124 | # name: ${{ runner.os }}-artifact 125 | # path: ./ 126 | # overwrite: true 127 | -------------------------------------------------------------------------------- /.github/workflows/cleanup-caches.yml: -------------------------------------------------------------------------------- 1 | name: cleanup caches by a branch 2 | on: 3 | pull_request: 4 | types: 5 | - closed 6 | 7 | jobs: 8 | cleanup: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Cleanup 12 | run: | 13 | gh extension install actions/gh-actions-cache 14 | 15 | echo "Fetching list of cache key" 16 | cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) 17 | 18 | ## Setting this to not fail the workflow while deleting cache keys. 19 | set +e 20 | echo "Deleting caches..." 21 | for cacheKey in $cacheKeysForPR 22 | do 23 | gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm 24 | done 25 | echo "Done" 26 | env: 27 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | REPO: ${{ github.repository }} 29 | BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge 30 | -------------------------------------------------------------------------------- /.github/workflows/not-used-build-tdlib.yml: -------------------------------------------------------------------------------- 1 | name: Build TDLib 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | TDLIB_VERSION: 2589c3fd46925f5d57e4ec79233cd1bd0f5d0c09 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Restore cache TDLib 13 | id: cache-tdlib-restore 14 | uses: actions/cache/restore@v4 15 | with: 16 | path: td/ 17 | key: ${{ runner.os }}-TDLib-${{ env.TDLIB_VERSION }} 18 | - name: Build TDLib 19 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get -y upgrade 23 | sudo apt-get -y install make git zlib1g-dev libssl-dev gperf php-cli cmake clang-14 libc++-dev libc++abi-dev 24 | git clone https://github.com/tdlib/td.git 25 | cd td 26 | git checkout $TDLIB_VERSION 27 | rm -rf build 28 | mkdir build 29 | cd build 30 | CXXFLAGS="-stdlib=libc++" CC=/usr/bin/clang-14 CXX=/usr/bin/clang++-14 cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. 31 | cmake --build . --target install 32 | - name: Save cache TDLib 33 | uses: actions/cache/save@v4 34 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 35 | with: 36 | path: td/ 37 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 38 | - name: Upload artifact 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: ${{ runner.os }}-TDLib-${{ env.TDLIB_VERSION }} 42 | path: ./td/tdlib/lib/libtdjson.so.* 43 | overwrite: true 44 | 45 | build-macos: 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | include: 50 | - os: macos-14 # arm64 (M1) 51 | - os: macos-13 # Intel 52 | runs-on: ${{ matrix.os }} 53 | steps: 54 | - name: Restore cache TDLib 55 | id: cache-tdlib-restore 56 | uses: actions/cache/restore@v4 57 | with: 58 | path: td/ 59 | key: ${{ runner.os }}-${{ runner.arch }}-TDLib-${{ env.TDLIB_VERSION }} 60 | - name: Build TDLib 61 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 62 | run: | 63 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 64 | brew install gperf cmake openssl 65 | git clone https://github.com/tdlib/td.git 66 | cd td 67 | git checkout $TDLIB_VERSION 68 | rm -rf build 69 | mkdir build 70 | cd build 71 | cmake -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR=${{ runner.arch == 'ARM64' && '/opt/homebrew/opt/openssl/' || '/usr/local/opt/openssl/' }} -DCMAKE_INSTALL_PREFIX:PATH=../tdlib .. 72 | cmake --build . --target install 73 | - name: Save cache TDLib 74 | uses: actions/cache/save@v4 75 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 76 | with: 77 | path: td/ 78 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 79 | - name: Upload artifact 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: ${{ runner.os }}-${{ runner.arch }}-TDLib-${{ env.TDLIB_VERSION }} 83 | path: ./td/tdlib/lib/libtdjson.*.dylib 84 | overwrite: true 85 | 86 | build-windows: 87 | runs-on: windows-latest 88 | steps: 89 | - name: Restore cache TDLib 90 | id: cache-tdlib-restore 91 | uses: actions/cache/restore@v4 92 | with: 93 | path: td/ 94 | key: ${{ runner.os }}-TDLib-${{ env.TDLIB_VERSION }} 95 | - name: Build TDLib 96 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 97 | run: | 98 | git clone https://github.com/tdlib/td.git 99 | cd td 100 | git checkout $TDLIB_VERSION 101 | git clone https://github.com/Microsoft/vcpkg.git 102 | cd vcpkg 103 | git checkout cd5e746ec203c8c3c61647e0886a8df8c1e78e41 104 | ./bootstrap-vcpkg.bat 105 | ./vcpkg.exe install gperf:x64-windows openssl:x64-windows zlib:x64-windows 106 | cd .. 107 | rm -rf build 108 | mkdir build 109 | cd build 110 | cmake -A x64 -DCMAKE_INSTALL_PREFIX:PATH=../tdlib -DCMAKE_TOOLCHAIN_FILE:FILEPATH=../vcpkg/scripts/buildsystems/vcpkg.cmake .. 111 | cmake --build . --target install --config Release 112 | shell: bash 113 | - name: Save cache TDLib 114 | uses: actions/cache/save@v4 115 | if: steps.cache-tdlib-restore.outputs.cache-hit != 'true' 116 | with: 117 | path: td/ 118 | key: ${{ steps.cache-tdlib-restore.outputs.cache-primary-key }} 119 | - name: Upload artifact 120 | uses: actions/upload-artifact@v4 121 | with: 122 | name: ${{ runner.os }}-TDLib-${{ env.TDLIB_VERSION }} 123 | path: ./td/tdlib/bin/ 124 | overwrite: true 125 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.data 3 | /.log 4 | /.logs 5 | # /.vscode 6 | /.vscode/settings.json 7 | result 8 | outputs/ 9 | result-* 10 | result 11 | .direnv 12 | .envrc 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "Launch", 8 | "args": [], // array of string command-line arguments to pass to compiler 9 | "program": "${workspaceFolder}/target/debug/tgt", // path to the program to debug 10 | "windows": { // applicable if using windows 11 | "program": "${workspaceFolder}/target/debug/tgt.exe" 12 | }, 13 | "cwd": "${workspaceFolder}", // current working directory at program start 14 | "stopOnEntry": false, 15 | "sourceLanguages": ["rust"] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "cargo run", 8 | "type": "shell", 9 | "command": "~/.cargo/bin/cargo", // note: full path to the cargo 10 | "args": [ 11 | "run", 12 | // "--release", 13 | // "--", 14 | // "arg1" 15 | ], 16 | "group": { 17 | "kind": "build", 18 | "isDefault": true 19 | } 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] - yyyy-mm-dd 7 | Here we write upgrading notes for brands. It's a team effort to make them as straightforward as possible. 8 | ### Added 9 | ### Changed 10 | ### Fixed 11 | 12 | ## [1.0.0] - 2024-08-09 13 | 14 | ### Added 15 | 16 | #### Telegram API 17 | - project configuration for APIs 18 | - Authentication 19 | - Receive messages 20 | - Send messages 21 | - Handle updates from server 22 | - Handle login 23 | - Handle logout 24 | - Handle view message 25 | - Change user status to online and offline 26 | - Display messages with time of arrival 27 | - Display if messages has been readed 28 | - Edit message 29 | - Delete message 30 | - Copy message 31 | - Reply message 32 | - Handle message edited 33 | 34 | #### Config 35 | - Logger config 36 | - Keybindings config 37 | - App config 38 | - Theme config 39 | - Telegram config 40 | 41 | #### CI/CD 42 | - CI for Linux 43 | - CI for MacOS Intel 44 | - CI for MacOS arm64 45 | - CI for Windows 46 | - CD for release 47 | 48 | ### Changed 49 | 50 | ### Fixed 51 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tgt" 3 | version = "1.0.0" 4 | edition = "2021" 5 | description = "A simple TUI for Telegram" 6 | license = "MIT OR Apache-2.0" 7 | documentation = "https://docs.rs/crate/tgt/1.0.0" 8 | homepage = "https://github.com/FedericoBruzzone/tgt" 9 | repository = "https://github.com/FedericoBruzzone/tgt" 10 | readme = "README.md" 11 | keywords = [ 12 | "tgt", 13 | "tgtui", 14 | "telegram", 15 | "tui", 16 | "tdlib", 17 | ] 18 | authors = [ 19 | "Federico Bruzzone ", 20 | "Andrea Longoni", 21 | ] 22 | default-run = "tgt" 23 | build = "build.rs" 24 | categories = [] 25 | 26 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 27 | # [toolchain] 28 | # channel = "nightly-2020-07-10" 29 | # components = [ "rustfmt", "rustc-dev" ] 30 | # targets = [ "wasm32-unknown-unknown", "thumbv2-none-eabi" ] 31 | # profile = "minimal" 32 | 33 | [features] 34 | # By deafult you need to set the `LOCAL_TDLIB_PATH` environment variable to the path of the TDLib library. See CONTRIBUTING.md for more information. 35 | default = ["download-tdlib"] 36 | local-tdlib = ["tdlib-rs/local-tdlib"] 37 | download-tdlib = ["tdlib-rs/download-tdlib"] 38 | pkg-config = ["tdlib-rs/pkg-config"] 39 | 40 | [package.metadata.system-deps] 41 | tdjson = "1.8.29" 42 | 43 | [dependencies] 44 | config = "0.15.11" 45 | crossterm = { version = "0.29.0", features = ["event-stream"] } 46 | dirs = "6.0.0" 47 | futures = "0.3.31" 48 | lazy_static = "1.5.0" 49 | ratatui = "0.29.0" 50 | serde = "1.0.219" 51 | tdlib-rs = "1.1.0" 52 | tokio = { version = "1.45.1", features = ["full"] } 53 | tracing = "0.1.41" 54 | tracing-error = "0.2.1" 55 | tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] } 56 | tracing-appender = "0.2" 57 | arboard = { version = "3.5.0", features = ["wayland-data-control", "wl-clipboard-rs"] } 58 | chrono = "0.4.41" 59 | ratatui-image = "8.0.1" 60 | image = "0.25.6" 61 | signal-hook = "0.3.18" 62 | clap = { version = "4.5.39", features = ["derive"] } 63 | nucleo-matcher = "0.3.1" 64 | 65 | [build-dependencies] 66 | dirs = "6.0.0" 67 | reqwest = { version = "0.12.18", features = ["blocking"] } 68 | zip = { version = "3.0.0" } 69 | tdlib-rs = "1.0.5" 70 | 71 | [profile.release] 72 | lto = true 73 | opt-level = 'z' 74 | codegen-units = 1 75 | strip = "debuginfo" 76 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Federico Bruzzone 4 | Copyright (c) 2024 Andrea Longoni 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export RUST_BACKTRACE := 1 2 | 3 | # Usage: make ARGS="--features --bin " 4 | # 5 | # Avaialble commands: 6 | # all 7 | # build 8 | # run 9 | # test 10 | # clippy 11 | # fmt 12 | # clean 13 | # 14 | # Available features: 15 | # local-tdlib 16 | # download-tdlib 17 | # pkg-config 18 | # 19 | # Available bin_name: 20 | # tgt 21 | # example 22 | # telegram 23 | # get_me 24 | 25 | all: 26 | $(MAKE) fmt 27 | $(MAKE) clippy ARGS="--features local-tdlib" 28 | $(MAKE) test ARGS="--features local-tdlib" 29 | 30 | run_local: 31 | cargo run --no-default-features --features local-tdlib $(ARGS) 32 | 33 | build_local: 34 | cargo build --no-default-features --features local-tdlib $(ARGS) 35 | 36 | # Example 1: make build ARGS="--features download-tdlib" 37 | # Example 2: make build ARGS="--features download-tdlib --example telegram" 38 | build: 39 | cargo build --no-default-features $(ARGS) 40 | 41 | # Example 1: make run ARGS="--features download-tdlib" 42 | # Example 2: make run ARGS="--features download-tdlib --example telegram" 43 | run: 44 | cargo run --no-default-features $(ARGS) 45 | 46 | test: 47 | cargo test --no-default-features $(ARGS) -- --nocapture --test-threads=1 48 | 49 | clippy: 50 | cargo clippy --no-default-features --all-targets $(ARGS) -- -D warnings 51 | 52 | fmt: 53 | cargo fmt --all 54 | cargo fmt --all -- --check 55 | 56 | clean: 57 | cargo clean 58 | 59 | help: 60 | @echo "Usage: make [target]" 61 | @echo "" 62 | @echo "Available targets:" 63 | @echo " all # Run fmt, clippy and test" 64 | @echo " build # Build the project" 65 | @echo " run # Run the project" 66 | @echo " test # Run the tests" 67 | @echo " clippy # Run clippy" 68 | @echo " fmt # Run rustfmt" 69 | @echo " clean # Clean the project" 70 | @echo " help # Display this help message" 71 | 72 | # Each entry of .PHONY is a target that is not a file 73 | .PHONY: build run test clean 74 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | fn empty_tgt_folder() { 2 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 3 | let _ = std::fs::remove_dir_all(format!("{}/.tgt/config", home)); 4 | let _ = std::fs::remove_dir_all(format!("{}/.tgt/tdlib", home)); 5 | } 6 | 7 | fn move_config_folder_to_home_dottgt() { 8 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 9 | let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); 10 | 11 | std::fs::create_dir_all(format!("{}/.tgt/config", home)).unwrap(); 12 | for entry in std::fs::read_dir(format!("{}/config", manifest_dir)).unwrap() { 13 | let entry = entry.unwrap(); 14 | let path = entry.path(); 15 | let file_name = path.file_name().unwrap(); 16 | let new_path = format!("{}/.tgt/config/{}", home, file_name.to_str().unwrap()); 17 | std::fs::copy(path, new_path).unwrap(); 18 | } 19 | } 20 | 21 | fn main() -> std::io::Result<()> { 22 | if cfg!(debug_assertions) { 23 | tdlib_rs::build::build(None); 24 | return Ok(()); 25 | } 26 | 27 | empty_tgt_folder(); 28 | move_config_folder_to_home_dottgt(); 29 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 30 | let dest = format!("{}/.tgt/tdlib", home); 31 | tdlib_rs::build::build(Some(dest)); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /config/app.toml: -------------------------------------------------------------------------------- 1 | # `mouse_support` enables mouse support in the terminal. 2 | mouse_support = true 3 | # `paste_support` enables paste support in the terminal. 4 | paste_support = true 5 | # `frame_rate` is the frame rate of the terminal in frames per second. 6 | # The suggested frame rate are: 7 | # - 120.0 for ultra smooth animations 8 | # - 60.0 for smooth animations 9 | # - 30.0 for normal animations 10 | # - 15.0 for slow animations 11 | frame_rate = 60.0 12 | # `show_status_bar` enables the status bar at the bottom of the terminal. 13 | show_status_bar = true 14 | # `show_title_bar` enables the title bar at the top of the terminal. 15 | show_title_bar = true 16 | # `theme_enable` enables the theme. 17 | theme_enable = true 18 | # `theme_filename` is the name of the file that contains the theme. 19 | # This file must be in the configuration directory. 20 | theme_filename = "theme.toml" 21 | # `take_api_id_from_telegram_config` enables taking the API_ID from the Telegram configuration file 22 | # or from the environment variable `API_ID`. 23 | take_api_id_from_telegram_config = true 24 | # `take_api_hash_from_telegram_config` enables taking the API_HASH from the Telegram configuration file 25 | # or from the environment variable `API_HASH`. 26 | take_api_hash_from_telegram_config = true 27 | -------------------------------------------------------------------------------- /config/first_theme.toml: -------------------------------------------------------------------------------- 1 | [palette] 2 | black = "#000000" 3 | white = "#ffffff" 4 | background = "#000000" 5 | primary = "#00548e" 6 | primary_variant = "#0073b0" 7 | primary_light = "#94dbf7" 8 | secondary = "#ca3f04" 9 | secondary_variant = "#e06819" 10 | secondary_light = "#fcac77" 11 | ternary = "#696969" 12 | ternary_variant = "#808080" 13 | ternary_light = "#6e7e85" 14 | surface = "#141414" 15 | on_surface = "#dcdcdc" 16 | error = "#D50000" 17 | on_error = "#FFCDD2" 18 | 19 | [common] 20 | border_component_focused = { fg = "secondary", bg = "background", bold = false, underline = false, italic = false } 21 | item_selected = { fg = "", bg = "surface", bold = true, underline = false, italic = false } 22 | timestamp = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = false } 23 | 24 | [chat_list] 25 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 26 | item_selected = { fg = "", bg = "primary", bold = false, underline = false, italic = false } 27 | item_chat_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 28 | item_message_content = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = true } 29 | item_unread_counter = { fg = "secondary", bg = "background", bold = true, underline = false, italic = false } 30 | 31 | [chat] 32 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 33 | chat_name = { fg = "secondary", bg = "background", bold = true, underline = false, italic = false } 34 | message_myself_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 35 | message_myself_content = { fg = "primary_variant", bg = "background", bold = false, underline = false, italic = false } 36 | message_other_name = { fg = "secondary_light", bg = "background", bold = true, underline = false, italic = false } 37 | message_other_content = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 38 | message_reply_text = { fg = "ternary", bg = "background", bold = false, underline = false, italic = false } 39 | message_myself_reply_name = { fg = "secondary_light", bg = "background", bold = true, underline = false, italic = false } 40 | message_myself_reply_content = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 41 | message_other_reply_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 42 | message_other_reply_content = { fg = "primary_variant", bg = "background", bold = false, underline = false, italic = false } 43 | 44 | [prompt] 45 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 46 | message_text = { fg = "primary_light", bg = "background", bold = false, underline = false, italic = false } 47 | message_text_selected = { fg = "secondary_light", bg = "ternary", bold = false, underline = false, italic = true } 48 | message_preview_text = { fg = "ternary", bg = "background", bold = false, underline = false, italic = false } 49 | 50 | [reply_message] 51 | self = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 52 | message_text = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 53 | 54 | [status_bar] 55 | self = { fg = "on_surface", bg = "surface", bold = false, underline = false, italic = false } 56 | size_info_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 57 | size_info_numbers = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 58 | press_key_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 59 | press_key_key = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 60 | message_quit_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 61 | message_quit_key = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 62 | open_chat_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 63 | open_chat_name = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 64 | 65 | [title_bar] 66 | self = { fg = "on_surface", bg = "surface", bold = false, underline = false, italic = false } 67 | title1 = { fg = "primary_light", bg = "surface", bold = true, underline = true, italic = true } 68 | title2 = { fg = "secondary_light", bg = "surface", bold = true, underline = true, italic = true } 69 | title3 = { fg = "ternary_light", bg = "surface", bold = true, underline = true, italic = false } 70 | -------------------------------------------------------------------------------- /config/keymap.toml: -------------------------------------------------------------------------------- 1 | # The core_window key bindings are usable in any app component. 2 | [core_window] 3 | keymap = [ 4 | # Quit the application, example of multiple keys 5 | # { keys = ["w", "w"], command = "quit", description = "Quit the application"}, 6 | 7 | # Quit the application 8 | # Note that when the prompt is focused, the "q" key will be used to type the letter "q". 9 | { keys = ["q"], command = "try_quit", description = "Quit the application"}, 10 | # Quit the application 11 | { keys = ["ctrl+c"], command = "try_quit", description = "Quit the application"}, 12 | # Focus the chat list 13 | { keys = ["alt+1"], command = "focus_chat_list", description = "Focus the chat list"}, 14 | { keys = ["alt+left"], command = "focus_chat_list", description = "Focus the chat list"}, 15 | # Focus the chat 16 | { keys = ["alt+2"], command = "focus_chat", description = "Focus the chat"}, 17 | { keys = ["alt+right"], command = "focus_chat", description = "Focus the chat"}, 18 | # Focus the prompt 19 | { keys = ["alt+3"], command = "focus_prompt", description = "Focus the prompt"}, 20 | { keys = ["alt+down"], command = "focus_prompt", description = "Focus the prompt"}, 21 | # Unfocus the current component 22 | { keys = ["esc"], command = "unfocus_component", description = "Unfocus the current component"}, 23 | { keys = ["alt+up"], command = "unfocus_component", description = "Unfocus the current component"}, 24 | # Toggle chat_list visibility 25 | { keys = ["alt+n"], command = "toggle_chat_list", description = "Toggle chat_list visibility"}, 26 | # Increase the chat list size 27 | { keys = ["alt+l"], command = "increase_chat_list_size", description = "Increase the chat list size"}, 28 | # Decrease the chat list size 29 | { keys = ["alt+h"], command = "decrease_chat_list_size", description = "Decrease the chat list size"}, 30 | # Increase the prompt size 31 | { keys = ["alt+k"], command = "increase_prompt_size", description = "Increase the prompt size"}, 32 | # Decrease the prompt size 33 | { keys = ["alt+j"], command = "decrease_prompt_size", description = "Decrease the prompt size"}, 34 | ] 35 | 36 | # The chat_list key bindings are only usable in the chat list component. 37 | # When the chat list is focused, the chat list key bindings will be active. 38 | [chat_list] 39 | keymap = [ 40 | # Select the next chat 41 | { keys = ["down"], command = "chat_list_next", description = "Select the next chat"}, 42 | # Select the previous chat 43 | { keys = ["up"], command = "chat_list_previous", description = "Select the previous chat"}, 44 | # Unselect the current chat 45 | { keys = ["left"], command = "chat_list_unselect", description = "Unselect the current chat"}, 46 | # Open the selected chat 47 | { keys = ["right"], command = "chat_list_open", description = "Open the selected chat"}, 48 | # Open the selected chat 49 | { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"}, 50 | # Move focus to the prompt and set its state to receive a string used to order the entries in the chat list window. 51 | { keys = ["alt+r"], command = "chat_list_search", description = "Focus on prompt to start searching"}, 52 | # Restore the default sorting in the chat list window. 53 | { keys = ["alt+c"], command = "chat_list_restore_sort", description = "Restore the default ordering of the chat list"}, 54 | ] 55 | 56 | # The chat key bindings are only usable in the chat component. 57 | # When the chat is focused, the chat key bindings will be active. 58 | [chat] 59 | keymap = [ 60 | # Select the next message 61 | { keys = ["down"], command = "chat_window_next", description = "Select the next message"}, 62 | # Select the previous message 63 | { keys = ["up"], command = "chat_window_previous", description = "Select the previous message"}, 64 | # Unselect the current message 65 | { keys = ["left"], command = "chat_window_unselect", description = "Unselect the current message"}, 66 | # Delete the selected message for all users 67 | { keys = ["d"], command = "chat_window_delete_for_everyone", description = "Delete the selected message for all users"}, 68 | # Delete the selected message for "me" 69 | { keys = ["D"], command = "chat_window_delete_for_me", description = "Delete the selected message for 'me'"}, 70 | # Copy the selected message 71 | { keys = ["y"], command = "chat_window_copy", description = "Copy the selected message"}, 72 | # Copy the selected message 73 | { keys = ["ctrl+c"], command = "chat_window_copy", description = "Copy the selected message"}, 74 | # Edit the selected message 75 | { keys = ["e"], command = "chat_window_edit", description = "Edit the selected message"}, 76 | # Reply to the selected message 77 | { keys = ["r"], command = "chat_window_reply", description = "Reply to the selected message"}, 78 | ] 79 | 80 | # The prompt key bindings are only usable in the prompt component. 81 | # When the prompt is focused, the prompt key bindings will be active. 82 | [prompt] 83 | keymap = [] 84 | 85 | -------------------------------------------------------------------------------- /config/logger.toml: -------------------------------------------------------------------------------- 1 | # `log_dir` is the folder where the log file will be created 2 | log_dir = ".data/logs" 3 | # `log_file` is the name of the log file 4 | log_file = "tgt.log" 5 | # The rotation frequency of the log. 6 | # The log rotation frequency can be one of the following: 7 | # - minutely: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH-mm will be created minutely (once per minute) 8 | # - hourly: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH will be created hourly 9 | # - daily: A new log file in the format of log_dir/log_file.yyyy-MM-dd will be created daily 10 | # - never: This will result in log file located at log_dir/log_file 11 | rotation_frequency = "daily" 12 | # The maximum number of old log files that will be stored 13 | max_old_log_files = 7 14 | # `log_level` is the level of logging. 15 | # The levels are (based on `RUST_LOG`): 16 | # - error: only log errors 17 | # - warn: log errors and warnings 18 | # - info: log errors, warnings and info 19 | # - debug: log errors, warnings, info and debug 20 | # - trace: log errors, warnings, info, debug and trace 21 | # - off: turn off logging 22 | log_level = "info" 23 | -------------------------------------------------------------------------------- /config/telegram.toml: -------------------------------------------------------------------------------- 1 | # Application identifier for Telegram API access, which can be obtained at https:my.telegram.org 2 | api_id = "94575" 3 | # Application identifier hash for Telegram API access, which can be obtained at https:my.telegram.org 4 | api_hash = "a3406de8d171bb422bb6ddf3bbd800e2" 5 | # The path to the directory for the persistent database; if empty, the current working directory will be used 6 | # If is not overridden, the database will be in the `tgt` directory. 7 | # In Linux and MacOS, the path is: 8 | # $HOME/tgt/.data/tg 9 | # In Windows, the path is: 10 | # C:\Users\YourUsername\tgt\.data\tg 11 | database_dir = ".data/tg" 12 | # Pass true to keep information about downloaded and uploaded files between application restarts 13 | use_file_database = true 14 | # Pass true to keep cache of users, basic groups, supergroups, channels and secret chats between restarts. Implies use_file_database 15 | use_chat_info_database = true 16 | # Pass true to keep cache of chats and messages between restarts. Implies use_chat_info_database 17 | use_message_database = true 18 | # IETF language tag of the user's operating system language; must be non-empty 19 | system_language_code = "en" 20 | # Model of the device the application is being run on; must be non-empty 21 | device_model = "Desktop" 22 | # =========== logging =========== 23 | # New value of the verbosity level for logging. 24 | # Value 0 corresponds to fatal errors, 25 | # value 1 corresponds to errors, 26 | # value 2 corresponds to warnings and debug warnings, 27 | # value 3 corresponds to informational, 28 | # value 4 corresponds to debug, 29 | # value 5 corresponds to verbose debug, 30 | # value greater than 5 and up to 1023 can be used to enable even more logging 31 | verbosity_level = 2 32 | # Path to the file to where the internal TDLib log will be written 33 | # If is not overridden, the log will be in the `tgt` directory. 34 | # In Linux and MacOS, the path is: 35 | # $HOME/tgt/.data/tdlib_rs/tdlib_rs.log 36 | # In Windows, the path is: 37 | # C:\Users\YourUsername\tgt\.data\tdlib_rs\tdlib_rs.log 38 | log_path = ".data/tdlib_rs/tdlib_rs.log" 39 | # Pass true to additionally redirect stderr to the log file. Ignored on Windows 40 | redirect_stderr = false 41 | -------------------------------------------------------------------------------- /config/theme.toml: -------------------------------------------------------------------------------- 1 | [palette] 2 | black = "#000000" 3 | white = "#ffffff" 4 | 5 | background = "#000000" 6 | background_two = "#121212" 7 | background_three = "#242424" 8 | 9 | primary_light = "#e1e1e1" 10 | primary = "#545454" 11 | primary_dark = "#3d3d3d" 12 | 13 | # Current user 14 | secondary_light = "#c0d3f0" 15 | secondary = "#0767a3" 16 | secondary_dark = "#5f758f" 17 | 18 | # Other users 19 | ternary_light = "#f0d2c5" 20 | ternary = "#c75108" 21 | ternary_dark = "#8a3907" 22 | 23 | highlight_one = "#ec5bfc" 24 | highlight_two = "#8a3594" 25 | 26 | [common] 27 | border_component_focused = { fg = "highlight_one", bg = "background", bold = false, underline = false, italic = false } 28 | item_selected = { fg = "", bg = "background_two", bold = true, underline = false, italic = false } 29 | timestamp = { fg = "primary_dark", bg = "background", bold = false, underline = false, italic = false } 30 | 31 | [chat_list] 32 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 33 | item_selected = { fg = "", bg = "background_two", bold = false, underline = false, italic = false } 34 | item_chat_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 35 | item_message_content = { fg = "primary_dark", bg = "background", bold = false, underline = false, italic = true } 36 | item_unread_counter = { fg = "highlight_two", bg = "background", bold = true, underline = false, italic = false } 37 | 38 | [chat] 39 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 40 | chat_name = { fg = "ternary", bg = "background", bold = true, underline = false, italic = false } 41 | message_myself_name = { fg = "secondary", bg = "background", bold = true, underline = false, italic = false } 42 | message_myself_content = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 43 | message_other_name = { fg = "ternary", bg = "background", bold = true, underline = false, italic = false } 44 | message_other_content = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = false } 45 | message_reply_text = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 46 | message_myself_reply_name = { fg = "ternary_dark", bg = "background", bold = true, underline = false, italic = false } 47 | message_myself_reply_content = { fg = "primary_dark", bg = "background", bold = false, underline = false, italic = false } 48 | message_other_reply_name = { fg = "secondary_dark", bg = "background", bold = true, underline = false, italic = false } 49 | message_other_reply_content = { fg = "primary_dark", bg = "background", bold = false, underline = false, italic = false } 50 | 51 | [prompt] 52 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 53 | message_text = { fg = "primary_light", bg = "background", bold = false, underline = false, italic = false } 54 | message_text_selected = { fg = "primary_light", bg = "background_three", bold = false, underline = false, italic = true } 55 | message_preview_text = { fg = "primary_dark", bg = "background", bold = false, underline = false, italic = false } 56 | 57 | [reply_message] 58 | self = { fg = "highlight_two", bg = "background", bold = false, underline = false, italic = false } 59 | message_text = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 60 | 61 | [status_bar] 62 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 63 | size_info_text = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 64 | size_info_numbers = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = true } 65 | press_key_text = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 66 | press_key_key = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = true } 67 | message_quit_text = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 68 | message_quit_key = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = true } 69 | open_chat_text = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 70 | open_chat_name = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = true } 71 | 72 | [title_bar] 73 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 74 | title1 = { fg = "primary_light", bg = "background", bold = true, underline = true, italic = true } 75 | title2 = { fg = "secondary_light", bg = "background", bold = true, underline = true, italic = true } 76 | title3 = { fg = "ternary_light", bg = "background", bold = true, underline = true, italic = false } 77 | -------------------------------------------------------------------------------- /docs/configuration/README.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | `tgt` by default reads its **default** configurations from: 4 | - Linux: `/home//.tgt/config/` 5 | - macOS: `/Users//.tgt/config/` 6 | - Windows: `C:\Users\\.tgt\config/` 7 | 8 | We suggest you to not modify these files, but to create your own **custom** configuration files in the following directories (in order of precedence): 9 | 10 | - `$TGT_CONFIG_DIR` (if set) 11 | - `$HOME/.config/tgt/config` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\config` (for Windows) 12 | 13 | Reading configurations from the following directories will override the fields defined in the default configuration files. 14 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 15 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 16 | 17 | ## Configuration Files 18 | 19 | In the configuration directory, `tgt` looks for the following files: 20 | 21 | - `app.toml` for theme configuration (see [App Confguration](https://github.com/FedericoBruzzone/tgt/blob/main/docs/configuration/app.toml.md)) 22 | - `logger.toml` for logger configuration (see [Logger Configuration](https://github.com/FedericoBruzzone/tgt/blob/main/docs/configuration/logger.toml.md)) 23 | - `telegram.toml` for Telegram configuration (see [Telegram Configuration](https://github.com/FedericoBruzzone/tgt/blob/main/docs/configuration/telegram.toml.md)) 24 | - `theme.toml` for theme configuration (see [Theme Configuration](https://github.com/FedericoBruzzone/tgt/blob/main/docs/configuration/theme.toml.md)) 25 | - `keymap.toml` for keymap configuration (see [Keymap Configuration](https://github.com/FedericoBruzzone/tgt/blob/main/docs/configuration/keymap.toml.md)) 26 | -------------------------------------------------------------------------------- /docs/configuration/app.toml.md: -------------------------------------------------------------------------------- 1 | # app.toml 2 | 3 | ## Default application configuration 4 | 5 | ```toml 6 | # `mouse_support` enables mouse support in the terminal. 7 | mouse_support = true 8 | # `paste_support` enables paste support in the terminal. 9 | paste_support = true 10 | # `frame_rate` is the frame rate of the terminal in frames per second. 11 | # The suggested frame rate are: 12 | # - 120.0 for ultra smooth animations 13 | # - 60.0 for smooth animations 14 | # - 30.0 for normal animations 15 | # - 15.0 for slow animations 16 | frame_rate = 60.0 17 | # `show_status_bar` enables the status bar at the bottom of the terminal. 18 | show_status_bar = true 19 | # `show_title_bar` enables the title bar at the top of the terminal. 20 | show_title_bar = true 21 | # `theme_enable` enables the theme. 22 | theme_enable = true 23 | # `theme_filename` is the name of the file that contains the theme. 24 | # This file must be in the configuration directory. 25 | theme_filename = "theme.toml" 26 | # `take_api_id_from_telegram_config` enables taking the API_ID from the Telegram configuration file 27 | # or from the environment variable `API_ID`. 28 | take_api_id_from_telegram_config = true 29 | # `take_api_hash_from_telegram_config` enables taking the API_HASH from the Telegram configuration file 30 | # or from the environment variable `API_HASH`. 31 | take_api_hash_from_telegram_config = true 32 | ``` 33 | 34 | ## Custom configuration 35 | 36 | ### How create a custom configuration file 37 | 38 | `tgt` by default reads its **default** configurations from: 39 | - Linux: `/home//.tgt/config/` 40 | - macOS: `/Users//.tgt/config/` 41 | - Windows: `C:\Users\\.tgt\config/` 42 | 43 | We suggest you to not modify this file, but to create your own **custom** configuration file in the following directories (in order of precedence): 44 | 45 | - `$TGT_CONFIG_DIR` (if set) 46 | - `$HOME/.config/tgt/` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\` (for Windows) 47 | 48 | Reading configurations from the following directories will override the fields defined in the default configuration files. 49 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 50 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 51 | 52 | ### Example of a custom application configuration 53 | 54 | Example of `app.toml`: 55 | 56 | ```toml 57 | show_status_bar = false 58 | show_title_bar = false 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/configuration/keymap.toml.md: -------------------------------------------------------------------------------- 1 | # keymap.toml 2 | 3 | ## Default keymap configuration 4 | 5 | ```toml 6 | # The core_window key bindings are usable in any app component. 7 | [core_window] 8 | keymap = [ 9 | # Quit the application 10 | # Note that when the prompt is focused, the "q" key will be used to type the letter "q". 11 | { keys = ["q"], command = "try_quit", description = "Quit the application"}, 12 | # Quit the application 13 | { keys = ["ctrl+c"], command = "try_quit", description = "Quit the application"}, 14 | # Focus the chat list 15 | { keys = ["alt+1"], command = "focus_chat_list", description = "Focus the chat list"}, 16 | { keys = ["alt+left"], command = "focus_chat_list", description = "Focus the chat list"}, 17 | # Focus the chat 18 | { keys = ["alt+2"], command = "focus_chat", description = "Focus the chat"}, 19 | { keys = ["alt+right"], command = "focus_chat", description = "Focus the chat"}, 20 | # Focus the prompt 21 | { keys = ["alt+3"], command = "focus_prompt", description = "Focus the prompt"}, 22 | { keys = ["alt+down"], command = "focus_prompt", description = "Focus the prompt"}, 23 | # Unfocus the current component 24 | { keys = ["esc"], command = "unfocus_component", description = "Unfocus the current component"}, 25 | { keys = ["alt+up"], command = "unfocus_component", description = "Unfocus the current component"}, 26 | # Toggle chat_list visibility 27 | { keys = ["alt+n"], command = "toggle_chat_list", description = "Toggle chat_list visibility"}, 28 | # Increase the chat list size 29 | { keys = ["alt+l"], command = "increase_chat_list_size", description = "Increase the chat list size"}, 30 | # Decrease the chat list size 31 | { keys = ["alt+h"], command = "decrease_chat_list_size", description = "Decrease the chat list size"}, 32 | # Increase the prompt size 33 | { keys = ["alt+k"], command = "increase_prompt_size", description = "Increase the prompt size"}, 34 | # Decrease the prompt size 35 | { keys = ["alt+j"], command = "decrease_prompt_size", description = "Decrease the prompt size"}, 36 | ] 37 | 38 | # The chat_list key bindings are only usable in the chat list component. 39 | # When the chat list is focused, the chat list key bindings will be active. 40 | [chat_list] 41 | keymap = [ 42 | # Select the next chat 43 | { keys = ["down"], command = "chat_list_next", description = "Select the next chat"}, 44 | # Select the previous chat 45 | { keys = ["up"], command = "chat_list_previous", description = "Select the previous chat"}, 46 | # Unselect the current chat 47 | { keys = ["left"], command = "chat_list_unselect", description = "Unselect the current chat"}, 48 | # Open the selected chat 49 | { keys = ["right"], command = "chat_list_open", description = "Open the selected chat"}, 50 | # Open the selected chat 51 | { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"}, 52 | ] 53 | 54 | # The chat key bindings are only usable in the chat component. 55 | # When the chat is focused, the chat key bindings will be active. 56 | [chat] 57 | keymap = [ 58 | # Select the next message 59 | { keys = ["down"], command = "chat_window_next", description = "Select the next message"}, 60 | # Select the previous message 61 | { keys = ["up"], command = "chat_window_previous", description = "Select the previous message"}, 62 | # Unselect the current message 63 | { keys = ["left"], command = "chat_window_unselect", description = "Unselect the current message"}, 64 | # Delete the selected message for all users 65 | { keys = ["d"], command = "chat_window_delete_for_everyone", description = "Delete the selected message for all users"}, 66 | # Delete the selected message for "me" 67 | { keys = ["D"], command = "chat_window_delete_for_me", description = "Delete the selected message for 'me'"}, 68 | # Copy the selected message 69 | { keys = ["y"], command = "chat_window_copy", description = "Copy the selected message"}, 70 | # Copy the selected message 71 | { keys = ["ctrl+c"], command = "chat_window_copy", description = "Copy the selected message"}, 72 | # Edit the selected message 73 | { keys = ["e"], command = "chat_window_edit", description = "Edit the selected message"}, 74 | # Reply to the selected message 75 | { keys = ["r"], command = "chat_window_reply", description = "Reply to the selected message"}, 76 | ] 77 | 78 | # The prompt key bindings are only usable in the prompt component. 79 | # When the prompt is focused, the prompt key bindings will be active. 80 | [prompt] 81 | 82 | ``` 83 | 84 | ## Custom configuration 85 | 86 | ### How create a custom configuration file 87 | 88 | `tgt` by default reads its **default** configurations from: 89 | - Linux: `/home//.tgt/config/` 90 | - macOS: `/Users//.tgt/config/` 91 | - Windows: `C:\Users\\.tgt\config/` 92 | 93 | We suggest you to not modify this file, but to create your own **custom** configuration file in the following directories (in order of precedence): 94 | 95 | - `$TGT_CONFIG_DIR` (if set) 96 | - `$HOME/.config/tgt/` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\` (for Windows) 97 | 98 | Reading configurations from the following directories will override the fields defined in the default configuration files. 99 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 100 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 101 | 102 | ## Example of a custom keymap configuration 103 | 104 | Example of `keymap.toml`: 105 | 106 | ```toml 107 | [core_window] 108 | keymap = [ 109 | # Quit the application with "q" followed by "a" 110 | { keys = ["q", "a"], command = "quit", description = "Quit the application with 'q' followed by 'a'"}, 111 | ] 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/configuration/logger.toml.md: -------------------------------------------------------------------------------- 1 | # logger.toml 2 | 3 | ## Default logger configuration 4 | 5 | ```toml 6 | # `log_dir` is the folder where the log file will be created. 7 | # This folder is relative to the `tgt` home directory. 8 | log_dir = ".data/logs" 9 | # `log_file` is the name of the log file. 10 | log_file = "tgt.log" 11 | # The rotation frequency of the log. 12 | # The log rotation frequency can be one of the following: 13 | # - minutely: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH-mm will be created minutely (once per minute) 14 | # - hourly: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH will be created hourly 15 | # - daily: A new log file in the format of log_dir/log_file.yyyy-MM-dd will be created daily 16 | # - never: This will result in log file located at log_dir/log_file 17 | rotation_frequency = "daily" 18 | # The maximum number of old log files that will be stored 19 | max_old_log_files = 7 20 | # `log_level` is the level of logging. 21 | # The levels are (based on `RUST_LOG`): 22 | # - error: only log errors 23 | # - warn: log errors and warnings 24 | # - info: log errors, warnings and info 25 | # - debug: log errors, warnings, info and debug 26 | # - trace: log errors, warnings, info, debug and trace 27 | # - off: turn off logging 28 | log_level = "info" 29 | ``` 30 | 31 | ## Custom logger configuration 32 | 33 | ### How create a custom configuration file 34 | 35 | `tgt` by default reads its **default** configurations from: 36 | - Linux: `/home//.tgt/config/` 37 | - macOS: `/Users//.tgt/config/` 38 | - Windows: `C:\Users\\.tgt\config/` 39 | 40 | We suggest you to not modify these files, but to create your own **custom** configuration files in the following directories (in order of precedence): 41 | 42 | - `$TGT_CONFIG_DIR` (if set) 43 | - `$HOME/.config/tgt/` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\` (for Windows) 44 | 45 | Reading configurations from the following directories will override the fields defined in the default configuration files. 46 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 47 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 48 | 49 | ### Example of a custom logger configuration 50 | 51 | Example of `logger.toml`: 52 | 53 | ```toml 54 | log_level = "off" 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/configuration/telegram.toml.md: -------------------------------------------------------------------------------- 1 | # telegram.toml 2 | 3 | ## Default telegram configuration 4 | 5 | ```toml 6 | # Application identifier for Telegram API access, which can be obtained at https:my.telegram.org 7 | api_id = "94575" 8 | # Application identifier hash for Telegram API access, which can be obtained at https:my.telegram.org 9 | api_hash = "a3406de8d171bb422bb6ddf3bbd800e2" 10 | # The path to the directory for the persistent database; if empty, the current working directory will be used 11 | # If is not overridden, the database will be in the `tgt` directory. 12 | # In Linux and MacOS, the path is: 13 | # $HOME/tgt/.data/tg 14 | # In Windows, the path is: 15 | # C:\Users\YourUsername\tgt\.data\tg 16 | database_dir = ".data/tg" 17 | # Pass true to keep information about downloaded and uploaded files between application restarts 18 | use_file_database = true 19 | # Pass true to keep cache of users, basic groups, supergroups, channels and secret chats between restarts. Implies use_file_database 20 | use_chat_info_database = true 21 | # Pass true to keep cache of chats and messages between restarts. Implies use_chat_info_database 22 | use_message_database = true 23 | # IETF language tag of the user's operating system language; must be non-empty 24 | system_language_code = "en" 25 | # Model of the device the application is being run on; must be non-empty 26 | device_model = "Desktop" 27 | # =========== logging =========== 28 | # New value of the verbosity level for logging. 29 | # Value 0 corresponds to fatal errors, 30 | # value 1 corresponds to errors, 31 | # value 2 corresponds to warnings and debug warnings, 32 | # value 3 corresponds to informational, 33 | # value 4 corresponds to debug, 34 | # value 5 corresponds to verbose debug, 35 | # value greater than 5 and up to 1023 can be used to enable even more logging 36 | verbosity_level = 2 37 | # Path to the file to where the internal TDLib log will be written 38 | # If is not overridden, the log will be in the `tgt` directory. 39 | # In Linux and MacOS, the path is: 40 | # $HOME/tgt/.data/tdlib_rs/tdlib_rs.log 41 | # In Windows, the path is: 42 | # C:\Users\YourUsername\tgt\.data\tdlib_rs\tdlib_rs.log 43 | log_path = ".data/tdlib_rs/tdlib_rs.log" 44 | # Pass true to additionally redirect stderr to the log file. Ignored on Windows 45 | redirect_stderr = false 46 | ``` 47 | 48 | ## Custom configuration 49 | 50 | ### How create a custom configuration file 51 | 52 | `tgt` by default reads its **default** configurations from: 53 | - Linux: `/home//.tgt/config/` 54 | - macOS: `/Users//.tgt/config/` 55 | - Windows: `C:\Users\\.tgt\config/` 56 | 57 | We suggest you to not modify this file, but to create your own **custom** configuration file in the following directories (in order of precedence): 58 | 59 | - `$TGT_CONFIG_DIR` (if set) 60 | - `$HOME/.config/tgt/` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\` (for Windows) 61 | 62 | Reading configurations from the following directories will override the fields defined in the default configuration files. 63 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 64 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 65 | 66 | ### Example of a custom telegram configuration 67 | 68 | Example of `telegram.toml`: 69 | 70 | ```toml 71 | api_id = "" 72 | api_hash = "" 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/configuration/terminal-config.md: -------------------------------------------------------------------------------- 1 | # Terminal on MacOS 2 | 3 | - [Option -> Alt](https://github.com/helix-editor/helix/issues/2280) 4 | -------------------------------------------------------------------------------- /docs/configuration/theme.toml.md: -------------------------------------------------------------------------------- 1 | # theme.toml 2 | 3 | In the `theme.toml` you can define the `palette` (see palette section in the example below) and the styles for each component: 4 | 5 | - `common`: In the common section you can define the styles that are common to all components. For example, the style for the focused border component. 6 | - `chat_list`: In the chat_list section you can define the styles for the chat list component. 7 | - `chat`: In the chat section you can define the styles for the chat component. 8 | - `prompt`: In the prompt section you can define the styles for the prompt component. 9 | - `status_bar`: In the status_bar section you can define the styles for the status bar component. 10 | - `title_bar`: In the title_bar section you can define the styles for the title bar component. 11 | 12 | Each component has a `self` style that defines the style of the component itself. The other styles are specific to the component and define the style of the elements inside the component. 13 | 14 | ## The Palette 15 | 16 | The palette is a section in the theme configuration where you can define the colors that will be used in the theme. The colors defined in the palette can be used in the styles of the components. The palette section is optional, you can define the colors directly in the styles of the components but it is not recommended. 17 | 18 | By default, each component of `tgt` use the colors defined in the palette. So, if you want to create a custom theme, you can change the colors in the palette and the components will use the new colors. 19 | 20 | ## Color Format 21 | 22 | The supported color formats are: 23 | 24 | - Hexadecimal: `#RGB` or `#RRGGBB` where `R`, `G`, and `B` are hexadecimal digits (not case-sensitive). 25 | - RGB: `R, G, B` where `R`, `G`, and `B` are integers between 0 and 255. 26 | - Palette: The palette colors are defined in the `palette` section of the theme configuration. For example, `primary`, `secondary`, `background`, etc. 27 | - Default: 28 | - `black`: The default black color. 29 | - `red`: The default red color. 30 | - `green`: The default green color. 31 | - `yellow`: The default yellow color. 32 | - `blue`: The default blue color. 33 | - `magenta`: The default magenta color. 34 | - `cyan`: The default cyan color. 35 | - `gray`: The default gray color. 36 | - `dark_gray`: The default dark gray color. 37 | - `light_red`: The default light red color. 38 | - `light_green`: The default light green color. 39 | - `light_yellow`: The default light yellow color. 40 | - `light_blue`: The default light blue color. 41 | - `light_magenta`: The default light magenta color. 42 | - `light_cyan`: The default light cyan color. 43 | - `white`: The default white color. 44 | - `reset`: The default reset color. 45 | 46 | ## Default theme configuration 47 | 48 | ```toml 49 | [palette] 50 | black = "#000000" 51 | white = "#ffffff" 52 | background = "#000000" 53 | primary = "#00548e" 54 | primary_variant = "#0073b0" 55 | primary_light = "#94dbf7" 56 | secondary = "#ca3f04" 57 | secondary_variant = "#e06819" 58 | secondary_light = "#fcac77" 59 | ternary = "#696969" 60 | ternary_variant = "#808080" 61 | ternary_light = "#6e7e85" 62 | surface = "#141414" 63 | on_surface = "#dcdcdc" 64 | error = "#D50000" 65 | on_error = "#FFCDD2" 66 | 67 | [common] 68 | border_component_focused = { fg = "secondary", bg = "background", bold = false, underline = false, italic = false } 69 | item_selected = { fg = "", bg = "surface", bold = true, underline = false, italic = false } 70 | timestamp = { fg = "ternary_light", bg = "background", bold = false, underline = false, italic = false } 71 | 72 | [chat_list] 73 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 74 | item_selected = { fg = "", bg = "primary", bold = false, underline = false, italic = false } 75 | item_chat_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 76 | item_message_content = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = true } 77 | item_unread_counter = { fg = "secondary", bg = "background", bold = true, underline = false, italic = false } 78 | 79 | [chat] 80 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 81 | chat_name = { fg = "secondary", bg = "background", bold = true, underline = false, italic = false } 82 | message_myself_name = { fg = "primary_light", bg = "background", bold = true, underline = false, italic = false } 83 | message_myself_content = { fg = "primary_variant", bg = "background", bold = false, underline = false, italic = false } 84 | message_other_name = { fg = "secondary_light", bg = "background", bold = true, underline = false, italic = false } 85 | message_other_content = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 86 | message_reply_text = { fg = "ternary", bg = "background", bold = false, underline = false, italic = false } 87 | message_reply_name = { fg = "secondary_light", bg = "background", bold = true, underline = false, italic = false } 88 | message_reply_content = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 89 | 90 | [prompt] 91 | self = { fg = "primary", bg = "background", bold = false, underline = false, italic = false } 92 | message_text = { fg = "primary_light", bg = "background", bold = false, underline = false, italic = false } 93 | message_text_selected = { fg = "secondary_light", bg = "ternary", bold = false, underline = false, italic = true } 94 | message_preview_text = { fg = "ternary", bg = "background", bold = false, underline = false, italic = false } 95 | 96 | [reply_message] 97 | self = { fg = "secondary_light", bg = "background", bold = false, underline = false, italic = false } 98 | message_text = { fg = "secondary_variant", bg = "background", bold = false, underline = false, italic = false } 99 | 100 | [status_bar] 101 | self = { fg = "on_surface", bg = "surface", bold = false, underline = false, italic = false } 102 | size_info_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 103 | size_info_numbers = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 104 | press_key_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 105 | press_key_key = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 106 | message_quit_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 107 | message_quit_key = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 108 | open_chat_text = { fg = "primary_light", bg = "surface", bold = false, underline = false, italic = false } 109 | open_chat_name = { fg = "secondary_light", bg = "surface", bold = false, underline = false, italic = true } 110 | 111 | [title_bar] 112 | self = { fg = "on_surface", bg = "surface", bold = false, underline = false, italic = false } 113 | title1 = { fg = "primary_light", bg = "surface", bold = true, underline = true, italic = true } 114 | title2 = { fg = "secondary_light", bg = "surface", bold = true, underline = true, italic = true } 115 | title3 = { fg = "ternary_light", bg = "surface", bold = true, underline = true, italic = false } 116 | ``` 117 | 118 | ## Custom configuration 119 | 120 | ### How create a custom configuration file 121 | 122 | `tgt` by default reads its **default** configurations from: 123 | - Linux: `/home//.tgt/config/` 124 | - macOS: `/Users//.tgt/config/` 125 | - Windows: `C:\Users\\.tgt\config/` 126 | 127 | We suggest you to not modify this file, but to create your own **custom** configuration file in the following directories (in order of precedence): 128 | 129 | - `$TGT_CONFIG_DIR` (if set) 130 | - `$HOME/.config/tgt/` (for Linux and macOS) and `C:\Users\\AppData\Roaming\tgt\` (for Windows) 131 | 132 | Reading configurations from the following directories will override the fields defined in the default configuration files. 133 | It means that the fields that are not present in the custom configuration will be taken from the default configuration, while the fields that are present in the custom configuration will override the default configuration. 134 | Note that after the finding the first configuration file, `tgt` stops looking for more configurations, it is short-circuited. 135 | 136 | ### Example of a custom theme configuration 137 | 138 | Example of `theme.toml`: 139 | 140 | ```toml 141 | [palette] 142 | test_color = "#ff0000" 143 | background = "#ffffff" 144 | 145 | [common] 146 | border_component_focused = { fg = "test_color", bg = "background", bold = false, underline = false, italic = false } 147 | ``` 148 | 149 | -------------------------------------------------------------------------------- /docs/vim_keymap.toml: -------------------------------------------------------------------------------- 1 | # The core_window key bindings are usable in any app component. 2 | [core_window] 3 | keymap = [ 4 | # Quit the application 5 | # Note that when the prompt is focused, the "q" key will be used to type the letter "q". 6 | { keys = ["q"], command = "try_quit", description = "Quit the application"}, 7 | # Quit the application 8 | { keys = ["ctrl+c"], command = "try_quit", description = "Quit the application"}, 9 | # Focus the chat list 10 | { keys = ["alt+1"], command = "focus_chat_list", description = "Focus the chat list"}, 11 | # Focus the chat 12 | { keys = ["alt+2"], command = "focus_chat", description = "Focus the chat"}, 13 | # Focus the prompt 14 | { keys = ["alt+3"], command = "focus_prompt", description = "Focus the prompt"}, 15 | # Unfocus the current component 16 | { keys = ["esc"], command = "unfocus_component", description = "Unfocus the current component"}, 17 | # Toggle chat_list visibility 18 | { keys = ["ctrl+x"], command = "toggle_chat_list", description = "Toggle chat_list visibility"}, 19 | # Increase the chat list size 20 | { keys = ["ctrl+l"], command = "increase_chat_list_size", description = "Increase the chat list size"}, 21 | # Decrease the chat list size 22 | { keys = ["ctrl+h"], command = "decrease_chat_list_size", description = "Decrease the chat list size"}, 23 | # Increase the prompt size 24 | { keys = ["ctrl+k"], command = "increase_prompt_size", description = "Increase the prompt size"}, 25 | # Decrease the prompt size 26 | { keys = ["ctrl+j"], command = "decrease_prompt_size", description = "Decrease the prompt size"}, 27 | ] 28 | 29 | # The chat_list key bindings are only usable in the chat list component. 30 | # When the chat list is focused, the chat list key bindings will be active. 31 | [chat_list] 32 | keymap = [ 33 | # Select the next chat 34 | { keys = ["j"], command = "chat_list_next", description = "Select the next chat"}, 35 | # Select the previous chat 36 | { keys = ["k"], command = "chat_list_previous", description = "Select the previous chat"}, 37 | # Unselect the current chat 38 | { keys = ["h"], command = "chat_list_unselect", description = "Unselect the current chat"}, 39 | # Open the selected chat 40 | { keys = ["l"], command = "chat_list_open", description = "Open the selected chat"}, 41 | # Open the selected chat 42 | { keys = ["enter"], command = "chat_list_open", description = "Open the selected chat"}, 43 | ] 44 | 45 | # The chat key bindings are only usable in the chat component. 46 | # When the chat is focused, the chat key bindings will be active. 47 | [chat] 48 | keymap = [ 49 | # Select the next message 50 | { keys = ["j"], command = "chat_window_next", description = "Select the next message"}, 51 | # Select the previous message 52 | { keys = ["k"], command = "chat_window_previous", description = "Select the previous message"}, 53 | # Unselect the current message 54 | { keys = ["h"], command = "chat_window_unselect", description = "Unselect the current message"}, 55 | # Delete the selected message for all users 56 | { keys = ["d"], command = "chat_window_delete_for_everyone", description = "Delete the selected message for all users"}, 57 | # Delete the selected message for "me" 58 | { keys = ["D"], command = "chat_window_delete_for_me", description = "Delete the selected message for 'me'"}, 59 | # Copy the selected message 60 | { keys = ["y"], command = "chat_window_copy", description = "Copy the selected message"}, 61 | # Edit the selected message 62 | { keys = ["e"], command = "chat_window_edit", description = "Edit the selected message"}, 63 | # Reply to the selected message 64 | { keys = ["r"], command = "chat_window_reply", description = "Reply to the selected message"}, 65 | ] 66 | 67 | # The prompt key bindings are only usable in the prompt component. 68 | # When the prompt is focused, the prompt key bindings will be active. 69 | [prompt] 70 | -------------------------------------------------------------------------------- /examples/get_me.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicBool, Ordering}, 3 | Arc, 4 | }; 5 | use tdlib_rs::{ 6 | enums::{AuthorizationState, Update, User}, 7 | functions, 8 | }; 9 | use tokio::sync::mpsc::{self, Receiver, Sender}; 10 | 11 | fn ask_user(string: &str) -> String { 12 | println!("{}", string); 13 | let mut input = String::new(); 14 | std::io::stdin().read_line(&mut input).unwrap(); 15 | input.trim().to_string() 16 | } 17 | 18 | async fn handle_update(update: Update, auth_tx: &Sender) { 19 | if let Update::AuthorizationState(update) = update { 20 | auth_tx.send(update.authorization_state).await.unwrap(); 21 | } 22 | } 23 | 24 | async fn handle_authorization_state( 25 | client_id: i32, 26 | mut auth_rx: Receiver, 27 | run_flag: Arc, 28 | ) -> Option> { 29 | let api_id: i32 = { 30 | // `env!("API_ID").parse().unwrap()` generates a compile time error 31 | if let Ok(api_id) = std::env::var("API_ID") { 32 | api_id.parse().unwrap() 33 | } else { 34 | tracing::error!("API_ID not found in environment"); 35 | "94575".parse().unwrap() // This will throw the tdlib-rs error message 36 | } 37 | }; 38 | let api_hash: String = { 39 | // `env!("API_HASH").into()` generates a compile time error 40 | if let Ok(api_hash) = std::env::var("API_HASH") { 41 | api_hash 42 | } else { 43 | "a3406de8d171bb422bb6ddf3bbd800e2".into() // This will throw the tdlib-rs error message 44 | } 45 | }; 46 | 47 | while let Some(state) = auth_rx.recv().await { 48 | match state { 49 | AuthorizationState::WaitTdlibParameters => { 50 | let response = functions::set_tdlib_parameters( 51 | false, 52 | ".data/example".into(), 53 | String::new(), 54 | String::new(), 55 | false, 56 | false, 57 | false, 58 | false, 59 | api_id, 60 | api_hash.clone(), 61 | "en".into(), 62 | "Desktop".into(), 63 | String::new(), 64 | env!("CARGO_PKG_VERSION").into(), 65 | client_id, 66 | ) 67 | .await; 68 | 69 | if let Err(error) = response { 70 | println!("{}", error.message); 71 | } 72 | } 73 | AuthorizationState::WaitPhoneNumber => loop { 74 | let input = ask_user("Enter your phone number (include the country calling code):"); 75 | let response = 76 | functions::set_authentication_phone_number(input, None, client_id).await; 77 | match response { 78 | Ok(_) => break, 79 | Err(e) => println!("{}", e.message), 80 | } 81 | }, 82 | AuthorizationState::WaitCode(_) => loop { 83 | let input = ask_user("Enter the verification code:"); 84 | let response = functions::check_authentication_code(input, client_id).await; 85 | match response { 86 | Ok(_) => break, 87 | Err(e) => println!("{}", e.message), 88 | } 89 | }, 90 | AuthorizationState::Ready => { 91 | break; 92 | } 93 | AuthorizationState::Closed => { 94 | // Set the flag to false to stop receiving updates from the 95 | // spawned task 96 | run_flag.store(false, Ordering::Release); 97 | return None; 98 | // break; 99 | } 100 | _ => (), 101 | } 102 | } 103 | 104 | Some(auth_rx) 105 | } 106 | 107 | #[tokio::main] 108 | async fn main() { 109 | // Create the client object 110 | let client_id = tdlib_rs::create_client(); 111 | 112 | // Create a mpsc channel for handling AuthorizationState updates separately 113 | // from the task 114 | let (auth_tx, auth_rx) = mpsc::channel(5); 115 | 116 | // Create a flag to make it possible to stop receiving updates 117 | let run_flag = Arc::new(AtomicBool::new(true)); 118 | let run_flag_clone = run_flag.clone(); 119 | 120 | // Spawn a task to receive updates/responses 121 | let handle = tokio::spawn(async move { 122 | loop { 123 | if !run_flag_clone.load(Ordering::Acquire) { 124 | break; 125 | } 126 | 127 | if let Some((update, _client_id)) = tdlib_rs::receive() { 128 | handle_update(update, &auth_tx).await; 129 | } 130 | } 131 | }); 132 | 133 | // Set a fairly low verbosity level. We mainly do this because tdlib_rs 134 | // requires to perform a random request with the client to start receiving 135 | // updates for it. 136 | functions::set_log_verbosity_level(2, client_id) 137 | .await 138 | .unwrap(); 139 | 140 | // Handle the authorization state to authenticate the client 141 | let auth_rx = handle_authorization_state(client_id, auth_rx, run_flag.clone()) 142 | .await 143 | .unwrap(); 144 | 145 | // Run the get_me() method to get user information 146 | let User::User(me) = functions::get_me(client_id).await.unwrap(); 147 | println!("Hi, I'm {}", me.first_name); 148 | 149 | // Tell the client to close 150 | functions::close(client_id).await.unwrap(); 151 | 152 | // Handle the authorization state to wait for the "Closed" state 153 | if (handle_authorization_state(client_id, auth_rx, run_flag.clone()).await).is_none() { 154 | std::process::exit(0) 155 | } 156 | 157 | println!("BEFORE"); 158 | // Wait for the previously spawned task to end the execution 159 | match handle.await { 160 | Ok(_) => (), 161 | Err(e) => println!("Error: {:?}", e), 162 | } 163 | 164 | println!("AFTER"); 165 | } 166 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1730785428, 6 | "narHash": "sha256-Zwl8YgTVJTEum+L+0zVAWvXAGbWAuXHax3KzuejaDyo=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "4aa36568d413aca0ea84a1684d2d46f55dbabad7", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 3 | 4 | outputs = 5 | inputs: 6 | let 7 | systems = [ 8 | "aarch64-darwin" 9 | "aarch64-linux" 10 | "x86_64-darwin" 11 | "x86_64-linux" 12 | ]; 13 | forEachSystem = inputs.nixpkgs.lib.genAttrs systems; 14 | in 15 | { 16 | packages = forEachSystem ( 17 | system: 18 | let 19 | pkgs = inputs.nixpkgs.legacyPackages.${system}; 20 | inherit (pkgs) lib; 21 | inherit (pkgs.stdenvNoCC.hostPlatform) isDarwin; 22 | tdlib = pkgs.tdlib.overrideAttrs { 23 | version = "1.8.29"; 24 | src = pkgs.fetchFromGitHub { 25 | owner = "tdlib"; 26 | repo = "td"; 27 | rev = "af69dd4397b6dc1bf23ba0fd0bf429fcba6454f6"; 28 | hash = "sha256-2RhKSxy0AvuA74LHI86pqUxv9oJZ+ZxxDe4TPI5UYxE="; 29 | }; 30 | }; 31 | rlinkLibs = builtins.attrValues { 32 | inherit (pkgs) 33 | pkg-config 34 | openssl 35 | ; 36 | inherit tdlib; 37 | }; 38 | in 39 | { 40 | default = pkgs.rustPlatform.buildRustPackage { 41 | pname = "tgt"; 42 | version = "unstable-2024-11-04"; 43 | src = pkgs.fetchFromGitHub { 44 | owner = "FedericoBruzzone"; 45 | repo = "tgt"; 46 | rev = "39fb4acec241e2db384e268c77e875bd13a48c12"; 47 | sha256 = "sha256-McZEnRwtGEuhDA1uJ1FgUl6QiPfzCDr/Pl2haF9+MRw="; 48 | }; 49 | 50 | nativeBuildInputs = 51 | rlinkLibs 52 | ++ lib.optional isDarwin (builtins.attrValues { inherit (pkgs) apple-sdk_12; }); 53 | 54 | buildInputs = rlinkLibs; 55 | 56 | patches = [ ./patches/0001-check-filesystem-writability-before-operations.patch ]; 57 | 58 | # Tests are broken on nix 59 | doCheck = false; 60 | 61 | cargoHash = "sha256-WIs9rVhTQn217DHIw1SPnQrkDtozEl2jfqVjTwJHF2w="; 62 | buildNoDefaultFeatures = true; 63 | buildFeatures = [ "pkg-config" ]; 64 | 65 | env = { 66 | RUSTFLAGS = "-C link-arg=-Wl,-rpath,${tdlib}/lib -L ${pkgs.openssl}/lib"; 67 | LOCAL_TDLIB_PATH = "${tdlib}/lib"; 68 | }; 69 | 70 | meta = { 71 | description = "TUI for Telegram written in Rust"; 72 | homepage = "https://github.com/FedericoBruzzone/tgt"; 73 | license = lib.licenses.free; 74 | maintainers = with lib.maintainers; [ donteatoreo ]; 75 | }; 76 | }; 77 | } 78 | ); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /imgs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoBruzzone/tgt/50f25dce125c12a2028ba8aeb2666ae44cd9b573/imgs/example.png -------------------------------------------------------------------------------- /imgs/example_movie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoBruzzone/tgt/50f25dce125c12a2028ba8aeb2666ae44cd9b573/imgs/example_movie.gif -------------------------------------------------------------------------------- /imgs/example_movie.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoBruzzone/tgt/50f25dce125c12a2028ba8aeb2666ae44cd9b573/imgs/example_movie.webm -------------------------------------------------------------------------------- /imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FedericoBruzzone/tgt/50f25dce125c12a2028ba8aeb2666ae44cd9b573/imgs/logo.png -------------------------------------------------------------------------------- /patches/0001-check-filesystem-writability-before-operations.patch: -------------------------------------------------------------------------------- 1 | diff --git a/build.rs b/build.rs 2 | index e211e5b..3da6109 100644 3 | --- a/build.rs 4 | +++ b/build.rs 5 | @@ -1,3 +1,7 @@ 6 | +fn is_writable>(path: P) -> bool { 7 | + std::fs::OpenOptions::new().write(true).open(path).is_ok() 8 | +} 9 | + 10 | fn empty_tgt_folder() { 11 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 12 | let _ = std::fs::remove_dir_all(format!("{}/.tgt/config", home)); 13 | @@ -24,11 +28,18 @@ fn main() -> std::io::Result<()> { 14 | return Ok(()); 15 | } 16 | 17 | - empty_tgt_folder(); 18 | - move_config_folder_to_home_dottgt(); 19 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 20 | - let dest = format!("{}/.tgt/tdlib", home); 21 | - tdlib_rs::build::build(Some(dest)); 22 | + let tgt_config_path = format!("{}/.tgt/config", home); 23 | + 24 | + if is_writable(&tgt_config_path) { 25 | + empty_tgt_folder(); 26 | + move_config_folder_to_home_dottgt(); 27 | + let dest = format!("{}/.tgt/tdlib", home); 28 | + tdlib_rs::build::build(Some(dest)); 29 | + } else { 30 | + eprintln!("Filesystem is read-only. Skipping file operations."); 31 | + tdlib_rs::build::build(None); 32 | + } 33 | 34 | Ok(()) 35 | } 36 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | reorder_imports = true 2 | 3 | # unstable_features = true 4 | # tab_spaces = 4 5 | # max_width = 80 6 | # wrap_comments = true 7 | # imports_granularity = "One" 8 | 9 | # NOT USED 10 | # use_small_heuristics = "Max" 11 | # match_block_trailing_comma = true 12 | # reorder_imports = true 13 | # use_field_init_shorthand = true 14 | # use_try_shorthand = true 15 | -------------------------------------------------------------------------------- /src/action.rs: -------------------------------------------------------------------------------- 1 | use { 2 | super::component_name::ComponentName, 3 | crate::{ 4 | app_error::AppError, 5 | tg::td_enums::{TdChatList, TdMessageReplyToMessage}, 6 | }, 7 | crossterm::event::{KeyCode, KeyModifiers}, 8 | ratatui::layout::Rect, 9 | std::str::FromStr, 10 | }; 11 | 12 | #[derive(Debug, Clone, Eq, PartialEq)] 13 | /// `Modifiers` is a struct that represents the modifiers of a key event. 14 | /// It is used to determine the state of the modifiers when a key event is 15 | /// generated. 16 | pub struct Modifiers { 17 | /// A boolean that represents the shift modifier. 18 | pub shift: bool, 19 | /// A boolean that represents the control modifier. 20 | pub control: bool, 21 | /// A boolean that represents the alt modifier. 22 | pub alt: bool, 23 | /// A boolean that represents the super modifier. 24 | pub super_: bool, 25 | /// A boolean that represents the hyper modifier. 26 | pub hyper: bool, 27 | /// A boolean that represents the meta modifier. 28 | pub meta: bool, 29 | } 30 | /// Implement the `From` trait for `KeyModifiers` 31 | impl From for Modifiers { 32 | fn from(modifiers: KeyModifiers) -> Self { 33 | Modifiers { 34 | shift: modifiers.contains(KeyModifiers::SHIFT), 35 | control: modifiers.contains(KeyModifiers::CONTROL), 36 | alt: modifiers.contains(KeyModifiers::ALT), 37 | super_: modifiers.contains(KeyModifiers::SUPER), 38 | hyper: modifiers.contains(KeyModifiers::HYPER), 39 | meta: modifiers.contains(KeyModifiers::META), 40 | } 41 | } 42 | } 43 | /// Implement the `From` trait for `Modifiers` 44 | impl From for KeyModifiers { 45 | fn from(modifiers: Modifiers) -> Self { 46 | let mut key_modifiers = KeyModifiers::empty(); 47 | if modifiers.shift { 48 | key_modifiers.insert(KeyModifiers::SHIFT); 49 | } 50 | if modifiers.control { 51 | key_modifiers.insert(KeyModifiers::CONTROL); 52 | } 53 | if modifiers.alt { 54 | key_modifiers.insert(KeyModifiers::ALT); 55 | } 56 | if modifiers.super_ { 57 | key_modifiers.insert(KeyModifiers::SUPER); 58 | } 59 | if modifiers.hyper { 60 | key_modifiers.insert(KeyModifiers::HYPER); 61 | } 62 | if modifiers.meta { 63 | key_modifiers.insert(KeyModifiers::META); 64 | } 65 | key_modifiers 66 | } 67 | } 68 | 69 | #[derive(Debug, Clone, Eq, PartialEq)] 70 | // Action` is an enum that represents an action that can be handled by the 71 | /// main application loop and the components of the user interface. 72 | pub enum Action { 73 | /// Unknown action. 74 | Unknown, 75 | /// Init action. 76 | Init, 77 | /// Quit action. 78 | Quit, 79 | /// TryQuit action, it is used to try to quit the application. 80 | /// It asks the the core window to confirm the quit action. 81 | /// If the prompt is not focused, we can quit. 82 | TryQuit, 83 | /// Render action. 84 | Render, 85 | /// Resize action with width and height. 86 | Resize(u16, u16), 87 | /// Paste action with a `String`. 88 | Paste(String), 89 | /// Focus Lost action. 90 | FocusLost, 91 | /// Focus Gained action. 92 | FocusGained, 93 | 94 | /// GetMe action. 95 | GetMe, 96 | /// LoadChats action with a `ChatList` and a limit. 97 | LoadChats(TdChatList, i32), 98 | /// SendMessage action with a `String`. 99 | /// The first parameter is the `text`. 100 | /// The second parameter is the `reply_to` field. 101 | SendMessage(String, Option), 102 | /// SendMessageEdited action with a `i64` and a `String`. 103 | /// The first parameter is the `message_id` and the second parameter is the `text`. 104 | SendMessageEdited(i64, String), 105 | /// GetChatHistory action. 106 | GetChatHistory, 107 | /// DeleteMessages action. 108 | /// The first parameter is the `message_ids` and the second parameter is the `revoke`. 109 | /// If `revoke` is true, the message will be deleted for everyone. 110 | /// If `revoke` is false, the message will be deleted only for the current user. 111 | DeleteMessages(Vec, bool), 112 | /// ViewAllMessages action. 113 | ViewAllMessages, 114 | 115 | /// Focus action with a `ComponentName`. 116 | FocusComponent(ComponentName), 117 | /// Unfocus action. 118 | UnfocusComponent, 119 | /// Toggle ChatList action. 120 | ToggleChatList, 121 | /// Increase ChatList size action. 122 | IncreaseChatListSize, 123 | /// Decrease ChatList size action. 124 | DecreaseChatListSize, 125 | /// Increase Prompt size action. 126 | IncreasePromptSize, 127 | /// Decrease Prompt size action. 128 | DecreasePromptSize, 129 | /// Key action with a key code. 130 | Key(KeyCode, Modifiers), 131 | /// Update area action with a rectangular area. 132 | UpdateArea(Rect), 133 | /// ShowChatWindowReply action. 134 | ShowChatWindowReply, 135 | /// HideChatWindowReply action. 136 | HideChatWindowReply, 137 | 138 | /// ChatListNext action. 139 | ChatListNext, 140 | /// ChatListPrevious action. 141 | ChatListPrevious, 142 | /// ChatListSelect action. 143 | ChatListUnselect, 144 | /// ChatListOpen action. 145 | ChatListOpen, 146 | /// ChatListSortWithString action. 147 | ChatListSortWithString(String), 148 | 149 | /// ChatWindowNext action. 150 | ChatWindowNext, 151 | /// ChatWindowPrevious action. 152 | ChatWindowPrevious, 153 | /// ChatWindowUnselect action. 154 | ChatWindowUnselect, 155 | /// ChatWindowDeleteForEveryone action. 156 | /// It is used to delete a message for everyone. 157 | ChatWindowDeleteForEveryone, 158 | /// ChatWindowDeleteForMe action. 159 | /// It is used to delete a message only for the current user. 160 | ChatWindowDeleteForMe, 161 | /// ChatWindowCopy action. 162 | ChatWindowCopy, 163 | /// ChatWindowEdit action. 164 | ChatWindowEdit, 165 | 166 | /// EditMessage action with a `String`. 167 | /// This action is used to edit a message. 168 | /// The first parameter is the `message_id` and the second parameter is the `text`. 169 | EditMessage(i64, String), 170 | /// ReplyMessage event with a `String`. 171 | /// This event is used to reply to a message. 172 | /// The first parameter is the `message_id` and the second parameter is the `text`. 173 | ReplyMessage(i64, String), 174 | /// ChatListSearch event. 175 | /// This event is used to set the prompt to search to set the search string 176 | /// for the ChatListWindow. 177 | ChatListSearch, 178 | /// ChatListRestoreSort event. 179 | /// This event is used to restore the default ordering. 180 | /// I.e. pinned first then chronological order. 181 | ChatListRestoreSort, 182 | } 183 | /// Implement the `Action` enum. 184 | impl Action { 185 | /// Create an action from a key event. 186 | /// 187 | /// # Arguments 188 | /// * `key` - A `KeyCode` that represents the key code. 189 | /// * `modifiers` - A `KeyModifiers` struct that represents the modifiers. 190 | /// 191 | /// # Returns 192 | /// * `Action` - An action. 193 | pub fn from_key_event(key: KeyCode, modifiers: KeyModifiers) -> Self { 194 | Action::Key(key, Modifiers::from(modifiers)) 195 | } 196 | } 197 | 198 | /// Implement the `FromStr` trait for `Action`. 199 | impl FromStr for Action { 200 | type Err = AppError<()>; 201 | 202 | fn from_str(s: &str) -> Result { 203 | match s { 204 | "quit" => Ok(Action::Quit), 205 | "try_quit" => Ok(Action::TryQuit), 206 | "render" => Ok(Action::Render), 207 | "focus_chat_list" => Ok(Action::FocusComponent(ComponentName::ChatList)), 208 | "focus_chat" => Ok(Action::FocusComponent(ComponentName::Chat)), 209 | "focus_prompt" => Ok(Action::FocusComponent(ComponentName::Prompt)), 210 | "unfocus_component" => Ok(Action::UnfocusComponent), 211 | "toggle_chat_list" => Ok(Action::ToggleChatList), 212 | "increase_chat_list_size" => Ok(Action::IncreaseChatListSize), 213 | "decrease_chat_list_size" => Ok(Action::DecreaseChatListSize), 214 | "increase_prompt_size" => Ok(Action::IncreasePromptSize), 215 | "decrease_prompt_size" => Ok(Action::DecreasePromptSize), 216 | "chat_list_next" => Ok(Action::ChatListNext), 217 | "chat_list_previous" => Ok(Action::ChatListPrevious), 218 | "chat_list_unselect" => Ok(Action::ChatListUnselect), 219 | "chat_list_open" => Ok(Action::ChatListOpen), 220 | "chat_list_search" => Ok(Action::ChatListSearch), 221 | "chat_list_restore_sort" => Ok(Action::ChatListRestoreSort), 222 | "chat_window_next" => Ok(Action::ChatWindowNext), 223 | "chat_window_previous" => Ok(Action::ChatWindowPrevious), 224 | "chat_window_unselect" => Ok(Action::ChatWindowUnselect), 225 | "chat_window_delete_for_everyone" => Ok(Action::ChatWindowDeleteForEveryone), 226 | "chat_window_delete_for_me" => Ok(Action::ChatWindowDeleteForMe), 227 | "chat_window_copy" => Ok(Action::ChatWindowCopy), 228 | "chat_window_edit" => Ok(Action::ChatWindowEdit), 229 | "chat_window_reply" => Ok(Action::ShowChatWindowReply), 230 | _ => Err(AppError::InvalidAction(s.to_string())), 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/app_error.rs: -------------------------------------------------------------------------------- 1 | use config::ConfigError; 2 | use std::{fmt::Display, io}; 3 | use tokio::sync::mpsc::error::SendError; 4 | 5 | #[derive(Debug)] 6 | /// An error type for the application. 7 | /// It is used to represent the application error. 8 | /// It can be one of the following: 9 | /// * Io: An IO error. 10 | /// * Send: A send error. 11 | /// * Config: A configuration error. 12 | /// * ConfigFile: A configuration file error. 13 | pub enum AppError { 14 | /// It is a wrapper for the `std::io::Error`. 15 | Io(io::Error), 16 | /// It is a wrapper for the `tokio::sync::mpsc::error::SendError`. 17 | Send(SendError), 18 | /// It is a wrapper for the `config::ConfigError`. 19 | Config(ConfigError), 20 | /// It is an invalid action. 21 | InvalidAction(String), 22 | /// It is an invalid event. 23 | InvalidEvent(String), 24 | /// It is a configuration file error. It is used when a key is already 25 | /// bound. 26 | AlreadyBound, 27 | /// It is an invalid color. 28 | InvalidColor(String), 29 | } 30 | impl From for AppError { 31 | fn from(error: io::Error) -> Self { 32 | Self::Io(error) 33 | } 34 | } 35 | impl From> for AppError { 36 | fn from(error: SendError) -> Self { 37 | Self::Send(error) 38 | } 39 | } 40 | impl From for AppError { 41 | fn from(error: ConfigError) -> Self { 42 | Self::Config(error) 43 | } 44 | } 45 | impl Display for AppError { 46 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 47 | match self { 48 | Self::Io(error) => write!(f, "IO error: {}", error), 49 | Self::Send(error) => write!(f, "Send error: {}", error), 50 | Self::Config(error) => write!(f, "Config error: {}", error), 51 | Self::InvalidAction(action) => { 52 | write!(f, "Invalid action: {}", action) 53 | } 54 | Self::InvalidEvent(event) => { 55 | write!(f, "Invalid event: {}", event) 56 | } 57 | Self::AlreadyBound => { 58 | write!(f, "Key already bound") 59 | } 60 | Self::InvalidColor(color) => { 61 | write!(f, "Invalid color: {}", color) 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | // use clap::Subcommand; 3 | 4 | /// The CLI arguments for the application. 5 | #[derive(Parser, Debug)] 6 | #[command(version, about, long_about = None)] 7 | pub struct CliArgs { 8 | #[command(flatten)] 9 | telegram_cli: TelegramCli, 10 | // #[command(subcommand)] 11 | // telegram: Option, 12 | } 13 | 14 | impl CliArgs { 15 | /// Get the Telegram CLI arguments. 16 | pub fn telegram_cli(&self) -> &TelegramCli { 17 | &self.telegram_cli 18 | } 19 | } 20 | 21 | #[derive(Parser, Debug)] 22 | /// The Telegram commands. 23 | pub struct TelegramCli { 24 | #[arg( 25 | short, 26 | long, 27 | visible_alias = "lo", 28 | help = "Logout from the tgt", 29 | default_value_t = false 30 | )] 31 | logout: bool, 32 | 33 | #[arg( 34 | short, 35 | long, 36 | visible_alias = "sm", 37 | number_of_values = 2, 38 | value_names = &["CHAT_NAME", "MESSAGE"], 39 | help = "Send a message to a chat" 40 | )] 41 | send_message: Option>, 42 | } 43 | 44 | impl TelegramCli { 45 | /// Get the logout flag. 46 | pub fn logout(&self) -> bool { 47 | self.logout 48 | } 49 | /// Get the send message arguments. 50 | pub fn send_message(&self) -> Option<&Vec> { 51 | self.send_message.as_ref() 52 | } 53 | } 54 | 55 | // #[derive(Parser, Debug)] 56 | // // #[derive(Subcommand, Debug)] 57 | // pub enum Telegram { 58 | // Test(TelegramStartSubcommand), 59 | // Add { name: Option } 60 | // } 61 | -------------------------------------------------------------------------------- /src/component_name.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result}; 2 | 3 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 4 | /// `ComponentName` is an enum that represents the name of a component in the 5 | /// user interface. 6 | pub enum ComponentName { 7 | /// The core window. 8 | CoreWindow, 9 | /// The chat list. 10 | ChatList, 11 | /// The chat. 12 | Chat, 13 | /// The prompt. 14 | Prompt, 15 | /// The reply message window. 16 | ReplyMessage, 17 | /// The title bar. 18 | TitleBar, 19 | /// The status bar. 20 | StatusBar, 21 | } 22 | 23 | impl Display for ComponentName { 24 | fn fmt(&self, f: &mut Formatter) -> Result { 25 | match self { 26 | ComponentName::CoreWindow => write!(f, "Core Window"), 27 | ComponentName::ChatList => write!(f, "Chat List"), 28 | ComponentName::Chat => write!(f, "Chat"), 29 | ComponentName::Prompt => write!(f, "Prompt"), 30 | ComponentName::TitleBar => write!(f, "Title Bar"), 31 | ComponentName::StatusBar => write!(f, "Status Bar"), 32 | ComponentName::ReplyMessage => write!(f, "Reply Message"), 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/components/component_traits.rs: -------------------------------------------------------------------------------- 1 | use crate::action::Action; 2 | use crate::app_error::AppError; 3 | use crate::event::Event; 4 | use crossterm::event; 5 | use ratatui::layout; 6 | use std::io; 7 | use tokio::sync::mpsc; 8 | 9 | /// `Component` is a trait that represents a visual and interactive element of 10 | /// the user interface. Implementors of this trait can be registered with the 11 | /// main application loop and will be able to receive events, update state, and 12 | /// be rendered on the screen. 13 | pub trait Component: HandleFocus { 14 | /// Register an action handler that can send actions for processing if 15 | /// necessary. 16 | /// 17 | /// # Arguments 18 | /// 19 | /// * `tx` - An unbounded sender that can send actions. 20 | /// 21 | /// # Returns 22 | /// 23 | /// * `Result<()>` - An Ok result or an error. 24 | #[allow(unused_variables)] 25 | fn register_action_handler(&mut self, tx: mpsc::UnboundedSender) -> io::Result<()> { 26 | Ok(()) 27 | } 28 | /// Initialize the component with a specified area if necessary. 29 | /// 30 | /// # Arguments 31 | /// 32 | /// * `area` - Rectangular area to initialize the component within. 33 | /// 34 | /// # Returns 35 | /// 36 | /// * `Result<()>` - An Ok result or an error. 37 | #[allow(unused_variables)] 38 | fn init(&mut self, area: layout::Rect) -> io::Result<()> { 39 | Ok(()) 40 | } 41 | /// Handle incoming events and produce actions if necessary. 42 | /// 43 | /// # Arguments 44 | /// 45 | /// * `event` - An optional event to be processed. 46 | /// 47 | /// # Returns 48 | /// 49 | /// * `Result>` - An action to be processed or none. 50 | fn handle_events(&mut self, event: Option) -> Result, AppError> { 51 | let r = match event { 52 | Some(Event::Key(key, modifiers)) => { 53 | self.handle_key_events(Event::Key(key, modifiers))? 54 | } 55 | Some(Event::Mouse(mouse_event)) => self.handle_mouse_events(mouse_event)?, 56 | _ => None, 57 | }; 58 | Ok(r) 59 | } 60 | /// Handle key events and produce actions if necessary. 61 | /// 62 | /// # Arguments 63 | /// 64 | /// * `key` - A key event to be processed. 65 | /// 66 | /// # Returns 67 | /// 68 | /// * `Result>` - An action to be processed or none. 69 | #[allow(unused_variables)] 70 | fn handle_key_events(&mut self, key: Event) -> io::Result> { 71 | Ok(None) 72 | } 73 | /// Handle mouse events and produce actions if necessary. 74 | /// 75 | /// # Arguments 76 | /// 77 | /// * `mouse` - A mouse event to be processed. 78 | /// 79 | /// # Returns 80 | /// 81 | /// * `Result>` - An action to be processed or none. 82 | #[allow(unused_variables)] 83 | fn handle_mouse_events(&mut self, mouse: event::MouseEvent) -> io::Result> { 84 | Ok(None) 85 | } 86 | /// Update the state of the component based on a received action. (REQUIRED) 87 | /// 88 | /// # Arguments 89 | /// 90 | /// * `action` - An action that may modify the state of the component. 91 | #[allow(unused_variables)] 92 | fn update(&mut self, action: Action) {} 93 | /// Render the component on the screen. (REQUIRED) 94 | /// 95 | /// # Arguments 96 | /// 97 | /// * `f` - A frame used for rendering. 98 | /// * `area` - The area in which the component should be drawn. 99 | /// 100 | /// # Returns 101 | /// 102 | /// * `Result<()>` - An Ok result or an error. 103 | fn draw(&mut self, f: &mut ratatui::Frame<'_>, area: layout::Rect) -> io::Result<()>; 104 | /// Create a new boxed instance of the component. 105 | /// 106 | /// # Returns 107 | /// 108 | /// * `Box` - A boxed instance of the component. 109 | fn new_boxed(self) -> Box 110 | where 111 | Self: Sized, 112 | { 113 | Box::new(self) 114 | } 115 | } 116 | /// `HandleFocus` is a trait that represents a component that can handle focus 117 | /// actions. Implementors of this trait can be notified when they have focus 118 | /// and when they lose focus. 119 | pub trait HandleFocus { 120 | /// This method is called when the component should focus. 121 | /// This should set the state of the component to reflect the fact that it 122 | /// has focus. 123 | fn focus(&mut self); 124 | /// This method is called when the component should unfocus. 125 | /// This should set the state of the component to reflect the fact that it 126 | /// has lost focus. 127 | fn unfocus(&mut self); 128 | } 129 | -------------------------------------------------------------------------------- /src/components/mod.rs: -------------------------------------------------------------------------------- 1 | pub const SMALL_AREA_WIDTH: u16 = 100; 2 | pub const SMALL_AREA_HEIGHT: u16 = 20; 3 | pub const MAX_CHAT_LIST_SIZE: u16 = 25; 4 | pub const MIN_CHAT_LIST_SIZE: u16 = 10; 5 | pub const MAX_PROMPT_SIZE: u16 = 20; 6 | pub const MIN_PROMPT_SIZE: u16 = 3; 7 | 8 | pub mod chat_list_window; 9 | pub mod chat_window; 10 | pub mod component_traits; 11 | pub mod core_window; 12 | pub mod prompt_window; 13 | pub mod reply_message; 14 | pub mod status_bar; 15 | pub mod title_bar; 16 | -------------------------------------------------------------------------------- /src/components/reply_message.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::Action, 4 | app_context::AppContext, 5 | components::component_traits::{Component, HandleFocus}, 6 | }, 7 | ratatui::{ 8 | layout::Rect, 9 | symbols::{ 10 | border::{Set, PLAIN}, 11 | line::NORMAL, 12 | }, 13 | text::{Line, Span, Text}, 14 | widgets::{block::Block, Borders, Paragraph, Wrap}, 15 | }, 16 | std::{io, sync::Arc}, 17 | tokio::sync::mpsc, 18 | }; 19 | 20 | /// `ReplyMessage` is a struct that represents a window for replying to messages. 21 | /// It is responsible for managing the layout and rendering of the reply message window. 22 | pub struct ReplyMessage { 23 | /// The application configuration. 24 | app_context: Arc, 25 | /// The name of the `ReplyMessage`. 26 | name: String, 27 | /// An unbounded sender that send action for processing. 28 | command_tx: Option>, 29 | /// Indicates whether the `ReplyMessage` is focused or not. 30 | focused: bool, 31 | } 32 | /// Implementation of `ReplyMessage` struct. 33 | impl ReplyMessage { 34 | pub fn new(app_context: Arc) -> Self { 35 | let command_tx = None; 36 | let name = "".to_string(); 37 | let focused = false; 38 | ReplyMessage { 39 | app_context, 40 | command_tx, 41 | name, 42 | focused, 43 | } 44 | } 45 | /// Set the name of the `ReplyMessage`. 46 | /// 47 | /// # Arguments 48 | /// * `name` - The name of the `ReplyMessage`. 49 | /// 50 | /// # Returns 51 | /// * `Self` - The modified instance of the `ReplyMessage`. 52 | pub fn with_name(mut self, name: impl AsRef) -> Self { 53 | self.name = name.as_ref().to_string(); 54 | self 55 | } 56 | } 57 | 58 | /// Implement the `HandleFocus` trait for the `ReplyMessage` struct. 59 | /// This trait allows the `ReplyMessage` to be focused or unfocused. 60 | impl HandleFocus for ReplyMessage { 61 | /// Set the `focused` flag for the `ReplyMessage`. 62 | fn focus(&mut self) { 63 | self.focused = true; 64 | } 65 | /// Set the `focused` flag for the `ReplyMessage`. 66 | fn unfocus(&mut self) { 67 | self.focused = false; 68 | } 69 | } 70 | 71 | /// Implement the `Component` trait for the `ReplyMessage` struct. 72 | impl Component for ReplyMessage { 73 | fn register_action_handler(&mut self, tx: mpsc::UnboundedSender) -> io::Result<()> { 74 | self.command_tx = Some(tx); 75 | Ok(()) 76 | } 77 | 78 | fn draw(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> io::Result<()> { 79 | let mut text = Text::default(); 80 | text.extend(vec![Line::from(vec![Span::styled( 81 | (*self.app_context.tg_context().reply_message_text()).to_string(), 82 | self.app_context.style_reply_message_message_text(), 83 | )])]); 84 | 85 | let collapsed_border = Set { 86 | top_left: NORMAL.vertical_right, 87 | top_right: NORMAL.vertical_left, 88 | bottom_left: NORMAL.vertical_right, 89 | bottom_right: NORMAL.vertical_left, 90 | ..PLAIN 91 | }; 92 | 93 | let block = Block::new() 94 | .border_set(collapsed_border) 95 | .borders(Borders::RIGHT | Borders::LEFT | Borders::TOP) 96 | .title("Reply Message") 97 | .border_style(self.app_context.style_reply_message()) 98 | .style(self.app_context.style_reply_message()); 99 | 100 | let paragraph = Paragraph::new(text) 101 | .block(block) 102 | .style(self.app_context.style_reply_message()) 103 | // .alignment(Alignment::Center) 104 | .wrap(Wrap { trim: true }); 105 | 106 | frame.render_widget(paragraph, area); 107 | 108 | Ok(()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/components/status_bar.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::Action, 4 | app_context::AppContext, 5 | components::component_traits::{Component, HandleFocus}, 6 | event::Event, 7 | }, 8 | ratatui::{ 9 | layout::{Alignment, Rect}, 10 | text::{Line, Span}, 11 | widgets::{block::Block, Borders, Paragraph, Wrap}, 12 | }, 13 | std::sync::Arc, 14 | tokio::sync::mpsc::UnboundedSender, 15 | }; 16 | 17 | /// `StatusBar` is a struct that represents a status bar. 18 | /// It is responsible for managing the layout and rendering of the status bar. 19 | pub struct StatusBar { 20 | /// The application configuration. 21 | app_context: Arc, 22 | /// The name of the `StatusBar`. 23 | name: String, 24 | /// An unbounded sender that send action for processing. 25 | command_tx: Option>, 26 | /// Indicates whether the `StatusBar` is focused or not. 27 | focused: bool, 28 | /// The area of the terminal where the all the content will be rendered. 29 | terminal_area: Rect, 30 | /// The last key pressed. 31 | last_key: Event, 32 | } 33 | /// Implementation of `StatusBar` struct. 34 | impl StatusBar { 35 | /// Create a new instance of the `StatusBar` struct. 36 | /// 37 | /// # Arguments 38 | /// * `app_context` - An Arc wrapped AppContext struct. 39 | /// 40 | /// # Returns 41 | /// * `Self` - The new instance of the `StatusBar` struct. 42 | pub fn new(app_context: Arc) -> Self { 43 | let command_tx = None; 44 | let name = "".to_string(); 45 | let terminal_area = Rect::default(); 46 | let last_key = Event::Unknown; 47 | let focused = false; 48 | 49 | StatusBar { 50 | app_context, 51 | command_tx, 52 | name, 53 | terminal_area, 54 | last_key, 55 | focused, 56 | } 57 | } 58 | /// Set the name of the `StatusBar`. 59 | /// 60 | /// # Arguments 61 | /// * `name` - The name of the `StatusBar`. 62 | /// 63 | /// # Returns 64 | /// * `Self` - The modified instance of the `StatusBar`. 65 | pub fn with_name(mut self, name: impl AsRef) -> Self { 66 | self.name = name.as_ref().to_string(); 67 | self 68 | } 69 | } 70 | 71 | /// Implement the `HandleFocus` trait for the `StatusBar` struct. 72 | /// This trait allows the `StatusBar` to be focused or unfocused. 73 | impl HandleFocus for StatusBar { 74 | /// Set the `focused` flag for the `StatusBar`. 75 | fn focus(&mut self) { 76 | self.focused = true; 77 | } 78 | /// Set the `focused` flag for the `StatusBar`. 79 | fn unfocus(&mut self) { 80 | self.focused = false; 81 | } 82 | } 83 | 84 | /// Implement the `Component` trait for the `ChatListWindow` struct. 85 | impl Component for StatusBar { 86 | fn register_action_handler(&mut self, tx: UnboundedSender) -> std::io::Result<()> { 87 | self.command_tx = Some(tx); 88 | Ok(()) 89 | } 90 | 91 | fn update(&mut self, action: Action) { 92 | match action { 93 | Action::UpdateArea(area) => { 94 | self.terminal_area = area; 95 | } 96 | Action::Key(key, modifiers) => self.last_key = Event::Key(key, modifiers.into()), 97 | _ => {} 98 | } 99 | } 100 | 101 | fn draw(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> std::io::Result<()> { 102 | let selected_chat = self 103 | .app_context 104 | .tg_context() 105 | .name_of_open_chat_id() 106 | .unwrap_or_default(); 107 | let text = vec![Line::from(vec![ 108 | Span::styled( 109 | "Press ", 110 | self.app_context.style_status_bar_message_quit_text(), 111 | ), 112 | Span::styled("q ", self.app_context.style_status_bar_message_quit_key()), 113 | Span::styled("or ", self.app_context.style_status_bar_message_quit_text()), 114 | Span::styled( 115 | "ctrl+c ", 116 | self.app_context.style_status_bar_message_quit_key(), 117 | ), 118 | Span::styled( 119 | "to quit", 120 | self.app_context.style_status_bar_message_quit_text(), 121 | ), 122 | // 123 | Span::raw(" "), 124 | Span::styled( 125 | "Open chat: ", 126 | self.app_context.style_status_bar_open_chat_text(), 127 | ), 128 | Span::styled( 129 | selected_chat, 130 | self.app_context.style_status_bar_open_chat_name(), 131 | ), 132 | // 133 | Span::raw(" "), 134 | Span::styled( 135 | "Key pressed: ", 136 | self.app_context.style_status_bar_press_key_text(), 137 | ), 138 | Span::styled( 139 | self.last_key.to_string(), 140 | self.app_context.style_status_bar_press_key_key(), 141 | ), 142 | // 143 | Span::raw(" "), 144 | Span::styled("Size: ", self.app_context.style_status_bar_size_info_text()), 145 | Span::styled( 146 | self.terminal_area.width.to_string(), 147 | self.app_context.style_status_bar_size_info_numbers(), 148 | ), 149 | Span::styled(" x ", self.app_context.style_status_bar_size_info_text()), 150 | Span::styled( 151 | self.terminal_area.height.to_string(), 152 | self.app_context.style_status_bar_size_info_numbers(), 153 | ), 154 | ])]; 155 | 156 | let paragraph = Paragraph::new(text) 157 | .block(Block::new().title(self.name.as_str()).borders(Borders::ALL)) 158 | .style(self.app_context.style_status_bar()) 159 | .alignment(Alignment::Center) 160 | .wrap(Wrap { trim: true }); 161 | 162 | frame.render_widget(paragraph, area); 163 | 164 | Ok(()) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/components/title_bar.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | action::Action, 4 | app_context::AppContext, 5 | components::component_traits::{Component, HandleFocus}, 6 | }, 7 | ratatui::{ 8 | layout::{Alignment, Constraint, Direction, Layout, Rect}, 9 | text::{Line, Span}, 10 | widgets::{block::Block, Borders, Paragraph, Wrap}, 11 | }, 12 | ratatui_image::picker::Picker, 13 | std::{io, sync::Arc}, 14 | tokio::sync::mpsc, 15 | }; 16 | 17 | /// `TitleBar` is a struct that represents a title bar. 18 | /// It is responsible for managing the layout and rendering of the title bar. 19 | pub struct TitleBar { 20 | /// The application configuration. 21 | app_context: Arc, 22 | /// The name of the `TitleBar`. 23 | name: String, 24 | /// An unbounded sender that send action for processing. 25 | command_tx: Option>, 26 | /// Indicates whether the `TitleBar` is focused or not. 27 | focused: bool, 28 | // The image of the `TitleBar`. 29 | // _image_state: Box, // Box, 30 | } 31 | /// Implementation of `TitleBar` struct. 32 | impl TitleBar { 33 | pub fn new(app_context: Arc) -> Self { 34 | let command_tx = None; 35 | let name = "".to_string(); 36 | let focused = false; 37 | 38 | let mut _picker = Picker::from_query_stdio(); 39 | // TODO: Add image to TUI (see https://docs.rs/ratatui-image/latest/ratatui_image/) 40 | 41 | TitleBar { 42 | app_context, 43 | command_tx, 44 | name, 45 | focused, 46 | // _image_state: image_state, 47 | } 48 | } 49 | /// Set the name of the `TitleBar`. 50 | /// 51 | /// # Arguments 52 | /// * `name` - The name of the `TitleBar`. 53 | /// 54 | /// # Returns 55 | /// * `Self` - The modified instance of the `TitleBar`. 56 | pub fn with_name(mut self, name: impl AsRef) -> Self { 57 | self.name = name.as_ref().to_string(); 58 | self 59 | } 60 | } 61 | 62 | /// Implement the `HandleFocus` trait for the `TitleBar` struct. 63 | /// This trait allows the `TitleBar` to be focused or unfocused. 64 | impl HandleFocus for TitleBar { 65 | /// Set the `focused` flag for the `TitleBar`. 66 | fn focus(&mut self) { 67 | self.focused = true; 68 | } 69 | /// Set the `focused` flag for the `TitleBar`. 70 | fn unfocus(&mut self) { 71 | self.focused = false; 72 | } 73 | } 74 | 75 | /// Implement the `Component` trait for the `TitleBar` struct. 76 | impl Component for TitleBar { 77 | fn register_action_handler(&mut self, tx: mpsc::UnboundedSender) -> io::Result<()> { 78 | self.command_tx = Some(tx); 79 | Ok(()) 80 | } 81 | 82 | fn draw(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> io::Result<()> { 83 | let chunks = Layout::default() 84 | .direction(Direction::Horizontal) 85 | .constraints([Constraint::Percentage(0), Constraint::Percentage(100)].as_ref()) 86 | .split(area); 87 | 88 | let name: Vec = self.name.chars().collect::>(); 89 | // Span::raw(" - A TUI for Telegram"), 90 | let text = vec![Line::from(vec![ 91 | Span::styled( 92 | name[0].to_string(), 93 | self.app_context.style_title_bar_title1(), 94 | ), 95 | Span::styled( 96 | name[1].to_string(), 97 | self.app_context.style_title_bar_title2(), 98 | ), 99 | Span::styled( 100 | name[2].to_string(), 101 | self.app_context.style_title_bar_title3(), 102 | ), 103 | Span::styled(" - ", self.app_context.style_title_bar_title1()), 104 | Span::styled("A", self.app_context.style_title_bar_title2()), 105 | Span::styled(" T", self.app_context.style_title_bar_title3()), 106 | Span::styled("U", self.app_context.style_title_bar_title1()), 107 | Span::styled("I", self.app_context.style_title_bar_title2()), 108 | Span::styled(" f", self.app_context.style_title_bar_title3()), 109 | Span::styled("o", self.app_context.style_title_bar_title1()), 110 | Span::styled("r", self.app_context.style_title_bar_title2()), 111 | Span::styled(" T", self.app_context.style_title_bar_title3()), 112 | Span::styled("e", self.app_context.style_title_bar_title1()), 113 | Span::styled("l", self.app_context.style_title_bar_title2()), 114 | Span::styled("e", self.app_context.style_title_bar_title3()), 115 | Span::styled("g", self.app_context.style_title_bar_title1()), 116 | Span::styled("r", self.app_context.style_title_bar_title2()), 117 | Span::styled("a", self.app_context.style_title_bar_title3()), 118 | Span::styled("m", self.app_context.style_title_bar_title1()), 119 | ])]; 120 | let block = Block::new().borders(Borders::ALL); 121 | let paragraph = Paragraph::new(text) 122 | .block(block.clone()) 123 | .style(self.app_context.style_title_bar()) 124 | .alignment(Alignment::Center) 125 | .wrap(Wrap { trim: true }); 126 | 127 | // let statefull_image = StatefulImage::new(None); 128 | // frame.render_stateful_widget(statefull_image, chunks[0], &mut self.image_state); 129 | // let image = Image::new(self.image_state.as_ref()); 130 | // frame.render_widget(image, chunks[0]); 131 | 132 | frame.render_widget(paragraph, chunks[1]); 133 | 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/configs/config_file.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::configs::{self, config_type::ConfigType}, 3 | crate::utils::{TGT, TGT_CONFIG_DIR}, 4 | lazy_static::lazy_static, 5 | serde::de::DeserializeOwned, 6 | std::path::PathBuf, 7 | }; 8 | 9 | lazy_static! { 10 | static ref CONFIG_DIR_HIERARCHY: Vec = { 11 | let mut config_dirs = vec![]; 12 | 13 | if let Ok(p) = std::env::var(TGT_CONFIG_DIR) { 14 | let p = PathBuf::from(p); 15 | if p.is_dir() { 16 | config_dirs.push(p); 17 | } 18 | tracing::info!("Using {} for config", TGT_CONFIG_DIR); 19 | } 20 | 21 | if let Some(p) = if cfg!(target_os = "macos") { 22 | dirs::home_dir().map(|h| h.join(".config")) 23 | } else { 24 | dirs::config_dir() 25 | } { 26 | let mut p = p; 27 | p.push(TGT); 28 | if p.is_dir() { 29 | config_dirs.push(p.clone()); 30 | } 31 | p.push("config"); 32 | if p.is_dir() { 33 | config_dirs.push(p.clone()); 34 | } 35 | tracing::info!("Using {} for config", p.display()); 36 | } 37 | 38 | config_dirs 39 | }; 40 | } 41 | 42 | /// A trait for configuration files. 43 | /// This trait is used to define a configuration file and its associated 44 | /// configuration struct. The configuration struct must implement the `Default` 45 | /// trait and the `Into` trait for the raw configuration type. 46 | pub trait ConfigFile: Sized + Default + Clone { 47 | /// The raw configuration type. 48 | /// This type is used to parse the configuration file and must implement the 49 | /// `DeserializeOwned` trait. 50 | type Raw: Into + DeserializeOwned; 51 | /// Get the configuration type. 52 | /// 53 | /// # Returns 54 | /// The configuration type. 55 | fn get_type() -> ConfigType; 56 | /// Get the configuration of the specified type. 57 | /// 58 | /// # Returns 59 | /// The configuration of the specified type. 60 | fn get_config() -> Self { 61 | if Self::override_fields() { 62 | let mut default = Self::default(); 63 | default.merge(Self::deserialize_custom_config::( 64 | Self::get_type().as_default_filename().as_str(), 65 | )) 66 | } else { 67 | Self::deserialize_config_or_default::( 68 | Self::get_type().as_default_filename().as_str(), 69 | ) 70 | } 71 | } 72 | /// Search for a configuration file in the configuration directories. 73 | /// This function searches the configuration directories for the specified 74 | /// file name and returns the path to the first matching file. If no 75 | /// matching file is found, `None` is returned. 76 | /// 77 | /// # Arguments 78 | /// * `file_name` - The name of the file (including the file extension) to 79 | /// search for in the configuration directories. 80 | /// 81 | /// # Returns 82 | /// The path to the first matching file or `None` if no matching file is 83 | /// found. 84 | fn search_config_file(file_name: &str) -> Option { 85 | CONFIG_DIR_HIERARCHY 86 | .iter() 87 | .map(|path| path.join(file_name)) 88 | .find(|path| path.exists()) 89 | } 90 | /// Deserialize a custom configuration file into a configuration struct. 91 | /// This function searches the configuration directories for the specified 92 | /// file name and attempts to parse it. If the file is found and parsed 93 | /// successfully, the parsed configuration is returned. If the file is 94 | /// not found or cannot be parsed, the process exits with an error message. 95 | /// 96 | /// # Arguments 97 | /// * `file_name` - The name of the file (including the file extension) to 98 | /// search for in the configuration directories. 99 | /// 100 | /// # Returns 101 | /// The parsed configuration or `None` if the file is not found or cannot be 102 | /// parsed. 103 | fn deserialize_custom_config(file_name: &str) -> Option 104 | where 105 | R: DeserializeOwned, 106 | { 107 | match Self::search_config_file(file_name) { 108 | Some(file_path) => match configs::deserialize_to_config::(&file_path) { 109 | Ok(s) => { 110 | tracing::info!("Loaded config from {}", file_path.display()); 111 | Some(s) 112 | } 113 | Err(e) => { 114 | tracing::error!("Failed to parse {}: {}", file_name, e); 115 | eprintln!("Failed to parse {}: {}", file_name, e); 116 | std::process::exit(1); 117 | } 118 | }, 119 | None => { 120 | tracing::info!("No config file found for {}", file_name); 121 | None 122 | } 123 | } 124 | } 125 | /// Deserialize a configuration file into a configuration struct or return 126 | /// the default configuration. This function searches the configuration 127 | /// directories for the specified file name and attempts to parse it. If 128 | /// the file is found and parsed successfully, the parsed configuration is 129 | /// returned. If the file is not found, the default configuration is 130 | /// returned. If the file is found but cannot be parsed, the program exits 131 | /// with an error message. 132 | /// 133 | /// # Arguments 134 | /// * `file_name` - The name of the file (including the file extension) to 135 | /// search for in the configuration directories. 136 | /// 137 | /// # Returns 138 | /// The parsed configuration or the default configuration if the file is not 139 | /// found or cannot be parsed. 140 | fn deserialize_config_or_default(file_name: &str) -> S 141 | where 142 | R: DeserializeOwned + Into, 143 | S: std::default::Default, 144 | { 145 | // [TODO] Handle CLI arguments 146 | match Self::search_config_file(file_name) { 147 | Some(file_path) => { 148 | match configs::deserialize_to_config_into::(&file_path) { 149 | Ok(s) => { 150 | tracing::info!("Loaded config from {}", file_path.display()); 151 | s 152 | } 153 | Err(e) => { 154 | tracing::error!("Failed to parse {}: {}", file_name, e); 155 | eprintln!("Failed to parse {}: {}", file_name, e); 156 | std::process::exit(1); 157 | // S::default() 158 | } 159 | } 160 | } 161 | None => { 162 | tracing::info!("No config file found for {}", file_name); 163 | S::default() 164 | } 165 | } 166 | } 167 | #[allow(unused_variables)] 168 | /// Merge the configuration with another configuration. 169 | /// If the other configuration is `None`, the current configuration is 170 | /// returned. This function is used to merge the default configuration 171 | /// with a custom configuration. The custom configuration is used to 172 | /// override the default configuration. 173 | /// 174 | /// # Arguments 175 | /// * `other` - The other configuration to merge with the current 176 | /// configuration. 177 | /// 178 | /// # Returns 179 | /// The merged configuration. 180 | fn merge(&mut self, other: Option) -> Self { 181 | self.clone() 182 | } 183 | /// Allow the fields of the default configuration to be overridden by the 184 | /// fields of the custom configuration when merging the configurations. 185 | /// Usually, the custom configuration is written by the user and the default 186 | /// configuration is provided by the application. 187 | /// 188 | /// # Returns 189 | /// `true` if the fields of the default configuration can be overridden by 190 | /// the fields of the custom configuration, `false` otherwise. 191 | fn override_fields() -> bool { 192 | false 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/configs/config_type.rs: -------------------------------------------------------------------------------- 1 | use { 2 | config::FileFormat, 3 | std::fmt::{Display, Formatter, Result}, 4 | }; 5 | 6 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 7 | /// `ConfigType` is an enum that represents the different types of configuration 8 | /// files that the application can use. The different types of configuration 9 | /// files are: 10 | /// * App - The application configuration file. 11 | /// * Keymap - The keymap configuration file. 12 | /// * Logger - The logger configuration file. 13 | /// * Palette - The palette configuration file. 14 | /// * Theme - The theme configuration file. 15 | /// * Telegram - The Telegram configuration file. 16 | pub enum ConfigType { 17 | App, 18 | Keymap, 19 | Logger, 20 | Palette, 21 | Theme, 22 | Telegram, 23 | } 24 | /// Implement the `ConfigType` enum. 25 | impl ConfigType { 26 | /// Get the different types of configuration files that the application can 27 | /// use. 28 | pub const fn enumerate() -> &'static [Self] { 29 | &[ 30 | Self::App, 31 | Self::Keymap, 32 | Self::Logger, 33 | Self::Palette, 34 | Self::Theme, 35 | Self::Telegram, 36 | ] 37 | } 38 | /// Get the file name without the file extension for the configuration file 39 | /// type. 40 | /// 41 | /// # Returns 42 | /// * `&'static str` - The file name without the file extension. 43 | pub const fn as_str(&self) -> &'static str { 44 | match self { 45 | Self::App => "app", 46 | Self::Keymap => "keymap", 47 | Self::Logger => "logger", 48 | // The palette configuration is defined in the theme configuration. 49 | Self::Palette => "theme", 50 | Self::Theme => "theme", 51 | Self::Telegram => "telegram", 52 | } 53 | } 54 | /// Get the default file extension for the configuration file type. 55 | /// The default file extension is `.toml`. 56 | /// 57 | /// # Returns 58 | /// * `&'static str` - The default file extension. 59 | const fn default_format(&self) -> &'static str { 60 | match self { 61 | Self::App => ".toml", 62 | Self::Keymap => ".toml", 63 | Self::Logger => ".toml", 64 | Self::Palette => ".toml", 65 | Self::Theme => ".toml", 66 | Self::Telegram => ".toml", 67 | } 68 | } 69 | /// Get the default file name for the configuration file type. 70 | /// 71 | /// # Returns 72 | /// * `String` - The default file name. 73 | pub fn as_default_filename(&self) -> String { 74 | format!("{}{}", self.as_str(), self.default_format()) 75 | } 76 | /// Get the supported file formats for the configuration file type. 77 | /// 78 | /// # Returns 79 | /// * `&'static [FileFormat]` - The supported file formats. 80 | pub const fn supported_formats(&self) -> &'static [FileFormat] { 81 | let formats = self.get_supported_formats(); 82 | match self { 83 | Self::App => formats, 84 | Self::Keymap => formats, 85 | Self::Logger => formats, 86 | Self::Palette => formats, 87 | Self::Theme => formats, 88 | Self::Telegram => formats, 89 | } 90 | } 91 | /// Get the supported file formats for the configuration file type. 92 | /// The supported file formats are: 93 | /// * Json5 94 | /// * Json 95 | /// * Yaml 96 | /// * Toml 97 | /// * Ini 98 | /// * Ron 99 | /// 100 | /// # Returns 101 | /// * `&'static [FileFormat]` - The supported file formats. 102 | const fn get_supported_formats(&self) -> &'static [FileFormat] { 103 | &[ 104 | FileFormat::Json5, 105 | FileFormat::Json, 106 | FileFormat::Yaml, 107 | FileFormat::Toml, 108 | FileFormat::Ini, 109 | FileFormat::Ron, 110 | ] 111 | } 112 | } 113 | 114 | /// Implement the `Display` trait for `ConfigType`. 115 | impl Display for ConfigType { 116 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 117 | f.write_str(self.as_str()) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/configs/custom/app_custom.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app_error::AppError, 3 | configs::{self, config_file::ConfigFile, config_type::ConfigType, raw::app_raw::AppRaw}, 4 | }; 5 | use std::path::Path; 6 | 7 | #[derive(Clone, Debug)] 8 | /// The application configuration. 9 | pub struct AppConfig { 10 | /// The mouse support. 11 | pub mouse_support: bool, 12 | /// The paste support. 13 | pub paste_support: bool, 14 | /// The frame rate. 15 | pub frame_rate: f64, 16 | /// The status bar visibility. 17 | pub show_status_bar: bool, 18 | /// The title bar visibility. 19 | pub show_title_bar: bool, 20 | /// Enable the theme. 21 | pub theme_enable: bool, 22 | /// The theme filename. 23 | pub theme_filename: String, 24 | /// Take the API ID from the Telegram configuration. 25 | pub take_api_id_from_telegram_config: bool, 26 | /// Take the API HASH from the Telegram configuration. 27 | pub take_api_hash_from_telegram_config: bool, 28 | } 29 | /// The application configuration implementation. 30 | impl AppConfig { 31 | /// Get the default application configuration. 32 | /// 33 | /// # Returns 34 | /// The default application configuration. 35 | pub fn default_result() -> Result> { 36 | configs::deserialize_to_config_into::(Path::new( 37 | &configs::custom::default_config_app_file_path()?, 38 | )) 39 | } 40 | } 41 | /// The implementation of the configuration file for the application. 42 | impl ConfigFile for AppConfig { 43 | type Raw = AppRaw; 44 | 45 | fn get_type() -> ConfigType { 46 | ConfigType::App 47 | } 48 | 49 | fn override_fields() -> bool { 50 | true 51 | } 52 | 53 | fn merge(&mut self, other: Option) -> Self { 54 | match other { 55 | None => self.clone(), 56 | Some(other) => { 57 | tracing::info!("Merging app config"); 58 | if let Some(mouse_support) = other.mouse_support { 59 | self.mouse_support = mouse_support; 60 | } 61 | if let Some(paste_support) = other.paste_support { 62 | self.paste_support = paste_support; 63 | } 64 | if let Some(frame_rate) = other.frame_rate { 65 | self.frame_rate = frame_rate; 66 | } 67 | if let Some(show_status_bar) = other.show_status_bar { 68 | self.show_status_bar = show_status_bar; 69 | } 70 | if let Some(show_title_bar) = other.show_title_bar { 71 | self.show_title_bar = show_title_bar; 72 | } 73 | if let Some(theme_enable) = other.theme_enable { 74 | self.theme_enable = theme_enable; 75 | } 76 | if let Some(theme_filename) = other.theme_filename { 77 | self.theme_filename = theme_filename; 78 | } 79 | if let Some(take_api_id_from_telegram_config) = 80 | other.take_api_id_from_telegram_config 81 | { 82 | self.take_api_id_from_telegram_config = take_api_id_from_telegram_config; 83 | } 84 | if let Some(take_api_hash_from_telegram_config) = 85 | other.take_api_hash_from_telegram_config 86 | { 87 | self.take_api_hash_from_telegram_config = take_api_hash_from_telegram_config; 88 | } 89 | self.clone() 90 | } 91 | } 92 | } 93 | } 94 | /// The default application configuration. 95 | impl Default for AppConfig { 96 | fn default() -> Self { 97 | Self::default_result().unwrap() 98 | } 99 | } 100 | /// The conversion from the raw logger configuration to the logger 101 | /// configuration. 102 | impl From for AppConfig { 103 | fn from(raw: AppRaw) -> Self { 104 | Self { 105 | mouse_support: raw.mouse_support.unwrap(), 106 | paste_support: raw.paste_support.unwrap(), 107 | frame_rate: raw.frame_rate.unwrap(), 108 | show_status_bar: raw.show_status_bar.unwrap(), 109 | show_title_bar: raw.show_title_bar.unwrap(), 110 | theme_enable: raw.theme_enable.unwrap(), 111 | theme_filename: raw.theme_filename.unwrap(), 112 | take_api_id_from_telegram_config: raw.take_api_id_from_telegram_config.unwrap(), 113 | take_api_hash_from_telegram_config: raw.take_api_hash_from_telegram_config.unwrap(), 114 | } 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use crate::configs::{ 121 | config_file::ConfigFile, custom::app_custom::AppConfig, raw::app_raw::AppRaw, 122 | }; 123 | 124 | #[test] 125 | fn test_app_config_default() { 126 | let app_config = AppConfig::default(); 127 | assert!(app_config.mouse_support); 128 | assert!(app_config.paste_support); 129 | assert_eq!(app_config.frame_rate, 60.0); 130 | assert!(app_config.show_status_bar); 131 | assert!(app_config.show_title_bar); 132 | assert!(app_config.theme_enable); 133 | assert_eq!(app_config.theme_filename, "theme.toml"); 134 | assert!(app_config.take_api_id_from_telegram_config); 135 | assert!(app_config.take_api_hash_from_telegram_config); 136 | } 137 | 138 | #[test] 139 | fn test_app_config_from_raw() { 140 | let app_raw = AppRaw { 141 | mouse_support: Some(true), 142 | paste_support: Some(true), 143 | frame_rate: Some(30.0), 144 | show_status_bar: Some(true), 145 | show_title_bar: Some(true), 146 | theme_enable: Some(true), 147 | theme_filename: Some("test".to_string()), 148 | take_api_id_from_telegram_config: Some(true), 149 | take_api_hash_from_telegram_config: Some(true), 150 | }; 151 | let app_config = AppConfig::from(app_raw); 152 | assert!(app_config.mouse_support); 153 | assert!(app_config.paste_support); 154 | assert_eq!(app_config.frame_rate, 30.0); 155 | assert!(app_config.show_status_bar); 156 | assert!(app_config.show_title_bar); 157 | assert!(app_config.theme_enable); 158 | assert_eq!(app_config.theme_filename, "test"); 159 | } 160 | 161 | #[test] 162 | fn test_app_config_merge() { 163 | let mut app_config = AppConfig::from(AppRaw { 164 | mouse_support: Some(true), 165 | paste_support: Some(true), 166 | frame_rate: Some(60.0), 167 | show_status_bar: Some(true), 168 | show_title_bar: Some(true), 169 | theme_enable: Some(true), 170 | theme_filename: Some("test".to_string()), 171 | take_api_id_from_telegram_config: Some(true), 172 | take_api_hash_from_telegram_config: Some(true), 173 | }); 174 | let app_raw = AppRaw { 175 | mouse_support: Some(false), 176 | paste_support: Some(false), 177 | frame_rate: None, 178 | show_status_bar: None, 179 | show_title_bar: None, 180 | theme_enable: None, 181 | theme_filename: None, 182 | take_api_id_from_telegram_config: None, 183 | take_api_hash_from_telegram_config: None, 184 | }; 185 | app_config = app_config.merge(Some(app_raw)); 186 | assert!(!app_config.mouse_support); 187 | assert!(!app_config.paste_support); 188 | assert_eq!(app_config.frame_rate, 60.0); 189 | assert!(app_config.show_status_bar); 190 | assert!(app_config.show_title_bar); 191 | assert!(app_config.theme_enable); 192 | assert_eq!(app_config.theme_filename, "test"); 193 | } 194 | 195 | #[test] 196 | fn test_app_config_override_fields() { 197 | assert!(AppConfig::override_fields()); 198 | } 199 | 200 | #[test] 201 | fn test_merge_all_fields() { 202 | let mut app_config = AppConfig::default(); 203 | let app_raw = AppRaw { 204 | mouse_support: None, 205 | paste_support: None, 206 | frame_rate: None, 207 | show_status_bar: None, 208 | show_title_bar: None, 209 | theme_enable: None, 210 | theme_filename: None, 211 | take_api_id_from_telegram_config: None, 212 | take_api_hash_from_telegram_config: None, 213 | }; 214 | app_config = app_config.merge(Some(app_raw)); 215 | assert!(app_config.mouse_support); 216 | assert!(app_config.paste_support); 217 | assert_eq!(app_config.frame_rate, 60.0); 218 | assert!(app_config.show_status_bar); 219 | assert!(app_config.show_title_bar); 220 | assert!(app_config.theme_enable); 221 | assert_eq!(app_config.theme_filename, "theme.toml"); 222 | assert!(app_config.take_api_id_from_telegram_config); 223 | assert!(app_config.take_api_hash_from_telegram_config); 224 | } 225 | 226 | #[test] 227 | fn test_get_type() { 228 | assert_eq!( 229 | AppConfig::get_type(), 230 | crate::configs::config_type::ConfigType::App 231 | ); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/configs/custom/logger_custom.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app_error::AppError, 3 | configs::{self, config_file::ConfigFile, config_type::ConfigType, raw::logger_raw::LoggerRaw}, 4 | utils, 5 | }; 6 | use std::path::Path; 7 | 8 | #[derive(Clone, Debug)] 9 | /// The logger configuration. 10 | pub struct LoggerConfig { 11 | /// The folder where the log file is stored. 12 | pub log_dir: String, 13 | /// The name of the log file. 14 | pub log_file: String, 15 | /// The rotation frequency of the log. 16 | /// The log rotation frequency can be one of the following: 17 | /// * minutely: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH-mm will be created minutely (once per minute) 18 | /// * hourly: A new log file in the format of log_dir/log_file.yyyy-MM-dd-HH will be created hourly 19 | /// * daily: A new log file in the format of log_dir/log_file.yyyy-MM-dd will be created daily 20 | /// * never: This will result in log file located at log_dir/log_file 21 | pub rotation_frequency: String, 22 | /// The maximum number of old log files that will be stored 23 | pub max_old_log_files: usize, 24 | /// The level of the log. 25 | /// The log level can be one of the following: 26 | /// * error: only log error 27 | /// * warn: log error and warning 28 | /// * info: log error, warning, and info 29 | /// * debug: log error, warning, info, and debug 30 | /// * trace: log error, warning, info, debug, and trace 31 | /// * off: turn off logging 32 | pub log_level: String, 33 | } 34 | /// The logger configuration implementation. 35 | impl LoggerConfig { 36 | /// Get the default logger configuration. 37 | /// 38 | /// # Returns 39 | /// The default logger configuration. 40 | pub fn default_result() -> Result> { 41 | configs::deserialize_to_config_into::(Path::new( 42 | &configs::custom::default_config_logger_file_path()?, 43 | )) 44 | } 45 | } 46 | /// The implementation of the configuration file for the logger. 47 | impl ConfigFile for LoggerConfig { 48 | type Raw = LoggerRaw; 49 | 50 | fn get_type() -> ConfigType { 51 | ConfigType::Logger 52 | } 53 | 54 | fn override_fields() -> bool { 55 | true 56 | } 57 | 58 | fn merge(&mut self, other: Option) -> Self { 59 | match other { 60 | None => self.clone(), 61 | Some(other) => { 62 | tracing::info!("Merging logger config"); 63 | if let Some(log_dir) = other.log_dir { 64 | if !Path::new(&log_dir).exists() { 65 | std::fs::create_dir_all(&log_dir).unwrap(); 66 | } 67 | self.log_dir = log_dir; 68 | } 69 | if let Some(log_file) = other.log_file { 70 | self.log_file = log_file; 71 | } 72 | if let Some(log_level) = other.log_level { 73 | self.log_level = log_level; 74 | } 75 | if let Some(rotation_frequency) = other.rotation_frequency { 76 | self.rotation_frequency = rotation_frequency; 77 | } 78 | if let Some(max_old_log_files) = other.max_old_log_files { 79 | self.max_old_log_files = max_old_log_files; 80 | } 81 | self.clone() 82 | } 83 | } 84 | } 85 | } 86 | /// The default logger configuration. 87 | impl Default for LoggerConfig { 88 | fn default() -> Self { 89 | Self::default_result().unwrap() 90 | } 91 | } 92 | /// The conversion from the raw logger configuration to the logger 93 | /// configuration. 94 | impl From for LoggerConfig { 95 | fn from(raw: LoggerRaw) -> Self { 96 | let log_dir = utils::tgt_dir() 97 | .unwrap() 98 | .join(raw.log_dir.unwrap()) 99 | .to_string_lossy() 100 | .to_string(); 101 | if !Path::new(&log_dir).exists() { 102 | std::fs::create_dir_all(&log_dir).unwrap(); 103 | } 104 | 105 | Self { 106 | log_dir, 107 | log_file: raw.log_file.unwrap(), 108 | rotation_frequency: raw.rotation_frequency.unwrap(), 109 | max_old_log_files: raw.max_old_log_files.unwrap(), 110 | log_level: raw.log_level.unwrap(), 111 | } 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use crate::configs::config_file::ConfigFile; 118 | use crate::configs::{custom::logger_custom::LoggerConfig, raw::logger_raw::LoggerRaw}; 119 | use crate::utils::tgt_dir; 120 | 121 | #[test] 122 | fn test_logger_config_default() { 123 | let logger_config = LoggerConfig::default(); 124 | assert_eq!( 125 | logger_config.log_dir, 126 | tgt_dir() 127 | .unwrap() 128 | .join(".data/logs") 129 | .to_string_lossy() 130 | .to_string() 131 | ); 132 | assert_eq!(logger_config.log_file, "tgt.log"); 133 | assert_eq!(logger_config.log_level, "info"); 134 | } 135 | 136 | #[test] 137 | fn test_logger_config_from_raw() { 138 | let logger_raw = LoggerRaw { 139 | log_dir: Some(".data/logs".to_string()), 140 | log_file: Some("tgt.log".to_string()), 141 | rotation_frequency: Some("hourly".to_string()), 142 | max_old_log_files: Some(3), 143 | log_level: Some("debug".to_string()), 144 | }; 145 | let logger_config = LoggerConfig::from(logger_raw); 146 | assert_eq!( 147 | logger_config.log_dir, 148 | tgt_dir() 149 | .unwrap() 150 | .join(".data/logs") 151 | .to_string_lossy() 152 | .to_string() 153 | ); 154 | assert_eq!(logger_config.log_file, "tgt.log"); 155 | assert_eq!(logger_config.rotation_frequency, "hourly"); 156 | assert_eq!(logger_config.max_old_log_files, 3); 157 | assert_eq!(logger_config.log_level, "debug"); 158 | } 159 | 160 | #[test] 161 | fn test_logger_config_merge() { 162 | let mut logger_config = LoggerConfig::from(LoggerRaw { 163 | log_dir: Some(".data/logs".to_string()), 164 | log_file: Some("tgt.log".to_string()), 165 | rotation_frequency: Some("never".to_string()), 166 | max_old_log_files: Some(5), 167 | log_level: Some("info".to_string()), 168 | }); 169 | let logger_raw = LoggerRaw { 170 | log_dir: None, 171 | log_file: None, 172 | rotation_frequency: None, 173 | max_old_log_files: None, 174 | log_level: Some("debug".to_string()), 175 | }; 176 | logger_config = logger_config.merge(Some(logger_raw)); 177 | assert_eq!( 178 | logger_config.log_dir, 179 | tgt_dir() 180 | .unwrap() 181 | .join(".data/logs") 182 | .to_string_lossy() 183 | .to_string() 184 | ); 185 | assert_eq!(logger_config.log_file, "tgt.log"); 186 | assert_eq!(logger_config.rotation_frequency, "never"); 187 | assert_eq!(logger_config.max_old_log_files, 5); 188 | assert_eq!(logger_config.log_level, "debug"); 189 | } 190 | 191 | #[test] 192 | fn test_logger_config_override_fields() { 193 | assert!(LoggerConfig::override_fields()); 194 | } 195 | 196 | #[test] 197 | fn test_merge_all_fields() { 198 | let mut logger_config = LoggerConfig::default(); 199 | let logger_raw = LoggerRaw { 200 | log_dir: None, 201 | log_file: None, 202 | rotation_frequency: None, 203 | max_old_log_files: None, 204 | log_level: None, 205 | }; 206 | logger_config = logger_config.merge(Some(logger_raw)); 207 | assert_eq!( 208 | logger_config.log_dir, 209 | tgt_dir() 210 | .unwrap() 211 | .join(".data/logs") 212 | .to_string_lossy() 213 | .to_string() 214 | ); 215 | assert_eq!(logger_config.log_file, "tgt.log"); 216 | assert_eq!(logger_config.rotation_frequency, "daily"); 217 | assert_eq!(logger_config.max_old_log_files, 7); 218 | assert_eq!(logger_config.log_level, "info"); 219 | } 220 | 221 | #[test] 222 | fn test_logger_config_get_type() { 223 | assert_eq!( 224 | LoggerConfig::get_type(), 225 | crate::configs::config_type::ConfigType::Logger 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/configs/custom/mod.rs: -------------------------------------------------------------------------------- 1 | use {super::config_type::ConfigType, crate::utils::tgt_config_dir, std::io}; 2 | 3 | pub mod app_custom; 4 | pub mod keymap_custom; 5 | pub mod logger_custom; 6 | pub mod palette_custom; 7 | pub mod telegram_custom; 8 | pub mod theme_custom; 9 | 10 | /// Get the default configuration file path of the specified configuration type. 11 | /// It is cross-platform. 12 | /// 13 | /// # Arguments 14 | /// * `config_type` - The configuration type. 15 | /// 16 | /// # Returns 17 | /// The default configuration file path of the specified configuration type. 18 | fn default_config_file_path_of(config_type: ConfigType) -> io::Result { 19 | Ok(tgt_config_dir()? 20 | .join(config_type.as_default_filename()) 21 | .to_str() 22 | .unwrap() 23 | .to_string()) 24 | } 25 | 26 | /// Get the default configuration file path for the logger. 27 | /// 28 | /// # Returns 29 | /// The default configuration file path for the logger. 30 | pub fn default_config_logger_file_path() -> io::Result { 31 | default_config_file_path_of(ConfigType::Logger) 32 | } 33 | 34 | /// Get the default configuration file path for the keymap. 35 | /// 36 | /// # Returns 37 | /// The default configuration file path for the keymap. 38 | pub fn default_config_keymap_file_path() -> io::Result { 39 | default_config_file_path_of(ConfigType::Keymap) 40 | } 41 | 42 | /// Get the default configuration file path for the application. 43 | /// 44 | /// # Returns 45 | /// The default configuration file path for the application. 46 | pub fn default_config_app_file_path() -> io::Result { 47 | default_config_file_path_of(ConfigType::App) 48 | } 49 | 50 | /// Get the default configuration file path for the theme. 51 | /// 52 | /// # Returns 53 | /// The default configuration file path for the theme. 54 | pub fn default_config_theme_file_path() -> io::Result { 55 | default_config_file_path_of(ConfigType::Theme) 56 | } 57 | 58 | /// Get the default configuration file path for the palette. 59 | /// 60 | /// # Returns 61 | /// The default configuration file path for the palette. 62 | pub fn default_config_palette_file_path() -> io::Result { 63 | default_config_file_path_of(ConfigType::Palette) 64 | } 65 | 66 | /// Get the default configuration file path for the telegram. 67 | /// 68 | /// # Returns 69 | /// The default configuration file path for the telegram. 70 | pub fn default_config_telegram_file_path() -> io::Result { 71 | default_config_file_path_of(ConfigType::Telegram) 72 | } 73 | 74 | // #[cfg(not(target_os = "windows"))] 75 | // pub const DEFAULT_CONFIG_LOGGER_FILE_PATH: &str = 76 | // include_str!("../../../config/logger.toml"); 77 | // 78 | // #[cfg(target_os = "windows")] 79 | // pub const DEFAULT_CONFIG_LOGGER_FILE_PATH: &str = 80 | // include_str!("..\\..\\..\\config\\icons.toml"); 81 | -------------------------------------------------------------------------------- /src/configs/custom/palette_custom.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | app_error::AppError, 3 | configs::{ 4 | self, config_file::ConfigFile, config_theme::ThemeStyle, config_type::ConfigType, 5 | raw::palette_raw::PaletteRaw, 6 | }, 7 | APP_CONFIG, 8 | }; 9 | use ratatui::style::Color; 10 | use std::{collections::HashMap, path::Path}; 11 | 12 | #[derive(Clone, Debug)] 13 | /// The palette configuration. 14 | pub struct PaletteConfig { 15 | /// The palette. 16 | pub palette: HashMap, 17 | } 18 | /// The palette configuration implementation. 19 | impl PaletteConfig { 20 | /// Get the default palette configuration. 21 | /// 22 | /// # Returns 23 | /// * `Result` - The default palette configuration. 24 | pub fn default_result() -> Result> { 25 | configs::deserialize_to_config_into::(Path::new( 26 | &configs::custom::default_config_palette_file_path()?, 27 | )) 28 | } 29 | } 30 | /// The implementation of the configuration file for the palette. 31 | impl ConfigFile for PaletteConfig { 32 | type Raw = PaletteRaw; 33 | 34 | fn get_type() -> ConfigType { 35 | ConfigType::Palette 36 | } 37 | 38 | fn override_fields() -> bool { 39 | true 40 | } 41 | 42 | // We need to override the default implementation of the get_config function 43 | // to use the theme_filename from the app config. 44 | // The default value of theme_filename is "theme.toml". 45 | fn get_config() -> Self { 46 | if Self::override_fields() { 47 | let mut default = Self::default(); 48 | default.merge(Self::deserialize_custom_config::( 49 | // Self::get_type().as_default_filename().as_str(), 50 | &APP_CONFIG.theme_filename, 51 | )) 52 | } else { 53 | Self::deserialize_config_or_default::( 54 | Self::get_type().as_default_filename().as_str(), 55 | ) 56 | } 57 | } 58 | 59 | fn merge(&mut self, other: Option) -> Self { 60 | match other { 61 | None => self.clone(), 62 | Some(other) => { 63 | tracing::info!("Merging palette config"); 64 | if let Some(palette) = other.palette { 65 | palette.into_iter().for_each(|(k, v)| { 66 | self.palette 67 | .insert(k, ThemeStyle::str_to_color(&v).unwrap()); 68 | }); 69 | } 70 | self.clone() 71 | } 72 | } 73 | } 74 | } 75 | /// The default implementation for the palette configuration. 76 | impl Default for PaletteConfig { 77 | fn default() -> Self { 78 | Self::default_result().unwrap() 79 | } 80 | } 81 | /// The conversion from the raw palette configuration to the palette 82 | /// configuration. 83 | impl From for PaletteConfig { 84 | fn from(raw: PaletteRaw) -> Self { 85 | let palette = raw 86 | .palette 87 | .unwrap() 88 | .into_iter() 89 | .map(|(k, v)| { 90 | ( 91 | k, 92 | match ThemeStyle::str_to_color(&v) { 93 | Ok(color) => color, 94 | Err(e) => { 95 | eprintln!("In the palette config: {}", e); 96 | std::process::exit(1); 97 | } 98 | }, 99 | ) 100 | }) 101 | .collect(); 102 | Self { palette } 103 | } 104 | } 105 | 106 | #[cfg(test)] 107 | mod tests { 108 | use { 109 | crate::configs::{ 110 | config_file::ConfigFile, config_type::ConfigType, 111 | custom::palette_custom::PaletteConfig, raw::palette_raw::PaletteRaw, 112 | }, 113 | std::collections::HashMap, 114 | }; 115 | 116 | #[test] 117 | fn test_palette_config_default() { 118 | let palette_config = crate::configs::custom::palette_custom::PaletteConfig::default(); 119 | assert_eq!(palette_config.palette.len(), 16); 120 | } 121 | 122 | #[test] 123 | fn test_palette_config_from_raw_empty() { 124 | let raw = crate::configs::raw::palette_raw::PaletteRaw { 125 | palette: Some(HashMap::new()), 126 | }; 127 | let palette_config = PaletteConfig::from(raw); 128 | assert_eq!(palette_config.palette.len(), 0); 129 | } 130 | 131 | #[test] 132 | fn test_palette_config_from_raw() { 133 | let mut palette = HashMap::new(); 134 | palette.insert("black".to_string(), "#000000".parse().unwrap()); 135 | palette.insert("white".to_string(), "#ffffff".parse().unwrap()); 136 | let raw = PaletteRaw { 137 | palette: Some(palette), 138 | }; 139 | let palette_config = PaletteConfig::from(raw); 140 | assert_eq!(palette_config.palette.len(), 2); 141 | assert_eq!( 142 | palette_config.palette.get("black").unwrap(), 143 | &"#000000".parse().unwrap() 144 | ); 145 | assert_eq!( 146 | palette_config.palette.get("white").unwrap(), 147 | &"#ffffff".parse().unwrap() 148 | ); 149 | } 150 | 151 | #[test] 152 | fn test_palette_config_merge() { 153 | let mut palette = HashMap::new(); 154 | palette.insert("black".to_string(), "#000000".parse().unwrap()); 155 | palette.insert("white".to_string(), "#ffffff".parse().unwrap()); 156 | let raw = PaletteRaw { 157 | palette: Some(palette), 158 | }; 159 | let palette_config = crate::configs::custom::palette_custom::PaletteConfig::from(raw); 160 | assert_eq!(palette_config.palette.len(), 2); 161 | assert_eq!( 162 | palette_config.palette.get("black").unwrap(), 163 | &"#000000".parse().unwrap() 164 | ); 165 | assert_eq!( 166 | palette_config.palette.get("white").unwrap(), 167 | &"#ffffff".parse().unwrap() 168 | ); 169 | } 170 | 171 | #[test] 172 | fn test_override_fields() { 173 | assert!(PaletteConfig::override_fields()); 174 | } 175 | 176 | #[test] 177 | fn test_get_type() { 178 | assert_eq!(PaletteConfig::get_type(), ConfigType::Palette); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/configs/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::app_error::AppError; 2 | use config::Config; 3 | use config::File; 4 | use config::FileFormat; 5 | use serde::de::DeserializeOwned; 6 | use std::path::Path; 7 | 8 | pub mod custom; 9 | pub mod raw; 10 | 11 | pub mod config_file; 12 | pub mod config_theme; 13 | pub mod config_type; 14 | 15 | /// Deserialize a configuration file into a configuration struct. 16 | /// This function attempts to parse the specified file and returns the parsed 17 | /// configuration. If the file cannot be parsed, an error is returned. 18 | /// 19 | /// # Arguments 20 | /// * `file_path` - The path to the file to parse. 21 | /// 22 | /// # Returns 23 | /// The parsed configuration or an error if the file cannot be parsed. 24 | pub fn deserialize_to_config(file_path: &Path) -> Result> 25 | where 26 | R: DeserializeOwned, 27 | { 28 | let builder: R = Config::builder() 29 | .add_source(File::from(file_path).format(FileFormat::Toml)) 30 | .build()? 31 | .try_deserialize::()?; 32 | Ok(builder) 33 | } 34 | /// Deserialize a configuration file into a configuration struct and convert it 35 | /// into another configuration struct. This function attempts to parse the 36 | /// specified file and returns the parsed configuration. If the file cannot be 37 | /// parsed, an error is returned. 38 | /// 39 | /// # Arguments 40 | /// * `file_path` - The path to the file to parse. 41 | /// 42 | /// # Returns 43 | /// The parsed configuration or an error if the file cannot be parsed. 44 | pub fn deserialize_to_config_into(file_path: &Path) -> Result> 45 | where 46 | R: DeserializeOwned + Into, 47 | { 48 | deserialize_to_config::(file_path).map(|s| s.into()) 49 | } 50 | -------------------------------------------------------------------------------- /src/configs/raw/app_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Clone, Debug, Deserialize)] 4 | /// The raw application configuration. 5 | pub struct AppRaw { 6 | /// A boolean flag that represents whether the mouse is enabled or not. 7 | pub mouse_support: Option, 8 | /// A boolean flag that represents whether the clipboard is enabled or not. 9 | pub paste_support: Option, 10 | /// The frame rate at which the user interface should be rendered. 11 | pub frame_rate: Option, 12 | /// A boolean flag that represents whether the status bar should be shown 13 | /// or not. 14 | pub show_status_bar: Option, 15 | /// A boolean flag that represents whether the title bar should be shown or 16 | /// not. 17 | pub show_title_bar: Option, 18 | /// A boolean flag that represents whether the theme should be enabled or 19 | /// not. 20 | pub theme_enable: Option, 21 | /// The name of the theme file that should be used. 22 | pub theme_filename: Option, 23 | /// A boolean flag that represents whether the API_ID should be taken from 24 | /// the Telegram configuration or from environment variable `API_ID`. 25 | pub take_api_id_from_telegram_config: Option, 26 | /// A boolean flag that represents whether the API_HASH should be taken from 27 | /// the Telegram configuration or from environment variables `API_HASH`. 28 | pub take_api_hash_from_telegram_config: Option, 29 | } 30 | -------------------------------------------------------------------------------- /src/configs/raw/keymap_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Clone, Debug, Deserialize)] 4 | /// The command keymap configuration. 5 | pub struct KeymapEntry { 6 | /// The key combination. 7 | /// It must be a valid key combination. 8 | pub keys: Vec, // Event 9 | /// The command to execute. 10 | /// It must be a valid command. 11 | pub command: String, // Action 12 | /// The description of the command. 13 | pub description: Option, 14 | } 15 | 16 | #[derive(Clone, Debug, Deserialize)] 17 | /// The keymap configuration. 18 | pub struct KeymapMode { 19 | #[serde(default)] 20 | /// The keymap entries. 21 | pub keymap: Vec, 22 | } 23 | 24 | #[derive(Clone, Debug, Deserialize)] 25 | /// The raw keymap configuration. 26 | pub struct KeymapRaw { 27 | /// The keymap for the core window mode, they are used in all components. 28 | pub core_window: Option, 29 | /// The keymap for the chat list mode. 30 | pub chat_list: Option, 31 | /// The keymap for the chat mode. 32 | pub chat: Option, 33 | /// The keymap for the chat edit mode. 34 | pub prompt: Option, 35 | } 36 | -------------------------------------------------------------------------------- /src/configs/raw/logger_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Clone, Debug, Deserialize)] 4 | /// The raw logger configuration. 5 | pub struct LoggerRaw { 6 | /// The folder where the log file is stored. 7 | pub log_dir: Option, 8 | /// The name of the log file. 9 | pub log_file: Option, 10 | /// The rotation frequency of the log. 11 | /// The log rotation frequency can be one of the following: 12 | /// * minutely: A new log file in the format of log_folder/log_file.yyyy-MM-dd-HH-mm will be created minutely (once per minute) 13 | /// * hourly: A new log file in the format of log_folder/log_file.yyyy-MM-dd-HH will be created hourly 14 | /// * daily: A new log file in the format of log_folder/log_file.yyyy-MM-dd will be created daily 15 | /// * never: This will result in log file located at log_folder/log_file 16 | pub rotation_frequency: Option, 17 | /// The maximum number of old log files that will be stored 18 | pub max_old_log_files: Option, 19 | /// The level of the log. 20 | /// The log level can be one of the following: 21 | /// * error: only log error 22 | /// * warn: log error and warning 23 | /// * info: log error, warning, and info 24 | /// * debug: log error, warning, info, and debug 25 | /// * trace: log error, warning, info, debug, and trace 26 | /// * off: turn off logging 27 | pub log_level: Option, 28 | } 29 | -------------------------------------------------------------------------------- /src/configs/raw/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod app_raw; 2 | pub mod keymap_raw; 3 | pub mod logger_raw; 4 | pub mod palette_raw; 5 | pub mod telegram_raw; 6 | pub mod theme_raw; 7 | -------------------------------------------------------------------------------- /src/configs/raw/palette_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Clone, Debug, Deserialize)] 5 | /// The raw palette configuration. 6 | pub struct PaletteRaw { 7 | /// The palette. 8 | pub palette: Option>, 9 | } 10 | -------------------------------------------------------------------------------- /src/configs/raw/telegram_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Clone, Debug, Deserialize)] 4 | /// The telegram raw configuration. 5 | pub struct TelegramRaw { 6 | /// The API ID. 7 | /// Note that the this field is used only if the `take_api_id_from_telegram_config` is `true` 8 | /// in the application configuration (`app.toml`). 9 | pub api_id: Option, 10 | /// The API hash. 11 | /// Note that the this field is used only if the `take_api_hash_from_telegram_config` is `true` 12 | /// in the application configuration (`app.toml`). 13 | pub api_hash: Option, 14 | /// The directory where the database is stored. 15 | pub database_dir: Option, 16 | /// A flag that indicates if the user database should be used. 17 | pub use_file_database: Option, 18 | /// A flag that indicates if the chat info database should be used. 19 | pub use_chat_info_database: Option, 20 | /// A flag that indicates if the message database should be used. 21 | pub use_message_database: Option, 22 | /// A language code. 23 | pub system_language_code: Option, 24 | /// The model of the device. 25 | pub device_model: Option, 26 | /// The verbosity level of the logging. 27 | pub verbosity_level: Option, 28 | /// The path to the working directory. 29 | pub log_path: Option, 30 | /// A flag that indicates if the log to stderr should be also redirected. 31 | pub redirect_stderr: Option, 32 | } 33 | -------------------------------------------------------------------------------- /src/configs/raw/theme_raw.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Clone, Debug, Deserialize)] 5 | /// The theme entry. 6 | pub struct ThemeEntry { 7 | /// The foreground color. 8 | pub fg: Option, 9 | /// The background color. 10 | pub bg: Option, 11 | /// The italic option. 12 | pub italic: Option, 13 | /// The bold option. 14 | pub bold: Option, 15 | /// The underline option. 16 | pub underline: Option, 17 | } 18 | 19 | #[derive(Clone, Debug, Deserialize)] 20 | /// The raw theme configuration. 21 | pub struct ThemeRaw { 22 | /// Customization for all components. 23 | pub common: Option>, 24 | /// The theme for the chat list. 25 | pub chat_list: Option>, 26 | /// The theme for the chat. 27 | pub chat: Option>, 28 | /// The theme for the prompt. 29 | pub prompt: Option>, 30 | /// The theme for the status bar. 31 | pub status_bar: Option>, 32 | /// The theme for the title bar. 33 | pub title_bar: Option>, 34 | /// The theme for the reply message. 35 | pub reply_message: Option>, 36 | } 37 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use crate::app_error::AppError; 2 | use crate::tg::td_enums::{TdChatList, TdMessageReplyToMessage}; 3 | use crossterm::event::{KeyCode, KeyModifiers, MouseEvent}; 4 | use ratatui::layout::Rect; 5 | use std::fmt::{self, Display, Formatter}; 6 | use std::{hash::Hash, str::FromStr}; 7 | 8 | #[derive(Debug, Clone, PartialEq, Hash, Eq)] 9 | /// `Event` is an enum that represents the different types of events that can be 10 | /// generated by the intraction with the terminal (`tui_backend`). 11 | /// These events are used to drive the user interface and the application logic 12 | /// and should be handled entirely. 13 | pub enum Event { 14 | /// Unknown event. 15 | Unknown, 16 | /// Resize event with width and height. 17 | Resize(u16, u16), 18 | /// Key event with a `KeyCode` and `KeyModifiers`. 19 | Key(KeyCode, KeyModifiers), 20 | /// Paste event with a `String`. 21 | Paste(String), 22 | /// Mouse event with a `MouseEvent` struct. 23 | Mouse(MouseEvent), 24 | /// Init event. 25 | Init, 26 | /// Render event. 27 | Render, 28 | /// Focus Lost event. 29 | FocusLost, 30 | /// Focus Gained event. 31 | FocusGained, 32 | 33 | /// Update area event with a `Rect` struct. 34 | UpdateArea(Rect), 35 | /// EditMessage event with a `String`. 36 | /// This event is used to edit a message. 37 | /// The first parameter is the `message_id` and the second parameter is the `text`. 38 | EditMessage(i64, String), 39 | /// ReplyMessage event with a `String`. 40 | /// This event is used to reply to a message. 41 | /// The first parameter is the `message_id` and the second parameter is the `text`. 42 | ReplyMessage(i64, String), 43 | 44 | /// GetMe event. 45 | GetMe, 46 | /// Load chats event with a `ChatList` and a limit. 47 | LoadChats(TdChatList, i32), 48 | /// Send message event with a `String`. 49 | /// This event is used to send a message. 50 | /// The first parameter is the `text`. 51 | /// The second parameter is the `reply_to` field. 52 | SendMessage(String, Option), 53 | /// Send message edited event with a `i64` and a `String`. 54 | /// The first parameter is the `message_id` and the second parameter is the `text`. 55 | SendMessageEdited(i64, String), 56 | /// Get chat history event. 57 | GetChatHistory, 58 | /// Delete messages event with a `Vec` and a `bool`. 59 | /// The first parameter is the `message_ids` and the second parameter is the `revoke`. 60 | /// If `revoke` is true, the message will be deleted for everyone. 61 | /// If `revoke` is false, the message will be deleted only for the current user. 62 | DeleteMessages(Vec, bool), 63 | /// View all messages event. 64 | ViewAllMessages, 65 | } 66 | /// Implement the `Event` enum. 67 | impl Event { 68 | /// Create an event with a key code and modifiers. 69 | /// 70 | /// # Arguments 71 | /// * `s` - A string that represents the key code. 72 | /// * `modifiers` - A `KeyModifiers` struct that represents the modifiers. 73 | /// 74 | /// # Returns 75 | /// * `Result` - An event or an error. 76 | pub fn event_with_modifiers(s: &str, modifiers: KeyModifiers) -> Result> { 77 | match s { 78 | "backspace" => Ok(Event::Key(KeyCode::Backspace, modifiers)), 79 | "enter" => Ok(Event::Key(KeyCode::Enter, modifiers)), 80 | "left" => Ok(Event::Key(KeyCode::Left, modifiers)), 81 | "right" => Ok(Event::Key(KeyCode::Right, modifiers)), 82 | "up" => Ok(Event::Key(KeyCode::Up, modifiers)), 83 | "down" => Ok(Event::Key(KeyCode::Down, modifiers)), 84 | "home" => Ok(Event::Key(KeyCode::Home, modifiers)), 85 | "end" => Ok(Event::Key(KeyCode::End, modifiers)), 86 | "page_up" => Ok(Event::Key(KeyCode::PageUp, modifiers)), 87 | "page_down" => Ok(Event::Key(KeyCode::PageDown, modifiers)), 88 | "tab" => Ok(Event::Key(KeyCode::Tab, modifiers)), 89 | "back_tab" => Ok(Event::Key(KeyCode::BackTab, modifiers)), 90 | "delete" => Ok(Event::Key(KeyCode::Delete, modifiers)), 91 | "insert" => Ok(Event::Key(KeyCode::Insert, modifiers)), 92 | "null" => Ok(Event::Key(KeyCode::Null, modifiers)), 93 | "esc" => Ok(Event::Key(KeyCode::Esc, modifiers)), 94 | "f1" => Ok(Event::Key(KeyCode::F(1), modifiers)), 95 | "f2" => Ok(Event::Key(KeyCode::F(2), modifiers)), 96 | "f3" => Ok(Event::Key(KeyCode::F(3), modifiers)), 97 | "f4" => Ok(Event::Key(KeyCode::F(4), modifiers)), 98 | "f5" => Ok(Event::Key(KeyCode::F(5), modifiers)), 99 | "f6" => Ok(Event::Key(KeyCode::F(6), modifiers)), 100 | "f7" => Ok(Event::Key(KeyCode::F(7), modifiers)), 101 | "f8" => Ok(Event::Key(KeyCode::F(8), modifiers)), 102 | "f9" => Ok(Event::Key(KeyCode::F(9), modifiers)), 103 | "f10" => Ok(Event::Key(KeyCode::F(10), modifiers)), 104 | "f11" => Ok(Event::Key(KeyCode::F(11), modifiers)), 105 | "f12" => Ok(Event::Key(KeyCode::F(12), modifiers)), 106 | e => { 107 | if e.len() == 1 && e.chars().next().unwrap().is_ascii() { 108 | Ok(Event::Key( 109 | KeyCode::Char(e.chars().next().unwrap()), 110 | modifiers, 111 | )) 112 | } else { 113 | Err(AppError::InvalidEvent(e.to_string())) 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | /// Implement the `FromStr` trait for `Event`. 121 | impl FromStr for Event { 122 | type Err = AppError<()>; 123 | 124 | fn from_str(s: &str) -> Result { 125 | let modifiers = s.split('+').collect::>(); 126 | if modifiers.len() > 1 { 127 | let key = modifiers[modifiers.len() - 1]; 128 | let modifiers = modifiers[..modifiers.len() - 1] 129 | .iter() 130 | .map(|m| match *m { 131 | "ctrl" => KeyModifiers::CONTROL, 132 | "alt" => KeyModifiers::ALT, 133 | "shift" => KeyModifiers::SHIFT, 134 | "super" => KeyModifiers::SUPER, 135 | "meta" => KeyModifiers::META, 136 | "hyper" => KeyModifiers::HYPER, 137 | _ => KeyModifiers::NONE, 138 | }) 139 | .fold(KeyModifiers::NONE, |acc, m| acc | m); 140 | Self::event_with_modifiers(key, modifiers) 141 | } else { 142 | Self::event_with_modifiers(s, KeyModifiers::NONE) 143 | } 144 | } 145 | } 146 | 147 | /// Implement the `Display` trait for `Event`. 148 | impl Display for Event { 149 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 150 | match self { 151 | Event::Unknown => write!(f, "Unknown"), 152 | Event::Init => write!(f, "Init"), 153 | Event::Render => write!(f, "Render"), 154 | Event::Resize(width, height) => { 155 | write!(f, "Resize({}, {})", width, height) 156 | } 157 | Event::Key(key, modifiers) => { 158 | let k = if let KeyCode::Char(c) = key { 159 | c.to_string() 160 | } else { 161 | format!("{:?}", key) 162 | }; 163 | 164 | match *modifiers { 165 | KeyModifiers::NONE => write!(f, "{}", k), 166 | KeyModifiers::CONTROL => write!(f, "Ctrl+{}", k), 167 | KeyModifiers::ALT => write!(f, "Alt+{}", k), 168 | KeyModifiers::SHIFT => write!(f, "Shift+{}", k), 169 | KeyModifiers::SUPER => write!(f, "Super+{}", k), 170 | KeyModifiers::META => write!(f, "Meta+{}", k), 171 | KeyModifiers::HYPER => write!(f, "Hyper+{}", k), 172 | _ => write!(f, "{:?}+{}", modifiers, k), 173 | } 174 | } 175 | Event::Mouse(mouse) => write!(f, "Mouse({:?})", mouse), 176 | Event::UpdateArea(area) => write!(f, "UpdateArea({:?})", area), 177 | Event::Paste(s) => write!(f, "Paste({})", s), 178 | Event::FocusLost => write!(f, "FocusLost"), 179 | Event::FocusGained => write!(f, "FocusGained"), 180 | Event::GetMe => write!(f, "GetMe"), 181 | Event::LoadChats(chat_list, limit) => { 182 | write!(f, "LoadChats({:?}, {})", chat_list, limit) 183 | } 184 | Event::SendMessage(s, reply_to) => { 185 | write!(f, "SendMessage({}, {:?})", s, reply_to) 186 | } 187 | Event::SendMessageEdited(message_id, s) => { 188 | write!(f, "SendMessageEdited({}, {})", message_id, s) 189 | } 190 | Event::GetChatHistory => { 191 | write!(f, "GetChatHistory") 192 | } 193 | Event::DeleteMessages(message_ids, revoke) => { 194 | write!(f, "DeleteMessages({:?}, {})", message_ids, revoke) 195 | } 196 | Event::EditMessage(message_id, text) => { 197 | write!(f, "EditMessage({}, {})", message_id, text) 198 | } 199 | Event::ReplyMessage(message_id, text) => { 200 | write!(f, "ReplyMessage({}, {})", message_id, text) 201 | } 202 | Event::ViewAllMessages => write!(f, "ViewAllMessages"), 203 | } 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/logger.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{app_error::AppError, configs::custom::logger_custom::LoggerConfig}, 3 | std::fs, 4 | tracing_error::ErrorLayer, 5 | tracing_subscriber::{ 6 | filter::EnvFilter, prelude::__tracing_subscriber_SubscriberExt, registry::Registry, 7 | util::SubscriberInitExt, Layer, 8 | }, 9 | }; 10 | 11 | #[derive(Clone, Debug)] 12 | /// The logger. 13 | /// This struct is used to initialize the logger for the application. 14 | pub struct Logger { 15 | /// The log folder. 16 | log_dir: String, 17 | /// The log file. 18 | log_file: String, 19 | /// The rotation frequency. 20 | rotation_frequency: tracing_appender::rolling::Rotation, 21 | /// The maximum number of old log files to keep. 22 | max_old_log_files: usize, 23 | /// The log level. 24 | log_level: String, 25 | } 26 | 27 | impl Logger { 28 | /// Create a new logger from the logger configuration. 29 | /// 30 | /// # Arguments 31 | /// * `logger_config` - The logger configuration. 32 | /// 33 | /// # Returns 34 | /// The new logger. 35 | pub fn from_config(logger_config: LoggerConfig) -> Self { 36 | logger_config.into() 37 | } 38 | 39 | /// Initialize the logger. 40 | /// This function initializes the logger for the application. 41 | /// The logger is initialized with the following layers: 42 | /// - a file subscriber 43 | /// - an error layer 44 | /// 45 | /// The file subscriber is initialized with the following settings: 46 | /// - file: true 47 | /// - line_number: true 48 | /// - target: true 49 | /// - ansi: false 50 | /// - writer: the log file 51 | /// - filter: the `RUST_LOG` environment variable 52 | /// 53 | /// The error layer is initialized with the default settings. 54 | pub fn init(&self) { 55 | self.set_rust_log_variable(); 56 | let _ = self.delete_old_log_files(); 57 | 58 | let file_appender = tracing_appender::rolling::RollingFileAppender::new( 59 | self.rotation_frequency.clone(), 60 | self.log_dir.clone(), 61 | self.log_file.clone(), 62 | ); 63 | 64 | let file_subscriber = tracing_subscriber::fmt::layer() 65 | .with_timer(tracing_subscriber::fmt::time::ChronoLocal::new( 66 | "%Y-%m-%dT%H:%M:%S%.6fZ".to_string(), 67 | )) 68 | .with_file(true) 69 | .with_line_number(true) 70 | .with_target(true) 71 | .with_ansi(false) 72 | .with_writer(file_appender) 73 | // Parsing an EnvFilter from the default environment variable 74 | // (RUST_LOG) 75 | .with_filter(EnvFilter::from_default_env()); //tracing_subscriber::filter::LevelFilter::TRACE 76 | 77 | Registry::default() 78 | .with(file_subscriber) 79 | .with(ErrorLayer::default()) 80 | .init(); 81 | } 82 | /// Deletes old log files from the specified log folder. 83 | /// 84 | /// This function iterates through the log files in the specified log folder, filters out files 85 | /// whose filenames match the provided prefix, sorts the remaining log files, and deletes the oldest ones 86 | /// if the number of log files exceeds a certain threshold. 87 | /// 88 | /// # Returns 89 | /// * `Result<(), AppError>` - The result of the operation. 90 | fn delete_old_log_files(&self) -> Result<(), AppError<()>> { 91 | let mut logs: Vec<_> = fs::read_dir(&self.log_dir)? 92 | .filter_map(Result::ok) 93 | .map(|entry| entry.path()) 94 | .filter(|path| { 95 | path.file_name() 96 | .and_then(|filename| filename.to_str()) 97 | .map(|name| name.starts_with(&self.log_file)) 98 | .unwrap_or(false) 99 | }) 100 | .collect(); 101 | 102 | logs.sort(); 103 | 104 | let logs_to_delete = logs.len().saturating_sub(self.max_old_log_files); 105 | 106 | for log in logs.iter().take(logs_to_delete) { 107 | fs::remove_file(log)?; 108 | } 109 | Ok(()) 110 | } 111 | /// Set the `RUST_LOG` environment variable. 112 | /// 113 | /// This function try to set the `RUST_LOG` environment variable to: 114 | /// - the value of the `RUST_LOG` environment variable 115 | /// - the value of `log_level` field of the `Logger` struct or to `CARGO_CRATE_NAME=info` if the `RUST_LOG` environment variable is not set. 116 | fn set_rust_log_variable(&self) { 117 | std::env::set_var( 118 | "RUST_LOG", 119 | std::env::var("RUST_LOG") 120 | .or_else(|_| Ok(self.log_level.clone())) 121 | .unwrap_or_else(|_: String| format!("{}=info", env!("CARGO_CRATE_NAME"))), 122 | ); 123 | } 124 | } 125 | /// The conversion from the logger configuration to the logger. 126 | impl From for Logger { 127 | fn from(config: LoggerConfig) -> Self { 128 | Self { 129 | log_dir: config.log_dir, 130 | log_file: config.log_file, 131 | rotation_frequency: match config.rotation_frequency.as_str() { 132 | "minutely" => tracing_appender::rolling::Rotation::MINUTELY, 133 | "hourly" => tracing_appender::rolling::Rotation::HOURLY, 134 | "daily" => tracing_appender::rolling::Rotation::DAILY, 135 | _ => tracing_appender::rolling::Rotation::NEVER, 136 | }, 137 | max_old_log_files: config.max_old_log_files, 138 | log_level: config.log_level, 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod action; 2 | pub mod app_context; 3 | pub mod app_error; 4 | pub mod cli; 5 | pub mod component_name; 6 | pub mod event; 7 | pub mod logger; 8 | pub mod tui; 9 | pub mod tui_backend; 10 | pub mod utils; 11 | 12 | // Folders 13 | pub mod components; 14 | pub mod configs; 15 | pub mod run; 16 | pub mod tg; 17 | 18 | use crate::app_context::AppContext; 19 | use crate::app_error::AppError; 20 | use crate::configs::{ 21 | config_file::ConfigFile, 22 | custom::{ 23 | app_custom::AppConfig, keymap_custom::KeymapConfig, logger_custom::LoggerConfig, 24 | palette_custom::PaletteConfig, theme_custom::ThemeConfig, 25 | }, 26 | }; 27 | use crate::logger::Logger; 28 | use crate::tg::{tg_backend::TgBackend, tg_context::TgContext}; 29 | use crate::tui::Tui; 30 | use crate::tui_backend::TuiBackend; 31 | use clap::Parser; 32 | use configs::custom::telegram_custom::TelegramConfig; 33 | use lazy_static::lazy_static; 34 | use std::panic::{set_hook, take_hook}; 35 | use std::sync::Arc; 36 | 37 | lazy_static! { 38 | pub static ref LOGGER_CONFIG: LoggerConfig = LoggerConfig::get_config(); 39 | pub static ref KEYMAP_CONFIG: KeymapConfig = KeymapConfig::get_config(); 40 | pub static ref APP_CONFIG: AppConfig = AppConfig::get_config(); 41 | pub static ref PALETTE_CONFIG: PaletteConfig = PaletteConfig::get_config(); 42 | pub static ref THEME_CONFIG: ThemeConfig = ThemeConfig::get_config(); 43 | pub static ref TELEGRAM_CONFIG: TelegramConfig = TelegramConfig::get_config(); 44 | } 45 | 46 | /// The main entry point for the application. 47 | /// This function initializes the application and runs the main event loop. 48 | /// 49 | /// # Returns 50 | /// * `Result<(), AppError>` - An Ok result or an error. 51 | async fn tokio_main() -> Result<(), AppError<()>> { 52 | tracing::info!("Starting tokio main"); 53 | 54 | // Initialize the lazy static variables 55 | // This is done to ensure that the configuration files are read only once 56 | // and the values are shared across the application. 57 | lazy_static::initialize(&LOGGER_CONFIG); 58 | lazy_static::initialize(&KEYMAP_CONFIG); 59 | lazy_static::initialize(&APP_CONFIG); 60 | lazy_static::initialize(&PALETTE_CONFIG); 61 | lazy_static::initialize(&THEME_CONFIG); 62 | lazy_static::initialize(&TELEGRAM_CONFIG); 63 | 64 | let cli_args = cli::CliArgs::parse(); 65 | tracing::info!("Parsed CLI arguments: {:?}", cli_args); 66 | 67 | let logger = Logger::from_config(LOGGER_CONFIG.clone()); 68 | logger.init(); 69 | tracing::info!("Logger initialized with config: {:?}", logger); 70 | 71 | let keymap_config = KEYMAP_CONFIG.clone(); 72 | tracing::info!("Keymap config: {:?}", keymap_config); 73 | 74 | let app_config = APP_CONFIG.clone(); 75 | tracing::info!("App config: {:?}", app_config); 76 | 77 | let palette_config = PALETTE_CONFIG.clone(); 78 | tracing::info!("Palette config: {:?}", palette_config); 79 | 80 | let theme_config = THEME_CONFIG.clone(); 81 | tracing::info!("Theme config: {:?}", theme_config); 82 | 83 | let mut telegram_config = TELEGRAM_CONFIG.clone(); 84 | tracing::info!("Telegram config: {:?}", telegram_config); 85 | // This is used to disable the message database when running the application as a CLI. 86 | // This is done to avoid that deleting a message other application in 87 | // a chats causes the `--send-message` to resend the messages that were deleted. 88 | telegram_config.use_message_database = std::env::args().count() <= 1; 89 | 90 | let tg_context = TgContext::default(); 91 | tracing::info!("Telegram context: {:?}", tg_context); 92 | let app_context = Arc::new(AppContext::new( 93 | app_config, 94 | keymap_config, 95 | theme_config, 96 | palette_config, 97 | telegram_config, 98 | tg_context, 99 | cli_args, 100 | )?); 101 | tracing::info!("App context: {:?}", app_context); 102 | 103 | let mut tui_backend = TuiBackend::new(Arc::clone(&app_context))?; 104 | tracing::info!("Tui backend initialized"); 105 | init_panic_hook(tui_backend.mouse, tui_backend.paste); 106 | let mut tui = Tui::new(Arc::clone(&app_context)); 107 | tracing::info!("Tui initialized"); 108 | let mut tg_backend = TgBackend::new(Arc::clone(&app_context)).unwrap(); 109 | tracing::info!("Telegram backend initialized"); 110 | 111 | match run::run_app( 112 | Arc::clone(&app_context), 113 | &mut tui, 114 | &mut tui_backend, 115 | &mut tg_backend, 116 | ) 117 | .await 118 | { 119 | Ok(_) => { 120 | tracing::info!("Application exited successfully"); 121 | std::process::exit(0); 122 | } 123 | Err(e) => { 124 | tracing::error!("Application exited with error: {}", e); 125 | std::process::exit(1); 126 | } 127 | } 128 | } 129 | 130 | /// Initialize the panic hook to exit the `TuiBackend` and log the panic stack 131 | /// backtrace. 132 | /// 133 | /// # Arguments 134 | /// * `mouse` - A boolean flag that represents whether the mouse was enabled 135 | /// during the execution and need to be disabled. 136 | /// * `paste` - A boolean flag that represents whether the paste mode was 137 | /// enabled during the execution and need to be disabled. 138 | fn init_panic_hook(mouse: bool, paste: bool) { 139 | let original_hook = take_hook(); 140 | set_hook(Box::new(move |panic_info| { 141 | // Intentionally ignore errors here since we're already in a panic 142 | TuiBackend::force_exit(mouse, paste).unwrap(); 143 | let backtrace = std::backtrace::Backtrace::capture(); 144 | tracing::error!("{}\nstack backtrace:\n{}", panic_info, backtrace); 145 | original_hook(panic_info); // comment to hide the stacktrace in stdout 146 | })); 147 | } 148 | 149 | #[tokio::main] 150 | async fn main() -> Result<(), AppError<()>> { 151 | if let Err(e) = tokio_main().await { 152 | tracing::error!("Something went wrong: {}", e); 153 | Err(e) 154 | } else { 155 | Ok(()) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/tg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod message_entry; 2 | pub mod ordered_chat; 3 | pub mod td_enums; 4 | pub mod tg_backend; 5 | pub mod tg_context; 6 | -------------------------------------------------------------------------------- /src/tg/ordered_chat.rs: -------------------------------------------------------------------------------- 1 | use {std::hash::Hash, tdlib_rs::types::ChatPosition}; 2 | 3 | #[derive(Debug, Clone, PartialEq)] 4 | pub struct OrderedChat { 5 | pub chat_id: i64, 6 | pub position: ChatPosition, // maybe can be changed with position.order 7 | } 8 | 9 | impl Hash for OrderedChat { 10 | fn hash(&self, state: &mut H) { 11 | self.chat_id.hash(state); 12 | 13 | // self.position.hash(state); 14 | format!("{:?}", self.position.list).hash(state); 15 | self.position.order.hash(state); 16 | self.position.is_pinned.hash(state); 17 | format!("{:?}", self.position.source).hash(state); 18 | } 19 | } 20 | 21 | impl Eq for OrderedChat {} 22 | 23 | impl PartialOrd for OrderedChat { 24 | fn partial_cmp(&self, other: &Self) -> Option { 25 | Some(self.cmp(other)) 26 | } 27 | } 28 | 29 | impl Ord for OrderedChat { 30 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 31 | if self.position.order != other.position.order { 32 | if self.position.order > other.position.order { 33 | return core::cmp::Ordering::Less; 34 | } else { 35 | return core::cmp::Ordering::Greater; 36 | } 37 | } 38 | if self.chat_id != other.chat_id { 39 | if self.chat_id > other.chat_id { 40 | return core::cmp::Ordering::Less; 41 | } else { 42 | return core::cmp::Ordering::Greater; 43 | } 44 | } 45 | core::cmp::Ordering::Equal 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/tg/td_enums.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | use tdlib_rs::{enums::ChatList, types::ChatListFolder}; 4 | 5 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 6 | pub enum TdMessageSender { 7 | User(i64), 8 | Chat(i64), 9 | } 10 | 11 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 12 | pub enum TdChatList { 13 | Main, 14 | Archive, 15 | Folder(i32), 16 | } 17 | 18 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 19 | pub enum TdMessageReplyTo { 20 | Message(TdMessageReplyToMessage), 21 | Story(TdMessageReplyToStory), 22 | } 23 | 24 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 25 | pub struct TdMessageReplyToMessage { 26 | /// The identifier of the chat to which the replied message belongs; ignored for outgoing replies. For example, messages in the Replies chat are replies to messages in different chats 27 | pub chat_id: i64, 28 | /// The identifier of the replied message 29 | pub message_id: i64, 30 | } 31 | 32 | impl From<&TdMessageReplyToMessage> for tdlib_rs::types::InputMessageReplyToMessage { 33 | fn from(reply_to_message: &TdMessageReplyToMessage) -> Self { 34 | tdlib_rs::types::InputMessageReplyToMessage { 35 | chat_id: reply_to_message.chat_id, 36 | message_id: reply_to_message.message_id, 37 | quote: None, 38 | } 39 | } 40 | } 41 | 42 | impl From<&tdlib_rs::types::MessageReplyToMessage> for TdMessageReplyToMessage { 43 | fn from(reply_to_message: &tdlib_rs::types::MessageReplyToMessage) -> Self { 44 | TdMessageReplyToMessage { 45 | chat_id: reply_to_message.chat_id, 46 | message_id: reply_to_message.message_id, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 52 | pub struct TdMessageReplyToStory { 53 | /// The identifier of the sender of the replied story. Currently, stories can be replied only in the sender's chat 54 | pub story_sender_chat_id: i64, 55 | /// The identifier of the replied story 56 | pub story_id: i32, 57 | } 58 | 59 | impl From<&TdMessageReplyToStory> for tdlib_rs::types::MessageReplyToStory { 60 | fn from(reply_to_story: &TdMessageReplyToStory) -> Self { 61 | tdlib_rs::types::MessageReplyToStory { 62 | story_sender_chat_id: reply_to_story.story_sender_chat_id, 63 | story_id: reply_to_story.story_id, 64 | } 65 | } 66 | } 67 | 68 | impl From<&tdlib_rs::types::MessageReplyToStory> for TdMessageReplyToStory { 69 | fn from(reply_to_story: &tdlib_rs::types::MessageReplyToStory) -> Self { 70 | TdMessageReplyToStory { 71 | story_sender_chat_id: reply_to_story.story_sender_chat_id, 72 | story_id: reply_to_story.story_id, 73 | } 74 | } 75 | } 76 | 77 | impl From for TdChatList { 78 | fn from(chat_list: ChatList) -> Self { 79 | match chat_list { 80 | ChatList::Main => TdChatList::Main, 81 | ChatList::Archive => TdChatList::Archive, 82 | ChatList::Folder(folder_id) => TdChatList::Folder(folder_id.chat_folder_id), 83 | } 84 | } 85 | } 86 | 87 | impl From for ChatList { 88 | fn from(td_chat_list: TdChatList) -> Self { 89 | match td_chat_list { 90 | TdChatList::Main => ChatList::Main, 91 | TdChatList::Archive => ChatList::Archive, 92 | TdChatList::Folder(folder_id) => ChatList::Folder(ChatListFolder { 93 | chat_folder_id: folder_id, 94 | }), 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/tui.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | action::Action, 3 | app_context::AppContext, 4 | app_error::AppError, 5 | component_name::ComponentName, 6 | components::{ 7 | component_traits::Component, core_window::CoreWindow, status_bar::StatusBar, 8 | title_bar::TitleBar, SMALL_AREA_HEIGHT, SMALL_AREA_WIDTH, 9 | }, 10 | event::Event, 11 | }; 12 | use ratatui::layout::{Constraint, Direction, Layout, Rect}; 13 | use std::{collections::HashMap, sync::Arc}; 14 | use tokio::sync::mpsc::UnboundedSender; 15 | 16 | /// `Tui` is a struct that represents the main user interface for the 17 | /// application. It is responsible for managing the layout and rendering of all 18 | /// the components. It also handles the distribution of events and actions to 19 | /// the appropriate components. 20 | pub struct Tui { 21 | /// The application configuration. 22 | app_context: Arc, 23 | /// An optional unbounded sender that can send actions to be processed. 24 | action_tx: Option>, 25 | /// A hashmap of components that make up the user interface. 26 | components: HashMap>, 27 | } 28 | /// Implement the `Tui` struct. 29 | impl Tui { 30 | /// Create a new instance of the `Tui` struct. 31 | /// 32 | /// # Arguments 33 | /// * `app_context` - An Arc wrapped AppContext struct. 34 | /// 35 | /// # Returns 36 | /// * `Self` - The new instance of the `Tui` struct. 37 | pub fn new(app_context: Arc) -> Self { 38 | let components_iter: Vec<(ComponentName, Box)> = vec![ 39 | ( 40 | ComponentName::TitleBar, 41 | TitleBar::new(Arc::clone(&app_context)) 42 | .with_name("Tgt") 43 | .new_boxed(), 44 | ), 45 | ( 46 | ComponentName::CoreWindow, 47 | CoreWindow::new(Arc::clone(&app_context)) 48 | .with_name("Core Window") 49 | .new_boxed(), 50 | ), 51 | ( 52 | ComponentName::StatusBar, 53 | StatusBar::new(Arc::clone(&app_context)) 54 | .with_name("Status Bar") 55 | .new_boxed(), 56 | ), 57 | ]; 58 | let action_tx = None; 59 | let components: HashMap> = 60 | components_iter.into_iter().collect(); 61 | 62 | Tui { 63 | action_tx, 64 | components, 65 | app_context, 66 | } 67 | } 68 | /// Register an action handler that can send actions for processing if 69 | /// necessary. 70 | /// 71 | /// # Arguments 72 | /// * `tx` - An unbounded sender that can send actions. 73 | /// 74 | /// # Returns 75 | /// 76 | /// * `Result<()>` - An Ok result or an error. 77 | pub fn register_action_handler( 78 | &mut self, 79 | tx: UnboundedSender, 80 | ) -> Result<(), AppError> { 81 | self.action_tx = Some(tx.clone()); 82 | self.components 83 | .iter_mut() 84 | .try_for_each(|(_, component)| component.register_action_handler(tx.clone()))?; 85 | Ok(()) 86 | } 87 | /// Handle incoming events and produce actions if necessary. 88 | /// 89 | /// # Arguments 90 | /// * `event` - An optional event to be processed. 91 | /// 92 | /// # Returns 93 | /// 94 | /// * `Result>` - An action to be processed or none. 95 | pub fn handle_events( 96 | &mut self, 97 | event: Option, 98 | ) -> Result, AppError> { 99 | self.components 100 | .get_mut(&ComponentName::CoreWindow) 101 | .unwrap() 102 | .handle_events(event.clone()) 103 | } 104 | /// Update the state of the component based on a received action. 105 | /// 106 | /// # Arguments 107 | /// 108 | /// * `action` - An action that may modify the state of the component. 109 | pub fn update(&mut self, action: Action) { 110 | // We can not send the action only to the `CoreWindow` component because 111 | // the `StatusBar` component needs to know the area to render the size. 112 | self.components 113 | .iter_mut() 114 | .for_each(|(_, component)| component.update(action.clone())); 115 | } 116 | /// Render the user interface to the screen. 117 | /// 118 | /// # Arguments 119 | /// * `frame` - A mutable reference to the frame to be rendered. 120 | /// * `area` - A rectangular area to render the user interface within. 121 | /// 122 | /// # Returns 123 | /// * `Result<()>` - An Ok result or an error. 124 | pub fn draw(&mut self, frame: &mut ratatui::Frame<'_>, area: Rect) -> Result<(), AppError<()>> { 125 | self.components 126 | .get_mut(&ComponentName::StatusBar) 127 | .unwrap() 128 | .update(Action::UpdateArea(area)); 129 | 130 | let core_window: &mut dyn std::any::Any = 131 | self.components.get_mut(&ComponentName::CoreWindow).unwrap(); 132 | if let Some(core_window) = core_window.downcast_mut::() { 133 | core_window.with_small_area(area.width < SMALL_AREA_WIDTH); 134 | } 135 | 136 | let main_layout = Layout::new( 137 | Direction::Vertical, 138 | [ 139 | Constraint::Length(if self.app_context.app_config().show_title_bar { 140 | if area.height > SMALL_AREA_HEIGHT + 5 { 141 | 3 142 | } else { 143 | 0 144 | } 145 | } else { 146 | 0 147 | }), 148 | Constraint::Min(SMALL_AREA_HEIGHT), 149 | Constraint::Length(if self.app_context.app_config().show_status_bar { 150 | if area.height > SMALL_AREA_HEIGHT + 5 { 151 | 3 152 | } else { 153 | 0 154 | } 155 | } else { 156 | 0 157 | }), 158 | ], 159 | ) 160 | .split(area); 161 | 162 | self.components 163 | .get_mut(&ComponentName::TitleBar) 164 | .unwrap_or_else(|| { 165 | tracing::error!("Failed to get component: {}", ComponentName::TitleBar); 166 | panic!("Failed to get component: {}", ComponentName::TitleBar) 167 | }) 168 | .draw(frame, main_layout[0])?; 169 | 170 | self.components 171 | .get_mut(&ComponentName::CoreWindow) 172 | .unwrap_or_else(|| { 173 | tracing::error!("Failed to get component: {}", ComponentName::CoreWindow); 174 | panic!("Failed to get component: {}", ComponentName::CoreWindow) 175 | }) 176 | .draw(frame, main_layout[1])?; 177 | 178 | self.components 179 | .get_mut(&ComponentName::StatusBar) 180 | .unwrap_or_else(|| { 181 | tracing::error!("Failed to get component: {}", ComponentName::StatusBar); 182 | panic!("Failed to get component: {}", ComponentName::StatusBar) 183 | }) 184 | .draw(frame, main_layout[2])?; 185 | 186 | Ok(()) 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use dirs; 2 | use std::{env, io, path::PathBuf}; 3 | 4 | pub const TGT: &str = "tgt"; 5 | pub const TGT_CONFIG_DIR: &str = "TGT_CONFIG_DIR"; 6 | 7 | /// Get the project directory. 8 | /// 9 | /// # Returns 10 | /// The project directory. 11 | pub fn tgt_dir() -> io::Result { 12 | // Debug 13 | if cfg!(debug_assertions) { 14 | return env::current_dir(); 15 | } 16 | 17 | // Release 18 | let home = dirs::home_dir().unwrap().to_str().unwrap().to_owned(); 19 | let tgt = format!("{}/.tgt", home); 20 | // Check if the directory exists 21 | if PathBuf::from(&tgt).exists() { 22 | Ok(PathBuf::from(&tgt)) 23 | } else { 24 | panic!("The directory {} does not exist.", tgt); 25 | } 26 | } 27 | /// Get the default configuration directory. 28 | /// 29 | /// # Returns 30 | /// The default configuration directory. 31 | pub fn tgt_config_dir() -> io::Result { 32 | Ok(tgt_dir()?.join("config")) 33 | } 34 | 35 | /// Fail with an error message and exit the application. 36 | /// 37 | /// # Arguments 38 | /// * `msg` - A string slice that holds the error message. 39 | /// * `e` - A generic type that holds the error. 40 | /// 41 | /// # Returns 42 | /// * `!` - This function does not return a value. 43 | fn fail_with(msg: &str, e: E) -> ! { 44 | eprintln!("[ERROR]: {} {:?}", msg, e); 45 | std::process::exit(1); 46 | } 47 | 48 | /// Unwrap a result or fail with an error message. 49 | /// This function will unwrap a result and return the value if it is Ok. 50 | /// If the result is an error, this function will fail with an error message. 51 | /// 52 | /// # Arguments 53 | /// * `result` - A result that holds a value or an error. 54 | /// * `msg` - A string slice that holds the error message. 55 | /// 56 | /// # Returns 57 | /// * `T` - The value if the result is Ok. 58 | pub fn unwrap_or_fail(result: Result, msg: &str) -> T { 59 | match result { 60 | Ok(v) => v, 61 | Err(e) => fail_with(msg, e), 62 | } 63 | } 64 | --------------------------------------------------------------------------------