Compare commits

..

2 commits

Author SHA1 Message Date
f1a3c0a8cf TODO Debug sqlite performance 2025-02-21 00:32:48 +01:00
40814a7efb TODO Update dependencies 2025-02-21 00:32:48 +01:00
56 changed files with 951 additions and 1361 deletions

View file

@ -1,75 +0,0 @@
# What software is installed by default:
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
name: build
on:
push:
pull_request:
defaults:
run:
shell: bash
jobs:
build:
strategy:
matrix:
os:
- ubuntu-22.04
- windows-latest
- macos-latest
- macos-13
runs-on: ${{ matrix.os }}
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Set up rust
run: rustup update
- name: Build
run: cargo build --release
- name: Test
run: cargo test --release
- name: Record target triple
run: rustc -vV | awk '/^host/ { print $2 }' > target/release/host
- name: Upload
uses: actions/upload-artifact@v4
with:
name: cove-${{ matrix.os }}
path: |
target/release/cove
target/release/cove.exe
target/release/host
release:
runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
needs:
- build
permissions:
contents: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
- name: Zip artifacts
run: |
chmod +x cove-ubuntu-22.04/cove
chmod +x cove-windows-latest/cove.exe
chmod +x cove-macos-latest/cove
chmod +x cove-macos-13/cove
zip -jr "cove-$(cat cove-ubuntu-22.04/host).zip" cove-ubuntu-22.04/cove
zip -jr "cove-$(cat cove-windows-latest/host).zip" cove-windows-latest/cove.exe
zip -jr "cove-$(cat cove-macos-latest/host).zip" cove-macos-latest/cove
zip -jr "cove-$(cat cove-macos-13/host).zip" cove-macos-13/cove
- name: Create new release
uses: softprops/action-gh-release@v2
with:
body: Automated release, see [CHANGELOG.md](CHANGELOG.md) for more details.
files: "*.zip"

View file

@ -2,7 +2,7 @@
"files.insertFinalNewline": true,
"rust-analyzer.cargo.features": "all",
"rust-analyzer.imports.granularity.enforce": true,
"rust-analyzer.imports.granularity.group": "crate",
"rust-analyzer.imports.granularity.group": "module",
"rust-analyzer.imports.group.enable": true,
"evenBetterToml.formatter.columnWidth": 100,
}

View file

@ -4,83 +4,32 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
Procedure when bumping the version number:
1. Update dependencies in a separate commit
1. Update dependencies and flake in a separate commit
2. Set version number in `Cargo.toml`
3. Add new section in this changelog
4. Run `cargo run help-config > CONFIG.md`
5. Commit with message `Bump version to X.Y.Z`
6. Create tag named `vX.Y.Z`
7. Push `master` and the new tag
7. Fast-forward branch `latest`
8. Push `master`, `latest` and the new tag
## Unreleased
### Changed
- Display emoji user id hashes in the nick list
- Compile linux binary with older glibc version
## v0.9.3 - 2025-05-31
### Added
- Key bindings for emoji-based user id hashing
### Fixed
- `keys.rooms.action.connect_autojoin` connecting to non-autojoin rooms
## v0.9.2 - 2025-03-14
### Added
- `bell_on_mention` config option
## v0.9.1 - 2025-03-01
### Fixed
- Rendering glitches with unicode-based width estimation
## v0.9.0 - 2025-02-23
### Added
- Unicode-based grapheme width estimation method
- `width_estimation_method` config option
- `--width-estimation-method` option
- Room links are now included in the `I` message links list
### Changed
- Updated documentation for `time_zone` config option
- When connecting to a room using `n` in the room list, the cursor now moves to that room
- Updated list of emoji names
### Removed
- Special handling of &rl2dev
### Fixed
- Nick color in rare edge cases
- Message link list rendering bug
### Updated
- Documentation for `time_zone` config option
## v0.8.3 - 2024-05-20
### Changed
- Updated list of emoji names
## v0.8.2 - 2024-04-25
### Changed
- Renamed `json-stream` export format to `json-lines` (see <https://jsonlines.org/>)
- Changed `json-lines` file extension from `.json` to `.jsonl`
### Fixed
- Crash when window is too small while empty message editor is visible
- Mistakes in output and docs
- Cove not cleaning up terminal state properly
@ -88,19 +37,16 @@ Procedure when bumping the version number:
## v0.8.1 - 2024-01-11
### Added
- Support for setting window title
- More information to room list heading
- Key bindings for live caesar cipher de- and encoding
### Removed
- Key binding to open present page
## v0.8.0 - 2024-01-04
### Added
- Support for multiple euph server domains
- Support for `TZ` environment variable
- `time_zone` config option
@ -110,7 +56,6 @@ Procedure when bumping the version number:
- Welcome info box next to room list
### Changed
- The default euph domain is now https://euphoria.leet.nu/ everywhere
- The config file format was changed to support multiple euph servers with different domains.
Options previously located at `euph.rooms.*` should be reviewed and moved to `euph.servers."euphoria.leet.nu".rooms.*`.
@ -119,20 +64,17 @@ Procedure when bumping the version number:
- Reduced connection timeout from 30 seconds to 10 seconds
### Fixed
- Room deletion popup accepting any room name
- Duplicated key presses on Windows
## v0.7.1 - 2023-08-31
### Changed
- Updated dependencies
## v0.7.0 - 2023-05-14
### Added
- Auto-generated config documentation
- in [CONFIG.md](CONFIG.md)
- via `help-config` CLI command
@ -140,7 +82,6 @@ Procedure when bumping the version number:
- `measure_widths` config option
### Changed
- Overhauled widget system and extracted generic widgets to [toss](https://github.com/Garmelon/toss)
- Overhauled config system to support auto-generating documentation
- Overhauled key binding system to make key bindings configurable
@ -154,18 +95,15 @@ Procedure when bumping the version number:
## v0.6.1 - 2023-04-10
### Changed
- Improved JSON export performance
- Always show rooms from config file in room list
### Fixed
- Rooms reconnecting instead of showing error popups
## v0.6.0 - 2023-04-04
### Added
- Emoji support
- `flake.nix`, making cove available as a nix flake
- `json-stream` room export format
@ -173,37 +111,31 @@ Procedure when bumping the version number:
- `--verbose` flag
### Changed
- Non-export info is now printed to stderr instead of stdout
- Recognizes links without scheme (e.g. `euphoria.io` instead of `https://euphoria.io`)
- Rooms waiting for reconnect are no longer sorted to bottom in default sort order
### Fixed
- Mentions not being stopped by `>`
## v0.5.2 - 2023-01-14
### Added
- Key binding to open present page
### Changed
- Always connect to &rl2dev in ephemeral mode
- Reduce amount of messages per &rl2dev log request
## v0.5.1 - 2022-11-27
### Changed
- Increase reconnect delay to one minute
- Print errors that occurred while cove was running more compactly
## v0.5.0 - 2022-09-26
### Added
- Key bindings to navigate nick list
- Room deletion confirmation popup
- Message inspection popup
@ -212,12 +144,10 @@ Procedure when bumping the version number:
- `rooms_sort_order` config option
### Changed
- Use nick changes to detect sessions for nick list
- Support Unicode 15
### Fixed
- Cursor being visible through popups
- Cursor in lists when highlighted item moves off-screen
- User disappearing from nick list when only one of their sessions disconnects
@ -225,7 +155,6 @@ Procedure when bumping the version number:
## v0.4.0 - 2022-09-01
### Added
- Config file and `--config` cli option
- `data_dir` config option
- `ephemeral` config option
@ -241,17 +170,14 @@ Procedure when bumping the version number:
- Key bindings to view and open links in a message
### Changed
- Some key bindings in the rooms list
### Fixed
- Rooms being stuck in "Connecting" state
## v0.3.0 - 2022-08-22
### Added
- Account login and logout
- Authentication dialog for password-protected rooms
- Error popups in rooms when something goes wrong
@ -259,12 +185,10 @@ Procedure when bumping the version number:
- Key binding to download more logs
### Changed
- Reduced amount of unnecessary redraws
- Description of `export` CLI command
### Fixed
- Crash when connecting to nonexistent rooms
- Crash when connecting to rooms that require authentication
- Pasting multi-line strings into the editor
@ -272,18 +196,15 @@ Procedure when bumping the version number:
## v0.2.1 - 2022-08-11
### Added
- Support for modifiers on special keys via the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/)
### Fixed
- Joining new rooms no longer crashes cove
- Scrolling when exiting message editor
## v0.2.0 - 2022-08-10
### Added
- New messages are now marked as unseen
- Sub-trees can now be folded
- Support for pasting text into editors
@ -296,12 +217,10 @@ Procedure when bumping the version number:
- Support for exporting multiple/all rooms at once
### Changed
- Reorganized export command
- Slowed down room history download speed
### Fixed
- Chat rendering when deleting and re-joining a room
- Spacing in some popups

View file

@ -53,14 +53,6 @@ Available modifiers:
## Available options
### `bell_on_mention`
**Required:** yes
**Type:** boolean
**Default:** `false`
Ring the bell (character 0x07) when you are mentioned in a room.
### `data_dir`
**Required:** no
@ -101,9 +93,9 @@ Whether to automatically join this room on startup.
**Type:** boolean
**Default:** `false`
If `euph.servers.<domain>.rooms.<room>.username` is set, this will force
cove to set the username even if there is already a different username
associated with the current session.
If `euph.rooms.<room>.username` is set, this will force cove to set the
username even if there is already a different username associated with
the current session.
### `euph.servers.<domain>.rooms.<room>.password`
@ -537,14 +529,6 @@ Reply to message, inline if possible.
Reply opposite to normal reply.
### `keys.tree.action.toggle_nick_emoji`
**Required:** yes
**Type:** key binding
**Default:** `"e"`
Toggle agent id based nick emoji.
### `keys.tree.action.toggle_seen`
**Required:** yes
@ -623,11 +607,12 @@ Move to root.
**Type:** boolean
**Default:** `false`
Whether to measure the width of graphemes (i.e. characters) as displayed
by the terminal emulator instead of estimating the width.
Whether to measure the width of characters as displayed by the terminal
emulator instead of guessing the width.
Enabling this makes rendering a bit slower but more accurate. The screen
might also flash when encountering new graphemes.
might also flash when encountering new characters (or, more accurately,
graphemes).
See also the `--measure-widths` command line option.
@ -671,41 +656,18 @@ order of priority):
Time zone that chat timestamps should be displayed in.
This option can either be the string `"localtime"`, a [POSIX TZ string],
or a [tz identifier] from the [tz database].
This option is interpreted as a POSIX TZ string. It is described here in
further detail:
<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html>
When not set or when set to `"localtime"`, cove attempts to use your
system's configured time zone, falling back to UTC.
On a normal system, the string `"localtime"` as well as any value from
the "TZ identifier" column of the following wikipedia article should be
valid TZ strings:
<https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
When the string begins with a colon or doesn't match the a POSIX TZ
string format, it is interpreted as a tz identifier and looked up in
your system's tz database (or a bundled tz database on Windows).
If the `TZ` environment variable exists, it overrides this option. If
neither exist, cove uses the system's local time zone.
If the `TZ` environment variable exists, it overrides this option.
[POSIX TZ string]: https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html#tag_08_03
[tz identifier]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
[tz database]: https://en.wikipedia.org/wiki/Tz_database
### `width_estimation_method`
**Required:** yes
**Type:** string
**Values:** `"legacy"`, `"unicode"`
**Default:** `"legacy"`
How to estimate the width of graphemes (i.e. characters) as displayed by
the terminal emulator.
`"legacy"`: Use a legacy method that should mostly work on most terminal
emulators. This method will never be correct in all cases since every
terminal emulator handles grapheme widths slightly differently. However,
those cases are usually rare (unless you view a lot of emoji).
`"unicode"`: Use the unicode standard in a best-effort manner to
determine grapheme widths. Some terminals (e.g. ghostty) can make use of
this.
This method is used when `measure_widths` is set to `false`.
See also the `--width-estimation-method` command line option.
**Warning:** On Windows, cove can't get the local time zone and uses UTC
instead. However, you can still specify a path to a tz data file or a
custom time zone string.

275
Cargo.lock generated
View file

@ -90,15 +90,15 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
[[package]]
name = "async-trait"
version = "0.1.87"
version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [
"proc-macro2",
"quote",
@ -113,25 +113,27 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "aws-lc-rs"
version = "1.12.6"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01"
checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
dependencies = [
"aws-lc-sys",
"paste",
"zeroize",
]
[[package]]
name = "aws-lc-sys"
version = "0.27.0"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bbe221bbf523b625a4dd8585c7f38166e31167ec2ca98051dbcb4c3b6e825d2"
checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f"
dependencies = [
"bindgen",
"cc",
"cmake",
"dunce",
"fs_extra",
"paste",
]
[[package]]
@ -174,9 +176,9 @@ dependencies = [
[[package]]
name = "bitflags"
version = "2.9.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "block-buffer"
@ -188,10 +190,16 @@ dependencies = [
]
[[package]]
name = "bytes"
version = "1.10.1"
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]]
name = "caseless"
@ -204,9 +212,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.16"
version = "1.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
dependencies = [
"jobserver",
"libc",
@ -241,9 +249,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.32"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
dependencies = [
"clap_builder",
"clap_derive",
@ -251,9 +259,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.32"
version = "4.5.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
dependencies = [
"anstream",
"anstyle",
@ -263,9 +271,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.32"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
dependencies = [
"heck",
"proc-macro2",
@ -322,7 +330,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cove"
version = "0.9.3"
version = "0.8.3"
dependencies = [
"anyhow",
"async-trait",
@ -336,6 +344,7 @@ dependencies = [
"jiff",
"linkify",
"log",
"once_cell",
"open",
"parking_lot",
"rusqlite",
@ -350,7 +359,7 @@ dependencies = [
[[package]]
name = "cove-config"
version = "0.9.3"
version = "0.8.3"
dependencies = [
"cove-input",
"cove-macro",
@ -361,7 +370,7 @@ dependencies = [
[[package]]
name = "cove-input"
version = "0.9.3"
version = "0.8.3"
dependencies = [
"cove-macro",
"crossterm",
@ -375,7 +384,7 @@ dependencies = [
[[package]]
name = "cove-macro"
version = "0.9.3"
version = "0.8.3"
dependencies = [
"proc-macro2",
"quote",
@ -401,7 +410,7 @@ dependencies = [
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"rustix",
"signal-hook",
"signal-hook-mio",
"winapi",
@ -490,9 +499,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.15.0"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "equivalent"
@ -512,8 +521,7 @@ dependencies = [
[[package]]
name = "euphoxide"
version = "0.6.1"
source = "git+https://github.com/Garmelon/euphoxide.git?tag=v0.6.1#7a292c429ad44aa6aa52fc381e3168841d6303b0"
version = "0.5.1"
dependencies = [
"async-trait",
"caseless",
@ -678,9 +686,9 @@ dependencies = [
[[package]]
name = "http"
version = "1.3.1"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
dependencies = [
"bytes",
"fnv",
@ -689,15 +697,15 @@ dependencies = [
[[package]]
name = "httparse"
version = "1.10.1"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a"
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@ -739,17 +747,16 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jiff"
version = "0.2.4"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
checksum = "3590fea8e9e22d449600c9bbd481a8163bef223e4ff938e5f55899f8cf1adb93"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
@ -758,22 +765,11 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "jiff-static"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "962e1dfe9b2d75a84536cf5bf5eaaa4319aa7906c7160134a22883ac316d5f31"
checksum = "cf2cec2f5d266af45a071ece48b1fb89f3b00b2421ac3a5fe10285a6caaa60d3"
[[package]]
name = "jiff-tzdb-platform"
@ -807,9 +803,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
@ -837,7 +833,6 @@ version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
@ -857,12 +852,6 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
[[package]]
name = "linux-raw-sys"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413"
[[package]]
name = "lock_api"
version = "0.4.12"
@ -875,9 +864,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.26"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "memchr"
@ -893,9 +882,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
dependencies = [
"adler2",
]
@ -948,9 +937,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.1"
version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
name = "open"
@ -1007,6 +996,12 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pathdiff"
version = "0.2.3"
@ -1027,15 +1022,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.32"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "portable-atomic"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
[[package]]
name = "portable-atomic-util"
@ -1054,18 +1049,18 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy 0.8.23",
"zerocopy 0.7.35",
]
[[package]]
name = "prettyplease"
version = "0.2.31"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn",
@ -1073,18 +1068,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
@ -1097,7 +1092,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
dependencies = [
"rand_chacha",
"rand_core",
"zerocopy 0.8.23",
"zerocopy 0.8.20",
]
[[package]]
@ -1112,18 +1107,19 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.9.3"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
dependencies = [
"getrandom 0.3.1",
"zerocopy 0.8.20",
]
[[package]]
name = "redox_syscall"
version = "0.5.10"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
@ -1170,9 +1166,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "ring"
version = "0.17.14"
version = "0.17.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24"
dependencies = [
"cc",
"cfg-if",
@ -1218,20 +1214,7 @@ dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
]
[[package]]
name = "rustix"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys 0.9.3",
"linux-raw-sys",
"windows-sys 0.59.0",
]
@ -1282,9 +1265,9 @@ dependencies = [
[[package]]
name = "ryu"
version = "1.0.20"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "schannel"
@ -1326,9 +1309,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
dependencies = [
"serde_derive",
]
@ -1345,9 +1328,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.218"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
dependencies = [
"proc-macro2",
"quote",
@ -1366,9 +1349,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
dependencies = [
"itoa",
"memchr",
@ -1471,9 +1454,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.100"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [
"proc-macro2",
"quote",
@ -1482,31 +1465,32 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.19.0"
version = "3.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600"
checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix 1.0.2",
"rustix",
"windows-sys 0.59.0",
]
[[package]]
name = "thiserror"
version = "2.0.12"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@ -1515,9 +1499,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.39"
version = "0.3.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
dependencies = [
"deranged",
"itoa",
@ -1530,15 +1514,15 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.3"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.20"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
dependencies = [
"num-conv",
"time-core",
@ -1546,9 +1530,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.9.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
dependencies = [
"tinyvec_macros",
]
@ -1561,9 +1545,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.1"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
@ -1590,9 +1574,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
version = "0.26.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls",
"tokio",
@ -1661,8 +1645,7 @@ dependencies = [
[[package]]
name = "toss"
version = "0.3.4"
source = "git+https://github.com/Garmelon/toss.git?tag=v0.3.4#57aa8c59308f6f0aa82bde415a42b56c3d6f7c4d"
version = "0.3.0"
dependencies = [
"async-trait",
"crossterm",
@ -1698,9 +1681,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-linebreak"
@ -1749,8 +1732,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "vault"
version = "0.4.0"
source = "git+https://github.com/Garmelon/vault.git?tag=v0.4.0#a53254d2e787d15fd2d00584fddf9b84e79572ee"
version = "0.5.0"
dependencies = [
"rusqlite",
"tokio",
@ -1792,7 +1774,7 @@ dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.44",
"rustix",
]
[[package]]
@ -1901,9 +1883,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.4"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
dependencies = [
"memchr",
]
@ -1923,16 +1905,17 @@ version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive 0.7.35",
]
[[package]]
name = "zerocopy"
version = "0.8.23"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6"
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
dependencies = [
"zerocopy-derive 0.8.23",
"zerocopy-derive 0.8.20",
]
[[package]]
@ -1948,9 +1931,9 @@ dependencies = [
[[package]]
name = "zerocopy-derive"
version = "0.8.23"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154"
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
dependencies = [
"proc-macro2",
"quote",

View file

@ -1,49 +1,55 @@
[workspace]
resolver = "3"
resolver = "2"
members = ["cove", "cove-*"]
[workspace.package]
version = "0.9.3"
edition = "2024"
version = "0.8.3"
edition = "2021"
[workspace.dependencies]
anyhow = "1.0.97"
async-trait = "0.1.87"
clap = { version = "4.5.32", features = ["derive", "deprecated"] }
anyhow = "1.0.96"
async-trait = "0.1.86"
clap = { version = "4.5.30", features = ["derive", "deprecated"] }
cookie = "0.18.1"
crossterm = "0.28.1"
directories = "6.0.0"
edit = "0.1.5"
jiff = "0.2.4"
jiff = "0.2.1"
linkify = "0.10.0"
log = { version = "0.4.26", features = ["std"] }
log = { version = "0.4.25", features = ["std"] }
once_cell = "1.20.2"
open = "5.3.2"
parking_lot = "0.12.3"
proc-macro2 = "1.0.94"
quote = "1.0.40"
rusqlite = { version = "0.31.0", features = ["bundled", "time"] }
proc-macro2 = "1.0.93"
quote = "1.0.38"
rusqlite = { version = "0.31.0", features = [
# "bundled",
"time",
] }
rustls = "0.23.23"
serde = { version = "1.0.219", features = ["derive"] }
serde = { version = "1.0.218", features = ["derive"] }
serde_either = "0.2.1"
serde_json = "1.0.140"
syn = "2.0.100"
thiserror = "2.0.12"
tokio = { version = "1.44.1", features = ["full"] }
serde_json = "1.0.139"
syn = "2.0.98"
thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["full"] }
toml = "0.8.20"
unicode-width = "0.2.0"
[workspace.dependencies.euphoxide]
git = "https://github.com/Garmelon/euphoxide.git"
tag = "v0.6.1"
path = "../euphoxide"
# git = "https://github.com/Garmelon/euphoxide.git"
features = ["bot"]
[workspace.dependencies.toss]
git = "https://github.com/Garmelon/toss.git"
tag = "v0.3.4"
path = "../toss"
# git = "https://github.com/Garmelon/toss.git"
# tag = "v0.2.3"
[workspace.dependencies.vault]
git = "https://github.com/Garmelon/vault.git"
tag = "v0.4.0"
path = "../vault"
# git = "https://github.com/Garmelon/vault.git"
# tag = "v0.5.0"
features = ["tokio"]
[workspace.lints]
@ -68,5 +74,14 @@ rust.unused_qualifications = "warn"
# Clippy
clippy.use_self = "warn"
[profile.dev.package."*"]
opt-level = 3
# For profiling
[profile.release]
debug = 1
[rust]
debuginfo-level = 1

View file

@ -7,11 +7,6 @@ real-time chat platform.
It runs on Linux, Windows, and macOS.
## Installing cove
Download a binary of your choice from the
[latest release on GitHub](https://github.com/Garmelon/cove/releases/latest).
## Using cove
To start cove, simply run `cove` in your terminal. For more info about the
@ -31,3 +26,61 @@ file or via `cove help-config`.
When launched, cove prints the location it is loading its config file from. To
configure cove, create a config file at that location. This location can be
changed via the `--config` command line option.
## Installation
At this point, cove is not available via any package manager.
Cove is available as a Nix Flake. To try it out, you can use
```bash
$ nix run --override-input nixpkgs nixpkgs github:Garmelon/cove/latest
```
## Manual installation
This section contains instructions on how to install cove by compiling it yourself.
It doesn't assume you know how to program, but it does assume basic familiarity with the command line on your platform of choice.
Cove runs in the terminal, after all.
### Installing rustup
Cove is written in Rust, so the first step is to install rustup. Either install
it from your package manager of choice (if you have one) or use the
[installer](https://rustup.rs/).
Test your installation by running `rustup --version` and `cargo --version`. If
rustup is installed correctly, both of these should show a version number.
Cove is designed on the current version of the stable toolchain. If cove doesn't
compile, you can try switching to the stable toolchain and updating it using the
following commands:
```bash
$ rustup default stable
$ rustup update
```
### Installing cove
To install or update to the latest release of cove, run the following command:
```bash
$ cargo install --force --git https://github.com/Garmelon/cove --branch latest
```
If you like to live dangerously and want to install or update to the latest,
bleeding-edge, possibly-broken commit from the repo's main branch, run the
following command.
**Warning:** This could corrupt your vault. Make sure to make a backup before
running the command.
```bash
$ cargo install --force --git https://github.com/Garmelon/cove
```
To install a specific version of cove, run the following command and substitute
in the full version you want to install:
```bash
$ cargo install --force --git https://github.com/Garmelon/cove --tag v0.1.0
```

View file

@ -1,6 +1,7 @@
//! Auto-generate markdown documentation.
use std::{collections::HashMap, path::PathBuf};
use std::collections::HashMap;
use std::path::PathBuf;
use cove_input::KeyBinding;
pub use cove_macro::Document;

View file

@ -104,7 +104,6 @@ default_bindings! {
pub fn mark_older_seen => ["ctrl+s"];
pub fn info => ["i"];
pub fn links => ["I"];
pub fn toggle_nick_emoji => ["e"];
pub fn increase_caesar => ["c"];
pub fn decrease_caesar => ["C"];
}
@ -357,9 +356,6 @@ pub struct TreeAction {
/// List links found in message.
#[serde(default = "default::tree_action::links")]
pub links: KeyBinding,
/// Toggle agent id based nick emoji.
#[serde(default = "default::tree_action::toggle_nick_emoji")]
pub toggle_nick_emoji: KeyBinding,
/// Increase caesar cipher rotation.
#[serde(default = "default::tree_action::increase_caesar")]
pub increase_caesar: KeyBinding,

View file

@ -1,18 +1,17 @@
use std::{
fs,
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use doc::Document;
use serde::{Deserialize, Serialize};
pub use crate::{euph::*, keys::*};
pub mod doc;
mod euph;
mod keys;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::{fs, io};
use doc::Document;
use serde::Deserialize;
pub use crate::euph::*;
pub use crate::keys::*;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("failed to read config file")]
@ -21,14 +20,6 @@ pub enum Error {
Toml(#[from] toml::de::Error),
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, Document)]
#[serde(rename_all = "snake_case")]
pub enum WidthEstimationMethod {
#[default]
Legacy,
Unicode,
}
#[derive(Debug, Default, Deserialize, Document)]
pub struct Config {
/// The directory that cove stores its data in when not running in ephemeral
@ -49,29 +40,12 @@ pub struct Config {
#[serde(default)]
pub ephemeral: bool,
/// How to estimate the width of graphemes (i.e. characters) as displayed by
/// the terminal emulator.
///
/// `"legacy"`: Use a legacy method that should mostly work on most terminal
/// emulators. This method will never be correct in all cases since every
/// terminal emulator handles grapheme widths slightly differently. However,
/// those cases are usually rare (unless you view a lot of emoji).
///
/// `"unicode"`: Use the unicode standard in a best-effort manner to
/// determine grapheme widths. Some terminals (e.g. ghostty) can make use of
/// this.
///
/// This method is used when `measure_widths` is set to `false`.
///
/// See also the `--width-estimation-method` command line option.
#[serde(default)]
pub width_estimation_method: WidthEstimationMethod,
/// Whether to measure the width of graphemes (i.e. characters) as displayed
/// by the terminal emulator instead of estimating the width.
/// Whether to measure the width of characters as displayed by the terminal
/// emulator instead of guessing the width.
///
/// Enabling this makes rendering a bit slower but more accurate. The screen
/// might also flash when encountering new graphemes.
/// might also flash when encountering new characters (or, more accurately,
/// graphemes).
///
/// See also the `--measure-widths` command line option.
#[serde(default)]
@ -100,10 +74,6 @@ pub struct Config {
#[serde(default)]
pub rooms_sort_order: RoomsSortOrder,
/// Ring the bell (character 0x07) when you are mentioned in a room.
#[serde(default)]
pub bell_on_mention: bool,
/// Time zone that chat timestamps should be displayed in.
///
/// This option can either be the string `"localtime"`, a [POSIX TZ string],

View file

@ -1,7 +1,10 @@
use std::{fmt, num::ParseIntError, str::FromStr};
use std::fmt;
use std::num::ParseIntError;
use std::str::FromStr;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
use serde::{de::Error, Deserialize, Deserializer};
use serde::{Serialize, Serializer};
use serde_either::SingleOrVec;
#[derive(Debug, thiserror::Error)]
@ -114,7 +117,7 @@ impl KeyPress {
"alt" if !self.alt => self.alt = true,
"any" if !self.shift && !self.ctrl && !self.alt => self.any = true,
m @ ("shift" | "ctrl" | "alt" | "any") => {
return Err(ParseKeysError::ConflictingModifier(m.to_string()));
return Err(ParseKeysError::ConflictingModifier(m.to_string()))
}
m => return Err(ParseKeysError::UnknownModifier(m.to_string())),
}

View file

@ -1,4 +1,7 @@
use std::{io, sync::Arc};
mod keys;
use std::io;
use std::sync::Arc;
pub use cove_macro::KeyGroup;
use crossterm::event::{Event, KeyEvent, KeyEventKind};
@ -7,8 +10,6 @@ use toss::{Frame, Terminal, WidthDb};
pub use crate::keys::*;
mod keys;
pub struct KeyBindingInfo<'a> {
pub name: &'static str,
pub binding: &'a KeyBinding,

View file

@ -1,6 +1,7 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Ident, LitStr, spanned::Spanned};
use syn::spanned::Spanned;
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Ident, LitStr};
use crate::util::{self, SerdeDefault};

View file

@ -1,6 +1,7 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, spanned::Spanned};
use syn::spanned::Spanned;
use syn::{Data, DeriveInput};
use crate::util;

View file

@ -1,4 +1,4 @@
use syn::{DeriveInput, parse_macro_input};
use syn::{parse_macro_input, DeriveInput};
mod document;
mod key_group;

View file

@ -1,9 +1,8 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
Attribute, Expr, ExprLit, ExprPath, Field, Lit, LitStr, Path, Token, Type, parse::Parse,
punctuated::Punctuated,
};
use syn::parse::Parse;
use syn::punctuated::Punctuated;
use syn::{Attribute, Expr, ExprLit, ExprPath, Field, Lit, LitStr, Path, Token, Type};
pub fn bail<T>(span: Span, message: &str) -> syn::Result<T> {
Err(syn::Error::new(span, message))

View file

@ -17,6 +17,7 @@ euphoxide.workspace = true
jiff.workspace = true
linkify.workspace = true
log.workspace = true
once_cell.workspace = true
open.workspace = true
parking_lot.workspace = true
rusqlite.workspace = true

View file

@ -1,9 +1,7 @@
pub use highlight::*;
pub use room::*;
pub use small_message::*;
pub use util::*;
mod highlight;
mod room;
mod small_message;
mod util;
pub use room::*;
pub use small_message::*;
pub use util::*;

View file

@ -1,211 +0,0 @@
use std::ops::Range;
use crossterm::style::Stylize;
use toss::{Style, Styled};
use crate::euph::util;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanType {
Mention,
Room,
Emoji,
}
fn nick_char(ch: char) -> bool {
// Closely following the heim mention regex:
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/stores/chat.js#L14-L15
// `>` has been experimentally confirmed to delimit mentions as well.
match ch {
',' | '.' | '!' | '?' | ';' | '&' | '<' | '>' | '\'' | '"' => false,
_ => !ch.is_whitespace(),
}
}
fn room_char(ch: char) -> bool {
// Basically just \w, see also
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/ui/MessageText.js#L66
ch.is_ascii_alphanumeric() || ch == '_'
}
struct SpanFinder<'a> {
content: &'a str,
span: Option<(SpanType, usize)>,
room_or_mention_possible: bool,
result: Vec<(SpanType, Range<usize>)>,
}
impl<'a> SpanFinder<'a> {
fn is_valid_span(&self, span: SpanType, range: Range<usize>) -> bool {
let text = &self.content[range.start..range.end];
match span {
SpanType::Mention => range.len() > 1 && text.starts_with('@'),
SpanType::Room => range.len() > 1 && text.starts_with('&'),
SpanType::Emoji => {
if range.len() <= 2 {
return false;
}
let Some(name) = Some(text)
.and_then(|it| it.strip_prefix(':'))
.and_then(|it| it.strip_suffix(':'))
else {
return false;
};
util::EMOJI.get(name).is_some()
}
}
}
fn close_span(&mut self, end: usize) {
let Some((span, start)) = self.span else {
return;
};
if self.is_valid_span(span, start..end) {
self.result.push((span, start..end));
}
self.span = None;
}
fn open_span(&mut self, span: SpanType, start: usize) {
self.close_span(start);
self.span = Some((span, start))
}
fn step(&mut self, idx: usize, char: char) {
match (char, self.span) {
('@', Some((SpanType::Mention, _))) => {} // Continue the mention
('@', _) if self.room_or_mention_possible => self.open_span(SpanType::Mention, idx),
('&', _) if self.room_or_mention_possible => self.open_span(SpanType::Room, idx),
(':', None) => self.open_span(SpanType::Emoji, idx),
(':', Some((SpanType::Emoji, _))) => self.close_span(idx + 1),
(c, Some((SpanType::Mention, _))) if !nick_char(c) => self.close_span(idx),
(c, Some((SpanType::Room, _))) if !room_char(c) => self.close_span(idx),
_ => {}
}
// More permissive than the heim web client
self.room_or_mention_possible = !char.is_alphanumeric();
}
fn find(content: &'a str) -> Vec<(SpanType, Range<usize>)> {
let mut this = Self {
content,
span: None,
room_or_mention_possible: true,
result: vec![],
};
for (idx, char) in content.char_indices() {
this.step(idx, char);
}
this.close_span(content.len());
this.result
}
}
pub fn find_spans(content: &str) -> Vec<(SpanType, Range<usize>)> {
SpanFinder::find(content)
}
/// Highlight spans in a string.
///
/// The list of spans must be non-overlapping and in ascending order.
///
/// If `exact` is specified, colon-delimited emoji are not replaced with their
/// unicode counterparts.
pub fn apply_spans(
content: &str,
spans: &[(SpanType, Range<usize>)],
base: Style,
exact: bool,
) -> Styled {
let mut result = Styled::default();
let mut i = 0;
for (span, range) in spans {
assert!(i <= range.start);
assert!(range.end <= content.len());
if i < range.start {
result = result.then(&content[i..range.start], base);
}
let text = &content[range.start..range.end];
result = match span {
SpanType::Mention if exact => result.and_then(util::style_mention_exact(text, base)),
SpanType::Mention => result.and_then(util::style_mention(text, base)),
SpanType::Room => result.then(text, base.blue().bold()),
SpanType::Emoji if exact => result.then(text, base.magenta()),
SpanType::Emoji => {
let name = text.strip_prefix(':').unwrap_or(text);
let name = name.strip_suffix(':').unwrap_or(name);
if let Some(Some(replacement)) = util::EMOJI.get(name) {
result.then(replacement, base)
} else {
result.then(text, base.magenta())
}
}
};
i = range.end;
}
if i < content.len() {
result = result.then(&content[i..], base);
}
result
}
/// Highlight an euphoria message's content.
///
/// If `exact` is specified, colon-delimited emoji are not replaced with their
/// unicode counterparts.
pub fn highlight(content: &str, base: Style, exact: bool) -> Styled {
apply_spans(content, &find_spans(content), base, exact)
}
#[cfg(test)]
mod tests {
use crate::euph::SpanType;
use super::find_spans;
#[test]
fn mentions() {
assert_eq!(find_spans("@foo"), vec![(SpanType::Mention, 0..4)]);
assert_eq!(find_spans("&@foo"), vec![(SpanType::Mention, 1..5)]);
assert_eq!(find_spans("a @foo b"), vec![(SpanType::Mention, 2..6)]);
assert_eq!(find_spans("@@foo@@"), vec![(SpanType::Mention, 0..7)]);
assert_eq!(find_spans("a @b@c d"), vec![(SpanType::Mention, 2..6)]);
assert_eq!(
find_spans("a @b @c d"),
vec![(SpanType::Mention, 2..4), (SpanType::Mention, 5..7)]
);
}
#[test]
fn rooms() {
assert_eq!(find_spans("&foo"), vec![(SpanType::Room, 0..4)]);
assert_eq!(find_spans("@&foo"), vec![(SpanType::Room, 1..5)]);
assert_eq!(find_spans("a &foo b"), vec![(SpanType::Room, 2..6)]);
assert_eq!(find_spans("&&foo&&"), vec![(SpanType::Room, 1..5)]);
assert_eq!(find_spans("a &b&c d"), vec![(SpanType::Room, 2..4)]);
assert_eq!(
find_spans("a &b &c d"),
vec![(SpanType::Room, 2..4), (SpanType::Room, 5..7)]
);
}
#[test]
fn emoji_in_mentions() {
assert_eq!(find_spans(" @a:b:c "), vec![(SpanType::Mention, 1..7)]);
}
}

View file

@ -1,17 +1,21 @@
use std::{convert::Infallible, time::Duration};
// TODO Remove rl2dev-specific code
use euphoxide::{
api::{
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply,
Time, UserId, packet::ParsedPacket,
},
bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig},
conn::{self, ConnTx, Joined},
use std::convert::Infallible;
use std::time::Duration;
use euphoxide::api::packet::ParsedPacket;
use euphoxide::api::{
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply, Time,
UserId,
};
use log::{debug, info, warn};
use tokio::{select, sync::oneshot};
use euphoxide::bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig};
use euphoxide::conn::{self, ConnTx, Joined};
use log::{debug, error, info, warn};
use tokio::select;
use tokio::sync::oneshot;
use crate::{macros::logging_unwrap, vault::EuphRoomVault};
use crate::macros::logging_unwrap;
use crate::vault::EuphRoomVault;
const LOG_INTERVAL: Duration = Duration::from_secs(10);
@ -69,13 +73,20 @@ impl Room {
where
F: Fn(Event) + std::marker::Send + Sync + 'static,
{
// &rl2dev's message history is broken and requesting old messages past
// a certain point results in errors. Cove should not keep retrying log
// requests when hitting that limit, so &rl2dev is always opened in
// ephemeral mode.
let is_rl2dev = vault.room().domain == "euphoria.io" && vault.room().name == "rl2dev";
let ephemeral = vault.vault().vault().ephemeral() || is_rl2dev;
Self {
ephemeral: vault.vault().vault().ephemeral(),
vault,
ephemeral,
instance: instance_config.build(on_event),
state: State::Disconnected,
last_msg_id: None,
log_request_canary: None,
vault,
}
}
@ -183,7 +194,14 @@ impl Room {
debug!("{:?}: requesting logs", vault.room());
let _ = conn_tx.send(Log { n: 1000, before }).await;
// &rl2dev's message history is broken and requesting old messages past
// a certain point results in errors. By reducing the amount of messages
// in each log request, we can get closer to this point. Since &rl2dev
// is fairly low in activity, this should be fine.
let is_rl2dev = vault.room().domain == "euphoria.io" && vault.room().name == "rl2dev";
let n = if is_rl2dev { 50 } else { 1000 };
let _ = conn_tx.send(Log { n, before }).await;
// The code handling incoming events and replies also handles
// `LogReply`s, so we don't need to do anything special here.
}

View file

@ -1,18 +1,212 @@
use std::mem;
use crossterm::style::Stylize;
use euphoxide::api::{MessageId, Snowflake, Time, UserId};
use euphoxide::api::{MessageId, Snowflake, Time};
use jiff::Timestamp;
use toss::{Style, Styled};
use crate::{store::Msg, ui::ChatMsg};
use crate::store::Msg;
use crate::ui::ChatMsg;
use super::util;
fn nick_char(ch: char) -> bool {
// Closely following the heim mention regex:
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/stores/chat.js#L14-L15
// `>` has been experimentally confirmed to delimit mentions as well.
match ch {
',' | '.' | '!' | '?' | ';' | '&' | '<' | '>' | '\'' | '"' => false,
_ => !ch.is_whitespace(),
}
}
fn room_char(ch: char) -> bool {
// Basically just \w, see also
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/ui/MessageText.js#L66
ch.is_ascii_alphanumeric() || ch == '_'
}
enum Span {
Nothing,
Mention,
Room,
Emoji,
}
struct Highlighter<'a> {
content: &'a str,
base_style: Style,
exact: bool,
span: Span,
span_start: usize,
room_or_mention_possible: bool,
result: Styled,
}
impl<'a> Highlighter<'a> {
/// Does *not* guarantee `self.span_start == idx` after running!
fn close_mention(&mut self, idx: usize) {
let span_length = idx.saturating_sub(self.span_start);
if span_length <= 1 {
// We can repurpose the current span
self.span = Span::Nothing;
return;
}
let text = &self.content[self.span_start..idx]; // Includes @
self.result = mem::take(&mut self.result).and_then(if self.exact {
util::style_nick_exact(text, self.base_style)
} else {
util::style_nick(text, self.base_style)
});
self.span = Span::Nothing;
self.span_start = idx;
}
/// Does *not* guarantee `self.span_start == idx` after running!
fn close_room(&mut self, idx: usize) {
let span_length = idx.saturating_sub(self.span_start);
if span_length <= 1 {
// We can repurpose the current span
self.span = Span::Nothing;
return;
}
self.result = mem::take(&mut self.result).then(
&self.content[self.span_start..idx],
self.base_style.blue().bold(),
);
self.span = Span::Nothing;
self.span_start = idx;
}
// Warning: `idx` is the index of the closing colon.
fn close_emoji(&mut self, idx: usize) {
let name = &self.content[self.span_start + 1..idx];
if let Some(replace) = util::EMOJI.get(name) {
match replace {
Some(replace) if !self.exact => {
self.result = mem::take(&mut self.result).then(replace, self.base_style);
}
_ => {
let text = &self.content[self.span_start..=idx];
let style = self.base_style.magenta();
self.result = mem::take(&mut self.result).then(text, style);
}
}
self.span = Span::Nothing;
self.span_start = idx + 1;
} else {
self.close_plain(idx);
self.span = Span::Emoji;
}
}
/// Guarantees `self.span_start == idx` after running.
fn close_plain(&mut self, idx: usize) {
if self.span_start == idx {
// Span has length 0
return;
}
self.result =
mem::take(&mut self.result).then(&self.content[self.span_start..idx], self.base_style);
self.span = Span::Nothing;
self.span_start = idx;
}
fn close_span_before_current_char(&mut self, idx: usize, char: char) {
match self.span {
Span::Mention if !nick_char(char) => self.close_mention(idx),
Span::Room if !room_char(char) => self.close_room(idx),
Span::Emoji if char == '&' || char == '@' => {
self.span = Span::Nothing;
}
_ => {}
}
}
fn update_span_with_current_char(&mut self, idx: usize, char: char) {
match self.span {
Span::Nothing if char == '@' && self.room_or_mention_possible => {
self.close_plain(idx);
self.span = Span::Mention;
}
Span::Nothing if char == '&' && self.room_or_mention_possible => {
self.close_plain(idx);
self.span = Span::Room;
}
Span::Nothing if char == ':' => {
self.close_plain(idx);
self.span = Span::Emoji;
}
Span::Emoji if char == ':' => self.close_emoji(idx),
_ => {}
}
}
fn close_final_span(&mut self) {
let idx = self.content.len();
if self.span_start >= idx {
return; // Span has no contents
}
match self.span {
Span::Mention => self.close_mention(idx),
Span::Room => self.close_room(idx),
_ => {}
}
self.close_plain(idx);
}
fn step(&mut self, idx: usize, char: char) {
if self.span_start < idx {
self.close_span_before_current_char(idx, char);
}
self.update_span_with_current_char(idx, char);
// More permissive than the heim web client
self.room_or_mention_possible = !char.is_alphanumeric();
}
fn highlight(content: &'a str, base_style: Style, exact: bool) -> Styled {
let mut this = Self {
content: if exact { content } else { content.trim() },
base_style,
exact,
span: Span::Nothing,
span_start: 0,
room_or_mention_possible: true,
result: Styled::default(),
};
for (idx, char) in (if exact { content } else { content.trim() }).char_indices() {
this.step(idx, char);
}
this.close_final_span();
this.result
}
}
fn highlight_content(content: &str, base_style: Style, exact: bool) -> Styled {
Highlighter::highlight(content, base_style, exact)
}
#[derive(Debug, Clone)]
pub struct SmallMessage {
pub id: MessageId,
pub parent: Option<MessageId>,
pub time: Time,
pub user_id: UserId,
pub nick: String,
pub content: String,
pub seen: bool,
@ -28,22 +222,22 @@ fn style_me() -> Style {
fn styled_nick(nick: &str) -> Styled {
Styled::new_plain("[")
.and_then(super::style_nick(nick, Style::new()))
.and_then(util::style_nick(nick, Style::new()))
.then_plain("]")
}
fn styled_nick_me(nick: &str) -> Styled {
let style = style_me();
Styled::new("*", style).and_then(super::style_nick(nick, style))
Styled::new("*", style).and_then(util::style_nick(nick, style))
}
fn styled_content(content: &str) -> Styled {
super::highlight(content.trim(), Style::new(), false)
highlight_content(content.trim(), Style::new(), false)
}
fn styled_content_me(content: &str) -> Styled {
let style = style_me();
super::highlight(content.trim(), style, false).then("*", style)
highlight_content(content.trim(), style, false).then("*", style)
}
fn styled_editor_content(content: &str) -> Styled {
@ -52,7 +246,7 @@ fn styled_editor_content(content: &str) -> Styled {
} else {
Style::new()
};
super::highlight(content, style, true)
highlight_content(content, style, true)
}
impl Msg for SmallMessage {
@ -73,10 +267,6 @@ impl Msg for SmallMessage {
fn last_possible_id() -> Self::Id {
MessageId(Snowflake::MAX)
}
fn nick_emoji(&self) -> Option<String> {
Some(util::user_id_emoji(&self.user_id))
}
}
impl ChatMsg for SmallMessage {

View file

@ -1,27 +1,9 @@
use std::{
collections::HashSet,
hash::{DefaultHasher, Hash, Hasher},
sync::LazyLock,
};
use crossterm::style::{Color, Stylize};
use euphoxide::{Emoji, api::UserId};
use euphoxide::Emoji;
use once_cell::sync::Lazy;
use toss::{Style, Styled};
pub static EMOJI: LazyLock<Emoji> = LazyLock::new(Emoji::load);
pub static EMOJI_LIST: LazyLock<Vec<String>> = LazyLock::new(|| {
let mut list = EMOJI
.0
.values()
.flatten()
.cloned()
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
list.sort_unstable();
list
});
pub static EMOJI: Lazy<Emoji> = Lazy::new(Emoji::load);
/// Convert HSL to RGB following [this approach from wikipedia][1].
///
@ -72,25 +54,3 @@ pub fn style_nick(nick: &str, base: Style) -> Styled {
pub fn style_nick_exact(nick: &str, base: Style) -> Styled {
Styled::new(nick, nick_style(nick, base))
}
pub fn style_mention(mention: &str, base: Style) -> Styled {
let nick = mention
.strip_prefix('@')
.expect("mention must start with @");
Styled::new(EMOJI.replace(mention), nick_style(nick, base))
}
pub fn style_mention_exact(mention: &str, base: Style) -> Styled {
let nick = mention
.strip_prefix('@')
.expect("mention must start with @");
Styled::new(mention, nick_style(nick, base))
}
pub fn user_id_emoji(user_id: &UserId) -> String {
let mut hasher = DefaultHasher::new();
user_id.0.hash(&mut hasher);
let hash = hasher.finish();
let emoji = &EMOJI_LIST[hash as usize % EMOJI_LIST.len()];
emoji.clone()
}

View file

@ -1,15 +1,13 @@
//! Export logs from the vault to plain text files.
use std::{
fs::File,
io::{self, BufWriter, Write},
};
use crate::vault::{EuphRoomVault, EuphVault, RoomIdentifier};
mod json;
mod text;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use crate::vault::{EuphRoomVault, EuphVault, RoomIdentifier};
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum Format {
/// Human-readable tree-structured messages.

View file

@ -3,7 +3,9 @@ use std::io::Write;
use euphoxide::api::MessageId;
use unicode_width::UnicodeWidthStr;
use crate::{euph::SmallMessage, store::Tree, vault::EuphRoomVault};
use crate::euph::SmallMessage;
use crate::store::Tree;
use crate::vault::EuphRoomVault;
const TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
const TIME_EMPTY: &str = " ";

View file

@ -1,4 +1,6 @@
use std::{convert::Infallible, sync::Arc, vec};
use std::convert::Infallible;
use std::sync::Arc;
use std::vec;
use async_trait::async_trait;
use crossterm::style::Stylize;
@ -8,10 +10,8 @@ use parking_lot::Mutex;
use tokio::sync::mpsc;
use toss::{Style, Styled};
use crate::{
store::{Msg, MsgStore, Path, Tree},
ui::ChatMsg,
};
use crate::store::{Msg, MsgStore, Path, Tree};
use crate::ui::ChatMsg;
#[derive(Debug, Clone)]
pub struct LogMsg {

View file

@ -1,23 +1,6 @@
// TODO Remove unnecessary Debug impls and compare compile times
// TODO Invoke external notification command?
use std::path::PathBuf;
use anyhow::Context;
use clap::Parser;
use cove_config::{Config, doc::Document};
use directories::{BaseDirs, ProjectDirs};
use log::info;
use tokio::sync::mpsc;
use toss::Terminal;
use crate::{
logger::Logger,
ui::Ui,
vault::Vault,
version::{NAME, VERSION},
};
mod euph;
mod export;
mod logger;
@ -28,6 +11,22 @@ mod util;
mod vault;
mod version;
use std::path::PathBuf;
use anyhow::Context;
use clap::Parser;
use cove_config::doc::Document;
use cove_config::Config;
use directories::{BaseDirs, ProjectDirs};
use log::info;
use tokio::sync::mpsc;
use toss::Terminal;
use crate::logger::Logger;
use crate::ui::Ui;
use crate::vault::Vault;
use crate::version::{NAME, VERSION};
#[derive(Debug, clap::Parser)]
enum Command {
/// Run the client interactively (default).
@ -46,12 +45,6 @@ enum Command {
HelpConfig,
}
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum WidthEstimationMethod {
Legacy,
Unicode,
}
impl Default for Command {
fn default() -> Self {
Self::Run
@ -85,11 +78,6 @@ struct Args {
#[arg(long, short)]
offline: bool,
/// Method for estimating the width of characters as displayed by the
/// terminal emulator.
#[arg(long, short)]
width_estimation_method: Option<WidthEstimationMethod>,
/// Measure the width of characters as displayed by the terminal emulator
/// instead of guessing the width.
#[arg(long, short)]
@ -125,12 +113,6 @@ fn update_config_with_args(config: &mut Config, args: &Args) {
}
config.ephemeral |= args.ephemeral;
if let Some(method) = args.width_estimation_method {
config.width_estimation_method = match method {
WidthEstimationMethod::Legacy => cove_config::WidthEstimationMethod::Legacy,
WidthEstimationMethod::Unicode => cove_config::WidthEstimationMethod::Unicode,
}
}
config.measure_widths |= args.measure_widths;
config.offline |= args.offline;
}
@ -199,10 +181,6 @@ async fn run(
let mut terminal = Terminal::new()?;
terminal.set_measuring(config.measure_widths);
terminal.set_width_estimation_method(match config.width_estimation_method {
cove_config::WidthEstimationMethod::Legacy => toss::WidthEstimationMethod::Legacy,
cove_config::WidthEstimationMethod::Unicode => toss::WidthEstimationMethod::Unicode,
});
Ui::run(config, tz, &mut terminal, vault.clone(), logger, logger_rx).await?;
drop(terminal);

View file

@ -1,4 +1,7 @@
use std::{collections::HashMap, fmt::Debug, hash::Hash, vec};
use std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::vec;
use async_trait::async_trait;
@ -8,10 +11,6 @@ pub trait Msg {
fn parent(&self) -> Option<Self::Id>;
fn seen(&self) -> bool;
fn nick_emoji(&self) -> Option<String> {
None
}
fn last_possible_id() -> Self::Id;
}

View file

@ -1,30 +1,3 @@
use std::{
convert::Infallible,
io,
sync::{Arc, Weak},
time::{Duration, Instant},
};
use cove_config::Config;
use cove_input::InputEvent;
use jiff::tz::TimeZone;
use parking_lot::FairMutex;
use tokio::{
sync::mpsc::{self, UnboundedReceiver, UnboundedSender, error::TryRecvError},
task,
};
use toss::{Terminal, WidgetExt, widgets::BoxedAsync};
use crate::{
logger::{LogMsg, Logger},
macros::logging_unwrap,
util::InfallibleExt,
vault::Vault,
};
pub use self::chat::ChatMsg;
use self::{chat::ChatState, rooms::Rooms, widgets::ListState};
mod chat;
mod euph;
mod key_bindings;
@ -32,6 +5,31 @@ mod rooms;
mod util;
mod widgets;
use std::convert::Infallible;
use std::io;
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
use cove_config::Config;
use cove_input::InputEvent;
use jiff::tz::TimeZone;
use parking_lot::FairMutex;
use tokio::sync::mpsc::error::TryRecvError;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::task;
use toss::widgets::BoxedAsync;
use toss::{Terminal, WidgetExt};
use crate::logger::{LogMsg, Logger};
use crate::macros::logging_unwrap;
use crate::util::InfallibleExt;
use crate::vault::Vault;
pub use self::chat::ChatMsg;
use self::chat::ChatState;
use self::rooms::Rooms;
use self::widgets::ListState;
/// Time to spend batch processing events before redrawing the screen.
const EVENT_PROCESSING_TIME: Duration = Duration::from_millis(1000 / 15); // 15 fps
@ -50,7 +48,6 @@ impl From<Infallible> for UiError {
}
}
#[expect(clippy::large_enum_variant)]
pub enum UiEvent {
GraphemeWidthsChanged,
LogChanged,

View file

@ -1,26 +1,24 @@
use cove_config::Keys;
use cove_input::InputEvent;
use jiff::{Timestamp, tz::TimeZone};
use toss::{
Styled, WidgetExt,
widgets::{BoxedAsync, EditorState},
};
use crate::{
store::{Msg, MsgStore},
util,
};
use super::UiError;
use self::{cursor::Cursor, tree::TreeViewState};
mod blocks;
mod cursor;
mod renderer;
mod tree;
mod widgets;
use cove_config::Keys;
use cove_input::InputEvent;
use jiff::tz::TimeZone;
use jiff::Timestamp;
use toss::widgets::{BoxedAsync, EditorState};
use toss::{Styled, WidgetExt};
use crate::store::{Msg, MsgStore};
use crate::util;
use self::cursor::Cursor;
use self::tree::TreeViewState;
use super::UiError;
pub trait ChatMsg {
fn time(&self) -> Option<Timestamp>;
fn styled(&self) -> (Styled, Styled);
@ -37,7 +35,6 @@ pub struct ChatState<M: Msg, S: MsgStore<M>> {
cursor: Cursor<M::Id>,
editor: EditorState,
nick_emoji: bool,
caesar: i8,
mode: Mode,
@ -49,7 +46,6 @@ impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
Self {
cursor: Cursor::Bottom,
editor: EditorState::new(),
nick_emoji: false,
caesar: 0,
mode: Mode::Tree,
@ -58,10 +54,6 @@ impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
store,
}
}
pub fn nick_emoji(&self) -> bool {
self.nick_emoji
}
}
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
@ -85,7 +77,6 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
&mut self.editor,
nick,
focused,
self.nick_emoji,
self.caesar,
)
.boxed_async(),
@ -124,11 +115,6 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
Reaction::Composed { parent, content }
}
Reaction::NotHandled if event.matches(&keys.tree.action.toggle_nick_emoji) => {
self.nick_emoji = !self.nick_emoji;
Reaction::Handled
}
Reaction::NotHandled if event.matches(&keys.tree.action.increase_caesar) => {
self.caesar = (self.caesar + 1).rem_euclid(26);
Reaction::Handled

View file

@ -1,6 +1,6 @@
//! Common rendering logic.
use std::collections::{VecDeque, vec_deque};
use std::collections::{vec_deque, VecDeque};
use toss::widgets::Predrawn;

View file

@ -1,6 +1,7 @@
//! Common cursor movement logic.
use std::{collections::HashSet, hash::Hash};
use std::collections::HashSet;
use std::hash::Hash;
use crate::store::{Msg, MsgStore, Tree};

View file

@ -2,27 +2,27 @@
// TODO Focusing on sub-trees
mod renderer;
mod scroll;
mod widgets;
use std::collections::HashSet;
use async_trait::async_trait;
use cove_config::Keys;
use cove_input::InputEvent;
use jiff::tz::TimeZone;
use toss::{AsyncWidget, Frame, Pos, Size, WidgetExt, WidthDb, widgets::EditorState};
use toss::widgets::EditorState;
use toss::{AsyncWidget, Frame, Pos, Size, WidgetExt, WidthDb};
use crate::{
store::{Msg, MsgStore},
ui::{UiError, util},
util::InfallibleExt,
};
use super::{ChatMsg, Reaction, cursor::Cursor};
use crate::store::{Msg, MsgStore};
use crate::ui::{util, ChatMsg, UiError};
use crate::util::InfallibleExt;
use self::renderer::{TreeContext, TreeRenderer};
mod renderer;
mod scroll;
mod widgets;
use super::cursor::Cursor;
use super::Reaction;
pub struct TreeViewState<M: Msg, S: MsgStore<M>> {
store: S,
@ -389,7 +389,6 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
editor: &'a mut EditorState,
nick: String,
focused: bool,
nick_emoji: bool,
caesar: i8,
) -> TreeView<'a, M, S> {
TreeView {
@ -398,7 +397,6 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
editor,
nick,
focused,
nick_emoji,
caesar,
}
}
@ -412,8 +410,6 @@ pub struct TreeView<'a, M: Msg, S: MsgStore<M>> {
nick: String,
focused: bool,
nick_emoji: bool,
caesar: i8,
}
@ -442,7 +438,6 @@ where
size,
nick: self.nick.clone(),
focused: self.focused,
nick_emoji: self.nick_emoji,
caesar: self.caesar,
last_cursor: self.state.last_cursor.clone(),
last_cursor_top: self.state.last_cursor_top,

View file

@ -1,26 +1,19 @@
//! A [`Renderer`] for message trees.
use std::{collections::HashSet, convert::Infallible};
use std::collections::HashSet;
use std::convert::Infallible;
use async_trait::async_trait;
use jiff::tz::TimeZone;
use toss::{
Size, Widget, WidthDb,
widgets::{EditorState, Empty, Predrawn, Resize},
};
use toss::widgets::{EditorState, Empty, Predrawn, Resize};
use toss::{Size, Widget, WidthDb};
use crate::{
store::{Msg, MsgStore, Tree},
ui::{
ChatMsg,
chat::{
blocks::{Block, Blocks, Range},
cursor::Cursor,
renderer::{self, Renderer, overlaps},
},
},
util::InfallibleExt,
};
use crate::store::{Msg, MsgStore, Tree};
use crate::ui::chat::blocks::{Block, Blocks, Range};
use crate::ui::chat::cursor::Cursor;
use crate::ui::chat::renderer::{self, overlaps, Renderer};
use crate::ui::ChatMsg;
use crate::util::InfallibleExt;
use super::widgets;
@ -80,7 +73,6 @@ pub struct TreeContext<Id> {
pub size: Size,
pub nick: String,
pub focused: bool,
pub nick_emoji: bool,
pub caesar: i8,
pub last_cursor: Cursor<Id>,
pub last_cursor_top: i32,
@ -208,7 +200,6 @@ where
self.tz.clone(),
indent,
msg,
self.context.nick_emoji,
self.context.caesar,
folded_info,
);
@ -446,7 +437,7 @@ where
pub fn into_visible_blocks(
self,
) -> impl Iterator<Item = (Range<i32>, Block<TreeBlockId<M::Id>>)> + use<M, S> {
) -> impl Iterator<Item = (Range<i32>, Block<TreeBlockId<M::Id>>)> {
let area = renderer::visible_area(&self);
self.blocks
.into_iter()

View file

@ -1,14 +1,12 @@
use toss::{WidthDb, widgets::EditorState};
use toss::widgets::EditorState;
use toss::WidthDb;
use crate::{
store::{Msg, MsgStore},
ui::{ChatMsg, chat::cursor::Cursor},
};
use crate::store::{Msg, MsgStore};
use crate::ui::chat::cursor::Cursor;
use crate::ui::ChatMsg;
use super::{
TreeViewState,
renderer::{TreeContext, TreeRenderer},
};
use super::renderer::{TreeContext, TreeRenderer};
use super::TreeViewState;
impl<M, S> TreeViewState<M, S>
where
@ -22,7 +20,6 @@ where
size: self.last_size,
nick: self.last_nick.clone(),
focused: true,
nick_emoji: false,
caesar: 0,
last_cursor: self.last_cursor.clone(),
last_cursor_top: self.last_cursor_top,

View file

@ -2,19 +2,13 @@ use std::convert::Infallible;
use crossterm::style::Stylize;
use jiff::tz::TimeZone;
use toss::{
Style, Styled, WidgetExt,
widgets::{Boxed, EditorState, Join2, Join4, Join5, Text},
};
use toss::widgets::{Boxed, EditorState, Join2, Join4, Join5, Text};
use toss::{Style, Styled, WidgetExt};
use crate::{
store::Msg,
ui::{
ChatMsg,
chat::widgets::{Indent, Seen, Time},
},
util,
};
use crate::store::Msg;
use crate::ui::chat::widgets::{Indent, Seen, Time};
use crate::ui::ChatMsg;
use crate::util;
pub const PLACEHOLDER: &str = "[...]";
@ -59,17 +53,10 @@ pub fn msg<M: Msg + ChatMsg>(
tz: TimeZone,
indent: usize,
msg: &M,
nick_emoji: bool,
caesar: i8,
folded_info: Option<usize>,
) -> Boxed<'static, Infallible> {
let (mut nick, mut content) = msg.styled();
if nick_emoji {
if let Some(emoji) = msg.nick_emoji() {
nick = nick.then_plain("(").then_plain(emoji).then_plain(")");
}
}
let (nick, mut content) = msg.styled();
if caesar != 0 {
// Apply caesar in inverse because we're decoding

View file

@ -2,10 +2,8 @@ use std::convert::Infallible;
use crossterm::style::Stylize;
use jiff::Zoned;
use toss::{
Frame, Pos, Size, Style, Widget, WidgetExt, WidthDb,
widgets::{Boxed, Empty, Text},
};
use toss::widgets::{Boxed, Empty, Text};
use toss::{Frame, Pos, Size, Style, Widget, WidgetExt, WidthDb};
use crate::util::InfallibleExt;

View file

@ -1,16 +1,14 @@
use cove_config::Keys;
use cove_input::InputEvent;
use crossterm::style::Stylize;
use euphoxide::{api::PersonalAccountView, conn};
use toss::{
Style, Widget, WidgetExt,
widgets::{EditorState, Empty, Join3, Join4, Join5, Text},
};
use euphoxide::api::PersonalAccountView;
use euphoxide::conn;
use toss::widgets::{EditorState, Empty, Join3, Join4, Join5, Text};
use toss::{Style, Widget, WidgetExt};
use crate::{
euph::{self, Room},
ui::{UiError, util, widgets::Popup},
};
use crate::euph::{self, Room};
use crate::ui::widgets::Popup;
use crate::ui::{util, UiError};
use super::popup::PopupResult;
@ -35,7 +33,7 @@ impl LoggedOut {
}
}
fn widget(&mut self) -> impl Widget<UiError> {
fn widget(&mut self) -> impl Widget<UiError> + '_ {
let bold = Style::new().bold();
Join4::vertical(
Text::new(("Not logged in", bold.yellow())).segment(),
@ -68,7 +66,7 @@ impl LoggedOut {
pub struct LoggedIn(PersonalAccountView);
impl LoggedIn {
fn widget(&self) -> impl Widget<UiError> + use<> {
fn widget(&self) -> impl Widget<UiError> {
let bold = Style::new().bold();
Join5::vertical(
Text::new(("Logged in", bold.green())).segment(),
@ -111,7 +109,7 @@ impl AccountUiState {
}
}
pub fn widget(&mut self) -> impl Widget<UiError> {
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
let inner = match self {
Self::LoggedOut(logged_out) => logged_out.widget().first2(),
Self::LoggedIn(logged_in) => logged_in.widget().second2(),

View file

@ -1,11 +1,11 @@
use cove_config::Keys;
use cove_input::InputEvent;
use toss::{Widget, widgets::EditorState};
use toss::widgets::EditorState;
use toss::Widget;
use crate::{
euph::Room,
ui::{UiError, util, widgets::Popup},
};
use crate::euph::Room;
use crate::ui::widgets::Popup;
use crate::ui::{util, UiError};
use super::popup::PopupResult;
@ -13,7 +13,7 @@ pub fn new() -> EditorState {
EditorState::new()
}
pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> {
pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> + '_ {
Popup::new(
editor.widget().with_hidden_default_placeholder(),
"Enter password",

View file

@ -1,13 +1,13 @@
use cove_config::Keys;
use cove_input::InputEvent;
use crossterm::style::Stylize;
use euphoxide::{
api::{Message, NickEvent, SessionView},
conn::SessionInfo,
};
use toss::{Style, Styled, Widget, widgets::Text};
use euphoxide::api::{Message, NickEvent, SessionView};
use euphoxide::conn::SessionInfo;
use toss::widgets::Text;
use toss::{Style, Styled, Widget};
use crate::ui::{UiError, widgets::Popup};
use crate::ui::widgets::Popup;
use crate::ui::UiError;
use super::popup::PopupResult;
@ -91,7 +91,7 @@ fn message_lines(mut text: Styled, msg: &Message) -> Styled {
text
}
pub fn session_widget(session: &SessionInfo) -> impl Widget<UiError> + use<> {
pub fn session_widget(session: &SessionInfo) -> impl Widget<UiError> {
let heading_style = Style::new().bold();
let text = match session {
@ -108,7 +108,7 @@ pub fn session_widget(session: &SessionInfo) -> impl Widget<UiError> + use<> {
Popup::new(Text::new(text), "Inspect session")
}
pub fn message_widget(msg: &Message) -> impl Widget<UiError> + use<> {
pub fn message_widget(msg: &Message) -> impl Widget<UiError> {
let heading_style = Style::new().bold();
let mut text = Styled::new("Message", heading_style).then_plain("\n");

View file

@ -1,31 +1,19 @@
use cove_config::{Config, Keys};
use cove_input::InputEvent;
use crossterm::{event::KeyCode, style::Stylize};
use crossterm::event::KeyCode;
use crossterm::style::Stylize;
use linkify::{LinkFinder, LinkKind};
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{Join2, Text},
};
use toss::widgets::{Join2, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
euph::{self, SpanType},
ui::{
UiError, key_bindings, util,
widgets::{ListBuilder, ListState, Popup},
},
};
use crate::ui::widgets::{ListBuilder, ListState, Popup};
use crate::ui::{key_bindings, util, UiError};
use super::popup::PopupResult;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Link {
Url(String),
Room(String),
}
pub struct LinksState {
config: &'static Config,
links: Vec<Link>,
links: Vec<String>,
list: ListState<usize>,
}
@ -33,34 +21,12 @@ const NUMBER_KEYS: [char; 10] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0
impl LinksState {
pub fn new(config: &'static Config, content: &str) -> Self {
let mut links = vec![];
// Collect URL-like links
for link in LinkFinder::new()
let links = LinkFinder::new()
.url_must_have_scheme(false)
.kinds(&[LinkKind::Url])
.links(content)
{
links.push((
link.start(),
link.end(),
Link::Url(link.as_str().to_string()),
));
}
// Collect room links
for (span, range) in euph::find_spans(content) {
if span == SpanType::Room {
let name = &content[range.start + 1..range.end];
links.push((range.start, range.end, Link::Room(name.to_string())));
}
}
links.sort();
let links = links
.into_iter()
.map(|(_, _, link)| link)
.collect::<Vec<_>>();
.map(|l| l.as_str().to_string())
.collect();
Self {
config,
@ -69,7 +35,7 @@ impl LinksState {
}
}
pub fn widget(&mut self) -> impl Widget<UiError> {
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
let style_selected = Style::new().black().on_white();
let mut list_builder = ListBuilder::new();
@ -80,29 +46,29 @@ impl LinksState {
for (id, link) in self.links.iter().enumerate() {
let link = link.clone();
list_builder.add_sel(id, move |selected| {
let mut text = Styled::default();
// Number key indicator
text = match NUMBER_KEYS.get(id) {
None if selected => text.then(" ", style_selected),
None => text.then_plain(" "),
Some(key) if selected => text.then(format!("[{key}] "), style_selected.bold()),
Some(key) => text.then(format!("[{key}] "), Style::new().dark_grey().bold()),
};
// The link itself
text = match link {
Link::Url(url) if selected => text.then(url, style_selected),
Link::Url(url) => text.then_plain(url),
Link::Room(name) if selected => {
text.then(format!("&{name}"), style_selected.bold())
}
Link::Room(name) => text.then(format!("&{name}"), Style::new().blue().bold()),
};
Text::new(text).with_wrap(false)
});
if let Some(&number_key) = NUMBER_KEYS.get(id) {
list_builder.add_sel(id, move |selected| {
let text = if selected {
Styled::new(format!("[{number_key}]"), style_selected.bold())
.then(" ", style_selected)
.then(link, style_selected)
} else {
Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold())
.then_plain(" ")
.then_plain(link)
};
Text::new(text)
});
} else {
list_builder.add_sel(id, move |selected| {
let text = if selected {
Styled::new(format!(" {link}"), style_selected)
} else {
Styled::new_plain(format!(" {link}"))
};
Text::new(text)
});
}
}
let hint_style = Style::new().grey().italic();
@ -126,24 +92,18 @@ impl LinksState {
}
fn open_link_by_id(&self, id: usize) -> PopupResult {
match self.links.get(id) {
Some(Link::Url(url)) => {
// The `http://` or `https://` schema is necessary for
// open::that to successfully open the link in the browser.
let link = if url.starts_with("http://") || url.starts_with("https://") {
url.clone()
} else {
format!("https://{url}")
};
if let Some(link) = self.links.get(id) {
// The `http://` or `https://` schema is necessary for open::that to
// successfully open the link in the browser.
let link = if link.starts_with("http://") || link.starts_with("https://") {
link.clone()
} else {
format!("https://{link}")
};
if let Err(error) = open::that(&link) {
return PopupResult::ErrorOpeningLink { link, error };
}
if let Err(error) = open::that(&link) {
return PopupResult::ErrorOpeningLink { link, error };
}
Some(Link::Room(name)) => return PopupResult::SwitchToRoom { name: name.clone() },
_ => {}
}
PopupResult::Handled
}

View file

@ -1,12 +1,12 @@
use cove_config::Keys;
use cove_input::InputEvent;
use euphoxide::conn::Joined;
use toss::{Style, Widget, widgets::EditorState};
use toss::widgets::EditorState;
use toss::{Style, Widget};
use crate::{
euph::{self, Room},
ui::{UiError, util, widgets::Popup},
};
use crate::euph::{self, Room};
use crate::ui::widgets::Popup;
use crate::ui::{util, UiError};
use super::popup::PopupResult;
@ -14,7 +14,7 @@ pub fn new(joined: Joined) -> EditorState {
EditorState::with_initial_text(joined.session.name)
}
pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> {
pub fn widget(editor: &mut EditorState) -> impl Widget<UiError> + '_ {
let inner = editor
.widget()
.with_highlight(|s| euph::style_nick_exact(s, Style::new()));

View file

@ -1,31 +1,22 @@
use std::iter;
use crossterm::style::{Color, Stylize};
use euphoxide::{
api::{NickEvent, SessionId, SessionType, SessionView, UserId},
conn::{Joined, SessionInfo},
};
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{Background, Text},
};
use euphoxide::api::{NickEvent, SessionId, SessionType, SessionView, UserId};
use euphoxide::conn::{Joined, SessionInfo};
use toss::widgets::{Background, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
euph,
ui::{
UiError,
widgets::{ListBuilder, ListState},
},
};
use crate::euph;
use crate::ui::widgets::{ListBuilder, ListState};
use crate::ui::UiError;
pub fn widget<'a>(
list: &'a mut ListState<SessionId>,
joined: &Joined,
focused: bool,
nick_emoji: bool,
) -> impl Widget<UiError> + use<'a> {
) -> impl Widget<UiError> + 'a {
let mut list_builder = ListBuilder::new();
render_rows(&mut list_builder, joined, focused, nick_emoji);
render_rows(&mut list_builder, joined, focused);
list_builder.build(list)
}
@ -71,7 +62,6 @@ fn render_rows(
list_builder: &mut ListBuilder<'_, SessionId, Background<Text>>,
joined: &Joined,
focused: bool,
nick_emoji: bool,
) {
let mut people = vec![];
let mut bots = vec![];
@ -97,38 +87,10 @@ fn render_rows(
lurkers.sort_unstable();
nurkers.sort_unstable();
render_section(
list_builder,
"People",
&people,
&joined.session,
focused,
nick_emoji,
);
render_section(
list_builder,
"Bots",
&bots,
&joined.session,
focused,
nick_emoji,
);
render_section(
list_builder,
"Lurkers",
&lurkers,
&joined.session,
focused,
nick_emoji,
);
render_section(
list_builder,
"Nurkers",
&nurkers,
&joined.session,
focused,
nick_emoji,
);
render_section(list_builder, "People", &people, &joined.session, focused);
render_section(list_builder, "Bots", &bots, &joined.session, focused);
render_section(list_builder, "Lurkers", &lurkers, &joined.session, focused);
render_section(list_builder, "Nurkers", &nurkers, &joined.session, focused);
}
fn render_section(
@ -137,7 +99,6 @@ fn render_section(
sessions: &[HalfSession],
own_session: &SessionView,
focused: bool,
nick_emoji: bool,
) {
if sessions.is_empty() {
return;
@ -155,7 +116,7 @@ fn render_section(
list_builder.add_unsel(Text::new(row).background());
for session in sessions {
render_row(list_builder, session, own_session, focused, nick_emoji);
render_row(list_builder, session, own_session, focused);
}
}
@ -164,7 +125,6 @@ fn render_row(
session: &HalfSession,
own_session: &SessionView,
focused: bool,
nick_emoji: bool,
) {
let (name, style, style_inv, perms_style_inv) = if session.name.is_empty() {
let name = "lurk".to_string();
@ -198,24 +158,16 @@ fn render_row(
" "
};
let emoji = if nick_emoji {
format!(" ({})", euph::user_id_emoji(&session.id))
} else {
"".to_string()
};
list_builder.add_sel(session.session_id.clone(), move |selected| {
if focused && selected {
let text = Styled::new_plain(owner)
.then(name, style_inv)
.then(perms, perms_style_inv)
.then(emoji, perms_style_inv);
.then(perms, perms_style_inv);
Text::new(text).background().with_style(style_inv)
} else {
let text = Styled::new_plain(owner)
.then(&name, style)
.then_plain(perms)
.then_plain(emoji);
.then_plain(perms);
Text::new(text).background()
}
});

View file

@ -1,16 +1,18 @@
use std::io;
use crossterm::style::Stylize;
use toss::{Style, Styled, Widget, widgets::Text};
use toss::widgets::Text;
use toss::{Style, Styled, Widget};
use crate::ui::{UiError, widgets::Popup};
use crate::ui::widgets::Popup;
use crate::ui::UiError;
pub enum RoomPopup {
Error { description: String, reason: String },
}
impl RoomPopup {
fn server_error_widget(description: &str, reason: &str) -> impl Widget<UiError> + use<> {
fn server_error_widget(description: &str, reason: &str) -> impl Widget<UiError> {
let border_style = Style::new().red().bold();
let text = Styled::new_plain(description)
.then_plain("\n\n")
@ -21,7 +23,7 @@ impl RoomPopup {
Popup::new(Text::new(text), ("Error", border_style)).with_border_style(border_style)
}
pub fn widget(&self) -> impl Widget<UiError> + use<> {
pub fn widget(&self) -> impl Widget<UiError> {
match self {
Self::Error {
description,
@ -35,6 +37,5 @@ pub enum PopupResult {
NotHandled,
Handled,
Close,
SwitchToRoom { name: String },
ErrorOpeningLink { link: String, error: io::Error },
}

View file

@ -3,40 +3,26 @@ use std::collections::VecDeque;
use cove_config::{Config, Keys};
use cove_input::InputEvent;
use crossterm::style::Stylize;
use euphoxide::{
api::{Data, Message, MessageId, PacketType, SessionId, packet::ParsedPacket},
bot::instance::{ConnSnapshot, Event, ServerConfig},
conn::{self, Joined, Joining, SessionInfo},
};
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
use euphoxide::bot::instance::{Event, ServerConfig};
use euphoxide::conn::{self, Joined, Joining, SessionInfo};
use jiff::tz::TimeZone;
use tokio::sync::{
mpsc,
oneshot::{self, error::TryRecvError},
};
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{BoxedAsync, EditorState, Join2, Layer, Text},
};
use tokio::sync::oneshot::error::TryRecvError;
use tokio::sync::{mpsc, oneshot};
use toss::widgets::{BoxedAsync, EditorState, Join2, Layer, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
euph::{self, SpanType},
macros::logging_unwrap,
ui::{
UiError, UiEvent,
chat::{ChatState, Reaction},
util,
widgets::ListState,
},
vault::{EuphRoomVault, RoomIdentifier},
};
use crate::euph;
use crate::macros::logging_unwrap;
use crate::ui::chat::{ChatState, Reaction};
use crate::ui::widgets::ListState;
use crate::ui::{util, UiError, UiEvent};
use crate::vault::EuphRoomVault;
use super::{
account::AccountUiState,
auth, inspect,
links::LinksState,
nick, nick_list,
popup::{PopupResult, RoomPopup},
};
use super::account::AccountUiState;
use super::links::LinksState;
use super::popup::{PopupResult, RoomPopup};
use super::{auth, inspect, nick, nick_list};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Focus {
@ -73,8 +59,6 @@ pub struct EuphRoom {
last_msg_sent: Option<oneshot::Receiver<MessageId>>,
nick_list: ListState<SessionId>,
mentioned: bool,
}
impl EuphRoom {
@ -98,7 +82,6 @@ impl EuphRoom {
chat: ChatState::new(vault, tz),
last_msg_sent: None,
nick_list: ListState::new(),
mentioned: false,
}
}
@ -121,7 +104,7 @@ impl EuphRoom {
.server_config
.clone()
.room(self.vault().room().name.clone())
.name(format!("{room:?}-{next_instance_id}"))
.name(format!("{room:?}-{}", next_instance_id))
.human(true)
.username(self.room_config.username.clone())
.force_username(self.room_config.force_username)
@ -167,12 +150,6 @@ impl EuphRoom {
}
}
pub fn retrieve_mentioned(&mut self) -> bool {
let mentioned = self.mentioned;
self.mentioned = false;
mentioned
}
pub async fn unseen_msgs_count(&self) -> usize {
logging_unwrap!(self.vault().unseen_msgs_count().await)
}
@ -291,16 +268,11 @@ impl EuphRoom {
joined: &Joined,
focus: Focus,
) -> BoxedAsync<'a, UiError> {
let nick_list_widget = nick_list::widget(
nick_list,
joined,
focus == Focus::NickList,
chat.nick_emoji(),
)
.padding()
.with_right(1)
.border()
.desync();
let nick_list_widget = nick_list::widget(nick_list, joined, focus == Focus::NickList)
.padding()
.with_right(1)
.border()
.desync();
let chat_widget = chat.widget(joined.session.name.clone(), focus == Focus::Chat);
@ -315,7 +287,7 @@ impl EuphRoom {
.boxed_async()
}
async fn status_widget(&self, state: Option<&euph::State>) -> impl Widget<UiError> + use<> {
async fn status_widget(&self, state: Option<&euph::State>) -> impl Widget<UiError> {
let room_style = Style::new().bold().blue();
let mut info = Styled::new(format!("{} ", self.domain()), Style::new().grey())
.then(format!("&{}", self.name()), room_style);
@ -514,22 +486,18 @@ impl EuphRoom {
false
}
pub async fn handle_input_event(
&mut self,
event: &mut InputEvent<'_>,
keys: &Keys,
) -> RoomResult {
pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
if !self.popups.is_empty() {
if event.matches(&keys.general.abort) {
self.popups.pop_back();
return RoomResult::Handled;
return true;
}
// Prevent event from reaching anything below the popup
return RoomResult::NotHandled;
return false;
}
let result = match &mut self.state {
State::Normal => return self.handle_normal_input_event(event, keys).await.into(),
State::Normal => return self.handle_normal_input_event(event, keys).await,
State::Auth(editor) => auth::handle_input_event(event, keys, &self.room, editor),
State::Nick(editor) => nick::handle_input_event(event, keys, &self.room, editor),
State::Account(account) => account.handle_input_event(event, keys, &self.room),
@ -540,24 +508,18 @@ impl EuphRoom {
};
match result {
PopupResult::NotHandled => RoomResult::NotHandled,
PopupResult::Handled => RoomResult::Handled,
PopupResult::NotHandled => false,
PopupResult::Handled => true,
PopupResult::Close => {
self.state = State::Normal;
RoomResult::Handled
true
}
PopupResult::SwitchToRoom { name } => RoomResult::SwitchToRoom {
room: RoomIdentifier {
domain: self.vault().room().domain.clone(),
name,
},
},
PopupResult::ErrorOpeningLink { link, error } => {
self.popups.push_front(RoomPopup::Error {
description: format!("Failed to open link: {link}"),
reason: format!("{error}"),
});
RoomResult::Handled
true
}
}
}
@ -571,35 +533,6 @@ impl EuphRoom {
return false;
}
if let Event::Packet(
_,
ParsedPacket {
content: Ok(Data::SendEvent(send)),
..
},
ConnSnapshot {
state: conn::State::Joined(joined),
..
},
) = &event
{
let normalized_name = euphoxide::nick::normalize(&joined.session.name);
let content = &*send.0.content;
for (rtype, rspan) in euph::find_spans(content) {
if rtype != SpanType::Mention {
continue;
}
let Some(mention) = content[rspan].strip_prefix('@') else {
continue;
};
let normalized_mention = euphoxide::nick::normalize(mention);
if normalized_name == normalized_mention {
self.mentioned = true;
break;
}
}
}
// We handle the packet internally first because the room event handling
// will consume it while we only need a reference.
let handled = if let Event::Packet(_, packet, _) = &event {
@ -691,18 +624,3 @@ impl EuphRoom {
true
}
}
pub enum RoomResult {
NotHandled,
Handled,
SwitchToRoom { room: RoomIdentifier },
}
impl From<bool> for RoomResult {
fn from(value: bool) -> Self {
match value {
true => Self::Handled,
false => Self::NotHandled,
}
}
}

View file

@ -5,15 +5,11 @@ use std::convert::Infallible;
use cove_config::{Config, Keys};
use cove_input::{InputEvent, KeyBinding, KeyBindingInfo, KeyGroupInfo};
use crossterm::style::Stylize;
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{Either2, Join2, Padding, Text},
};
use toss::widgets::{Either2, Join2, Padding, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use super::{
UiError, util,
widgets::{ListBuilder, ListState, Popup},
};
use super::widgets::{ListBuilder, ListState, Popup};
use super::{util, UiError};
type Line = Either2<Text, Join2<Padding<Text>, Text>>;
type Builder = ListBuilder<'static, Infallible, Line>;
@ -73,7 +69,7 @@ fn render_group_info(builder: &mut Builder, group_info: KeyGroupInfo<'_>) {
pub fn widget<'a>(
list: &'a mut ListState<Infallible>,
config: &Config,
) -> impl Widget<UiError> + use<'a> {
) -> impl Widget<UiError> + 'a {
let mut list_builder = ListBuilder::new();
for group_info in config.keys.groups() {

View file

@ -1,46 +1,34 @@
use std::{
collections::{HashMap, HashSet, hash_map::Entry},
iter,
sync::{Arc, Mutex},
time::Duration,
};
mod connect;
mod delete;
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::iter;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use cove_config::{Config, Keys, RoomsSortOrder};
use cove_input::InputEvent;
use crossterm::style::Stylize;
use euphoxide::{
api::SessionType,
bot::instance::{Event, ServerConfig},
conn::{self, Joined},
};
use euphoxide::api::SessionType;
use euphoxide::bot::instance::{Event, ServerConfig};
use euphoxide::conn::{self, Joined};
use jiff::tz::TimeZone;
use tokio::sync::mpsc;
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{BellState, BoxedAsync, Empty, Join2, Text},
};
use toss::widgets::{BoxedAsync, Empty, Join2, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
euph,
macros::logging_unwrap,
vault::{EuphVault, RoomIdentifier, Vault},
version::{NAME, VERSION},
};
use crate::euph;
use crate::macros::logging_unwrap;
use crate::vault::{EuphVault, RoomIdentifier, Vault};
use crate::version::{NAME, VERSION};
use super::{
UiError, UiEvent,
euph::room::{EuphRoom, RoomResult},
key_bindings, util,
widgets::{ListBuilder, ListState},
};
use self::connect::{ConnectResult, ConnectState};
use self::delete::{DeleteResult, DeleteState};
use self::{
connect::{ConnectResult, ConnectState},
delete::{DeleteResult, DeleteState},
};
mod connect;
mod delete;
use super::euph::room::EuphRoom;
use super::widgets::{ListBuilder, ListState};
use super::{key_bindings, util, UiError, UiEvent};
enum State {
ShowList,
@ -95,7 +83,6 @@ pub struct Rooms {
list: ListState<RoomIdentifier>,
order: Order,
bell: BellState,
euph_servers: HashMap<String, EuphServer>,
euph_rooms: HashMap<RoomIdentifier, EuphRoom>,
@ -116,7 +103,6 @@ impl Rooms {
state: State::ShowList,
list: ListState::new(),
order: Order::from_rooms_sort_order(config.rooms_sort_order),
bell: BellState::new(),
euph_servers: HashMap::new(),
euph_rooms: HashMap::new(),
};
@ -246,9 +232,7 @@ impl Rooms {
.retain(|n, r| !r.stopped() || rooms_set.contains(n));
for room in rooms_set {
let room = self.get_or_insert_room(room).await;
room.retain();
self.bell.ring |= room.retrieve_mentioned();
self.get_or_insert_room(room).await.retain();
}
}
@ -258,7 +242,7 @@ impl Rooms {
_ => self.stabilize_rooms().await,
}
let widget = match &mut self.state {
match &mut self.state {
State::ShowList => Self::rooms_widget(
&self.vault,
self.config,
@ -301,12 +285,6 @@ impl Rooms {
.below(delete.widget())
.desync()
.boxed_async(),
};
if self.config.bell_on_mention {
widget.above(self.bell.widget().desync()).boxed_async()
} else {
widget
}
}
@ -445,7 +423,7 @@ impl Rooms {
list: &'a mut ListState<RoomIdentifier>,
order: Order,
euph_rooms: &HashMap<RoomIdentifier, EuphRoom>,
) -> impl Widget<UiError> + use<'a> {
) -> impl Widget<UiError> + 'a {
let version_info = Styled::new_plain("Welcome to ")
.then(format!("{NAME} {VERSION}"), Style::new().yellow().bold())
.then_plain("!");
@ -536,10 +514,7 @@ impl Rooms {
}
if event.matches(&keys.rooms.action.connect_autojoin) {
for (domain, server) in &self.config.euph.servers {
for (name, room) in &server.rooms {
if !room.autojoin {
continue;
}
for name in server.rooms.keys() {
let id = RoomIdentifier::new(domain.clone(), name.clone());
self.connect_to_room(id).await;
}
@ -587,15 +562,8 @@ impl Rooms {
}
State::ShowRoom(name) => {
if let Some(room) = self.euph_rooms.get_mut(name) {
match room.handle_input_event(event, keys).await {
RoomResult::NotHandled => {}
RoomResult::Handled => return true,
RoomResult::SwitchToRoom { room } => {
self.list.move_cursor_to_id(&room);
self.connect_to_room(room.clone()).await;
self.state = State::ShowRoom(room);
return true;
}
if room.handle_input_event(event, keys).await {
return true;
}
if event.matches(&keys.general.abort) {
self.state = State::ShowList;
@ -609,7 +577,6 @@ impl Rooms {
return true;
}
ConnectResult::Connect(room) => {
self.list.move_cursor_to_id(&room);
self.connect_to_room(room.clone()).await;
self.state = State::ShowRoom(room);
return true;

View file

@ -1,15 +1,12 @@
use cove_config::Keys;
use cove_input::InputEvent;
use crossterm::style::Stylize;
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{EditorState, Empty, Join2, Join3, Text},
};
use toss::widgets::{EditorState, Empty, Join2, Join3, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
ui::{UiError, util, widgets::Popup},
vault::RoomIdentifier,
};
use crate::ui::widgets::Popup;
use crate::ui::{util, UiError};
use crate::vault::RoomIdentifier;
#[derive(Clone, Copy, PartialEq, Eq)]
enum Focus {
@ -84,7 +81,7 @@ impl ConnectState {
ConnectResult::Unhandled
}
pub fn widget(&mut self) -> impl Widget<UiError> {
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
let room_style = Style::new().bold().blue();
let domain_style = Style::new().grey();

View file

@ -1,15 +1,12 @@
use cove_config::Keys;
use cove_input::InputEvent;
use crossterm::style::Stylize;
use toss::{
Style, Styled, Widget, WidgetExt,
widgets::{EditorState, Empty, Join2, Text},
};
use toss::widgets::{EditorState, Empty, Join2, Text};
use toss::{Style, Styled, Widget, WidgetExt};
use crate::{
ui::{UiError, util, widgets::Popup},
vault::RoomIdentifier,
};
use crate::ui::widgets::Popup;
use crate::ui::{util, UiError};
use crate::vault::RoomIdentifier;
pub struct DeleteState {
id: RoomIdentifier,
@ -47,7 +44,7 @@ impl DeleteState {
DeleteResult::Unhandled
}
pub fn widget(&mut self) -> impl Widget<UiError> {
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
let warn_style = Style::new().bold().red();
let room_style = Style::new().bold().blue();
let text = Styled::new_plain("Are you sure you want to delete ")

View file

@ -1,5 +1,5 @@
pub use self::list::*;
pub use self::popup::*;
mod list;
mod popup;
pub use self::list::*;
pub use self::popup::*;

View file

@ -239,12 +239,6 @@ impl<Id: Clone + Eq> ListState<Id> {
})
}
pub fn move_cursor_to_id(&mut self, id: &Id) {
if let Some(new_cursor) = self.selectable_of_id(id) {
self.move_cursor_to(new_cursor);
}
}
fn fix_cursor(&mut self) {
let new_cursor = if let Some(cursor) = &self.cursor {
self.selectable_of_id(&cursor.id)

View file

@ -1,7 +1,5 @@
use toss::{
Frame, Size, Style, Styled, Widget, WidgetExt, WidthDb,
widgets::{Background, Border, Desync, Float, Layer2, Padding, Text},
};
use toss::widgets::{Background, Border, Desync, Float, Layer2, Padding, Text};
use toss::{Frame, Size, Style, Styled, Widget, WidgetExt, WidthDb};
type Body<I> = Background<Border<Padding<I>>>;
type Title = Float<Padding<Background<Padding<Text>>>>;

View file

@ -1,4 +1,5 @@
use std::{convert::Infallible, env};
use std::convert::Infallible;
use std::env;
use jiff::tz::TimeZone;

View file

@ -1,14 +1,16 @@
use std::{fs, path::Path};
use rusqlite::Connection;
use vault::{Action, tokio::TokioVault};
pub use self::euph::{EuphRoomVault, EuphVault, RoomIdentifier};
mod euph;
mod migrate;
mod prepare;
use std::fs;
use std::path::Path;
use rusqlite::Connection;
use vault::tokio::TokioVault;
use vault::Action;
pub use self::euph::{EuphRoomVault, EuphVault, RoomIdentifier};
#[derive(Debug, Clone)]
pub struct Vault {
tokio_vault: TokioVault,

View file

@ -1,25 +1,23 @@
use std::{fmt, mem, str::FromStr};
use std::str::FromStr;
use std::time::Instant;
use std::{fmt, mem};
use async_trait::async_trait;
use cookie::{Cookie, CookieJar};
use euphoxide::api::{Message, MessageId, SessionId, SessionView, Snowflake, Time, UserId};
use rusqlite::{
Connection, OptionalExtension, Row, ToSql, Transaction, named_params, params,
types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef},
};
use rusqlite::types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef};
use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql, Transaction};
use vault::Action;
use crate::{
euph::SmallMessage,
store::{MsgStore, Path, Tree},
};
use crate::euph::SmallMessage;
use crate::store::{MsgStore, Path, Tree};
/// Wrapper for [`Snowflake`] that implements useful rusqlite traits.
struct WSnowflake(Snowflake);
impl ToSql for WSnowflake {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
self.0.0.to_sql()
self.0 .0.to_sql()
}
}
@ -34,7 +32,7 @@ struct WTime(Time);
impl ToSql for WTime {
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
let timestamp = self.0.0;
let timestamp = self.0 .0;
Ok(ToSqlOutput::Owned(Value::Integer(timestamp)))
}
}
@ -611,7 +609,7 @@ impl Action for GetMsg {
let msg = conn
.query_row(
"
SELECT id, parent, time, user_id, name, content, seen
SELECT id, parent, time, name, content, seen
FROM euph_msgs
WHERE domain = ?
AND room = ?
@ -623,10 +621,9 @@ impl Action for GetMsg {
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
time: row.get::<_, WTime>(2)?.0,
user_id: UserId(row.get(3)?),
nick: row.get(4)?,
content: row.get(5)?,
seen: row.get(6)?,
nick: row.get(3)?,
content: row.get(4)?,
seen: row.get(5)?,
})
},
)
@ -690,12 +687,12 @@ impl Action for GetTree {
type Error = rusqlite::Error;
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error> {
let msgs = conn
.prepare(
"
let start = Instant::now();
let query = "
WITH RECURSIVE
tree (domain, room, id) AS (
VALUES (?, ?, ?)
VALUES (:domain, :room, :id)
UNION
SELECT euph_msgs.domain, euph_msgs.room, euph_msgs.id
FROM euph_msgs
@ -704,27 +701,49 @@ impl Action for GetTree {
AND tree.room = euph_msgs.room
AND tree.id = euph_msgs.parent
)
SELECT id, parent, time, user_id, name, content, seen
SELECT id, parent, time, 'name', 'content', 1
FROM euph_msgs
JOIN tree USING (domain, room, id)
ORDER BY id ASC
",
)?
";
let mut statement = conn.prepare(&format!("EXPLAIN QUERY PLAN {query}"))?;
let mut rows = statement.query(named_params! {
":domain": self.room.domain,
":room": self.room.name,
":id": WSnowflake(self.root_id.0),
})?;
while let Some(row) = rows.next()? {
let id = row.get::<_, i64>("id")?;
let parent = row.get::<_, i64>("parent")?;
let notused = row.get::<_, i64>("notused")?;
let detail = row.get::<_, String>("detail")?;
eprintln!("{parent:3} -> {id:3} (notused {notused:3}): {detail}");
}
let msgs = conn
.prepare(query)?
.query_map(
params![self.room.domain, self.room.name, WSnowflake(self.root_id.0)],
named_params! {
":domain": self.room.domain,
":room": self.room.name,
":id": WSnowflake(self.root_id.0),
},
|row| {
Ok(SmallMessage {
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
time: row.get::<_, WTime>(2)?.0,
user_id: UserId(row.get(3)?),
nick: row.get(4)?,
content: row.get(5)?,
seen: row.get(6)?,
nick: row.get(3)?,
content: row.get(4)?,
seen: row.get(5)?,
})
},
)?
.collect::<rusqlite::Result<_>>()?;
let end = Instant::now();
eprintln!("{:10}", end.duration_since(start).as_micros());
Ok(Tree::new(self.root_id, msgs))
}
}

47
flake.lock generated Normal file
View file

@ -0,0 +1,47 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1713520724,
"narHash": "sha256-CO8MmVDmqZX2FovL75pu5BvwhW+Vugc7Q6ze7Hj8heI=",
"owner": "nix-community",
"repo": "naersk",
"rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1714068967,
"narHash": "sha256-jfQUewdwBVs0HHLH10qxyn0+J53e1aQoPSkuBnYf15s=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "10b682b6e5ed139ee2bef863ada3043f2d79c1cc",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

29
flake.nix Normal file
View file

@ -0,0 +1,29 @@
{
description = "TUI client for euphoria.leet.nu, a threaded real-time chat platform";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
naersk.url = "github:nix-community/naersk";
naersk.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, naersk }:
let forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in {
packages = forAllSystems (system:
let
pkgs = import nixpkgs { inherit system; };
naersk' = pkgs.callPackage naersk { };
cargoToml = pkgs.lib.importTOML ./Cargo.toml;
in
{
default = naersk'.buildPackage {
name = "cove";
version = cargoToml.workspace.package.version;
root = ./.;
};
}
);
};
}