Compare commits

..

42 commits

Author SHA1 Message Date
10214f3369 Fix clippy warning 2025-06-27 11:03:06 +02:00
2ca6190d97 Use older ubuntu runner for older glibc version 2025-05-31 15:08:24 +02:00
67e77c8880 Show emoji hash in nick list 2025-05-31 15:06:55 +02:00
b70d7548da Bump version to 0.9.3 2025-05-31 13:54:07 +02:00
732d462775 Add user id based emoji hash
Helpful in scenarios where you want to disambiguate people based on
their user id at a glance.
2025-05-31 13:39:34 +02:00
40de073799 Silence clippy warning 2025-05-31 13:11:50 +02:00
8b928184e8 Fix autojoin key connecting to non-autojoin rooms 2025-04-08 23:52:27 +02:00
ca0f0b6c31 Bump version to 0.9.2 2025-03-14 15:13:14 +01:00
74fbf386b2 Update dependencies 2025-03-14 15:08:05 +01:00
a17630aeaa Ring bell when mentioned 2025-03-08 19:34:51 +01:00
496cdde18d Bump version to 0.9.1 2025-03-01 14:38:41 +01:00
6157ca5088 Update dependencies 2025-03-01 14:26:18 +01:00
30c344031a Fix rendering glitches with unicode-based width estimation 2025-02-28 14:32:30 +01:00
4cf6a15577 Bump version to 0.9.0 2025-02-24 00:00:15 +01:00
b207e91c25 Update dependencies 2025-02-24 00:00:15 +01:00
676c92752d Remove flake
A bit annoying to keep up-to-date since I don't use it myself.
2025-02-24 00:00:15 +01:00
cc436bbb3a Mention --width-estimation-method in config docs 2025-02-24 00:00:15 +01:00
56896a861e Mention release binaries in readme 2025-02-24 00:00:15 +01:00
03b91ec1cd Mention ghostty in config docs 2025-02-23 23:08:17 +01:00
cab37cb633 Fix non-replaceable emoji being rendered without colons 2025-02-23 22:55:47 +01:00
967293db37 Fix links wrapping into oblivion 2025-02-23 22:54:55 +01:00
972e4938aa Run tests in CI 2025-02-23 22:46:09 +01:00
b64f56fce5 Fix nick color in rare edge cases 2025-02-23 22:41:25 +01:00
b4c4a89625 Fix mention color of non-ascii nicks
The old code included the @ in mention color computations. If the nick
consisted only of weird unicode characters, this resulted in an
incorrect color being computed.
2025-02-23 22:03:43 +01:00
9435fbece6 Fix mention highlighting 2025-02-23 21:57:02 +01:00
315db43010 Fix /me not being grey and italic 2025-02-23 21:46:32 +01:00
24c8c92070 Update emoji 2025-02-23 21:42:13 +01:00
bf9a9d640b Navigate to rooms using message links list 2025-02-23 21:32:44 +01:00
8040b82ff1 Refactor euph message content highlighting 2025-02-23 20:49:43 +01:00
17185ea536 Add unicode-based grapheme width estimation method 2025-02-23 18:37:17 +01:00
900a686d0d Fix changelog heading
"Updated" does not conform to Keep a Changelog.
2025-02-23 04:28:21 +01:00
2fa1bec421 Remove special rl2dev code 2025-02-23 04:27:22 +01:00
e750f81b11 Drop once_cell dependency
It's now part of the standard library :D
2025-02-22 16:42:26 +01:00
866176dab6 Move cursor to new room in room list 2025-02-22 12:33:59 +01:00
bf11e055b6 Reformat changelog
There should always be space around headlines and lists in markdown
documents.
2025-02-22 12:33:59 +01:00
6c884f3077 Update RPIT lifetime bounds for the 2024 edition
When the bound matches the implicit bound, i.e. when you'd just write

    impl ... + use<'_>

then it can be omitted. My gut instinct is to always write the bound
explicitly, but maybe that'll harm readability once I'm more used to how
bounds work now.

Anyways, always try to keep the bound as small as possible, ideally just

    impl ... + use<>
2025-02-21 19:18:01 +01:00
d29e3e6651 Set up GitHub CI 2025-02-21 15:02:21 +01:00
fbc64de607 Merge and reorder imports
This change brings them in-line with the default settings of
rust-analyzer. Originally, I disliked this style, but by now, I've grown
used to it and prefer it slightly. Also, I like staying on the default
path since I usually don't need to check the imports to see if
rust-analyzer messed anything up (except for omitting the self:: when
importing modules defined in the current module, grr :D).

I've also come around to the idea of trailing mod definitions instead of
putting them at the top: I would also put module definitions that
contain code instead of referencing another file below the imports. It
feels weird to have code above imports. So it just makes sense to do the
same for all types of mod definitions.

If rustfmt ever gains stable support for grouping imports, I'll probably
use that instead.
2025-02-21 12:46:25 +01:00
816d8f86a3 Migrate rustfmt style to 2024 edition 2025-02-21 12:17:57 +01:00
25d2cc7c98 Migrate to 2024 edition 2025-02-21 12:17:57 +01:00
f45e66f572 Fix or ignore 2024 edition migration lints 2025-02-21 12:11:58 +01:00
bd43fe060b Update dependencies
Except rusqlite and vault, because newer sqlite versions appear to
result in a *very* big performance drop. I suspect the query planner,
though I really have no idea.
2025-02-21 00:41:01 +01:00
56 changed files with 1361 additions and 951 deletions

75
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,75 @@
# 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, "files.insertFinalNewline": true,
"rust-analyzer.cargo.features": "all", "rust-analyzer.cargo.features": "all",
"rust-analyzer.imports.granularity.enforce": true, "rust-analyzer.imports.granularity.enforce": true,
"rust-analyzer.imports.granularity.group": "module", "rust-analyzer.imports.granularity.group": "crate",
"rust-analyzer.imports.group.enable": true, "rust-analyzer.imports.group.enable": true,
"evenBetterToml.formatter.columnWidth": 100, "evenBetterToml.formatter.columnWidth": 100,
} }

View file

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

View file

@ -53,6 +53,14 @@ Available modifiers:
## Available options ## 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` ### `data_dir`
**Required:** no **Required:** no
@ -93,9 +101,9 @@ Whether to automatically join this room on startup.
**Type:** boolean **Type:** boolean
**Default:** `false` **Default:** `false`
If `euph.rooms.<room>.username` is set, this will force cove to set the If `euph.servers.<domain>.rooms.<room>.username` is set, this will force
username even if there is already a different username associated with cove to set the username even if there is already a different username
the current session. associated with the current session.
### `euph.servers.<domain>.rooms.<room>.password` ### `euph.servers.<domain>.rooms.<room>.password`
@ -529,6 +537,14 @@ Reply to message, inline if possible.
Reply opposite to normal reply. 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` ### `keys.tree.action.toggle_seen`
**Required:** yes **Required:** yes
@ -607,12 +623,11 @@ Move to root.
**Type:** boolean **Type:** boolean
**Default:** `false` **Default:** `false`
Whether to measure the width of characters as displayed by the terminal Whether to measure the width of graphemes (i.e. characters) as displayed
emulator instead of guessing the width. by the terminal emulator instead of estimating the width.
Enabling this makes rendering a bit slower but more accurate. The screen Enabling this makes rendering a bit slower but more accurate. The screen
might also flash when encountering new characters (or, more accurately, might also flash when encountering new graphemes.
graphemes).
See also the `--measure-widths` command line option. See also the `--measure-widths` command line option.
@ -656,18 +671,41 @@ order of priority):
Time zone that chat timestamps should be displayed in. Time zone that chat timestamps should be displayed in.
This option is interpreted as a POSIX TZ string. It is described here in This option can either be the string `"localtime"`, a [POSIX TZ string],
further detail: or a [tz identifier] from the [tz database].
<https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html>
On a normal system, the string `"localtime"` as well as any value from When not set or when set to `"localtime"`, cove attempts to use your
the "TZ identifier" column of the following wikipedia article should be system's configured time zone, falling back to UTC.
valid TZ strings:
<https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>
If the `TZ` environment variable exists, it overrides this option. If When the string begins with a colon or doesn't match the a POSIX TZ
neither exist, cove uses the system's local time zone. 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).
**Warning:** On Windows, cove can't get the local time zone and uses UTC If the `TZ` environment variable exists, it overrides this option.
instead. However, you can still specify a path to a tz data file or a
custom time zone string. [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.

275
Cargo.lock generated
View file

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

View file

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

View file

@ -7,6 +7,11 @@ real-time chat platform.
It runs on Linux, Windows, and macOS. 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 ## Using cove
To start cove, simply run `cove` in your terminal. For more info about the To start cove, simply run `cove` in your terminal. For more info about the
@ -26,61 +31,3 @@ file or via `cove help-config`.
When launched, cove prints the location it is loading its config file from. To 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 configure cove, create a config file at that location. This location can be
changed via the `--config` command line option. 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,7 +1,6 @@
//! Auto-generate markdown documentation. //! Auto-generate markdown documentation.
use std::collections::HashMap; use std::{collections::HashMap, path::PathBuf};
use std::path::PathBuf;
use cove_input::KeyBinding; use cove_input::KeyBinding;
pub use cove_macro::Document; pub use cove_macro::Document;

View file

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

View file

@ -1,17 +1,18 @@
use std::{
fs,
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use doc::Document;
use serde::{Deserialize, Serialize};
pub use crate::{euph::*, keys::*};
pub mod doc; pub mod doc;
mod euph; mod euph;
mod keys; 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)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("failed to read config file")] #[error("failed to read config file")]
@ -20,6 +21,14 @@ pub enum Error {
Toml(#[from] toml::de::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)] #[derive(Debug, Default, Deserialize, Document)]
pub struct Config { pub struct Config {
/// The directory that cove stores its data in when not running in ephemeral /// The directory that cove stores its data in when not running in ephemeral
@ -40,12 +49,29 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub ephemeral: bool, pub ephemeral: bool,
/// Whether to measure the width of characters as displayed by the terminal /// How to estimate the width of graphemes (i.e. characters) as displayed by
/// emulator instead of guessing the width. /// 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.
/// ///
/// Enabling this makes rendering a bit slower but more accurate. The screen /// Enabling this makes rendering a bit slower but more accurate. The screen
/// might also flash when encountering new characters (or, more accurately, /// might also flash when encountering new graphemes.
/// graphemes).
/// ///
/// See also the `--measure-widths` command line option. /// See also the `--measure-widths` command line option.
#[serde(default)] #[serde(default)]
@ -74,6 +100,10 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub rooms_sort_order: RoomsSortOrder, 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. /// Time zone that chat timestamps should be displayed in.
/// ///
/// This option can either be the string `"localtime"`, a [POSIX TZ string], /// This option can either be the string `"localtime"`, a [POSIX TZ string],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

211
cove/src/euph/highlight.rs Normal file
View file

@ -0,0 +1,211 @@
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,21 +1,17 @@
// TODO Remove rl2dev-specific code use std::{convert::Infallible, time::Duration};
use std::convert::Infallible; use euphoxide::{
use std::time::Duration; api::{
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply,
use euphoxide::api::packet::ParsedPacket; Time, UserId, packet::ParsedPacket,
use euphoxide::api::{ },
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply, Time, bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig},
UserId, conn::{self, ConnTx, Joined},
}; };
use euphoxide::bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig}; use log::{debug, info, warn};
use euphoxide::conn::{self, ConnTx, Joined}; use tokio::{select, sync::oneshot};
use log::{debug, error, info, warn};
use tokio::select;
use tokio::sync::oneshot;
use crate::macros::logging_unwrap; use crate::{macros::logging_unwrap, vault::EuphRoomVault};
use crate::vault::EuphRoomVault;
const LOG_INTERVAL: Duration = Duration::from_secs(10); const LOG_INTERVAL: Duration = Duration::from_secs(10);
@ -73,20 +69,13 @@ impl Room {
where where
F: Fn(Event) + std::marker::Send + Sync + 'static, 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 { Self {
vault, ephemeral: vault.vault().vault().ephemeral(),
ephemeral,
instance: instance_config.build(on_event), instance: instance_config.build(on_event),
state: State::Disconnected, state: State::Disconnected,
last_msg_id: None, last_msg_id: None,
log_request_canary: None, log_request_canary: None,
vault,
} }
} }
@ -194,14 +183,7 @@ impl Room {
debug!("{:?}: requesting logs", vault.room()); debug!("{:?}: requesting logs", vault.room());
// &rl2dev's message history is broken and requesting old messages past let _ = conn_tx.send(Log { n: 1000, before }).await;
// 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 // The code handling incoming events and replies also handles
// `LogReply`s, so we don't need to do anything special here. // `LogReply`s, so we don't need to do anything special here.
} }

View file

@ -1,212 +1,18 @@
use std::mem;
use crossterm::style::Stylize; use crossterm::style::Stylize;
use euphoxide::api::{MessageId, Snowflake, Time}; use euphoxide::api::{MessageId, Snowflake, Time, UserId};
use jiff::Timestamp; use jiff::Timestamp;
use toss::{Style, Styled}; use toss::{Style, Styled};
use crate::store::Msg; use crate::{store::Msg, ui::ChatMsg};
use crate::ui::ChatMsg;
use super::util; 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)] #[derive(Debug, Clone)]
pub struct SmallMessage { pub struct SmallMessage {
pub id: MessageId, pub id: MessageId,
pub parent: Option<MessageId>, pub parent: Option<MessageId>,
pub time: Time, pub time: Time,
pub user_id: UserId,
pub nick: String, pub nick: String,
pub content: String, pub content: String,
pub seen: bool, pub seen: bool,
@ -222,22 +28,22 @@ fn style_me() -> Style {
fn styled_nick(nick: &str) -> Styled { fn styled_nick(nick: &str) -> Styled {
Styled::new_plain("[") Styled::new_plain("[")
.and_then(util::style_nick(nick, Style::new())) .and_then(super::style_nick(nick, Style::new()))
.then_plain("]") .then_plain("]")
} }
fn styled_nick_me(nick: &str) -> Styled { fn styled_nick_me(nick: &str) -> Styled {
let style = style_me(); let style = style_me();
Styled::new("*", style).and_then(util::style_nick(nick, style)) Styled::new("*", style).and_then(super::style_nick(nick, style))
} }
fn styled_content(content: &str) -> Styled { fn styled_content(content: &str) -> Styled {
highlight_content(content.trim(), Style::new(), false) super::highlight(content.trim(), Style::new(), false)
} }
fn styled_content_me(content: &str) -> Styled { fn styled_content_me(content: &str) -> Styled {
let style = style_me(); let style = style_me();
highlight_content(content.trim(), style, false).then("*", style) super::highlight(content.trim(), style, false).then("*", style)
} }
fn styled_editor_content(content: &str) -> Styled { fn styled_editor_content(content: &str) -> Styled {
@ -246,7 +52,7 @@ fn styled_editor_content(content: &str) -> Styled {
} else { } else {
Style::new() Style::new()
}; };
highlight_content(content, style, true) super::highlight(content, style, true)
} }
impl Msg for SmallMessage { impl Msg for SmallMessage {
@ -267,6 +73,10 @@ impl Msg for SmallMessage {
fn last_possible_id() -> Self::Id { fn last_possible_id() -> Self::Id {
MessageId(Snowflake::MAX) MessageId(Snowflake::MAX)
} }
fn nick_emoji(&self) -> Option<String> {
Some(util::user_id_emoji(&self.user_id))
}
} }
impl ChatMsg for SmallMessage { impl ChatMsg for SmallMessage {

View file

@ -1,9 +1,27 @@
use std::{
collections::HashSet,
hash::{DefaultHasher, Hash, Hasher},
sync::LazyLock,
};
use crossterm::style::{Color, Stylize}; use crossterm::style::{Color, Stylize};
use euphoxide::Emoji; use euphoxide::{Emoji, api::UserId};
use once_cell::sync::Lazy;
use toss::{Style, Styled}; use toss::{Style, Styled};
pub static EMOJI: Lazy<Emoji> = Lazy::new(Emoji::load); 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
});
/// Convert HSL to RGB following [this approach from wikipedia][1]. /// Convert HSL to RGB following [this approach from wikipedia][1].
/// ///
@ -54,3 +72,25 @@ pub fn style_nick(nick: &str, base: Style) -> Styled {
pub fn style_nick_exact(nick: &str, base: Style) -> Styled { pub fn style_nick_exact(nick: &str, base: Style) -> Styled {
Styled::new(nick, nick_style(nick, base)) 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,13 +1,15 @@
//! Export logs from the vault to plain text files. //! 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 json;
mod text; mod text;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use crate::vault::{EuphRoomVault, EuphVault, RoomIdentifier};
#[derive(Debug, Clone, Copy, clap::ValueEnum)] #[derive(Debug, Clone, Copy, clap::ValueEnum)]
pub enum Format { pub enum Format {
/// Human-readable tree-structured messages. /// Human-readable tree-structured messages.

View file

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

View file

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

View file

@ -1,6 +1,23 @@
// TODO Remove unnecessary Debug impls and compare compile times // TODO Remove unnecessary Debug impls and compare compile times
// TODO Invoke external notification command? // 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 euph;
mod export; mod export;
mod logger; mod logger;
@ -11,22 +28,6 @@ mod util;
mod vault; mod vault;
mod version; 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)] #[derive(Debug, clap::Parser)]
enum Command { enum Command {
/// Run the client interactively (default). /// Run the client interactively (default).
@ -45,6 +46,12 @@ enum Command {
HelpConfig, HelpConfig,
} }
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
enum WidthEstimationMethod {
Legacy,
Unicode,
}
impl Default for Command { impl Default for Command {
fn default() -> Self { fn default() -> Self {
Self::Run Self::Run
@ -78,6 +85,11 @@ struct Args {
#[arg(long, short)] #[arg(long, short)]
offline: bool, 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 /// Measure the width of characters as displayed by the terminal emulator
/// instead of guessing the width. /// instead of guessing the width.
#[arg(long, short)] #[arg(long, short)]
@ -113,6 +125,12 @@ fn update_config_with_args(config: &mut Config, args: &Args) {
} }
config.ephemeral |= args.ephemeral; 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.measure_widths |= args.measure_widths;
config.offline |= args.offline; config.offline |= args.offline;
} }
@ -181,6 +199,10 @@ async fn run(
let mut terminal = Terminal::new()?; let mut terminal = Terminal::new()?;
terminal.set_measuring(config.measure_widths); 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?; Ui::run(config, tz, &mut terminal, vault.clone(), logger, logger_rx).await?;
drop(terminal); drop(terminal);

View file

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

View file

@ -1,3 +1,30 @@
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 chat;
mod euph; mod euph;
mod key_bindings; mod key_bindings;
@ -5,31 +32,6 @@ mod rooms;
mod util; mod util;
mod widgets; 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. /// Time to spend batch processing events before redrawing the screen.
const EVENT_PROCESSING_TIME: Duration = Duration::from_millis(1000 / 15); // 15 fps const EVENT_PROCESSING_TIME: Duration = Duration::from_millis(1000 / 15); // 15 fps
@ -48,6 +50,7 @@ impl From<Infallible> for UiError {
} }
} }
#[expect(clippy::large_enum_variant)]
pub enum UiEvent { pub enum UiEvent {
GraphemeWidthsChanged, GraphemeWidthsChanged,
LogChanged, LogChanged,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -239,6 +239,12 @@ 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) { fn fix_cursor(&mut self) {
let new_cursor = if let Some(cursor) = &self.cursor { let new_cursor = if let Some(cursor) = &self.cursor {
self.selectable_of_id(&cursor.id) self.selectable_of_id(&cursor.id)

View file

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

View file

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

View file

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

View file

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

47
flake.lock generated
View file

@ -1,47 +0,0 @@
{
"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
}

View file

@ -1,29 +0,0 @@
{
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 = ./.;
};
}
);
};
}