Compare commits
No commits in common. "master" and "v0.7.1" have entirely different histories.
68 changed files with 2012 additions and 3327 deletions
75
.github/workflows/build.yml
vendored
75
.github/workflows/build.yml
vendored
|
|
@ -1,75 +0,0 @@
|
||||||
# What software is installed by default:
|
|
||||||
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
|
|
||||||
|
|
||||||
name: build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os:
|
|
||||||
- ubuntu-22.04
|
|
||||||
- windows-latest
|
|
||||||
- macos-latest
|
|
||||||
- macos-13
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- name: Check out repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up rust
|
|
||||||
run: rustup update
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: cargo build --release
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: cargo test --release
|
|
||||||
|
|
||||||
- name: Record target triple
|
|
||||||
run: rustc -vV | awk '/^host/ { print $2 }' > target/release/host
|
|
||||||
|
|
||||||
- name: Upload
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: cove-${{ matrix.os }}
|
|
||||||
path: |
|
|
||||||
target/release/cove
|
|
||||||
target/release/cove.exe
|
|
||||||
target/release/host
|
|
||||||
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
|
||||||
needs:
|
|
||||||
- build
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Download artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
|
|
||||||
- name: Zip artifacts
|
|
||||||
run: |
|
|
||||||
chmod +x cove-ubuntu-22.04/cove
|
|
||||||
chmod +x cove-windows-latest/cove.exe
|
|
||||||
chmod +x cove-macos-latest/cove
|
|
||||||
chmod +x cove-macos-13/cove
|
|
||||||
zip -jr "cove-$(cat cove-ubuntu-22.04/host).zip" cove-ubuntu-22.04/cove
|
|
||||||
zip -jr "cove-$(cat cove-windows-latest/host).zip" cove-windows-latest/cove.exe
|
|
||||||
zip -jr "cove-$(cat cove-macos-latest/host).zip" cove-macos-latest/cove
|
|
||||||
zip -jr "cove-$(cat cove-macos-13/host).zip" cove-macos-13/cove
|
|
||||||
|
|
||||||
- name: Create new release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
body: Automated release, see [CHANGELOG.md](CHANGELOG.md) for more details.
|
|
||||||
files: "*.zip"
|
|
||||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
|
@ -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": "crate",
|
"rust-analyzer.imports.granularity.group": "module",
|
||||||
"rust-analyzer.imports.group.enable": true,
|
"rust-analyzer.imports.group.enable": true,
|
||||||
"evenBetterToml.formatter.columnWidth": 100,
|
"evenBetterToml.formatter.columnWidth": 100,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
137
CHANGELOG.md
137
CHANGELOG.md
|
|
@ -4,135 +4,25 @@ 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 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. Push `master` and the new tag
|
7. Fast-forward branch `latest`
|
||||||
|
8. Push `master`, `latest` and the new tag
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Display emoji user id hashes in the nick list
|
|
||||||
- Compile linux binary with older glibc version
|
|
||||||
|
|
||||||
## v0.9.3 - 2025-05-31
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Key bindings for emoji-based user id hashing
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `keys.rooms.action.connect_autojoin` connecting to non-autojoin rooms
|
|
||||||
|
|
||||||
## v0.9.2 - 2025-03-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- `bell_on_mention` config option
|
|
||||||
|
|
||||||
## v0.9.1 - 2025-03-01
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Rendering glitches with unicode-based width estimation
|
|
||||||
|
|
||||||
## v0.9.0 - 2025-02-23
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Unicode-based grapheme width estimation method
|
|
||||||
- `width_estimation_method` config option
|
|
||||||
- `--width-estimation-method` option
|
|
||||||
- Room links are now included in the `I` message links list
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated documentation for `time_zone` config option
|
|
||||||
- When connecting to a room using `n` in the room list, the cursor now moves to that room
|
|
||||||
- Updated list of emoji names
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Special handling of &rl2dev
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Nick color in rare edge cases
|
|
||||||
- Message link list rendering bug
|
|
||||||
|
|
||||||
## v0.8.3 - 2024-05-20
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Updated list of emoji names
|
|
||||||
|
|
||||||
## v0.8.2 - 2024-04-25
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Renamed `json-stream` export format to `json-lines` (see <https://jsonlines.org/>)
|
|
||||||
- Changed `json-lines` file extension from `.json` to `.jsonl`
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Crash when window is too small while empty message editor is visible
|
|
||||||
- Mistakes in output and docs
|
|
||||||
- Cove not cleaning up terminal state properly
|
|
||||||
|
|
||||||
## v0.8.1 - 2024-01-11
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for setting window title
|
|
||||||
- More information to room list heading
|
|
||||||
- Key bindings for live caesar cipher de- and encoding
|
|
||||||
|
|
||||||
### Removed
|
|
||||||
|
|
||||||
- Key binding to open present page
|
|
||||||
|
|
||||||
## v0.8.0 - 2024-01-04
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Support for multiple euph server domains
|
|
||||||
- Support for `TZ` environment variable
|
|
||||||
- `time_zone` config option
|
|
||||||
- `--domain` option to `cove export` command
|
|
||||||
- `--domain` option to `cove clear-cookies` command
|
|
||||||
- Domain field to "connect to new room" popup
|
|
||||||
- Welcome info box next to room list
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- The default euph domain is now https://euphoria.leet.nu/ everywhere
|
|
||||||
- The config file format was changed to support multiple euph servers with different domains.
|
|
||||||
Options previously located at `euph.rooms.*` should be reviewed and moved to `euph.servers."euphoria.leet.nu".rooms.*`.
|
|
||||||
- Tweaked F1 popup
|
|
||||||
- Tweaked chat message editor when nick list is foused
|
|
||||||
- Reduced connection timeout from 30 seconds to 10 seconds
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Room deletion popup accepting any room name
|
|
||||||
- Duplicated key presses on Windows
|
|
||||||
|
|
||||||
## v0.7.1 - 2023-08-31
|
## 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
|
||||||
|
|
@ -140,7 +30,6 @@ 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
|
||||||
|
|
@ -154,18 +43,15 @@ 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
|
||||||
|
|
@ -173,37 +59,31 @@ 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
|
||||||
|
|
@ -212,12 +92,10 @@ 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
|
||||||
|
|
@ -225,7 +103,6 @@ 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
|
||||||
|
|
@ -241,17 +118,14 @@ 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
|
||||||
|
|
@ -259,12 +133,10 @@ 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
|
||||||
|
|
@ -272,18 +144,15 @@ 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
|
||||||
|
|
@ -296,12 +165,10 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
143
CONFIG.md
143
CONFIG.md
|
|
@ -8,11 +8,15 @@ Here is an example config that changes a few different options:
|
||||||
measure_widths = true
|
measure_widths = true
|
||||||
rooms_sort_order = "importance"
|
rooms_sort_order = "importance"
|
||||||
|
|
||||||
[euph.servers."euphoria.leet.nu".rooms]
|
[euph.rooms.welcome]
|
||||||
welcome.autojoin = true
|
autojoin = true
|
||||||
test.username = "badingle"
|
|
||||||
test.force_username = true
|
[euph.rooms.test]
|
||||||
private.password = "foobar"
|
username = "badingle"
|
||||||
|
force_username = true
|
||||||
|
|
||||||
|
[euph.rooms.private]
|
||||||
|
password = "foobar"
|
||||||
|
|
||||||
[keys]
|
[keys]
|
||||||
general.abort = ["esc", "ctrl+c"]
|
general.abort = ["esc", "ctrl+c"]
|
||||||
|
|
@ -20,6 +24,17 @@ general.exit = "ctrl+q"
|
||||||
tree.action.fold_tree = "f"
|
tree.action.fold_tree = "f"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to configure lots of rooms, TOML lets you write this in a more
|
||||||
|
compact way:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[euph.rooms]
|
||||||
|
foo = { autojoin = true }
|
||||||
|
bar = { autojoin = true }
|
||||||
|
baz = { autojoin = true }
|
||||||
|
private = { autojoin = true, password = "foobar" }
|
||||||
|
```
|
||||||
|
|
||||||
## Key bindings
|
## Key bindings
|
||||||
|
|
||||||
Key bindings are specified as strings or lists of strings. Each string specifies
|
Key bindings are specified as strings or lists of strings. Each string specifies
|
||||||
|
|
@ -53,14 +68,6 @@ 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
|
||||||
|
|
@ -87,7 +94,7 @@ any options related to the data dir.
|
||||||
|
|
||||||
See also the `--ephemeral` command line option.
|
See also the `--ephemeral` command line option.
|
||||||
|
|
||||||
### `euph.servers.<domain>.rooms.<room>.autojoin`
|
### `euph.rooms.<room>.autojoin`
|
||||||
|
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
**Type:** boolean
|
**Type:** boolean
|
||||||
|
|
@ -95,17 +102,17 @@ See also the `--ephemeral` command line option.
|
||||||
|
|
||||||
Whether to automatically join this room on startup.
|
Whether to automatically join this room on startup.
|
||||||
|
|
||||||
### `euph.servers.<domain>.rooms.<room>.force_username`
|
### `euph.rooms.<room>.force_username`
|
||||||
|
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
**Type:** boolean
|
**Type:** boolean
|
||||||
**Default:** `false`
|
**Default:** `false`
|
||||||
|
|
||||||
If `euph.servers.<domain>.rooms.<room>.username` is set, this will force
|
If `euph.rooms.<room>.username` is set, this will force cove to set the
|
||||||
cove to set the username even if there is already a different username
|
username even if there is already a different username associated with
|
||||||
associated with the current session.
|
the current session.
|
||||||
|
|
||||||
### `euph.servers.<domain>.rooms.<room>.password`
|
### `euph.rooms.<room>.password`
|
||||||
|
|
||||||
**Required:** no
|
**Required:** no
|
||||||
**Type:** string
|
**Type:** string
|
||||||
|
|
@ -113,7 +120,7 @@ associated with the current session.
|
||||||
If set, cove will try once to use this password to authenticate, should
|
If set, cove will try once to use this password to authenticate, should
|
||||||
the room be password-protected.
|
the room be password-protected.
|
||||||
|
|
||||||
### `euph.servers.<domain>.rooms.<room>.username`
|
### `euph.rooms.<room>.username`
|
||||||
|
|
||||||
**Required:** no
|
**Required:** no
|
||||||
**Type:** string
|
**Type:** string
|
||||||
|
|
@ -329,6 +336,14 @@ Download more messages.
|
||||||
|
|
||||||
Change nick.
|
Change nick.
|
||||||
|
|
||||||
|
### `keys.room.action.present`
|
||||||
|
|
||||||
|
**Required:** yes
|
||||||
|
**Type:** key binding
|
||||||
|
**Default:** `"ctrl+p"`
|
||||||
|
|
||||||
|
Open room's plugh.de/present page.
|
||||||
|
|
||||||
### `keys.rooms.action.change_sort_order`
|
### `keys.rooms.action.change_sort_order`
|
||||||
|
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
|
|
@ -457,14 +472,6 @@ Scroll up half a screen.
|
||||||
|
|
||||||
Scroll up one line.
|
Scroll up one line.
|
||||||
|
|
||||||
### `keys.tree.action.decrease_caesar`
|
|
||||||
|
|
||||||
**Required:** yes
|
|
||||||
**Type:** key binding
|
|
||||||
**Default:** `"C"`
|
|
||||||
|
|
||||||
Decrease caesar cipher rotation.
|
|
||||||
|
|
||||||
### `keys.tree.action.fold_tree`
|
### `keys.tree.action.fold_tree`
|
||||||
|
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
|
|
@ -473,14 +480,6 @@ Decrease caesar cipher rotation.
|
||||||
|
|
||||||
Fold current message's subtree.
|
Fold current message's subtree.
|
||||||
|
|
||||||
### `keys.tree.action.increase_caesar`
|
|
||||||
|
|
||||||
**Required:** yes
|
|
||||||
**Type:** key binding
|
|
||||||
**Default:** `"c"`
|
|
||||||
|
|
||||||
Increase caesar cipher rotation.
|
|
||||||
|
|
||||||
### `keys.tree.action.inspect`
|
### `keys.tree.action.inspect`
|
||||||
|
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
|
|
@ -537,14 +536,6 @@ 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
|
||||||
|
|
@ -623,13 +614,14 @@ Move to root.
|
||||||
**Type:** boolean
|
**Type:** boolean
|
||||||
**Default:** `false`
|
**Default:** `false`
|
||||||
|
|
||||||
Whether to measure the width of graphemes (i.e. characters) as displayed
|
Whether to measure the width of characters as displayed by the terminal
|
||||||
by the terminal emulator instead of estimating the width.
|
emulator instead of guessing 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 graphemes.
|
might also flash when encountering new characters (or, more accurately,
|
||||||
|
graphemes).
|
||||||
|
|
||||||
See also the `--measure-widths` command line option.
|
See also the `--measure-graphemes` command line option.
|
||||||
|
|
||||||
### `offline`
|
### `offline`
|
||||||
|
|
||||||
|
|
@ -650,62 +642,15 @@ See also the `--offline` command line option.
|
||||||
**Required:** yes
|
**Required:** yes
|
||||||
**Type:** string
|
**Type:** string
|
||||||
**Values:** `"alphabet"`, `"importance"`
|
**Values:** `"alphabet"`, `"importance"`
|
||||||
**Default:** `"alphabet"`
|
**Default:** `alphabet`
|
||||||
|
|
||||||
Initial sort order of rooms list.
|
Initial sort order of rooms list.
|
||||||
|
|
||||||
`"alphabet"` sorts rooms in alphabetic order.
|
`alphabet` sorts rooms in alphabetic order.
|
||||||
|
|
||||||
`"importance"` sorts rooms by the following criteria (in descending
|
`importance` sorts rooms by the following criteria (in descending order
|
||||||
order of priority):
|
of priority):
|
||||||
|
|
||||||
1. connected rooms before unconnected rooms
|
1. connected rooms before unconnected rooms
|
||||||
2. rooms with unread messages before rooms without
|
2. rooms with unread messages before rooms without
|
||||||
3. alphabetic order
|
3. alphabetic order
|
||||||
|
|
||||||
### `time_zone`
|
|
||||||
|
|
||||||
**Required:** no
|
|
||||||
**Type:** string
|
|
||||||
**Default:** `$TZ` or local system time zone
|
|
||||||
|
|
||||||
Time zone that chat timestamps should be displayed in.
|
|
||||||
|
|
||||||
This option can either be the string `"localtime"`, a [POSIX TZ string],
|
|
||||||
or a [tz identifier] from the [tz database].
|
|
||||||
|
|
||||||
When not set or when set to `"localtime"`, cove attempts to use your
|
|
||||||
system's configured time zone, falling back to UTC.
|
|
||||||
|
|
||||||
When the string begins with a colon or doesn't match the a POSIX TZ
|
|
||||||
string format, it is interpreted as a tz identifier and looked up in
|
|
||||||
your system's tz database (or a bundled tz database on Windows).
|
|
||||||
|
|
||||||
If the `TZ` environment variable exists, it overrides this option.
|
|
||||||
|
|
||||||
[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.
|
|
||||||
|
|
|
||||||
1339
Cargo.lock
generated
1339
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
67
Cargo.toml
67
Cargo.toml
|
|
@ -1,72 +1,21 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "2"
|
||||||
members = ["cove", "cove-*"]
|
members = ["cove", "cove-*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.9.3"
|
version = "0.7.1"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
anyhow = "1.0.97"
|
crossterm = "0.27.0"
|
||||||
async-trait = "0.1.87"
|
parking_lot = "0.12.1"
|
||||||
clap = { version = "4.5.32", features = ["derive", "deprecated"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
cookie = "0.18.1"
|
|
||||||
crossterm = "0.28.1"
|
|
||||||
directories = "6.0.0"
|
|
||||||
edit = "0.1.5"
|
|
||||||
jiff = "0.2.4"
|
|
||||||
linkify = "0.10.0"
|
|
||||||
log = { version = "0.4.26", features = ["std"] }
|
|
||||||
open = "5.3.2"
|
|
||||||
parking_lot = "0.12.3"
|
|
||||||
proc-macro2 = "1.0.94"
|
|
||||||
quote = "1.0.40"
|
|
||||||
rusqlite = { version = "0.31.0", features = ["bundled", "time"] }
|
|
||||||
rustls = "0.23.23"
|
|
||||||
serde = { version = "1.0.219", features = ["derive"] }
|
|
||||||
serde_either = "0.2.1"
|
serde_either = "0.2.1"
|
||||||
serde_json = "1.0.140"
|
thiserror = "1.0.47"
|
||||||
syn = "2.0.100"
|
|
||||||
thiserror = "2.0.12"
|
|
||||||
tokio = { version = "1.44.1", features = ["full"] }
|
|
||||||
toml = "0.8.20"
|
|
||||||
unicode-width = "0.2.0"
|
|
||||||
|
|
||||||
[workspace.dependencies.euphoxide]
|
|
||||||
git = "https://github.com/Garmelon/euphoxide.git"
|
|
||||||
tag = "v0.6.1"
|
|
||||||
features = ["bot"]
|
|
||||||
|
|
||||||
[workspace.dependencies.toss]
|
[workspace.dependencies.toss]
|
||||||
git = "https://github.com/Garmelon/toss.git"
|
git = "https://github.com/Garmelon/toss.git"
|
||||||
tag = "v0.3.4"
|
tag = "v0.2.0"
|
||||||
|
|
||||||
[workspace.dependencies.vault]
|
|
||||||
git = "https://github.com/Garmelon/vault.git"
|
|
||||||
tag = "v0.4.0"
|
|
||||||
features = ["tokio"]
|
|
||||||
|
|
||||||
[workspace.lints]
|
|
||||||
rust.unsafe_code = { level = "forbid", priority = 1 }
|
|
||||||
# Lint groups
|
|
||||||
rust.deprecated_safe = "warn"
|
|
||||||
rust.future_incompatible = "warn"
|
|
||||||
rust.keyword_idents = "warn"
|
|
||||||
rust.rust_2018_idioms = "warn"
|
|
||||||
rust.unused = "warn"
|
|
||||||
# Individual lints
|
|
||||||
rust.non_local_definitions = "warn"
|
|
||||||
rust.redundant_imports = "warn"
|
|
||||||
rust.redundant_lifetimes = "warn"
|
|
||||||
rust.single_use_lifetimes = "warn"
|
|
||||||
rust.unit_bindings = "warn"
|
|
||||||
rust.unnameable_types = "warn"
|
|
||||||
rust.unused_crate_dependencies = "warn"
|
|
||||||
rust.unused_import_braces = "warn"
|
|
||||||
rust.unused_lifetimes = "warn"
|
|
||||||
rust.unused_qualifications = "warn"
|
|
||||||
# Clippy
|
|
||||||
clippy.use_self = "warn"
|
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
65
README.md
65
README.md
|
|
@ -1,17 +1,12 @@
|
||||||
# cove
|
# cove
|
||||||
|
|
||||||
Cove is a TUI client for [euphoria.leet.nu](https://euphoria.leet.nu/), a threaded
|
Cove is a TUI client for [euphoria.io](https://euphoria.io/), a threaded
|
||||||
real-time chat platform.
|
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
|
||||||
|
|
@ -31,3 +26,61 @@ 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
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cove-config"
|
name = "cove-config"
|
||||||
version.workspace = true
|
version = { workspace = true }
|
||||||
edition.workspace = true
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cove-input = { path = "../cove-input" }
|
cove-input = { path = "../cove-input" }
|
||||||
cove-macro = { path = "../cove-macro" }
|
cove-macro = { path = "../cove-macro" }
|
||||||
|
|
||||||
serde.workspace = true
|
serde = { workspace = true }
|
||||||
thiserror.workspace = true
|
thiserror = { workspace = true }
|
||||||
toml.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
toml = "0.7.6"
|
||||||
workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Auto-generate markdown documentation.
|
//! Auto-generate markdown documentation.
|
||||||
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use cove_input::KeyBinding;
|
use cove_input::KeyBinding;
|
||||||
pub use cove_macro::Document;
|
pub use cove_macro::Document;
|
||||||
|
|
@ -16,11 +17,15 @@ Here is an example config that changes a few different options:
|
||||||
measure_widths = true
|
measure_widths = true
|
||||||
rooms_sort_order = "importance"
|
rooms_sort_order = "importance"
|
||||||
|
|
||||||
[euph.servers."euphoria.leet.nu".rooms]
|
[euph.rooms.welcome]
|
||||||
welcome.autojoin = true
|
autojoin = true
|
||||||
test.username = "badingle"
|
|
||||||
test.force_username = true
|
[euph.rooms.test]
|
||||||
private.password = "foobar"
|
username = "badingle"
|
||||||
|
force_username = true
|
||||||
|
|
||||||
|
[euph.rooms.private]
|
||||||
|
password = "foobar"
|
||||||
|
|
||||||
[keys]
|
[keys]
|
||||||
general.abort = ["esc", "ctrl+c"]
|
general.abort = ["esc", "ctrl+c"]
|
||||||
|
|
@ -28,6 +33,17 @@ general.exit = "ctrl+q"
|
||||||
tree.action.fold_tree = "f"
|
tree.action.fold_tree = "f"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to configure lots of rooms, TOML lets you write this in a more
|
||||||
|
compact way:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[euph.rooms]
|
||||||
|
foo = { autojoin = true }
|
||||||
|
bar = { autojoin = true }
|
||||||
|
baz = { autojoin = true }
|
||||||
|
private = { autojoin = true, password = "foobar" }
|
||||||
|
```
|
||||||
|
|
||||||
## Key bindings
|
## Key bindings
|
||||||
|
|
||||||
Key bindings are specified as strings or lists of strings. Each string specifies
|
Key bindings are specified as strings or lists of strings. Each string specifies
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,9 @@ pub struct EuphRoom {
|
||||||
/// associated with the current session.
|
/// associated with the current session.
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
|
|
||||||
/// If `euph.servers.<domain>.rooms.<room>.username` is set, this will force
|
/// If `euph.rooms.<room>.username` is set, this will force cove to set the
|
||||||
/// cove to set the username even if there is already a different username
|
/// username even if there is already a different username associated with
|
||||||
/// associated with the current session.
|
/// the current session.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force_username: bool,
|
pub force_username: bool,
|
||||||
|
|
||||||
|
|
@ -35,13 +35,7 @@ pub struct EuphRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Document)]
|
#[derive(Debug, Default, Deserialize, Document)]
|
||||||
pub struct EuphServer {
|
pub struct Euph {
|
||||||
#[document(metavar = "room")]
|
#[document(metavar = "room")]
|
||||||
pub rooms: HashMap<String, EuphRoom>,
|
pub rooms: HashMap<String, EuphRoom>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Document)]
|
|
||||||
pub struct Euph {
|
|
||||||
#[document(metavar = "domain")]
|
|
||||||
pub servers: HashMap<String, EuphServer>,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ default_bindings! {
|
||||||
pub fn nick => ["n"];
|
pub fn nick => ["n"];
|
||||||
pub fn more_messages => ["m"];
|
pub fn more_messages => ["m"];
|
||||||
pub fn account => ["A"];
|
pub fn account => ["A"];
|
||||||
|
pub fn present => ["ctrl+p"];
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod tree_cursor {
|
pub mod tree_cursor {
|
||||||
|
|
@ -104,9 +105,6 @@ 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 decrease_caesar => ["C"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +122,7 @@ pub struct General {
|
||||||
#[serde(default = "default::general::confirm")]
|
#[serde(default = "default::general::confirm")]
|
||||||
pub confirm: KeyBinding,
|
pub confirm: KeyBinding,
|
||||||
/// Advance focus.
|
/// Advance focus.
|
||||||
|
// TODO Mention examples where this is used
|
||||||
#[serde(default = "default::general::focus")]
|
#[serde(default = "default::general::focus")]
|
||||||
pub focus: KeyBinding,
|
pub focus: KeyBinding,
|
||||||
/// Show this help.
|
/// Show this help.
|
||||||
|
|
@ -288,6 +287,9 @@ pub struct RoomAction {
|
||||||
/// Manage account.
|
/// Manage account.
|
||||||
#[serde(default = "default::room_action::account")]
|
#[serde(default = "default::room_action::account")]
|
||||||
pub account: KeyBinding,
|
pub account: KeyBinding,
|
||||||
|
/// Open room's plugh.de/present page.
|
||||||
|
#[serde(default = "default::room_action::present")]
|
||||||
|
pub present: KeyBinding,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Document)]
|
#[derive(Debug, Default, Deserialize, Document)]
|
||||||
|
|
@ -357,15 +359,6 @@ 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.
|
|
||||||
#[serde(default = "default::tree_action::increase_caesar")]
|
|
||||||
pub increase_caesar: KeyBinding,
|
|
||||||
/// Decrease caesar cipher rotation.
|
|
||||||
#[serde(default = "default::tree_action::decrease_caesar")]
|
|
||||||
pub decrease_caesar: KeyBinding,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Document)]
|
#[derive(Debug, Default, Deserialize, Document)]
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,28 @@
|
||||||
use std::{
|
#![forbid(unsafe_code)]
|
||||||
fs,
|
// Rustc lint groups
|
||||||
io::{self, ErrorKind},
|
#![warn(future_incompatible)]
|
||||||
path::{Path, PathBuf},
|
#![warn(rust_2018_idioms)]
|
||||||
};
|
#![warn(unused)]
|
||||||
|
// Rustc lints
|
||||||
use doc::Document;
|
#![warn(noop_method_call)]
|
||||||
use serde::{Deserialize, Serialize};
|
#![warn(single_use_lifetimes)]
|
||||||
|
// Clippy lints
|
||||||
pub use crate::{euph::*, keys::*};
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
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")]
|
||||||
|
|
@ -21,14 +31,6 @@ 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
|
||||||
|
|
@ -47,34 +49,19 @@ pub struct Config {
|
||||||
///
|
///
|
||||||
/// See also the `--ephemeral` command line option.
|
/// See also the `--ephemeral` command line option.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[document(default = "`false`")]
|
||||||
pub ephemeral: bool,
|
pub ephemeral: bool,
|
||||||
|
|
||||||
/// How to estimate the width of graphemes (i.e. characters) as displayed by
|
/// Whether to measure the width of characters as displayed by the terminal
|
||||||
/// the terminal emulator.
|
/// emulator instead of guessing the width.
|
||||||
///
|
|
||||||
/// `"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 graphemes.
|
/// might also flash when encountering new characters (or, more accurately,
|
||||||
|
/// graphemes).
|
||||||
///
|
///
|
||||||
/// See also the `--measure-widths` command line option.
|
/// See also the `--measure-graphemes` command line option.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[document(default = "`false`")]
|
||||||
pub measure_widths: bool,
|
pub measure_widths: bool,
|
||||||
|
|
||||||
/// Whether to start in offline mode.
|
/// Whether to start in offline mode.
|
||||||
|
|
@ -85,46 +72,23 @@ pub struct Config {
|
||||||
///
|
///
|
||||||
/// See also the `--offline` command line option.
|
/// See also the `--offline` command line option.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[document(default = "`false`")]
|
||||||
pub offline: bool,
|
pub offline: bool,
|
||||||
|
|
||||||
/// Initial sort order of rooms list.
|
/// Initial sort order of rooms list.
|
||||||
///
|
///
|
||||||
/// `"alphabet"` sorts rooms in alphabetic order.
|
/// `alphabet` sorts rooms in alphabetic order.
|
||||||
///
|
///
|
||||||
/// `"importance"` sorts rooms by the following criteria (in descending
|
/// `importance` sorts rooms by the following criteria (in descending order
|
||||||
/// order of priority):
|
/// of priority):
|
||||||
///
|
///
|
||||||
/// 1. connected rooms before unconnected rooms
|
/// 1. connected rooms before unconnected rooms
|
||||||
/// 2. rooms with unread messages before rooms without
|
/// 2. rooms with unread messages before rooms without
|
||||||
/// 3. alphabetic order
|
/// 3. alphabetic order
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
#[document(default = "`alphabet`")]
|
||||||
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.
|
|
||||||
///
|
|
||||||
/// This option can either be the string `"localtime"`, a [POSIX TZ string],
|
|
||||||
/// or a [tz identifier] from the [tz database].
|
|
||||||
///
|
|
||||||
/// When not set or when set to `"localtime"`, cove attempts to use your
|
|
||||||
/// system's configured time zone, falling back to UTC.
|
|
||||||
///
|
|
||||||
/// When the string begins with a colon or doesn't match the a POSIX TZ
|
|
||||||
/// string format, it is interpreted as a tz identifier and looked up in
|
|
||||||
/// your system's tz database (or a bundled tz database on Windows).
|
|
||||||
///
|
|
||||||
/// If the `TZ` environment variable exists, it overrides this option.
|
|
||||||
///
|
|
||||||
/// [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
|
|
||||||
#[serde(default)]
|
|
||||||
#[document(default = "`$TZ` or local system time zone")]
|
|
||||||
pub time_zone: Option<String>,
|
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[document(no_default)]
|
#[document(no_default)]
|
||||||
pub euph: Euph,
|
pub euph: Euph,
|
||||||
|
|
@ -143,16 +107,7 @@ impl Config {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn euph_room(&self, domain: &str, name: &str) -> EuphRoom {
|
pub fn euph_room(&self, name: &str) -> EuphRoom {
|
||||||
if let Some(server) = self.euph.servers.get(domain) {
|
self.euph.rooms.get(name).cloned().unwrap_or_default()
|
||||||
if let Some(room) = server.rooms.get(name) {
|
|
||||||
return room.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EuphRoom::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn time_zone_ref(&self) -> Option<&str> {
|
|
||||||
self.time_zone.as_ref().map(|s| s as &str)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cove-input"
|
name = "cove-input"
|
||||||
version.workspace = true
|
version = { workspace = true }
|
||||||
edition.workspace = true
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cove-macro = { path = "../cove-macro" }
|
cove-macro = { path = "../cove-macro" }
|
||||||
|
|
||||||
crossterm.workspace = true
|
crossterm = { workspace = true }
|
||||||
edit.workspace = true
|
parking_lot = { workspace = true }
|
||||||
parking_lot.workspace = true
|
serde = { workspace = true }
|
||||||
serde.workspace = true
|
serde_either = { workspace = true }
|
||||||
serde_either.workspace = true
|
thiserror = { workspace = true }
|
||||||
thiserror.workspace = true
|
toss = { workspace = true }
|
||||||
toss.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
edit = "0.1.4"
|
||||||
workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
use std::{fmt, num::ParseIntError, str::FromStr};
|
use std::fmt;
|
||||||
|
use std::num::ParseIntError;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
|
use serde::{de::Error, Deserialize, Deserializer};
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
use serde_either::SingleOrVec;
|
use serde_either::SingleOrVec;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -114,7 +117,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())),
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +151,7 @@ impl FromStr for KeyPress {
|
||||||
let mut parts = s.split('+');
|
let mut parts = s.split('+');
|
||||||
let code = parts.next_back().ok_or(ParseKeysError::NoKeyCode)?;
|
let code = parts.next_back().ok_or(ParseKeysError::NoKeyCode)?;
|
||||||
|
|
||||||
let mut keys = Self::parse_key_code(code)?;
|
let mut keys = KeyPress::parse_key_code(code)?;
|
||||||
let shift_allowed = !conflicts_with_shift(keys.code);
|
let shift_allowed = !conflicts_with_shift(keys.code);
|
||||||
for modifier in parts {
|
for modifier in parts {
|
||||||
keys.parse_modifier(modifier, shift_allowed)?;
|
keys.parse_modifier(modifier, shift_allowed)?;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
use std::{io, sync::Arc};
|
mod keys;
|
||||||
|
|
||||||
|
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};
|
||||||
use parking_lot::FairMutex;
|
use parking_lot::FairMutex;
|
||||||
use toss::{Frame, Terminal, WidthDb};
|
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,
|
||||||
|
|
@ -39,7 +40,7 @@ impl<'a> KeyGroupInfo<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InputEvent<'a> {
|
pub struct InputEvent<'a> {
|
||||||
event: Event,
|
event: crossterm::event::Event,
|
||||||
terminal: &'a mut Terminal,
|
terminal: &'a mut Terminal,
|
||||||
crossterm_lock: Arc<FairMutex<()>>,
|
crossterm_lock: Arc<FairMutex<()>>,
|
||||||
}
|
}
|
||||||
|
|
@ -57,15 +58,11 @@ impl<'a> InputEvent<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If the current event represents a key press, returns the [`KeyEvent`]
|
|
||||||
/// associated with that key press.
|
|
||||||
pub fn key_event(&self) -> Option<KeyEvent> {
|
pub fn key_event(&self) -> Option<KeyEvent> {
|
||||||
if let Event::Key(event) = &self.event {
|
match &self.event {
|
||||||
if matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
|
Event::Key(event) => Some(*event),
|
||||||
return Some(*event);
|
_ => None,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_event(&self) -> Option<&str> {
|
pub fn paste_event(&self) -> Option<&str> {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cove-macro"
|
name = "cove-macro"
|
||||||
version.workspace = true
|
version = { workspace = true }
|
||||||
edition.workspace = true
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
proc-macro2.workspace = true
|
case = "1.0.0"
|
||||||
quote.workspace = true
|
proc-macro2 = "1.0.66"
|
||||||
syn.workspace = true
|
quote = "1.0.33"
|
||||||
|
syn = "2.0.29"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Ident, LitStr, spanned::Spanned};
|
use syn::spanned::Spanned;
|
||||||
|
use syn::{Data, DataEnum, DataStruct, DeriveInput, Field, Ident, LitStr};
|
||||||
|
|
||||||
use crate::util::{self, SerdeDefault};
|
use crate::util::{self, SerdeDefault};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use proc_macro2::TokenStream;
|
use proc_macro2::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{Data, DeriveInput, spanned::Spanned};
|
use syn::spanned::Spanned;
|
||||||
|
use syn::{Data, DeriveInput};
|
||||||
|
|
||||||
use crate::util;
|
use crate::util::{self, bail};
|
||||||
|
|
||||||
fn decapitalize(s: &str) -> String {
|
fn decapitalize(s: &str) -> String {
|
||||||
let mut chars = s.chars();
|
let mut chars = s.chars();
|
||||||
|
|
@ -33,7 +34,7 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<TokenStream> {
|
||||||
|
|
||||||
let default = util::serde_default(field)?;
|
let default = util::serde_default(field)?;
|
||||||
let Some(default) = default else {
|
let Some(default) = default else {
|
||||||
return util::bail(field_ident.span(), "must have serde default");
|
return bail(field_ident.span(), "must have serde default");
|
||||||
};
|
};
|
||||||
let default_value = default.value();
|
let default_value = default.value();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
use syn::{DeriveInput, parse_macro_input};
|
#![forbid(unsafe_code)]
|
||||||
|
// Rustc lint groups
|
||||||
|
#![warn(future_incompatible)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
#![warn(unused)]
|
||||||
|
// Rustc lints
|
||||||
|
#![warn(noop_method_call)]
|
||||||
|
#![warn(single_use_lifetimes)]
|
||||||
|
// Clippy lints
|
||||||
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
|
use syn::{parse_macro_input, DeriveInput};
|
||||||
|
|
||||||
mod document;
|
mod document;
|
||||||
mod key_group;
|
mod key_group;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use proc_macro2::{Span, TokenStream};
|
use proc_macro2::{Span, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{
|
use syn::parse::Parse;
|
||||||
Attribute, Expr, ExprLit, ExprPath, Field, Lit, LitStr, Path, Token, Type, parse::Parse,
|
use syn::punctuated::Punctuated;
|
||||||
punctuated::Punctuated,
|
use syn::{Attribute, Expr, ExprLit, ExprPath, Field, Lit, LitStr, Path, Token, Type};
|
||||||
};
|
|
||||||
|
|
||||||
pub fn bail<T>(span: Span, message: &str) -> syn::Result<T> {
|
pub fn bail<T>(span: Span, message: &str) -> syn::Result<T> {
|
||||||
Err(syn::Error::new(span, message))
|
Err(syn::Error::new(span, message))
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,46 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cove"
|
name = "cove"
|
||||||
version.workspace = true
|
version = { workspace = true }
|
||||||
edition.workspace = true
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
cove-config = { path = "../cove-config" }
|
cove-config = { path = "../cove-config" }
|
||||||
cove-input = { path = "../cove-input" }
|
cove-input = { path = "../cove-input" }
|
||||||
|
|
||||||
anyhow.workspace = true
|
crossterm = { workspace = true }
|
||||||
async-trait.workspace = true
|
parking_lot = { workspace = true }
|
||||||
clap.workspace = true
|
thiserror = { workspace = true }
|
||||||
cookie.workspace = true
|
toss = { workspace = true }
|
||||||
crossterm.workspace = true
|
|
||||||
directories.workspace = true
|
|
||||||
euphoxide.workspace = true
|
|
||||||
jiff.workspace = true
|
|
||||||
linkify.workspace = true
|
|
||||||
log.workspace = true
|
|
||||||
open.workspace = true
|
|
||||||
parking_lot.workspace = true
|
|
||||||
rusqlite.workspace = true
|
|
||||||
serde_json.workspace = true
|
|
||||||
thiserror.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
toss.workspace = true
|
|
||||||
unicode-width.workspace = true
|
|
||||||
vault.workspace = true
|
|
||||||
rustls.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
anyhow = "1.0.75"
|
||||||
workspace = true
|
async-trait = "0.1.73"
|
||||||
|
clap = { version = "4.4.1", features = ["derive", "deprecated"] }
|
||||||
|
cookie = "0.17.0"
|
||||||
|
directories = "5.0.1"
|
||||||
|
linkify = "0.10.0"
|
||||||
|
log = { version = "0.4.20", features = ["std"] }
|
||||||
|
once_cell = "1.18.0"
|
||||||
|
open = "5.0.0"
|
||||||
|
rusqlite = { version = "0.29.0", features = ["bundled", "time"] }
|
||||||
|
serde_json = "1.0.105"
|
||||||
|
tokio = { version = "1.32.0", features = ["full"] }
|
||||||
|
unicode-segmentation = "1.10.1"
|
||||||
|
unicode-width = "0.1.10"
|
||||||
|
|
||||||
|
[dependencies.time]
|
||||||
|
version = "0.3.28"
|
||||||
|
features = ["macros", "formatting", "parsing", "serde"]
|
||||||
|
|
||||||
|
[dependencies.tokio-tungstenite]
|
||||||
|
version = "0.20.0"
|
||||||
|
features = ["rustls-tls-native-roots"]
|
||||||
|
|
||||||
|
[dependencies.euphoxide]
|
||||||
|
git = "https://github.com/Garmelon/euphoxide.git"
|
||||||
|
tag = "v0.4.0"
|
||||||
|
features = ["bot"]
|
||||||
|
|
||||||
|
[dependencies.vault]
|
||||||
|
git = "https://github.com/Garmelon/vault.git"
|
||||||
|
tag = "v0.2.0"
|
||||||
|
features = ["tokio"]
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
pub use highlight::*;
|
|
||||||
pub use room::*;
|
|
||||||
pub use small_message::*;
|
|
||||||
pub use util::*;
|
|
||||||
|
|
||||||
mod highlight;
|
|
||||||
mod room;
|
mod room;
|
||||||
mod small_message;
|
mod small_message;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
pub use room::*;
|
||||||
|
pub use small_message::*;
|
||||||
|
pub use util::*;
|
||||||
|
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
use std::ops::Range;
|
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
|
||||||
use toss::{Style, Styled};
|
|
||||||
|
|
||||||
use crate::euph::util;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum SpanType {
|
|
||||||
Mention,
|
|
||||||
Room,
|
|
||||||
Emoji,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nick_char(ch: char) -> bool {
|
|
||||||
// Closely following the heim mention regex:
|
|
||||||
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/stores/chat.js#L14-L15
|
|
||||||
// `>` has been experimentally confirmed to delimit mentions as well.
|
|
||||||
match ch {
|
|
||||||
',' | '.' | '!' | '?' | ';' | '&' | '<' | '>' | '\'' | '"' => false,
|
|
||||||
_ => !ch.is_whitespace(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn room_char(ch: char) -> bool {
|
|
||||||
// Basically just \w, see also
|
|
||||||
// https://github.com/euphoria-io/heim/blob/978c921063e6b06012fc8d16d9fbf1b3a0be1191/client/lib/ui/MessageText.js#L66
|
|
||||||
ch.is_ascii_alphanumeric() || ch == '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
struct SpanFinder<'a> {
|
|
||||||
content: &'a str,
|
|
||||||
|
|
||||||
span: Option<(SpanType, usize)>,
|
|
||||||
room_or_mention_possible: bool,
|
|
||||||
|
|
||||||
result: Vec<(SpanType, Range<usize>)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> SpanFinder<'a> {
|
|
||||||
fn is_valid_span(&self, span: SpanType, range: Range<usize>) -> bool {
|
|
||||||
let text = &self.content[range.start..range.end];
|
|
||||||
match span {
|
|
||||||
SpanType::Mention => range.len() > 1 && text.starts_with('@'),
|
|
||||||
SpanType::Room => range.len() > 1 && text.starts_with('&'),
|
|
||||||
SpanType::Emoji => {
|
|
||||||
if range.len() <= 2 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(name) = Some(text)
|
|
||||||
.and_then(|it| it.strip_prefix(':'))
|
|
||||||
.and_then(|it| it.strip_suffix(':'))
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
util::EMOJI.get(name).is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_span(&mut self, end: usize) {
|
|
||||||
let Some((span, start)) = self.span else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if self.is_valid_span(span, start..end) {
|
|
||||||
self.result.push((span, start..end));
|
|
||||||
}
|
|
||||||
self.span = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_span(&mut self, span: SpanType, start: usize) {
|
|
||||||
self.close_span(start);
|
|
||||||
self.span = Some((span, start))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn step(&mut self, idx: usize, char: char) {
|
|
||||||
match (char, self.span) {
|
|
||||||
('@', Some((SpanType::Mention, _))) => {} // Continue the mention
|
|
||||||
('@', _) if self.room_or_mention_possible => self.open_span(SpanType::Mention, idx),
|
|
||||||
('&', _) if self.room_or_mention_possible => self.open_span(SpanType::Room, idx),
|
|
||||||
(':', None) => self.open_span(SpanType::Emoji, idx),
|
|
||||||
(':', Some((SpanType::Emoji, _))) => self.close_span(idx + 1),
|
|
||||||
(c, Some((SpanType::Mention, _))) if !nick_char(c) => self.close_span(idx),
|
|
||||||
(c, Some((SpanType::Room, _))) if !room_char(c) => self.close_span(idx),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// More permissive than the heim web client
|
|
||||||
self.room_or_mention_possible = !char.is_alphanumeric();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find(content: &'a str) -> Vec<(SpanType, Range<usize>)> {
|
|
||||||
let mut this = Self {
|
|
||||||
content,
|
|
||||||
span: None,
|
|
||||||
room_or_mention_possible: true,
|
|
||||||
result: vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (idx, char) in content.char_indices() {
|
|
||||||
this.step(idx, char);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.close_span(content.len());
|
|
||||||
|
|
||||||
this.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_spans(content: &str) -> Vec<(SpanType, Range<usize>)> {
|
|
||||||
SpanFinder::find(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight spans in a string.
|
|
||||||
///
|
|
||||||
/// The list of spans must be non-overlapping and in ascending order.
|
|
||||||
///
|
|
||||||
/// If `exact` is specified, colon-delimited emoji are not replaced with their
|
|
||||||
/// unicode counterparts.
|
|
||||||
pub fn apply_spans(
|
|
||||||
content: &str,
|
|
||||||
spans: &[(SpanType, Range<usize>)],
|
|
||||||
base: Style,
|
|
||||||
exact: bool,
|
|
||||||
) -> Styled {
|
|
||||||
let mut result = Styled::default();
|
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
for (span, range) in spans {
|
|
||||||
assert!(i <= range.start);
|
|
||||||
assert!(range.end <= content.len());
|
|
||||||
|
|
||||||
if i < range.start {
|
|
||||||
result = result.then(&content[i..range.start], base);
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = &content[range.start..range.end];
|
|
||||||
result = match span {
|
|
||||||
SpanType::Mention if exact => result.and_then(util::style_mention_exact(text, base)),
|
|
||||||
SpanType::Mention => result.and_then(util::style_mention(text, base)),
|
|
||||||
SpanType::Room => result.then(text, base.blue().bold()),
|
|
||||||
SpanType::Emoji if exact => result.then(text, base.magenta()),
|
|
||||||
SpanType::Emoji => {
|
|
||||||
let name = text.strip_prefix(':').unwrap_or(text);
|
|
||||||
let name = name.strip_suffix(':').unwrap_or(name);
|
|
||||||
if let Some(Some(replacement)) = util::EMOJI.get(name) {
|
|
||||||
result.then(replacement, base)
|
|
||||||
} else {
|
|
||||||
result.then(text, base.magenta())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
i = range.end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if i < content.len() {
|
|
||||||
result = result.then(&content[i..], base);
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Highlight an euphoria message's content.
|
|
||||||
///
|
|
||||||
/// If `exact` is specified, colon-delimited emoji are not replaced with their
|
|
||||||
/// unicode counterparts.
|
|
||||||
pub fn highlight(content: &str, base: Style, exact: bool) -> Styled {
|
|
||||||
apply_spans(content, &find_spans(content), base, exact)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use crate::euph::SpanType;
|
|
||||||
|
|
||||||
use super::find_spans;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mentions() {
|
|
||||||
assert_eq!(find_spans("@foo"), vec![(SpanType::Mention, 0..4)]);
|
|
||||||
assert_eq!(find_spans("&@foo"), vec![(SpanType::Mention, 1..5)]);
|
|
||||||
assert_eq!(find_spans("a @foo b"), vec![(SpanType::Mention, 2..6)]);
|
|
||||||
assert_eq!(find_spans("@@foo@@"), vec![(SpanType::Mention, 0..7)]);
|
|
||||||
assert_eq!(find_spans("a @b@c d"), vec![(SpanType::Mention, 2..6)]);
|
|
||||||
assert_eq!(
|
|
||||||
find_spans("a @b @c d"),
|
|
||||||
vec![(SpanType::Mention, 2..4), (SpanType::Mention, 5..7)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rooms() {
|
|
||||||
assert_eq!(find_spans("&foo"), vec![(SpanType::Room, 0..4)]);
|
|
||||||
assert_eq!(find_spans("@&foo"), vec![(SpanType::Room, 1..5)]);
|
|
||||||
assert_eq!(find_spans("a &foo b"), vec![(SpanType::Room, 2..6)]);
|
|
||||||
assert_eq!(find_spans("&&foo&&"), vec![(SpanType::Room, 1..5)]);
|
|
||||||
assert_eq!(find_spans("a &b&c d"), vec![(SpanType::Room, 2..4)]);
|
|
||||||
assert_eq!(
|
|
||||||
find_spans("a &b &c d"),
|
|
||||||
vec![(SpanType::Room, 2..4), (SpanType::Room, 5..7)]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn emoji_in_mentions() {
|
|
||||||
assert_eq!(find_spans(" @a:b:c "), vec![(SpanType::Mention, 1..7)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,24 @@
|
||||||
use std::{convert::Infallible, time::Duration};
|
// TODO Stop if room does not exist (e.g. 404)
|
||||||
|
|
||||||
use euphoxide::{
|
use std::convert::Infallible;
|
||||||
api::{
|
use std::time::Duration;
|
||||||
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply,
|
|
||||||
Time, UserId, packet::ParsedPacket,
|
use euphoxide::api::packet::ParsedPacket;
|
||||||
},
|
use euphoxide::api::{
|
||||||
bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig},
|
Auth, AuthOption, Data, Log, Login, Logout, MessageId, Nick, Send, SendEvent, SendReply, Time,
|
||||||
conn::{self, ConnTx, Joined},
|
UserId,
|
||||||
};
|
};
|
||||||
use log::{debug, info, warn};
|
use euphoxide::bot::instance::{ConnSnapshot, Event, Instance, InstanceConfig};
|
||||||
use tokio::{select, sync::oneshot};
|
use euphoxide::conn::{self, ConnTx};
|
||||||
|
use log::{debug, error, info, warn};
|
||||||
|
use tokio::select;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use crate::{macros::logging_unwrap, vault::EuphRoomVault};
|
use crate::macros::logging_unwrap;
|
||||||
|
use crate::vault::EuphRoomVault;
|
||||||
|
|
||||||
const LOG_INTERVAL: Duration = Duration::from_secs(10);
|
const LOG_INTERVAL: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Disconnected,
|
Disconnected,
|
||||||
|
|
@ -32,13 +35,6 @@ impl State {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn joined(&self) -> Option<&Joined> {
|
|
||||||
match self {
|
|
||||||
Self::Connected(_, conn::State::Joined(joined)) => Some(joined),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -69,13 +65,19 @@ 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 ephemeral = vault.vault().vault().ephemeral() || vault.room() == "rl2dev";
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
ephemeral: vault.vault().vault().ephemeral(),
|
vault,
|
||||||
|
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,8 +125,7 @@ impl Room {
|
||||||
|
|
||||||
let cookies = &*self.instance.config().server.cookies;
|
let cookies = &*self.instance.config().server.cookies;
|
||||||
let cookies = cookies.lock().unwrap().clone();
|
let cookies = cookies.lock().unwrap().clone();
|
||||||
let domain = self.vault.room().domain.clone();
|
logging_unwrap!(self.vault.vault().set_cookies(cookies).await);
|
||||||
logging_unwrap!(self.vault.vault().set_cookies(domain, cookies).await);
|
|
||||||
}
|
}
|
||||||
Event::Packet(_, packet, ConnSnapshot { conn_tx, state }) => {
|
Event::Packet(_, packet, ConnSnapshot { conn_tx, state }) => {
|
||||||
self.state = State::Connected(conn_tx, state);
|
self.state = State::Connected(conn_tx, state);
|
||||||
|
|
@ -136,6 +137,7 @@ impl Room {
|
||||||
self.log_request_canary = None;
|
self.log_request_canary = None;
|
||||||
}
|
}
|
||||||
Event::Stopped(_) => {
|
Event::Stopped(_) => {
|
||||||
|
// TODO Remove room somewhere if this happens? If it doesn't already happen during stabilization
|
||||||
self.state = State::Stopped;
|
self.state = State::Stopped;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -181,9 +183,15 @@ impl Room {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("{:?}: requesting logs", vault.room());
|
debug!("{}: requesting logs", vault.room());
|
||||||
|
|
||||||
let _ = conn_tx.send(Log { n: 1000, before }).await;
|
// &rl2dev's message history is broken and requesting old messages past
|
||||||
|
// a certain point results in errors. By reducing the amount of messages
|
||||||
|
// in each log request, we can get closer to this point. Since &rl2dev
|
||||||
|
// is fairly low in activity, this should be fine.
|
||||||
|
let n = if vault.room() == "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.
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,212 @@
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use euphoxide::api::{MessageId, Snowflake, Time, UserId};
|
use euphoxide::api::{MessageId, Snowflake, Time};
|
||||||
use jiff::Timestamp;
|
use time::OffsetDateTime;
|
||||||
use toss::{Style, Styled};
|
use toss::{Style, Styled};
|
||||||
|
|
||||||
use crate::{store::Msg, ui::ChatMsg};
|
use crate::store::Msg;
|
||||||
|
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,
|
||||||
|
|
@ -28,22 +222,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(super::style_nick(nick, Style::new()))
|
.and_then(util::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(super::style_nick(nick, style))
|
Styled::new("*", style).and_then(util::style_nick(nick, style))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled_content(content: &str) -> Styled {
|
fn styled_content(content: &str) -> Styled {
|
||||||
super::highlight(content.trim(), Style::new(), false)
|
highlight_content(content.trim(), Style::new(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled_content_me(content: &str) -> Styled {
|
fn styled_content_me(content: &str) -> Styled {
|
||||||
let style = style_me();
|
let style = style_me();
|
||||||
super::highlight(content.trim(), style, false).then("*", style)
|
highlight_content(content.trim(), style, false).then("*", style)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled_editor_content(content: &str) -> Styled {
|
fn styled_editor_content(content: &str) -> Styled {
|
||||||
|
|
@ -52,7 +246,7 @@ fn styled_editor_content(content: &str) -> Styled {
|
||||||
} else {
|
} else {
|
||||||
Style::new()
|
Style::new()
|
||||||
};
|
};
|
||||||
super::highlight(content, style, true)
|
highlight_content(content, style, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Msg for SmallMessage {
|
impl Msg for SmallMessage {
|
||||||
|
|
@ -73,15 +267,11 @@ 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 {
|
||||||
fn time(&self) -> Option<Timestamp> {
|
fn time(&self) -> OffsetDateTime {
|
||||||
Some(self.time.as_timestamp())
|
self.time.0
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled(&self) -> (Styled, Styled) {
|
fn styled(&self) -> (Styled, Styled) {
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,9 @@
|
||||||
use std::{
|
|
||||||
collections::HashSet,
|
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
|
||||||
sync::LazyLock,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crossterm::style::{Color, Stylize};
|
use crossterm::style::{Color, Stylize};
|
||||||
use euphoxide::{Emoji, api::UserId};
|
use euphoxide::Emoji;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use toss::{Style, Styled};
|
use toss::{Style, Styled};
|
||||||
|
|
||||||
pub static EMOJI: LazyLock<Emoji> = LazyLock::new(Emoji::load);
|
pub static EMOJI: Lazy<Emoji> = Lazy::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].
|
||||||
///
|
///
|
||||||
|
|
@ -72,25 +54,3 @@ 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()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,21 @@
|
||||||
//! 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};
|
||||||
|
|
||||||
#[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.
|
||||||
Text,
|
Text,
|
||||||
/// Array of message objects in the same format as the euphoria API uses.
|
/// Array of message objects in the same format as the euphoria API uses.
|
||||||
Json,
|
Json,
|
||||||
/// Message objects in the same format as the euphoria API uses, one per
|
/// Message objects in the same format as the euphoria API uses, one per line.
|
||||||
/// line (https://jsonlines.org/).
|
JsonStream,
|
||||||
JsonLines,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Format {
|
impl Format {
|
||||||
|
|
@ -26,15 +23,14 @@ impl Format {
|
||||||
match self {
|
match self {
|
||||||
Self::Text => "text",
|
Self::Text => "text",
|
||||||
Self::Json => "json",
|
Self::Json => "json",
|
||||||
Self::JsonLines => "json lines",
|
Self::JsonStream => "json stream",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extension(&self) -> &'static str {
|
fn extension(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Text => "txt",
|
Self::Text => "txt",
|
||||||
Self::Json => "json",
|
Self::Json | Self::JsonStream => "json",
|
||||||
Self::JsonLines => "jsonl",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,10 +43,6 @@ pub struct Args {
|
||||||
#[arg(long, short)]
|
#[arg(long, short)]
|
||||||
all: bool,
|
all: bool,
|
||||||
|
|
||||||
/// Domain to resolve the room names with.
|
|
||||||
#[arg(long, short, default_value = "euphoria.leet.nu")]
|
|
||||||
domain: String,
|
|
||||||
|
|
||||||
/// Format of the output file.
|
/// Format of the output file.
|
||||||
#[arg(long, short, value_enum, default_value_t = Format::Text)]
|
#[arg(long, short, value_enum, default_value_t = Format::Text)]
|
||||||
format: Format,
|
format: Format,
|
||||||
|
|
@ -82,7 +74,7 @@ async fn export_room<W: Write>(
|
||||||
match format {
|
match format {
|
||||||
Format::Text => text::export(vault, out).await?,
|
Format::Text => text::export(vault, out).await?,
|
||||||
Format::Json => json::export(vault, out).await?,
|
Format::Json => json::export(vault, out).await?,
|
||||||
Format::JsonLines => json::export_lines(vault, out).await?,
|
Format::JsonStream => json::export_stream(vault, out).await?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -93,12 +85,7 @@ pub async fn export(vault: &EuphVault, mut args: Args) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let rooms = if args.all {
|
let rooms = if args.all {
|
||||||
let mut rooms = vault
|
let mut rooms = vault.rooms().await?;
|
||||||
.rooms()
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| id.name)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
rooms.sort_unstable();
|
rooms.sort_unstable();
|
||||||
rooms
|
rooms
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -114,14 +101,14 @@ pub async fn export(vault: &EuphVault, mut args: Args) -> anyhow::Result<()> {
|
||||||
for room in rooms {
|
for room in rooms {
|
||||||
if args.out == "-" {
|
if args.out == "-" {
|
||||||
eprintln!("Exporting &{room} as {} to stdout", args.format.name());
|
eprintln!("Exporting &{room} as {} to stdout", args.format.name());
|
||||||
let vault = vault.room(RoomIdentifier::new(args.domain.clone(), room));
|
let vault = vault.room(room);
|
||||||
let mut stdout = BufWriter::new(io::stdout());
|
let mut stdout = BufWriter::new(io::stdout());
|
||||||
export_room(&vault, &mut stdout, args.format).await?;
|
export_room(&vault, &mut stdout, args.format).await?;
|
||||||
stdout.flush()?;
|
stdout.flush()?;
|
||||||
} else {
|
} else {
|
||||||
let out = format_out(&args.out, &room, args.format);
|
let out = format_out(&args.out, &room, args.format);
|
||||||
eprintln!("Exporting &{room} as {} to {out}", args.format.name());
|
eprintln!("Exporting &{room} as {} to {out}", args.format.name());
|
||||||
let vault = vault.room(RoomIdentifier::new(args.domain.clone(), room));
|
let vault = vault.room(room);
|
||||||
let mut file = BufWriter::new(File::create(out)?);
|
let mut file = BufWriter::new(File::create(out)?);
|
||||||
export_room(&vault, &mut file, args.format).await?;
|
export_room(&vault, &mut file, args.format).await?;
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ pub async fn export<W: Write>(vault: &EuphRoomVault, file: &mut W) -> anyhow::Re
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn export_lines<W: Write>(vault: &EuphRoomVault, file: &mut W) -> anyhow::Result<()> {
|
pub async fn export_stream<W: Write>(vault: &EuphRoomVault, file: &mut W) -> anyhow::Result<()> {
|
||||||
let mut total = 0;
|
let mut total = 0;
|
||||||
let mut last_msg_id = None;
|
let mut last_msg_id = None;
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use euphoxide::api::MessageId;
|
use euphoxide::api::MessageId;
|
||||||
|
use time::format_description::FormatItem;
|
||||||
|
use time::macros::format_description;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::{euph::SmallMessage, store::Tree, vault::EuphRoomVault};
|
use crate::euph::SmallMessage;
|
||||||
|
use crate::store::Tree;
|
||||||
|
use crate::vault::EuphRoomVault;
|
||||||
|
|
||||||
const TIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
|
const TIME_FORMAT: &[FormatItem<'_>] =
|
||||||
|
format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||||
const TIME_EMPTY: &str = " ";
|
const TIME_EMPTY: &str = " ";
|
||||||
|
|
||||||
pub async fn export<W: Write>(vault: &EuphRoomVault, out: &mut W) -> anyhow::Result<()> {
|
pub async fn export<W: Write>(vault: &EuphRoomVault, out: &mut W) -> anyhow::Result<()> {
|
||||||
|
|
@ -62,7 +67,11 @@ fn write_msg<W: Write>(
|
||||||
|
|
||||||
for (i, line) in msg.content.lines().enumerate() {
|
for (i, line) in msg.content.lines().enumerate() {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
let time = msg.time.as_timestamp().strftime(TIME_FORMAT);
|
let time = msg
|
||||||
|
.time
|
||||||
|
.0
|
||||||
|
.format(TIME_FORMAT)
|
||||||
|
.expect("time can be formatted");
|
||||||
writeln!(file, "{time} {indent_string}[{nick}] {line}")?;
|
writeln!(file, "{time} {indent_string}[{nick}] {line}")?;
|
||||||
} else {
|
} else {
|
||||||
writeln!(file, "{TIME_EMPTY} {indent_string}| {nick_empty} {line}")?;
|
writeln!(file, "{TIME_EMPTY} {indent_string}| {nick_empty} {line}")?;
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
use std::{convert::Infallible, sync::Arc, vec};
|
use std::convert::Infallible;
|
||||||
|
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;
|
||||||
use jiff::Timestamp;
|
|
||||||
use log::{Level, LevelFilter, Log};
|
use log::{Level, LevelFilter, Log};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use time::OffsetDateTime;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use toss::{Style, Styled};
|
use toss::{Style, Styled};
|
||||||
|
|
||||||
use crate::{
|
use crate::store::{Msg, MsgStore, Path, Tree};
|
||||||
store::{Msg, MsgStore, Path, Tree},
|
use crate::ui::ChatMsg;
|
||||||
ui::ChatMsg,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LogMsg {
|
pub struct LogMsg {
|
||||||
id: usize,
|
id: usize,
|
||||||
time: Timestamp,
|
time: OffsetDateTime,
|
||||||
level: Level,
|
level: Level,
|
||||||
content: String,
|
content: String,
|
||||||
}
|
}
|
||||||
|
|
@ -42,8 +42,8 @@ impl Msg for LogMsg {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatMsg for LogMsg {
|
impl ChatMsg for LogMsg {
|
||||||
fn time(&self) -> Option<Timestamp> {
|
fn time(&self) -> OffsetDateTime {
|
||||||
Some(self.time)
|
self.time
|
||||||
}
|
}
|
||||||
|
|
||||||
fn styled(&self) -> (Styled, Styled) {
|
fn styled(&self) -> (Styled, Styled) {
|
||||||
|
|
@ -209,7 +209,7 @@ impl Log for Logger {
|
||||||
let mut guard = self.messages.lock();
|
let mut guard = self.messages.lock();
|
||||||
let msg = LogMsg {
|
let msg = LogMsg {
|
||||||
id: guard.len(),
|
id: guard.len(),
|
||||||
time: Timestamp::now(),
|
time: OffsetDateTime::now_utc(),
|
||||||
level: record.level(),
|
level: record.level(),
|
||||||
content: format!("<{}> {}", record.target(), record.args()),
|
content: format!("<{}> {}", record.target(), record.args()),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
// TODO Get rid of this macro as much as possible
|
||||||
macro_rules! logging_unwrap {
|
macro_rules! logging_unwrap {
|
||||||
($e:expr) => {
|
($e:expr) => {
|
||||||
match $e {
|
match $e {
|
||||||
|
|
|
||||||
110
cove/src/main.rs
110
cove/src/main.rs
|
|
@ -1,23 +1,19 @@
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
// Rustc lint groups
|
||||||
|
#![warn(future_incompatible)]
|
||||||
|
#![warn(rust_2018_idioms)]
|
||||||
|
#![warn(unused)]
|
||||||
|
// Rustc lints
|
||||||
|
#![warn(noop_method_call)]
|
||||||
|
#![warn(single_use_lifetimes)]
|
||||||
|
// Clippy lints
|
||||||
|
#![warn(clippy::use_self)]
|
||||||
|
|
||||||
|
// TODO Enable warn(unreachable_pub)?
|
||||||
// TODO Remove unnecessary Debug impls and compare compile times
|
// TODO Remove unnecessary Debug impls and compare compile times
|
||||||
|
// TODO Time zones other than UTC
|
||||||
// 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;
|
||||||
|
|
@ -26,7 +22,21 @@ mod store;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod util;
|
mod util;
|
||||||
mod vault;
|
mod vault;
|
||||||
mod version;
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use cookie::CookieJar;
|
||||||
|
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;
|
||||||
|
|
||||||
#[derive(Debug, clap::Parser)]
|
#[derive(Debug, clap::Parser)]
|
||||||
enum Command {
|
enum Command {
|
||||||
|
|
@ -37,21 +47,11 @@ enum Command {
|
||||||
/// Compact and clean up vault.
|
/// Compact and clean up vault.
|
||||||
Gc,
|
Gc,
|
||||||
/// Clear euphoria session cookies.
|
/// Clear euphoria session cookies.
|
||||||
ClearCookies {
|
ClearCookies,
|
||||||
/// Clear cookies for a specific domain only.
|
|
||||||
#[arg(long, short)]
|
|
||||||
domain: Option<String>,
|
|
||||||
},
|
|
||||||
/// Print config documentation as markdown.
|
/// Print config documentation as markdown.
|
||||||
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
|
||||||
|
|
@ -85,11 +85,6 @@ 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)]
|
||||||
|
|
@ -125,26 +120,18 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_vault(config: &Config, dirs: &ProjectDirs) -> anyhow::Result<Vault> {
|
fn open_vault(config: &Config, dirs: &ProjectDirs) -> rusqlite::Result<Vault> {
|
||||||
let vault = if config.ephemeral {
|
if config.ephemeral {
|
||||||
vault::launch_in_memory()?
|
vault::launch_in_memory()
|
||||||
} else {
|
} else {
|
||||||
let data_dir = data_dir(config, dirs);
|
let data_dir = data_dir(config, dirs);
|
||||||
eprintln!("Data dir: {}", data_dir.to_string_lossy());
|
eprintln!("Data dir: {}", data_dir.to_string_lossy());
|
||||||
vault::launch(&data_dir.join("vault.db"))?
|
vault::launch(&data_dir.join("vault.db"))
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(vault)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -154,11 +141,6 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let (logger, logger_guard, logger_rx) = Logger::init(args.verbose);
|
let (logger, logger_guard, logger_rx) = Logger::init(args.verbose);
|
||||||
let dirs = ProjectDirs::from("de", "plugh", "cove").expect("failed to find config directory");
|
let dirs = ProjectDirs::from("de", "plugh", "cove").expect("failed to find config directory");
|
||||||
|
|
||||||
// https://github.com/snapview/tokio-tungstenite/issues/353#issuecomment-2455247837
|
|
||||||
rustls::crypto::aws_lc_rs::default_provider()
|
|
||||||
.install_default()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Locate config
|
// Locate config
|
||||||
let config_path = config_path(&args, &dirs);
|
let config_path = config_path(&args, &dirs);
|
||||||
eprintln!("Config file: {}", config_path.to_string_lossy());
|
eprintln!("Config file: {}", config_path.to_string_lossy());
|
||||||
|
|
@ -172,7 +154,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
Command::Run => run(logger, logger_rx, config, &dirs).await?,
|
Command::Run => run(logger, logger_rx, config, &dirs).await?,
|
||||||
Command::Export(args) => export(config, &dirs, args).await?,
|
Command::Export(args) => export(config, &dirs, args).await?,
|
||||||
Command::Gc => gc(config, &dirs).await?,
|
Command::Gc => gc(config, &dirs).await?,
|
||||||
Command::ClearCookies { domain } => clear_cookies(config, &dirs, domain).await?,
|
Command::ClearCookies => clear_cookies(config, &dirs).await?,
|
||||||
Command::HelpConfig => help_config(),
|
Command::HelpConfig => help_config(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,19 +173,17 @@ async fn run(
|
||||||
config: &'static Config,
|
config: &'static Config,
|
||||||
dirs: &ProjectDirs,
|
dirs: &ProjectDirs,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
info!("Welcome to {NAME} {VERSION}",);
|
info!(
|
||||||
|
"Welcome to {} {}",
|
||||||
let tz = util::load_time_zone(config.time_zone_ref()).context("failed to load time zone")?;
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
);
|
||||||
|
|
||||||
let vault = open_vault(config, dirs)?;
|
let vault = open_vault(config, dirs)?;
|
||||||
|
|
||||||
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 {
|
Ui::run(config, &mut terminal, vault.clone(), logger, logger_rx).await?;
|
||||||
cove_config::WidthEstimationMethod::Legacy => toss::WidthEstimationMethod::Legacy,
|
|
||||||
cove_config::WidthEstimationMethod::Unicode => toss::WidthEstimationMethod::Unicode,
|
|
||||||
});
|
|
||||||
Ui::run(config, tz, &mut terminal, vault.clone(), logger, logger_rx).await?;
|
|
||||||
drop(terminal);
|
drop(terminal);
|
||||||
|
|
||||||
vault.close().await;
|
vault.close().await;
|
||||||
|
|
@ -234,15 +214,11 @@ async fn gc(config: &'static Config, dirs: &ProjectDirs) -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_cookies(
|
async fn clear_cookies(config: &'static Config, dirs: &ProjectDirs) -> anyhow::Result<()> {
|
||||||
config: &'static Config,
|
|
||||||
dirs: &ProjectDirs,
|
|
||||||
domain: Option<String>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let vault = open_vault(config, dirs)?;
|
let vault = open_vault(config, dirs)?;
|
||||||
|
|
||||||
eprintln!("Clearing cookies");
|
eprintln!("Clearing cookies");
|
||||||
vault.euph().clear_cookies(domain).await?;
|
vault.euph().set_cookies(CookieJar::new()).await?;
|
||||||
|
|
||||||
vault.close().await;
|
vault.close().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
use std::{collections::HashMap, fmt::Debug, hash::Hash, vec};
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::hash::Hash;
|
||||||
|
use std::vec;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
|
@ -8,10 +11,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -28,6 +27,10 @@ impl<I> Path<I> {
|
||||||
self.0.iter().take(self.0.len() - 1)
|
self.0.iter().take(self.0.len() - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, segment: I) {
|
||||||
|
self.0.push(segment)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn first(&self) -> &I {
|
pub fn first(&self) -> &I {
|
||||||
self.0.first().expect("path is empty")
|
self.0.first().expect("path is empty")
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +134,6 @@ impl<M: Msg> Tree<M> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait MsgStore<M: Msg> {
|
pub trait MsgStore<M: Msg> {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,3 @@
|
||||||
use std::{
|
|
||||||
convert::Infallible,
|
|
||||||
io,
|
|
||||||
sync::{Arc, Weak},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use cove_config::Config;
|
|
||||||
use cove_input::InputEvent;
|
|
||||||
use jiff::tz::TimeZone;
|
|
||||||
use parking_lot::FairMutex;
|
|
||||||
use tokio::{
|
|
||||||
sync::mpsc::{self, UnboundedReceiver, UnboundedSender, error::TryRecvError},
|
|
||||||
task,
|
|
||||||
};
|
|
||||||
use toss::{Terminal, WidgetExt, widgets::BoxedAsync};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
logger::{LogMsg, Logger},
|
|
||||||
macros::logging_unwrap,
|
|
||||||
util::InfallibleExt,
|
|
||||||
vault::Vault,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use self::chat::ChatMsg;
|
|
||||||
use self::{chat::ChatState, rooms::Rooms, widgets::ListState};
|
|
||||||
|
|
||||||
mod chat;
|
mod chat;
|
||||||
mod euph;
|
mod euph;
|
||||||
mod key_bindings;
|
mod key_bindings;
|
||||||
|
|
@ -32,6 +5,30 @@ 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 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
|
||||||
|
|
||||||
|
|
@ -50,7 +47,6 @@ impl From<Infallible> for UiError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::large_enum_variant)]
|
|
||||||
pub enum UiEvent {
|
pub enum UiEvent {
|
||||||
GraphemeWidthsChanged,
|
GraphemeWidthsChanged,
|
||||||
LogChanged,
|
LogChanged,
|
||||||
|
|
@ -88,7 +84,6 @@ impl Ui {
|
||||||
|
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
config: &'static Config,
|
config: &'static Config,
|
||||||
tz: TimeZone,
|
|
||||||
terminal: &mut Terminal,
|
terminal: &mut Terminal,
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
|
@ -117,8 +112,8 @@ impl Ui {
|
||||||
config,
|
config,
|
||||||
event_tx: event_tx.clone(),
|
event_tx: event_tx.clone(),
|
||||||
mode: Mode::Main,
|
mode: Mode::Main,
|
||||||
rooms: Rooms::new(config, tz.clone(), vault, event_tx.clone()).await,
|
rooms: Rooms::new(config, vault, event_tx.clone()).await,
|
||||||
log_chat: ChatState::new(logger, tz),
|
log_chat: ChatState::new(logger),
|
||||||
key_bindings_visible: false,
|
key_bindings_visible: false,
|
||||||
key_bindings_list: ListState::new(),
|
key_bindings_list: ListState::new(),
|
||||||
};
|
};
|
||||||
|
|
@ -186,8 +181,9 @@ impl Ui {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle events (in batches)
|
// Handle events (in batches)
|
||||||
let Some(mut event) = event_rx.recv().await else {
|
let mut event = match event_rx.recv().await {
|
||||||
return Ok(());
|
Some(event) => event,
|
||||||
|
None => return Ok(()),
|
||||||
};
|
};
|
||||||
let end_time = Instant::now() + EVENT_PROCESSING_TIME;
|
let end_time = Instant::now() + EVENT_PROCESSING_TIME;
|
||||||
loop {
|
loop {
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,24 @@
|
||||||
use cove_config::Keys;
|
|
||||||
use cove_input::InputEvent;
|
|
||||||
use jiff::{Timestamp, tz::TimeZone};
|
|
||||||
use toss::{
|
|
||||||
Styled, WidgetExt,
|
|
||||||
widgets::{BoxedAsync, EditorState},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
store::{Msg, MsgStore},
|
|
||||||
util,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::UiError;
|
|
||||||
|
|
||||||
use self::{cursor::Cursor, tree::TreeViewState};
|
|
||||||
|
|
||||||
mod blocks;
|
mod 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 time::OffsetDateTime;
|
||||||
|
use toss::widgets::{BoxedAsync, EditorState};
|
||||||
|
use toss::{Styled, WidgetExt};
|
||||||
|
|
||||||
|
use crate::store::{Msg, MsgStore};
|
||||||
|
|
||||||
|
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) -> OffsetDateTime;
|
||||||
fn styled(&self) -> (Styled, Styled);
|
fn styled(&self) -> (Styled, Styled);
|
||||||
fn edit(nick: &str, content: &str) -> (Styled, Styled);
|
fn edit(nick: &str, content: &str) -> (Styled, Styled);
|
||||||
fn pseudo(nick: &str, content: &str) -> (Styled, Styled);
|
fn pseudo(nick: &str, content: &str) -> (Styled, Styled);
|
||||||
|
|
@ -37,31 +33,23 @@ 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,
|
|
||||||
|
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
tree: TreeViewState<M, S>,
|
tree: TreeViewState<M, S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
|
impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
|
||||||
pub fn new(store: S, tz: TimeZone) -> Self {
|
pub fn new(store: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cursor: Cursor::Bottom,
|
cursor: Cursor::Bottom,
|
||||||
editor: EditorState::new(),
|
editor: EditorState::new(),
|
||||||
nick_emoji: false,
|
|
||||||
caesar: 0,
|
|
||||||
|
|
||||||
mode: Mode::Tree,
|
mode: Mode::Tree,
|
||||||
tree: TreeViewState::new(store.clone(), tz),
|
tree: TreeViewState::new(store.clone()),
|
||||||
|
|
||||||
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> {
|
||||||
|
|
@ -80,14 +68,7 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::Tree => self
|
Mode::Tree => self
|
||||||
.tree
|
.tree
|
||||||
.widget(
|
.widget(&mut self.cursor, &mut self.editor, nick, focused)
|
||||||
&mut self.cursor,
|
|
||||||
&mut self.editor,
|
|
||||||
nick,
|
|
||||||
focused,
|
|
||||||
self.nick_emoji,
|
|
||||||
self.caesar,
|
|
||||||
)
|
|
||||||
.boxed_async(),
|
.boxed_async(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,7 +85,7 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
S::Error: Send,
|
S::Error: Send,
|
||||||
{
|
{
|
||||||
let reaction = match self.mode {
|
match self.mode {
|
||||||
Mode::Tree => {
|
Mode::Tree => {
|
||||||
self.tree
|
self.tree
|
||||||
.handle_input_event(
|
.handle_input_event(
|
||||||
|
|
@ -114,33 +95,9 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||||
&mut self.editor,
|
&mut self.editor,
|
||||||
can_compose,
|
can_compose,
|
||||||
)
|
)
|
||||||
.await?
|
.await
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Ok(match reaction {
|
|
||||||
Reaction::Composed { parent, content } if self.caesar != 0 => {
|
|
||||||
let content = util::caesar(&content, self.caesar);
|
|
||||||
Reaction::Composed { parent, content }
|
|
||||||
}
|
|
||||||
|
|
||||||
Reaction::NotHandled if event.matches(&keys.tree.action.toggle_nick_emoji) => {
|
|
||||||
self.nick_emoji = !self.nick_emoji;
|
|
||||||
Reaction::Handled
|
|
||||||
}
|
|
||||||
|
|
||||||
Reaction::NotHandled if event.matches(&keys.tree.action.increase_caesar) => {
|
|
||||||
self.caesar = (self.caesar + 1).rem_euclid(26);
|
|
||||||
Reaction::Handled
|
|
||||||
}
|
|
||||||
|
|
||||||
Reaction::NotHandled if event.matches(&keys.tree.action.decrease_caesar) => {
|
|
||||||
self.caesar = (self.caesar - 1).rem_euclid(26);
|
|
||||||
Reaction::Handled
|
|
||||||
}
|
|
||||||
|
|
||||||
reaction => reaction,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> Option<&M::Id> {
|
pub fn cursor(&self) -> Option<&M::Id> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Common rendering logic.
|
//! Common rendering logic.
|
||||||
|
|
||||||
use std::collections::{VecDeque, vec_deque};
|
use std::collections::{vec_deque, VecDeque};
|
||||||
|
|
||||||
use toss::widgets::Predrawn;
|
use toss::widgets::Predrawn;
|
||||||
|
|
||||||
|
|
@ -161,6 +161,14 @@ impl<Id> Blocks<Id> {
|
||||||
pub fn shift(&mut self, delta: i32) {
|
pub fn shift(&mut self, delta: i32) {
|
||||||
self.range = self.range.shifted(delta);
|
self.range = self.range.shifted(delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_top(&mut self, top: i32) {
|
||||||
|
self.shift(top - self.range.top);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_bottom(&mut self, bottom: i32) {
|
||||||
|
self.shift(bottom - self.range.bottom);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Iter<'a, Id> {
|
pub struct Iter<'a, Id> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
//! Common cursor movement logic.
|
//! Common cursor movement logic.
|
||||||
|
|
||||||
use std::{collections::HashSet, hash::Hash};
|
use std::collections::HashSet;
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
use crate::store::{Msg, MsgStore, Tree};
|
use crate::store::{Msg, MsgStore, Tree};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ pub trait Renderer<Id> {
|
||||||
|
|
||||||
fn blocks(&self) -> &Blocks<Id>;
|
fn blocks(&self) -> &Blocks<Id>;
|
||||||
fn blocks_mut(&mut self) -> &mut Blocks<Id>;
|
fn blocks_mut(&mut self) -> &mut Blocks<Id>;
|
||||||
|
fn into_blocks(self) -> Blocks<Id>;
|
||||||
|
|
||||||
async fn expand_top(&mut self) -> Result<(), Self::Error>;
|
async fn expand_top(&mut self) -> Result<(), Self::Error>;
|
||||||
async fn expand_bottom(&mut self) -> Result<(), Self::Error>;
|
async fn expand_bottom(&mut self) -> Result<(), Self::Error>;
|
||||||
|
|
@ -274,6 +275,27 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clamp_scroll_biased_upwards<Id, R>(r: &mut R)
|
||||||
|
where
|
||||||
|
R: Renderer<Id>,
|
||||||
|
{
|
||||||
|
let area = visible_area(r);
|
||||||
|
let blocks = r.blocks().range();
|
||||||
|
|
||||||
|
// Delta that moves blocks.top to the top of the screen. If this is
|
||||||
|
// negative, we need to move the blocks because they're too low.
|
||||||
|
let move_to_top = blocks.top - area.top;
|
||||||
|
|
||||||
|
// Delta that moves blocks.bottom to the bottom of the screen. If this is
|
||||||
|
// positive, we need to move the blocks because they're too high.
|
||||||
|
let move_to_bottom = blocks.bottom - area.bottom;
|
||||||
|
|
||||||
|
// If the screen is higher, the blocks should rather be moved to the top
|
||||||
|
// than the bottom because of the upwards bias.
|
||||||
|
let delta = 0.max(move_to_bottom).min(move_to_top);
|
||||||
|
r.blocks_mut().shift(delta);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clamp_scroll_biased_downwards<Id, R>(r: &mut R)
|
pub fn clamp_scroll_biased_downwards<Id, R>(r: &mut R)
|
||||||
where
|
where
|
||||||
R: Renderer<Id>,
|
R: Renderer<Id>,
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,29 @@
|
||||||
|
|
||||||
// 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 toss::widgets::EditorState;
|
||||||
use toss::{AsyncWidget, Frame, Pos, Size, WidgetExt, WidthDb, widgets::EditorState};
|
use toss::{AsyncWidget, Frame, Pos, Size, WidgetExt, WidthDb};
|
||||||
|
|
||||||
use crate::{
|
use crate::store::{Msg, MsgStore};
|
||||||
store::{Msg, MsgStore},
|
use crate::ui::{util, ChatMsg, UiError};
|
||||||
ui::{UiError, util},
|
use crate::util::InfallibleExt;
|
||||||
util::InfallibleExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{ChatMsg, Reaction, cursor::Cursor};
|
|
||||||
|
|
||||||
use self::renderer::{TreeContext, TreeRenderer};
|
use self::renderer::{TreeContext, TreeRenderer};
|
||||||
|
|
||||||
mod renderer;
|
use super::cursor::Cursor;
|
||||||
mod scroll;
|
use super::Reaction;
|
||||||
mod widgets;
|
|
||||||
|
|
||||||
pub struct TreeViewState<M: Msg, S: MsgStore<M>> {
|
pub struct TreeViewState<M: Msg, S: MsgStore<M>> {
|
||||||
store: S,
|
store: S,
|
||||||
tz: TimeZone,
|
|
||||||
|
|
||||||
last_size: Size,
|
last_size: Size,
|
||||||
last_nick: String,
|
last_nick: String,
|
||||||
|
|
@ -38,10 +36,9 @@ pub struct TreeViewState<M: Msg, S: MsgStore<M>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
pub fn new(store: S, tz: TimeZone) -> Self {
|
pub fn new(store: S) -> Self {
|
||||||
Self {
|
Self {
|
||||||
store,
|
store,
|
||||||
tz,
|
|
||||||
last_size: Size::ZERO,
|
last_size: Size::ZERO,
|
||||||
last_nick: String::new(),
|
last_nick: String::new(),
|
||||||
last_cursor: Cursor::Bottom,
|
last_cursor: Cursor::Bottom,
|
||||||
|
|
@ -389,8 +386,6 @@ 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,
|
|
||||||
) -> TreeView<'a, M, S> {
|
) -> TreeView<'a, M, S> {
|
||||||
TreeView {
|
TreeView {
|
||||||
state: self,
|
state: self,
|
||||||
|
|
@ -398,8 +393,6 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
||||||
editor,
|
editor,
|
||||||
nick,
|
nick,
|
||||||
focused,
|
focused,
|
||||||
nick_emoji,
|
|
||||||
caesar,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -412,9 +405,6 @@ pub struct TreeView<'a, M: Msg, S: MsgStore<M>> {
|
||||||
|
|
||||||
nick: String,
|
nick: String,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
|
|
||||||
nick_emoji: bool,
|
|
||||||
caesar: i8,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|
@ -442,8 +432,6 @@ 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,
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
@ -451,7 +439,6 @@ where
|
||||||
let mut renderer = TreeRenderer::new(
|
let mut renderer = TreeRenderer::new(
|
||||||
context,
|
context,
|
||||||
&self.state.store,
|
&self.state.store,
|
||||||
&self.state.tz,
|
|
||||||
&mut self.state.folded,
|
&mut self.state.folded,
|
||||||
self.cursor,
|
self.cursor,
|
||||||
self.editor,
|
self.editor,
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
//! A [`Renderer`] for message trees.
|
//! A [`Renderer`] for message trees.
|
||||||
|
|
||||||
use std::{collections::HashSet, convert::Infallible};
|
use std::collections::HashSet;
|
||||||
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
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::{
|
use crate::store::{Msg, MsgStore, Tree};
|
||||||
store::{Msg, MsgStore, Tree},
|
use crate::ui::chat::blocks::{Block, Blocks, Range};
|
||||||
ui::{
|
use crate::ui::chat::cursor::Cursor;
|
||||||
ChatMsg,
|
use crate::ui::chat::renderer::{self, overlaps, Renderer};
|
||||||
chat::{
|
use crate::ui::ChatMsg;
|
||||||
blocks::{Block, Blocks, Range},
|
use crate::util::InfallibleExt;
|
||||||
cursor::Cursor,
|
|
||||||
renderer::{self, Renderer, overlaps},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
util::InfallibleExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::widgets;
|
use super::widgets;
|
||||||
|
|
||||||
|
|
@ -80,8 +72,6 @@ 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 last_cursor: Cursor<Id>,
|
pub last_cursor: Cursor<Id>,
|
||||||
pub last_cursor_top: i32,
|
pub last_cursor_top: i32,
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +80,6 @@ pub struct TreeRenderer<'a, M: Msg, S: MsgStore<M>> {
|
||||||
context: TreeContext<M::Id>,
|
context: TreeContext<M::Id>,
|
||||||
|
|
||||||
store: &'a S,
|
store: &'a S,
|
||||||
tz: &'a TimeZone,
|
|
||||||
folded: &'a mut HashSet<M::Id>,
|
folded: &'a mut HashSet<M::Id>,
|
||||||
cursor: &'a mut Cursor<M::Id>,
|
cursor: &'a mut Cursor<M::Id>,
|
||||||
editor: &'a mut EditorState,
|
editor: &'a mut EditorState,
|
||||||
|
|
@ -118,7 +107,6 @@ where
|
||||||
pub fn new(
|
pub fn new(
|
||||||
context: TreeContext<M::Id>,
|
context: TreeContext<M::Id>,
|
||||||
store: &'a S,
|
store: &'a S,
|
||||||
tz: &'a TimeZone,
|
|
||||||
folded: &'a mut HashSet<M::Id>,
|
folded: &'a mut HashSet<M::Id>,
|
||||||
cursor: &'a mut Cursor<M::Id>,
|
cursor: &'a mut Cursor<M::Id>,
|
||||||
editor: &'a mut EditorState,
|
editor: &'a mut EditorState,
|
||||||
|
|
@ -127,7 +115,6 @@ where
|
||||||
Self {
|
Self {
|
||||||
context,
|
context,
|
||||||
store,
|
store,
|
||||||
tz,
|
|
||||||
folded,
|
folded,
|
||||||
cursor,
|
cursor,
|
||||||
editor,
|
editor,
|
||||||
|
|
@ -161,12 +148,8 @@ where
|
||||||
None => TreeBlockId::Bottom,
|
None => TreeBlockId::Bottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
let widget = widgets::editor::<M>(
|
// TODO Unhighlighted version when focusing on nick list
|
||||||
indent,
|
let widget = widgets::editor::<M>(indent, &self.context.nick, self.editor);
|
||||||
&self.context.nick,
|
|
||||||
self.context.focused,
|
|
||||||
self.editor,
|
|
||||||
);
|
|
||||||
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
||||||
let mut block = Block::new(id, widget, false);
|
let mut block = Block::new(id, widget, false);
|
||||||
|
|
||||||
|
|
@ -184,6 +167,7 @@ where
|
||||||
None => TreeBlockId::Bottom,
|
None => TreeBlockId::Bottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO Unhighlighted version when focusing on nick list
|
||||||
let widget = widgets::pseudo::<M>(indent, &self.context.nick, self.editor);
|
let widget = widgets::pseudo::<M>(indent, &self.context.nick, self.editor);
|
||||||
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
||||||
Block::new(id, widget, false)
|
Block::new(id, widget, false)
|
||||||
|
|
@ -203,15 +187,7 @@ where
|
||||||
};
|
};
|
||||||
let highlighted = highlighted && self.context.focused;
|
let highlighted = highlighted && self.context.focused;
|
||||||
|
|
||||||
let widget = widgets::msg(
|
let widget = widgets::msg(highlighted, indent, msg, folded_info);
|
||||||
highlighted,
|
|
||||||
self.tz.clone(),
|
|
||||||
indent,
|
|
||||||
msg,
|
|
||||||
self.context.nick_emoji,
|
|
||||||
self.context.caesar,
|
|
||||||
folded_info,
|
|
||||||
);
|
|
||||||
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
let widget = Self::predraw(widget, self.context.size, self.widthdb);
|
||||||
Block::new(TreeBlockId::Msg(msg_id), widget, true)
|
Block::new(TreeBlockId::Msg(msg_id), widget, true)
|
||||||
}
|
}
|
||||||
|
|
@ -446,7 +422,7 @@ where
|
||||||
|
|
||||||
pub fn into_visible_blocks(
|
pub fn into_visible_blocks(
|
||||||
self,
|
self,
|
||||||
) -> impl Iterator<Item = (Range<i32>, Block<TreeBlockId<M::Id>>)> + use<M, S> {
|
) -> impl Iterator<Item = (Range<i32>, Block<TreeBlockId<M::Id>>)> {
|
||||||
let area = renderer::visible_area(&self);
|
let area = renderer::visible_area(&self);
|
||||||
self.blocks
|
self.blocks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -480,6 +456,10 @@ where
|
||||||
&mut self.blocks
|
&mut self.blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn into_blocks(self) -> TreeBlocks<M::Id> {
|
||||||
|
self.blocks
|
||||||
|
}
|
||||||
|
|
||||||
async fn expand_top(&mut self) -> Result<(), Self::Error> {
|
async fn expand_top(&mut self) -> Result<(), Self::Error> {
|
||||||
let prev_root_id = if let Some(top_root_id) = &self.top_root_id {
|
let prev_root_id = if let Some(top_root_id) = &self.top_root_id {
|
||||||
self.store.prev_root_id(top_root_id).await?
|
self.store.prev_root_id(top_root_id).await?
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
use toss::{WidthDb, widgets::EditorState};
|
use toss::widgets::EditorState;
|
||||||
|
use toss::WidthDb;
|
||||||
|
|
||||||
use crate::{
|
use crate::store::{Msg, MsgStore};
|
||||||
store::{Msg, MsgStore},
|
use crate::ui::chat::cursor::Cursor;
|
||||||
ui::{ChatMsg, chat::cursor::Cursor},
|
use crate::ui::ChatMsg;
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::renderer::{TreeContext, TreeRenderer};
|
||||||
TreeViewState,
|
use super::TreeViewState;
|
||||||
renderer::{TreeContext, TreeRenderer},
|
|
||||||
};
|
|
||||||
|
|
||||||
impl<M, S> TreeViewState<M, S>
|
impl<M, S> TreeViewState<M, S>
|
||||||
where
|
where
|
||||||
|
|
@ -22,8 +20,6 @@ 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,
|
|
||||||
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,
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +36,6 @@ where
|
||||||
let mut renderer = TreeRenderer::new(
|
let mut renderer = TreeRenderer::new(
|
||||||
context,
|
context,
|
||||||
&self.store,
|
&self.store,
|
||||||
&self.tz,
|
|
||||||
&mut self.folded,
|
&mut self.folded,
|
||||||
cursor,
|
cursor,
|
||||||
editor,
|
editor,
|
||||||
|
|
@ -68,7 +63,6 @@ where
|
||||||
let mut renderer = TreeRenderer::new(
|
let mut renderer = TreeRenderer::new(
|
||||||
context,
|
context,
|
||||||
&self.store,
|
&self.store,
|
||||||
&self.tz,
|
|
||||||
&mut self.folded,
|
&mut self.folded,
|
||||||
cursor,
|
cursor,
|
||||||
editor,
|
editor,
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,12 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
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::{
|
use crate::store::Msg;
|
||||||
store::Msg,
|
use crate::ui::chat::widgets::{Indent, Seen, Time};
|
||||||
ui::{
|
use crate::ui::ChatMsg;
|
||||||
ChatMsg,
|
|
||||||
chat::widgets::{Indent, Seen, Time},
|
|
||||||
},
|
|
||||||
util,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const PLACEHOLDER: &str = "[...]";
|
pub const PLACEHOLDER: &str = "[...]";
|
||||||
|
|
||||||
|
|
@ -38,10 +30,6 @@ fn style_indent(highlighted: bool) -> Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_caesar() -> Style {
|
|
||||||
Style::new().green()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn style_info() -> Style {
|
fn style_info() -> Style {
|
||||||
Style::new().italic().dark_grey()
|
Style::new().italic().dark_grey()
|
||||||
}
|
}
|
||||||
|
|
@ -56,28 +44,11 @@ fn style_pseudo_highlight() -> Style {
|
||||||
|
|
||||||
pub fn msg<M: Msg + ChatMsg>(
|
pub fn msg<M: Msg + ChatMsg>(
|
||||||
highlighted: bool,
|
highlighted: bool,
|
||||||
tz: TimeZone,
|
|
||||||
indent: usize,
|
indent: usize,
|
||||||
msg: &M,
|
msg: &M,
|
||||||
nick_emoji: bool,
|
|
||||||
caesar: i8,
|
|
||||||
folded_info: Option<usize>,
|
folded_info: Option<usize>,
|
||||||
) -> Boxed<'static, Infallible> {
|
) -> Boxed<'static, Infallible> {
|
||||||
let (mut nick, mut content) = msg.styled();
|
let (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 {
|
|
||||||
// Apply caesar in inverse because we're decoding
|
|
||||||
let rotated = util::caesar(content.text(), -caesar);
|
|
||||||
content = content
|
|
||||||
.then_plain("\n")
|
|
||||||
.then(format!("{rotated} [rot{caesar}]"), style_caesar());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(amount) = folded_info {
|
if let Some(amount) = folded_info {
|
||||||
content = content
|
content = content
|
||||||
|
|
@ -87,7 +58,7 @@ pub fn msg<M: Msg + ChatMsg>(
|
||||||
|
|
||||||
Join5::horizontal(
|
Join5::horizontal(
|
||||||
Seen::new(msg.seen()).segment().with_fixed(true),
|
Seen::new(msg.seen()).segment().with_fixed(true),
|
||||||
Time::new(msg.time().map(|t| t.to_zoned(tz)), style_time(highlighted))
|
Time::new(Some(msg.time()), style_time(highlighted))
|
||||||
.padding()
|
.padding()
|
||||||
.with_right(1)
|
.with_right(1)
|
||||||
.with_stretch(true)
|
.with_stretch(true)
|
||||||
|
|
@ -145,14 +116,10 @@ pub fn msg_placeholder(
|
||||||
pub fn editor<'a, M: ChatMsg>(
|
pub fn editor<'a, M: ChatMsg>(
|
||||||
indent: usize,
|
indent: usize,
|
||||||
nick: &str,
|
nick: &str,
|
||||||
focus: bool,
|
|
||||||
editor: &'a mut EditorState,
|
editor: &'a mut EditorState,
|
||||||
) -> Boxed<'a, Infallible> {
|
) -> Boxed<'a, Infallible> {
|
||||||
let (nick, content) = M::edit(nick, editor.text());
|
let (nick, content) = M::edit(nick, editor.text());
|
||||||
let editor = editor
|
let editor = editor.widget().with_highlight(|_| content);
|
||||||
.widget()
|
|
||||||
.with_highlight(|_| content)
|
|
||||||
.with_focus(focus);
|
|
||||||
|
|
||||||
Join5::horizontal(
|
Join5::horizontal(
|
||||||
Seen::new(true).segment().with_fixed(true),
|
Seen::new(true).segment().with_fixed(true),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use jiff::Zoned;
|
use time::format_description::FormatItem;
|
||||||
use toss::{
|
use time::macros::format_description;
|
||||||
Frame, Pos, Size, Style, Widget, WidgetExt, WidthDb,
|
use time::OffsetDateTime;
|
||||||
widgets::{Boxed, Empty, Text},
|
use toss::widgets::{Boxed, Empty, Text};
|
||||||
};
|
use toss::{Frame, Pos, Size, Style, Widget, WidgetExt, WidthDb};
|
||||||
|
|
||||||
use crate::util::InfallibleExt;
|
use crate::util::InfallibleExt;
|
||||||
|
|
||||||
|
|
@ -46,15 +46,15 @@ impl<E> Widget<E> for Indent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIME_FORMAT: &str = "%Y-%m-%d %H:%M";
|
const TIME_FORMAT: &[FormatItem<'_>] = format_description!("[year]-[month]-[day] [hour]:[minute]");
|
||||||
const TIME_WIDTH: u16 = 16;
|
const TIME_WIDTH: u16 = 16;
|
||||||
|
|
||||||
pub struct Time(Boxed<'static, Infallible>);
|
pub struct Time(Boxed<'static, Infallible>);
|
||||||
|
|
||||||
impl Time {
|
impl Time {
|
||||||
pub fn new(time: Option<Zoned>, style: Style) -> Self {
|
pub fn new(time: Option<OffsetDateTime>, style: Style) -> Self {
|
||||||
let widget = if let Some(time) = time {
|
let widget = if let Some(time) = time {
|
||||||
let text = time.strftime(TIME_FORMAT).to_string();
|
let text = time.format(TIME_FORMAT).expect("could not format time");
|
||||||
Text::new((text, style))
|
Text::new((text, style))
|
||||||
.background()
|
.background()
|
||||||
.with_style(style)
|
.with_style(style)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,14 @@
|
||||||
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, conn};
|
use euphoxide::api::PersonalAccountView;
|
||||||
use toss::{
|
use euphoxide::conn;
|
||||||
Style, Widget, WidgetExt,
|
use toss::widgets::{EditorState, Empty, Join3, Join4, Join5, Text};
|
||||||
widgets::{EditorState, Empty, Join3, Join4, Join5, Text},
|
use toss::{Style, Widget, WidgetExt};
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::euph::{self, Room};
|
||||||
euph::{self, Room},
|
use crate::ui::widgets::Popup;
|
||||||
ui::{UiError, util, widgets::Popup},
|
use crate::ui::{util, UiError};
|
||||||
};
|
|
||||||
|
|
||||||
use super::popup::PopupResult;
|
use super::popup::PopupResult;
|
||||||
|
|
||||||
|
|
@ -35,7 +33,7 @@ impl LoggedOut {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn widget(&mut self) -> impl Widget<UiError> {
|
fn widget(&mut self) -> impl Widget<UiError> + '_ {
|
||||||
let bold = Style::new().bold();
|
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(),
|
||||||
|
|
@ -68,7 +66,7 @@ impl LoggedOut {
|
||||||
pub struct LoggedIn(PersonalAccountView);
|
pub struct LoggedIn(PersonalAccountView);
|
||||||
|
|
||||||
impl LoggedIn {
|
impl LoggedIn {
|
||||||
fn widget(&self) -> impl Widget<UiError> + use<> {
|
fn widget(&self) -> impl Widget<UiError> {
|
||||||
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(),
|
||||||
|
|
@ -111,7 +109,7 @@ impl AccountUiState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget(&mut self) -> impl Widget<UiError> {
|
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
|
||||||
let inner = match self {
|
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(),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use cove_config::Keys;
|
use cove_config::Keys;
|
||||||
use cove_input::InputEvent;
|
use cove_input::InputEvent;
|
||||||
use toss::{Widget, widgets::EditorState};
|
use toss::widgets::EditorState;
|
||||||
|
use toss::Widget;
|
||||||
|
|
||||||
use crate::{
|
use crate::euph::Room;
|
||||||
euph::Room,
|
use crate::ui::widgets::Popup;
|
||||||
ui::{UiError, util, widgets::Popup},
|
use crate::ui::{util, UiError};
|
||||||
};
|
|
||||||
|
|
||||||
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",
|
||||||
|
|
|
||||||
|
|
@ -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::{
|
use euphoxide::api::{Message, NickEvent, SessionView};
|
||||||
api::{Message, NickEvent, SessionView},
|
use euphoxide::conn::SessionInfo;
|
||||||
conn::SessionInfo,
|
use toss::widgets::Text;
|
||||||
};
|
use toss::{Style, Styled, Widget};
|
||||||
use toss::{Style, Styled, Widget, widgets::Text};
|
|
||||||
|
|
||||||
use crate::ui::{UiError, widgets::Popup};
|
use crate::ui::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> + use<> {
|
pub fn session_widget(session: &SessionInfo) -> impl Widget<UiError> {
|
||||||
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> + use<> {
|
||||||
Popup::new(Text::new(text), "Inspect session")
|
Popup::new(Text::new(text), "Inspect session")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn message_widget(msg: &Message) -> impl Widget<UiError> + use<> {
|
pub fn message_widget(msg: &Message) -> impl Widget<UiError> {
|
||||||
let heading_style = Style::new().bold();
|
let 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");
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,19 @@
|
||||||
use cove_config::{Config, Keys};
|
use cove_config::{Config, Keys};
|
||||||
use cove_input::InputEvent;
|
use cove_input::InputEvent;
|
||||||
use crossterm::{event::KeyCode, style::Stylize};
|
use crossterm::event::KeyCode;
|
||||||
|
use crossterm::style::Stylize;
|
||||||
use linkify::{LinkFinder, LinkKind};
|
use linkify::{LinkFinder, LinkKind};
|
||||||
use toss::{
|
use toss::widgets::{Join2, Text};
|
||||||
Style, Styled, Widget, WidgetExt,
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
widgets::{Join2, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::ui::widgets::{ListBuilder, ListState, Popup};
|
||||||
euph::{self, SpanType},
|
use crate::ui::{key_bindings, util, UiError};
|
||||||
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<Link>,
|
links: Vec<String>,
|
||||||
list: ListState<usize>,
|
list: ListState<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,34 +21,12 @@ 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 mut links = vec![];
|
let links = LinkFinder::new()
|
||||||
|
|
||||||
// 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())
|
||||||
links.push((
|
.collect();
|
||||||
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,
|
||||||
|
|
@ -69,7 +35,7 @@ impl LinksState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn widget(&mut self) -> impl Widget<UiError> {
|
pub fn widget(&mut self) -> impl Widget<UiError> + '_ {
|
||||||
let style_selected = Style::new().black().on_white();
|
let style_selected = Style::new().black().on_white();
|
||||||
|
|
||||||
let mut list_builder = ListBuilder::new();
|
let mut list_builder = ListBuilder::new();
|
||||||
|
|
@ -80,29 +46,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();
|
||||||
list_builder.add_sel(id, move |selected| {
|
if let Some(&number_key) = NUMBER_KEYS.get(id) {
|
||||||
let mut text = Styled::default();
|
list_builder.add_sel(id, move |selected| {
|
||||||
|
let text = if selected {
|
||||||
// Number key indicator
|
Styled::new(format!("[{number_key}]"), style_selected.bold())
|
||||||
text = match NUMBER_KEYS.get(id) {
|
.then(" ", style_selected)
|
||||||
None if selected => text.then(" ", style_selected),
|
.then(link, style_selected)
|
||||||
None => text.then_plain(" "),
|
} else {
|
||||||
Some(key) if selected => text.then(format!("[{key}] "), style_selected.bold()),
|
Styled::new(format!("[{number_key}]"), Style::new().dark_grey().bold())
|
||||||
Some(key) => text.then(format!("[{key}] "), Style::new().dark_grey().bold()),
|
.then_plain(" ")
|
||||||
};
|
.then_plain(link)
|
||||||
|
};
|
||||||
// The link itself
|
Text::new(text)
|
||||||
text = match link {
|
});
|
||||||
Link::Url(url) if selected => text.then(url, style_selected),
|
} else {
|
||||||
Link::Url(url) => text.then_plain(url),
|
list_builder.add_sel(id, move |selected| {
|
||||||
Link::Room(name) if selected => {
|
let text = if selected {
|
||||||
text.then(format!("&{name}"), style_selected.bold())
|
Styled::new(format!(" {link}"), style_selected)
|
||||||
}
|
} else {
|
||||||
Link::Room(name) => text.then(format!("&{name}"), Style::new().blue().bold()),
|
Styled::new_plain(format!(" {link}"))
|
||||||
};
|
};
|
||||||
|
Text::new(text)
|
||||||
Text::new(text).with_wrap(false)
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let hint_style = Style::new().grey().italic();
|
let hint_style = Style::new().grey().italic();
|
||||||
|
|
@ -126,24 +92,18 @@ impl LinksState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_link_by_id(&self, id: usize) -> PopupResult {
|
fn open_link_by_id(&self, id: usize) -> PopupResult {
|
||||||
match self.links.get(id) {
|
if let Some(link) = self.links.get(id) {
|
||||||
Some(Link::Url(url)) => {
|
// The `http://` or `https://` schema is necessary for open::that to
|
||||||
// The `http://` or `https://` schema is necessary for
|
// successfully open the link in the browser.
|
||||||
// open::that to successfully open the link in the browser.
|
let link = if link.starts_with("http://") || link.starts_with("https://") {
|
||||||
let link = if url.starts_with("http://") || url.starts_with("https://") {
|
link.clone()
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::{Style, Widget, widgets::EditorState};
|
use toss::widgets::EditorState;
|
||||||
|
use toss::{Style, Widget};
|
||||||
|
|
||||||
use crate::{
|
use crate::euph::{self, Room};
|
||||||
euph::{self, Room},
|
use crate::ui::widgets::Popup;
|
||||||
ui::{UiError, util, widgets::Popup},
|
use crate::ui::{util, UiError};
|
||||||
};
|
|
||||||
|
|
||||||
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()));
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,22 @@
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
use crossterm::style::{Color, Stylize};
|
use crossterm::style::{Color, Stylize};
|
||||||
use euphoxide::{
|
use euphoxide::api::{NickEvent, SessionId, SessionType, SessionView, UserId};
|
||||||
api::{NickEvent, SessionId, SessionType, SessionView, UserId},
|
use euphoxide::conn::{Joined, SessionInfo};
|
||||||
conn::{Joined, SessionInfo},
|
use toss::widgets::{Background, Text};
|
||||||
};
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
use toss::{
|
|
||||||
Style, Styled, Widget, WidgetExt,
|
|
||||||
widgets::{Background, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::euph;
|
||||||
euph,
|
use crate::ui::widgets::{ListBuilder, ListState};
|
||||||
ui::{
|
use crate::ui::UiError;
|
||||||
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,
|
||||||
nick_emoji: bool,
|
) -> impl Widget<UiError> + 'a {
|
||||||
) -> impl Widget<UiError> + use<'a> {
|
|
||||||
let mut list_builder = ListBuilder::new();
|
let mut list_builder = ListBuilder::new();
|
||||||
render_rows(&mut list_builder, joined, focused, nick_emoji);
|
render_rows(&mut list_builder, joined, focused);
|
||||||
list_builder.build(list)
|
list_builder.build(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +62,6 @@ 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![];
|
||||||
|
|
@ -97,38 +87,10 @@ fn render_rows(
|
||||||
lurkers.sort_unstable();
|
lurkers.sort_unstable();
|
||||||
nurkers.sort_unstable();
|
nurkers.sort_unstable();
|
||||||
|
|
||||||
render_section(
|
render_section(list_builder, "People", &people, &joined.session, focused);
|
||||||
list_builder,
|
render_section(list_builder, "Bots", &bots, &joined.session, focused);
|
||||||
"People",
|
render_section(list_builder, "Lurkers", &lurkers, &joined.session, focused);
|
||||||
&people,
|
render_section(list_builder, "Nurkers", &nurkers, &joined.session, focused);
|
||||||
&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(
|
||||||
|
|
@ -137,7 +99,6 @@ 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;
|
||||||
|
|
@ -155,7 +116,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, nick_emoji);
|
render_row(list_builder, session, own_session, focused);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +125,6 @@ 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();
|
||||||
|
|
@ -198,24 +158,16 @@ fn render_row(
|
||||||
" "
|
" "
|
||||||
};
|
};
|
||||||
|
|
||||||
let emoji = if nick_emoji {
|
|
||||||
format!(" ({})", euph::user_id_emoji(&session.id))
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
list_builder.add_sel(session.session_id.clone(), move |selected| {
|
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()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
use toss::{Style, Styled, Widget, widgets::Text};
|
use toss::widgets::Text;
|
||||||
|
use toss::{Style, Styled, Widget};
|
||||||
|
|
||||||
use crate::ui::{UiError, widgets::Popup};
|
use crate::ui::widgets::Popup;
|
||||||
|
use crate::ui::UiError;
|
||||||
|
|
||||||
pub enum RoomPopup {
|
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> + use<> {
|
fn server_error_widget(description: &str, reason: &str) -> impl Widget<UiError> {
|
||||||
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")
|
||||||
|
|
@ -21,7 +23,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> + use<> {
|
pub fn widget(&self) -> impl Widget<UiError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Error {
|
Self::Error {
|
||||||
description,
|
description,
|
||||||
|
|
@ -35,6 +37,5 @@ pub enum PopupResult {
|
||||||
NotHandled,
|
NotHandled,
|
||||||
Handled,
|
Handled,
|
||||||
Close,
|
Close,
|
||||||
SwitchToRoom { name: String },
|
|
||||||
ErrorOpeningLink { link: String, error: io::Error },
|
ErrorOpeningLink { link: String, error: io::Error },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,40 +3,25 @@ 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::{
|
use euphoxide::api::{Data, Message, MessageId, PacketType, SessionId};
|
||||||
api::{Data, Message, MessageId, PacketType, SessionId, packet::ParsedPacket},
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
bot::instance::{ConnSnapshot, Event, ServerConfig},
|
use euphoxide::conn::{self, Joined, Joining, SessionInfo};
|
||||||
conn::{self, Joined, Joining, SessionInfo},
|
use tokio::sync::oneshot::error::TryRecvError;
|
||||||
};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
use jiff::tz::TimeZone;
|
use toss::widgets::{BoxedAsync, EditorState, Join2, Layer, Text};
|
||||||
use tokio::sync::{
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
mpsc,
|
|
||||||
oneshot::{self, error::TryRecvError},
|
|
||||||
};
|
|
||||||
use toss::{
|
|
||||||
Style, Styled, Widget, WidgetExt,
|
|
||||||
widgets::{BoxedAsync, EditorState, Join2, Layer, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::euph;
|
||||||
euph::{self, SpanType},
|
use crate::macros::logging_unwrap;
|
||||||
macros::logging_unwrap,
|
use crate::ui::chat::{ChatState, Reaction};
|
||||||
ui::{
|
use crate::ui::widgets::ListState;
|
||||||
UiError, UiEvent,
|
use crate::ui::{util, UiError, UiEvent};
|
||||||
chat::{ChatState, Reaction},
|
use crate::vault::EuphRoomVault;
|
||||||
util,
|
|
||||||
widgets::ListState,
|
|
||||||
},
|
|
||||||
vault::{EuphRoomVault, RoomIdentifier},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::account::AccountUiState;
|
||||||
account::AccountUiState,
|
use super::links::LinksState;
|
||||||
auth, inspect,
|
use super::popup::{PopupResult, RoomPopup};
|
||||||
links::LinksState,
|
use super::{auth, inspect, nick, nick_list};
|
||||||
nick, nick_list,
|
|
||||||
popup::{PopupResult, RoomPopup},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
enum Focus {
|
enum Focus {
|
||||||
|
|
@ -73,8 +58,6 @@ 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 {
|
||||||
|
|
@ -83,7 +66,6 @@ impl EuphRoom {
|
||||||
server_config: ServerConfig,
|
server_config: ServerConfig,
|
||||||
room_config: cove_config::EuphRoom,
|
room_config: cove_config::EuphRoom,
|
||||||
vault: EuphRoomVault,
|
vault: EuphRoomVault,
|
||||||
tz: TimeZone,
|
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -95,10 +77,9 @@ impl EuphRoom {
|
||||||
focus: Focus::Chat,
|
focus: Focus::Chat,
|
||||||
state: State::Normal,
|
state: State::Normal,
|
||||||
popups: VecDeque::new(),
|
popups: VecDeque::new(),
|
||||||
chat: ChatState::new(vault, tz),
|
chat: ChatState::new(vault),
|
||||||
last_msg_sent: None,
|
last_msg_sent: None,
|
||||||
nick_list: ListState::new(),
|
nick_list: ListState::new(),
|
||||||
mentioned: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,12 +87,8 @@ impl EuphRoom {
|
||||||
self.chat.store()
|
self.chat.store()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn domain(&self) -> &str {
|
|
||||||
&self.vault().room().domain
|
|
||||||
}
|
|
||||||
|
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.vault().room().name
|
self.vault().room()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn connect(&mut self, next_instance_id: &mut usize) {
|
pub fn connect(&mut self, next_instance_id: &mut usize) {
|
||||||
|
|
@ -120,8 +97,8 @@ impl EuphRoom {
|
||||||
let instance_config = self
|
let instance_config = self
|
||||||
.server_config
|
.server_config
|
||||||
.clone()
|
.clone()
|
||||||
.room(self.vault().room().name.clone())
|
.room(self.vault().room().to_string())
|
||||||
.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)
|
||||||
|
|
@ -151,9 +128,7 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room_state_joined(&self) -> Option<&Joined> {
|
// TODO fn room_state_joined(&self) -> Option<&Joined> {}
|
||||||
self.room_state().and_then(|s| s.joined())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stopped(&self) -> bool {
|
pub fn stopped(&self) -> bool {
|
||||||
self.room.as_ref().map(|r| r.stopped()).unwrap_or(true)
|
self.room.as_ref().map(|r| r.stopped()).unwrap_or(true)
|
||||||
|
|
@ -167,12 +142,6 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn retrieve_mentioned(&mut self) -> bool {
|
|
||||||
let mentioned = self.mentioned;
|
|
||||||
self.mentioned = false;
|
|
||||||
mentioned
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn unseen_msgs_count(&self) -> usize {
|
pub async fn unseen_msgs_count(&self) -> usize {
|
||||||
logging_unwrap!(self.vault().unseen_msgs_count().await)
|
logging_unwrap!(self.vault().unseen_msgs_count().await)
|
||||||
}
|
}
|
||||||
|
|
@ -194,8 +163,9 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stabilize_focus(&mut self) {
|
fn stabilize_focus(&mut self) {
|
||||||
if self.room_state_joined().is_none() {
|
match self.room_state() {
|
||||||
self.focus = Focus::Chat; // There is no nick list to focus on
|
Some(euph::State::Connected(_, conn::State::Joined(_))) => {}
|
||||||
|
_ => self.focus = Focus::Chat, // There is no nick list to focus on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,15 +207,17 @@ impl EuphRoom {
|
||||||
|
|
||||||
let room_state = self.room.as_ref().map(|room| room.state());
|
let room_state = self.room.as_ref().map(|room| room.state());
|
||||||
let status_widget = self.status_widget(room_state).await;
|
let status_widget = self.status_widget(room_state).await;
|
||||||
let chat = match room_state.and_then(|s| s.joined()) {
|
let chat = if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = room_state
|
||||||
Some(joined) => Self::widget_with_nick_list(
|
{
|
||||||
|
Self::widget_with_nick_list(
|
||||||
&mut self.chat,
|
&mut self.chat,
|
||||||
status_widget,
|
status_widget,
|
||||||
&mut self.nick_list,
|
&mut self.nick_list,
|
||||||
joined,
|
joined,
|
||||||
self.focus,
|
self.focus,
|
||||||
),
|
)
|
||||||
None => Self::widget_without_nick_list(&mut self.chat, status_widget),
|
} else {
|
||||||
|
Self::widget_without_nick_list(&mut self.chat, status_widget)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut layers = vec![chat];
|
let mut layers = vec![chat];
|
||||||
|
|
@ -291,16 +263,11 @@ impl EuphRoom {
|
||||||
joined: &Joined,
|
joined: &Joined,
|
||||||
focus: Focus,
|
focus: Focus,
|
||||||
) -> BoxedAsync<'a, UiError> {
|
) -> BoxedAsync<'a, UiError> {
|
||||||
let nick_list_widget = nick_list::widget(
|
let nick_list_widget = nick_list::widget(nick_list, joined, focus == Focus::NickList)
|
||||||
nick_list,
|
.padding()
|
||||||
joined,
|
.with_right(1)
|
||||||
focus == Focus::NickList,
|
.border()
|
||||||
chat.nick_emoji(),
|
.desync();
|
||||||
)
|
|
||||||
.padding()
|
|
||||||
.with_right(1)
|
|
||||||
.border()
|
|
||||||
.desync();
|
|
||||||
|
|
||||||
let chat_widget = chat.widget(joined.session.name.clone(), focus == Focus::Chat);
|
let chat_widget = chat.widget(joined.session.name.clone(), focus == Focus::Chat);
|
||||||
|
|
||||||
|
|
@ -315,10 +282,9 @@ impl EuphRoom {
|
||||||
.boxed_async()
|
.boxed_async()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn status_widget(&self, state: Option<&euph::State>) -> impl Widget<UiError> + use<> {
|
async fn status_widget(&self, state: Option<&euph::State>) -> impl Widget<UiError> {
|
||||||
let room_style = Style::new().bold().blue();
|
let room_style = Style::new().bold().blue();
|
||||||
let mut info = Styled::new(format!("{} ", self.domain()), Style::new().grey())
|
let mut info = Styled::new(format!("&{}", self.name()), room_style);
|
||||||
.then(format!("&{}", self.name()), room_style);
|
|
||||||
|
|
||||||
info = match state {
|
info = match state {
|
||||||
None | Some(euph::State::Stopped) => info.then_plain(", archive"),
|
None | Some(euph::State::Stopped) => info.then_plain(", archive"),
|
||||||
|
|
@ -349,21 +315,14 @@ impl EuphRoom {
|
||||||
.then_plain(")");
|
.then_plain(")");
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = if unseen > 0 {
|
Text::new(info).padding().with_horizontal(1).border()
|
||||||
format!("&{} ({unseen})", self.name())
|
|
||||||
} else {
|
|
||||||
format!("&{}", self.name())
|
|
||||||
};
|
|
||||||
|
|
||||||
Text::new(info)
|
|
||||||
.padding()
|
|
||||||
.with_horizontal(1)
|
|
||||||
.border()
|
|
||||||
.title(title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_chat_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
async fn handle_chat_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
let can_compose = self.room_state_joined().is_some();
|
let can_compose = matches!(
|
||||||
|
self.room_state(),
|
||||||
|
Some(euph::State::Connected(_, conn::State::Joined(_)))
|
||||||
|
);
|
||||||
|
|
||||||
let reaction = self.chat.handle_input_event(event, keys, can_compose).await;
|
let reaction = self.chat.handle_input_event(event, keys, can_compose).await;
|
||||||
let reaction = logging_unwrap!(reaction);
|
let reaction = logging_unwrap!(reaction);
|
||||||
|
|
@ -422,6 +381,18 @@ impl EuphRoom {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always applicable
|
||||||
|
if event.matches(&keys.room.action.present) {
|
||||||
|
let link = format!("https://plugh.de/present/{}/", self.name());
|
||||||
|
if let Err(error) = open::that(&link) {
|
||||||
|
self.popups.push_front(RoomPopup::Error {
|
||||||
|
description: format!("Failed to open link: {link}"),
|
||||||
|
reason: format!("{error}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,7 +442,8 @@ impl EuphRoom {
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.matches(&keys.tree.action.inspect) {
|
if event.matches(&keys.tree.action.inspect) {
|
||||||
if let Some(joined) = self.room_state_joined() {
|
if let Some(euph::State::Connected(_, conn::State::Joined(joined))) = self.room_state()
|
||||||
|
{
|
||||||
if let Some(id) = self.nick_list.selected() {
|
if let Some(id) = self.nick_list.selected() {
|
||||||
if *id == joined.session.session_id {
|
if *id == joined.session.session_id {
|
||||||
self.state =
|
self.state =
|
||||||
|
|
@ -494,9 +466,11 @@ impl EuphRoom {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.room_state_joined().is_some() && event.matches(&keys.general.focus) {
|
if let Some(euph::State::Connected(_, conn::State::Joined(_))) = self.room_state() {
|
||||||
self.focus = Focus::NickList;
|
if event.matches(&keys.general.focus) {
|
||||||
return true;
|
self.focus = Focus::NickList;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Focus::NickList => {
|
Focus::NickList => {
|
||||||
|
|
@ -514,22 +488,18 @@ impl EuphRoom {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_input_event(
|
pub async fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
&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 RoomResult::Handled;
|
return true;
|
||||||
}
|
}
|
||||||
// Prevent event from reaching anything below the popup
|
// Prevent event from reaching anything below the popup
|
||||||
return RoomResult::NotHandled;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = match &mut self.state {
|
let result = match &mut self.state {
|
||||||
State::Normal => return self.handle_normal_input_event(event, keys).await.into(),
|
State::Normal => return self.handle_normal_input_event(event, keys).await,
|
||||||
State::Auth(editor) => auth::handle_input_event(event, keys, &self.room, editor),
|
State::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),
|
||||||
|
|
@ -540,30 +510,27 @@ impl EuphRoom {
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
PopupResult::NotHandled => RoomResult::NotHandled,
|
PopupResult::NotHandled => false,
|
||||||
PopupResult::Handled => RoomResult::Handled,
|
PopupResult::Handled => true,
|
||||||
PopupResult::Close => {
|
PopupResult::Close => {
|
||||||
self.state = State::Normal;
|
self.state = State::Normal;
|
||||||
RoomResult::Handled
|
true
|
||||||
}
|
}
|
||||||
PopupResult::SwitchToRoom { name } => RoomResult::SwitchToRoom {
|
|
||||||
room: RoomIdentifier {
|
|
||||||
domain: self.vault().room().domain.clone(),
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PopupResult::ErrorOpeningLink { link, error } => {
|
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}"),
|
||||||
});
|
});
|
||||||
RoomResult::Handled
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_event(&mut self, event: Event) -> bool {
|
pub async fn handle_event(&mut self, event: Event) -> bool {
|
||||||
let Some(room) = &self.room else { return false };
|
let room = match &self.room {
|
||||||
|
None => return false,
|
||||||
|
Some(room) => room,
|
||||||
|
};
|
||||||
|
|
||||||
if event.config().name != room.instance().config().name {
|
if event.config().name != room.instance().config().name {
|
||||||
// If we allowed names other than the current one, old instances
|
// If we allowed names other than the current one, old instances
|
||||||
|
|
@ -571,35 +538,6 @@ 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 {
|
||||||
|
|
@ -691,18 +629,3 @@ 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,11 @@ 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::{
|
use toss::widgets::{Either2, Join2, Padding, Text};
|
||||||
Style, Styled, Widget, WidgetExt,
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
widgets::{Either2, Join2, Padding, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::widgets::{ListBuilder, ListState, Popup};
|
||||||
UiError, util,
|
use super::{util, UiError};
|
||||||
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>;
|
||||||
|
|
@ -73,7 +69,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> + use<'a> {
|
) -> impl Widget<UiError> + '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() {
|
||||||
|
|
@ -83,23 +79,7 @@ pub fn widget<'a>(
|
||||||
render_group_info(&mut list_builder, group_info);
|
render_group_info(&mut list_builder, group_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
let scroll_info_style = Style::new().grey().italic();
|
Popup::new(list_builder.build(list), "Key bindings")
|
||||||
let scroll_info = Styled::new("(Scroll with ", scroll_info_style)
|
|
||||||
.and_then(format_binding(&config.keys.cursor.down))
|
|
||||||
.then(" and ", scroll_info_style)
|
|
||||||
.and_then(format_binding(&config.keys.cursor.up))
|
|
||||||
.then(")", scroll_info_style);
|
|
||||||
|
|
||||||
let inner = Join2::vertical(
|
|
||||||
list_builder.build(list).segment(),
|
|
||||||
Text::new(scroll_info)
|
|
||||||
.float()
|
|
||||||
.with_center_h()
|
|
||||||
.segment()
|
|
||||||
.with_growing(false),
|
|
||||||
);
|
|
||||||
|
|
||||||
Popup::new(inner, "Key bindings")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input_event(
|
pub fn handle_input_event(
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,30 @@
|
||||||
use std::{
|
use std::collections::{HashMap, HashSet};
|
||||||
collections::{HashMap, HashSet, hash_map::Entry},
|
use std::iter;
|
||||||
iter,
|
use std::sync::{Arc, Mutex};
|
||||||
sync::{Arc, Mutex},
|
|
||||||
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::{
|
use euphoxide::api::SessionType;
|
||||||
api::SessionType,
|
use euphoxide::bot::instance::{Event, ServerConfig};
|
||||||
bot::instance::{Event, ServerConfig},
|
use euphoxide::conn::{self, Joined};
|
||||||
conn::{self, Joined},
|
|
||||||
};
|
|
||||||
use jiff::tz::TimeZone;
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use toss::{
|
use toss::widgets::{BoxedAsync, EditorState, Empty, Join2, Text};
|
||||||
Style, Styled, Widget, WidgetExt,
|
use toss::{Style, Styled, Widget, WidgetExt};
|
||||||
widgets::{BellState, BoxedAsync, Empty, Join2, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::euph;
|
||||||
euph,
|
use crate::macros::logging_unwrap;
|
||||||
macros::logging_unwrap,
|
use crate::vault::Vault;
|
||||||
vault::{EuphVault, RoomIdentifier, Vault},
|
|
||||||
version::{NAME, VERSION},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::euph::room::EuphRoom;
|
||||||
UiError, UiEvent,
|
use super::widgets::{ListBuilder, ListState, Popup};
|
||||||
euph::room::{EuphRoom, RoomResult},
|
use super::{key_bindings, util, UiError, UiEvent};
|
||||||
key_bindings, util,
|
|
||||||
widgets::{ListBuilder, ListState},
|
|
||||||
};
|
|
||||||
|
|
||||||
use self::{
|
|
||||||
connect::{ConnectResult, ConnectState},
|
|
||||||
delete::{DeleteResult, DeleteState},
|
|
||||||
};
|
|
||||||
|
|
||||||
mod connect;
|
|
||||||
mod delete;
|
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
ShowList,
|
ShowList,
|
||||||
ShowRoom(RoomIdentifier),
|
ShowRoom(String),
|
||||||
Connect(ConnectState),
|
Connect(EditorState),
|
||||||
Delete(DeleteState),
|
Delete(String, EditorState),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
|
@ -64,70 +42,47 @@ impl Order {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct EuphServer {
|
|
||||||
config: ServerConfig,
|
|
||||||
next_instance_id: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EuphServer {
|
|
||||||
async fn new(vault: &EuphVault, domain: String) -> Self {
|
|
||||||
let cookies = logging_unwrap!(vault.cookies(domain.clone()).await);
|
|
||||||
let config = ServerConfig::default()
|
|
||||||
.domain(domain)
|
|
||||||
.cookies(Arc::new(Mutex::new(cookies)))
|
|
||||||
.timeout(Duration::from_secs(10));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
config,
|
|
||||||
next_instance_id: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Rooms {
|
pub struct Rooms {
|
||||||
config: &'static Config,
|
config: &'static Config,
|
||||||
tz: TimeZone,
|
|
||||||
|
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
|
|
||||||
state: State,
|
state: State,
|
||||||
|
|
||||||
list: ListState<RoomIdentifier>,
|
list: ListState<String>,
|
||||||
order: Order,
|
order: Order,
|
||||||
bell: BellState,
|
|
||||||
|
|
||||||
euph_servers: HashMap<String, EuphServer>,
|
euph_server_config: ServerConfig,
|
||||||
euph_rooms: HashMap<RoomIdentifier, EuphRoom>,
|
euph_next_instance_id: usize,
|
||||||
|
euph_rooms: HashMap<String, EuphRoom>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rooms {
|
impl Rooms {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
config: &'static Config,
|
config: &'static Config,
|
||||||
tz: TimeZone,
|
|
||||||
vault: Vault,
|
vault: Vault,
|
||||||
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
ui_event_tx: mpsc::UnboundedSender<UiEvent>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let cookies = logging_unwrap!(vault.euph().cookies().await);
|
||||||
|
let euph_server_config = ServerConfig::default().cookies(Arc::new(Mutex::new(cookies)));
|
||||||
|
|
||||||
let mut result = Self {
|
let mut result = Self {
|
||||||
config,
|
config,
|
||||||
tz,
|
|
||||||
vault,
|
vault,
|
||||||
ui_event_tx,
|
ui_event_tx,
|
||||||
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_server_config,
|
||||||
euph_servers: HashMap::new(),
|
euph_next_instance_id: 0,
|
||||||
euph_rooms: HashMap::new(),
|
euph_rooms: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !config.offline {
|
if !config.offline {
|
||||||
for (domain, server) in &config.euph.servers {
|
for (name, config) in &config.euph.rooms {
|
||||||
for (name, room) in &server.rooms {
|
if config.autojoin {
|
||||||
if room.autojoin {
|
result.connect_to_room(name.clone());
|
||||||
let id = RoomIdentifier::new(domain.clone(), name.clone());
|
|
||||||
result.connect_to_room(id).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,68 +90,39 @@ impl Rooms {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_or_insert_server<'a>(
|
fn get_or_insert_room(&mut self, name: String) -> &mut EuphRoom {
|
||||||
vault: &Vault,
|
self.euph_rooms.entry(name.clone()).or_insert_with(|| {
|
||||||
euph_servers: &'a mut HashMap<String, EuphServer>,
|
|
||||||
domain: String,
|
|
||||||
) -> &'a mut EuphServer {
|
|
||||||
match euph_servers.entry(domain.clone()) {
|
|
||||||
Entry::Occupied(entry) => entry.into_mut(),
|
|
||||||
Entry::Vacant(entry) => {
|
|
||||||
let server = EuphServer::new(&vault.euph(), domain).await;
|
|
||||||
entry.insert(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_or_insert_room(&mut self, room: RoomIdentifier) -> &mut EuphRoom {
|
|
||||||
let server =
|
|
||||||
Self::get_or_insert_server(&self.vault, &mut self.euph_servers, room.domain.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
self.euph_rooms.entry(room.clone()).or_insert_with(|| {
|
|
||||||
EuphRoom::new(
|
EuphRoom::new(
|
||||||
self.config,
|
self.config,
|
||||||
server.config.clone(),
|
self.euph_server_config.clone(),
|
||||||
self.config.euph_room(&room.domain, &room.name),
|
self.config.euph_room(&name),
|
||||||
self.vault.euph().room(room),
|
self.vault.euph().room(name),
|
||||||
self.tz.clone(),
|
|
||||||
self.ui_event_tx.clone(),
|
self.ui_event_tx.clone(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_to_room(&mut self, room: RoomIdentifier) {
|
fn connect_to_room(&mut self, name: String) {
|
||||||
let server =
|
let room = self.euph_rooms.entry(name.clone()).or_insert_with(|| {
|
||||||
Self::get_or_insert_server(&self.vault, &mut self.euph_servers, room.domain.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let room = self.euph_rooms.entry(room.clone()).or_insert_with(|| {
|
|
||||||
EuphRoom::new(
|
EuphRoom::new(
|
||||||
self.config,
|
self.config,
|
||||||
server.config.clone(),
|
self.euph_server_config.clone(),
|
||||||
self.config.euph_room(&room.domain, &room.name),
|
self.config.euph_room(&name),
|
||||||
self.vault.euph().room(room),
|
self.vault.euph().room(name),
|
||||||
self.tz.clone(),
|
|
||||||
self.ui_event_tx.clone(),
|
self.ui_event_tx.clone(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
room.connect(&mut self.euph_next_instance_id);
|
||||||
room.connect(&mut server.next_instance_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn connect_to_all_rooms(&mut self) {
|
fn connect_to_all_rooms(&mut self) {
|
||||||
for (id, room) in &mut self.euph_rooms {
|
for room in self.euph_rooms.values_mut() {
|
||||||
let server =
|
room.connect(&mut self.euph_next_instance_id);
|
||||||
Self::get_or_insert_server(&self.vault, &mut self.euph_servers, id.domain.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
room.connect(&mut server.next_instance_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disconnect_from_room(&mut self, room: &RoomIdentifier) {
|
fn disconnect_from_room(&mut self, name: &str) {
|
||||||
if let Some(room) = self.euph_rooms.get_mut(room) {
|
if let Some(room) = self.euph_rooms.get_mut(name) {
|
||||||
room.disconnect();
|
room.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -216,21 +142,10 @@ impl Rooms {
|
||||||
/// - rooms that were deleted from the db.
|
/// - rooms that were deleted from the db.
|
||||||
async fn stabilize_rooms(&mut self) {
|
async fn stabilize_rooms(&mut self) {
|
||||||
// Collect all rooms from the db and config file
|
// Collect all rooms from the db and config file
|
||||||
let rooms_from_db = logging_unwrap!(self.vault.euph().rooms().await);
|
let rooms = logging_unwrap!(self.vault.euph().rooms().await);
|
||||||
let rooms_from_config = self
|
let mut rooms_set = rooms
|
||||||
.config
|
|
||||||
.euph
|
|
||||||
.servers
|
|
||||||
.iter()
|
|
||||||
.flat_map(|(domain, server)| {
|
|
||||||
server
|
|
||||||
.rooms
|
|
||||||
.keys()
|
|
||||||
.map(|name| RoomIdentifier::new(domain.clone(), name.clone()))
|
|
||||||
});
|
|
||||||
let mut rooms_set = rooms_from_db
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(rooms_from_config)
|
.chain(self.config.euph.rooms.keys().cloned())
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
// Prevent room that is currently being shown from being removed. This
|
// Prevent room that is currently being shown from being removed. This
|
||||||
|
|
@ -246,9 +161,7 @@ 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 {
|
||||||
let room = self.get_or_insert_room(room).await;
|
self.get_or_insert_room(room).retain();
|
||||||
room.retain();
|
|
||||||
self.bell.ring |= room.retrieve_mentioned();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,58 +171,96 @@ impl Rooms {
|
||||||
_ => self.stabilize_rooms().await,
|
_ => self.stabilize_rooms().await,
|
||||||
}
|
}
|
||||||
|
|
||||||
let widget = match &mut self.state {
|
match &mut self.state {
|
||||||
State::ShowList => Self::rooms_widget(
|
State::ShowList => {
|
||||||
&self.vault,
|
Self::rooms_widget(self.config, &mut self.list, self.order, &self.euph_rooms)
|
||||||
self.config,
|
.await
|
||||||
&mut self.list,
|
.desync()
|
||||||
self.order,
|
.boxed_async()
|
||||||
&self.euph_rooms,
|
}
|
||||||
)
|
|
||||||
.await
|
|
||||||
.desync()
|
|
||||||
.boxed_async(),
|
|
||||||
|
|
||||||
State::ShowRoom(id) => {
|
State::ShowRoom(name) => {
|
||||||
self.euph_rooms
|
self.euph_rooms
|
||||||
.get_mut(id)
|
.get_mut(name)
|
||||||
.expect("room exists after stabilization")
|
.expect("room exists after stabilization")
|
||||||
.widget()
|
.widget()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
State::Connect(connect) => Self::rooms_widget(
|
State::Connect(editor) => {
|
||||||
&self.vault,
|
Self::rooms_widget(self.config, &mut self.list, self.order, &self.euph_rooms)
|
||||||
self.config,
|
.await
|
||||||
&mut self.list,
|
.below(Self::new_room_widget(editor))
|
||||||
self.order,
|
.desync()
|
||||||
&self.euph_rooms,
|
.boxed_async()
|
||||||
)
|
}
|
||||||
.await
|
|
||||||
.below(connect.widget())
|
|
||||||
.desync()
|
|
||||||
.boxed_async(),
|
|
||||||
|
|
||||||
State::Delete(delete) => Self::rooms_widget(
|
State::Delete(name, editor) => {
|
||||||
&self.vault,
|
Self::rooms_widget(self.config, &mut self.list, self.order, &self.euph_rooms)
|
||||||
self.config,
|
.await
|
||||||
&mut self.list,
|
.below(Self::delete_room_widget(name, editor))
|
||||||
self.order,
|
.desync()
|
||||||
&self.euph_rooms,
|
.boxed_async()
|
||||||
)
|
}
|
||||||
.await
|
|
||||||
.below(delete.widget())
|
|
||||||
.desync()
|
|
||||||
.boxed_async(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.config.bell_on_mention {
|
|
||||||
widget.above(self.bell.widget().desync()).boxed_async()
|
|
||||||
} else {
|
|
||||||
widget
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_room_widget(editor: &mut EditorState) -> impl Widget<UiError> + '_ {
|
||||||
|
let room_style = Style::new().bold().blue();
|
||||||
|
|
||||||
|
let inner = Join2::horizontal(
|
||||||
|
Text::new(("&", room_style)).segment().with_fixed(true),
|
||||||
|
editor
|
||||||
|
.widget()
|
||||||
|
.with_highlight(|s| Styled::new(s, room_style))
|
||||||
|
.segment(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Popup::new(inner, "Connect to")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_room_widget<'a>(
|
||||||
|
name: &str,
|
||||||
|
editor: &'a mut EditorState,
|
||||||
|
) -> impl Widget<UiError> + 'a {
|
||||||
|
let warn_style = Style::new().bold().red();
|
||||||
|
let room_style = Style::new().bold().blue();
|
||||||
|
let text = Styled::new_plain("Are you sure you want to delete ")
|
||||||
|
.then("&", room_style)
|
||||||
|
.then(name, room_style)
|
||||||
|
.then_plain("?\n\n")
|
||||||
|
.then_plain("This will delete the entire room history from your vault. ")
|
||||||
|
.then_plain("To shrink your vault afterwards, run ")
|
||||||
|
.then("cove gc", Style::new().italic().grey())
|
||||||
|
.then_plain(".\n\n")
|
||||||
|
.then_plain("To confirm the deletion, ")
|
||||||
|
.then_plain("enter the full name of the room and press enter:");
|
||||||
|
|
||||||
|
let inner = Join2::vertical(
|
||||||
|
// The Join prevents the text from filling up the entire available
|
||||||
|
// space if the editor is wider than the text.
|
||||||
|
Join2::horizontal(
|
||||||
|
Text::new(text)
|
||||||
|
.resize()
|
||||||
|
.with_max_width(54)
|
||||||
|
.segment()
|
||||||
|
.with_growing(false),
|
||||||
|
Empty::new().segment(),
|
||||||
|
)
|
||||||
|
.segment(),
|
||||||
|
Join2::horizontal(
|
||||||
|
Text::new(("&", room_style)).segment().with_fixed(true),
|
||||||
|
editor
|
||||||
|
.widget()
|
||||||
|
.with_highlight(|s| Styled::new(s, room_style))
|
||||||
|
.segment(),
|
||||||
|
)
|
||||||
|
.segment(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Popup::new(inner, "Delete room").with_border_style(warn_style)
|
||||||
|
}
|
||||||
|
|
||||||
fn format_pbln(joined: &Joined) -> String {
|
fn format_pbln(joined: &Joined) -> String {
|
||||||
let mut p = 0_usize;
|
let mut p = 0_usize;
|
||||||
let mut b = 0_usize;
|
let mut b = 0_usize;
|
||||||
|
|
@ -394,45 +345,48 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_rooms(rooms: &mut [(&RoomIdentifier, Option<&euph::State>, usize)], order: Order) {
|
fn sort_rooms(rooms: &mut [(&String, Option<&euph::State>, usize)], order: Order) {
|
||||||
match order {
|
match order {
|
||||||
Order::Alphabet => rooms.sort_unstable_by_key(|(id, _, _)| *id),
|
Order::Alphabet => rooms.sort_unstable_by_key(|(name, _, _)| *name),
|
||||||
Order::Importance => rooms
|
Order::Importance => rooms.sort_unstable_by_key(|(name, state, unseen)| {
|
||||||
.sort_unstable_by_key(|(id, state, unseen)| (state.is_none(), *unseen == 0, *id)),
|
(state.is_none(), *unseen == 0, *name)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_rows(
|
async fn render_rows(
|
||||||
list_builder: &mut ListBuilder<'_, RoomIdentifier, Text>,
|
config: &Config,
|
||||||
|
list_builder: &mut ListBuilder<'_, String, Text>,
|
||||||
order: Order,
|
order: Order,
|
||||||
euph_rooms: &HashMap<RoomIdentifier, EuphRoom>,
|
euph_rooms: &HashMap<String, EuphRoom>,
|
||||||
) {
|
) {
|
||||||
|
if euph_rooms.is_empty() {
|
||||||
|
let style = Style::new().grey().italic();
|
||||||
|
list_builder.add_unsel(Text::new(
|
||||||
|
Styled::new("Press ", style)
|
||||||
|
.and_then(key_bindings::format_binding(&config.keys.general.help))
|
||||||
|
.then(" for key bindings", style),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut rooms = vec![];
|
let mut rooms = vec![];
|
||||||
for (id, room) in euph_rooms {
|
for (name, room) in euph_rooms {
|
||||||
let state = room.room_state();
|
let state = room.room_state();
|
||||||
let unseen = room.unseen_msgs_count().await;
|
let unseen = room.unseen_msgs_count().await;
|
||||||
rooms.push((id, state, unseen));
|
rooms.push((name, state, unseen));
|
||||||
}
|
}
|
||||||
Self::sort_rooms(&mut rooms, order);
|
Self::sort_rooms(&mut rooms, order);
|
||||||
for (id, state, unseen) in rooms {
|
for (name, state, unseen) in rooms {
|
||||||
let id = id.clone();
|
let name = name.clone();
|
||||||
let info = Self::format_room_info(state, unseen);
|
let info = Self::format_room_info(state, unseen);
|
||||||
list_builder.add_sel(id.clone(), move |selected| {
|
list_builder.add_sel(name.clone(), move |selected| {
|
||||||
let domain_style = if selected {
|
let style = if selected {
|
||||||
Style::new().black().on_white()
|
|
||||||
} else {
|
|
||||||
Style::new().grey()
|
|
||||||
};
|
|
||||||
|
|
||||||
let room_style = if selected {
|
|
||||||
Style::new().bold().black().on_white()
|
Style::new().bold().black().on_white()
|
||||||
} else {
|
} else {
|
||||||
Style::new().bold().blue()
|
Style::new().bold().blue()
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = Styled::new(format!("{} ", id.domain), domain_style)
|
let text = Styled::new(format!("&{name}"), style).and_then(info);
|
||||||
.then(format!("&{}", id.name), room_style)
|
|
||||||
.and_then(info);
|
|
||||||
|
|
||||||
Text::new(text)
|
Text::new(text)
|
||||||
});
|
});
|
||||||
|
|
@ -440,66 +394,29 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn rooms_widget<'a>(
|
async fn rooms_widget<'a>(
|
||||||
vault: &Vault,
|
|
||||||
config: &Config,
|
config: &Config,
|
||||||
list: &'a mut ListState<RoomIdentifier>,
|
list: &'a mut ListState<String>,
|
||||||
order: Order,
|
order: Order,
|
||||||
euph_rooms: &HashMap<RoomIdentifier, EuphRoom>,
|
euph_rooms: &HashMap<String, EuphRoom>,
|
||||||
) -> impl Widget<UiError> + use<'a> {
|
) -> impl Widget<UiError> + 'a {
|
||||||
let version_info = Styled::new_plain("Welcome to ")
|
let heading_style = Style::new().bold();
|
||||||
.then(format!("{NAME} {VERSION}"), Style::new().yellow().bold())
|
let heading_text =
|
||||||
.then_plain("!");
|
Styled::new("Rooms", heading_style).then_plain(format!(" ({})", euph_rooms.len()));
|
||||||
let help_info = Styled::new("Press ", Style::new().grey())
|
|
||||||
.and_then(key_bindings::format_binding(&config.keys.general.help))
|
|
||||||
.then(" for key bindings.", Style::new().grey());
|
|
||||||
let info = Join2::vertical(
|
|
||||||
Text::new(version_info).float().with_center_h().segment(),
|
|
||||||
Text::new(help_info).segment(),
|
|
||||||
)
|
|
||||||
.padding()
|
|
||||||
.with_horizontal(1)
|
|
||||||
.border();
|
|
||||||
|
|
||||||
let mut heading = Styled::new("Rooms", Style::new().bold());
|
|
||||||
let mut title = "Rooms".to_string();
|
|
||||||
|
|
||||||
let total_rooms = euph_rooms.len();
|
|
||||||
let connected_rooms = euph_rooms
|
|
||||||
.iter()
|
|
||||||
.filter(|r| r.1.room_state().is_some())
|
|
||||||
.count();
|
|
||||||
let total_unseen = logging_unwrap!(vault.euph().total_unseen_msgs_count().await);
|
|
||||||
if total_unseen > 0 {
|
|
||||||
heading = heading
|
|
||||||
.then_plain(format!(" ({connected_rooms}/{total_rooms}, "))
|
|
||||||
.then(format!("{total_unseen}"), Style::new().bold().green())
|
|
||||||
.then_plain(")");
|
|
||||||
title.push_str(&format!(" ({total_unseen})"));
|
|
||||||
} else {
|
|
||||||
heading = heading.then_plain(format!(" ({connected_rooms}/{total_rooms})"))
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut list_builder = ListBuilder::new();
|
let mut list_builder = ListBuilder::new();
|
||||||
Self::render_rows(&mut list_builder, order, euph_rooms).await;
|
Self::render_rows(config, &mut list_builder, order, euph_rooms).await;
|
||||||
|
|
||||||
Join2::horizontal(
|
Join2::vertical(
|
||||||
Join2::vertical(
|
Text::new(heading_text).segment().with_fixed(true),
|
||||||
Text::new(heading).segment().with_fixed(true),
|
list_builder.build(list).segment(),
|
||||||
list_builder.build(list).segment(),
|
|
||||||
)
|
|
||||||
.segment(),
|
|
||||||
Join2::vertical(info.segment().with_growing(false), Empty::new().segment())
|
|
||||||
.segment()
|
|
||||||
.with_growing(false),
|
|
||||||
)
|
)
|
||||||
.title(title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_showlist_input_event(
|
fn room_char(c: char) -> bool {
|
||||||
&mut self,
|
c.is_ascii_alphanumeric() || c == '_'
|
||||||
event: &mut InputEvent<'_>,
|
}
|
||||||
keys: &Keys,
|
|
||||||
) -> bool {
|
fn handle_showlist_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> bool {
|
||||||
// Open room
|
// Open room
|
||||||
if event.matches(&keys.general.confirm) {
|
if event.matches(&keys.general.confirm) {
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
|
|
@ -516,17 +433,17 @@ impl Rooms {
|
||||||
// Room actions
|
// Room actions
|
||||||
if event.matches(&keys.rooms.action.connect) {
|
if event.matches(&keys.rooms.action.connect) {
|
||||||
if let Some(name) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.connect_to_room(name.clone()).await;
|
self.connect_to_room(name.clone());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if event.matches(&keys.rooms.action.connect_all) {
|
if event.matches(&keys.rooms.action.connect_all) {
|
||||||
self.connect_to_all_rooms().await;
|
self.connect_to_all_rooms();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if event.matches(&keys.rooms.action.disconnect) {
|
if event.matches(&keys.rooms.action.disconnect) {
|
||||||
if let Some(room) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.disconnect_from_room(&room.clone());
|
self.disconnect_from_room(&name.clone());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -535,20 +452,22 @@ impl Rooms {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
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 (name, options) in &self.config.euph.rooms {
|
||||||
for (name, room) in &server.rooms {
|
if options.autojoin {
|
||||||
if !room.autojoin {
|
self.connect_to_room(name.clone());
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let id = RoomIdentifier::new(domain.clone(), name.clone());
|
|
||||||
self.connect_to_room(id).await;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if event.matches(&keys.rooms.action.disconnect_non_autojoin) {
|
if event.matches(&keys.rooms.action.disconnect_non_autojoin) {
|
||||||
for (id, room) in &mut self.euph_rooms {
|
for (name, room) in &mut self.euph_rooms {
|
||||||
let autojoin = self.config.euph_room(&id.domain, &id.name).autojoin;
|
let autojoin = self
|
||||||
|
.config
|
||||||
|
.euph
|
||||||
|
.rooms
|
||||||
|
.get(name)
|
||||||
|
.map(|r| r.autojoin)
|
||||||
|
.unwrap_or(false);
|
||||||
if !autojoin {
|
if !autojoin {
|
||||||
room.disconnect();
|
room.disconnect();
|
||||||
}
|
}
|
||||||
|
|
@ -556,12 +475,12 @@ impl Rooms {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if event.matches(&keys.rooms.action.new) {
|
if event.matches(&keys.rooms.action.new) {
|
||||||
self.state = State::Connect(ConnectState::new());
|
self.state = State::Connect(EditorState::new());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if event.matches(&keys.rooms.action.delete) {
|
if event.matches(&keys.rooms.action.delete) {
|
||||||
if let Some(room) = self.list.selected() {
|
if let Some(name) = self.list.selected() {
|
||||||
self.state = State::Delete(DeleteState::new(room.clone()));
|
self.state = State::Delete(name.clone(), EditorState::new());
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -581,21 +500,14 @@ impl Rooms {
|
||||||
|
|
||||||
match &mut self.state {
|
match &mut self.state {
|
||||||
State::ShowList => {
|
State::ShowList => {
|
||||||
if self.handle_showlist_input_event(event, keys).await {
|
if self.handle_showlist_input_event(event, keys) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
match room.handle_input_event(event, keys).await {
|
if room.handle_input_event(event, keys).await {
|
||||||
RoomResult::NotHandled => {}
|
return true;
|
||||||
RoomResult::Handled => return true,
|
|
||||||
RoomResult::SwitchToRoom { room } => {
|
|
||||||
self.list.move_cursor_to_id(&room);
|
|
||||||
self.connect_to_room(room.clone()).await;
|
|
||||||
self.state = State::ShowRoom(room);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if event.matches(&keys.general.abort) {
|
if event.matches(&keys.general.abort) {
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
|
|
@ -603,54 +515,53 @@ impl Rooms {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
State::Connect(connect) => match connect.handle_input_event(event, keys) {
|
State::Connect(editor) => {
|
||||||
ConnectResult::Close => {
|
if event.matches(&keys.general.abort) {
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ConnectResult::Connect(room) => {
|
if event.matches(&keys.general.confirm) {
|
||||||
self.list.move_cursor_to_id(&room);
|
let name = editor.text().to_string();
|
||||||
self.connect_to_room(room.clone()).await;
|
if !name.is_empty() {
|
||||||
self.state = State::ShowRoom(room);
|
self.connect_to_room(name.clone());
|
||||||
|
self.state = State::ShowRoom(name);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ConnectResult::Handled => {
|
if util::handle_editor_input_event(editor, event, keys, Self::room_char) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ConnectResult::Unhandled => {}
|
}
|
||||||
},
|
State::Delete(name, editor) => {
|
||||||
State::Delete(delete) => match delete.handle_input_event(event, keys) {
|
if event.matches(&keys.general.abort) {
|
||||||
DeleteResult::Close => {
|
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
DeleteResult::Delete(room) => {
|
if event.matches(&keys.general.confirm) {
|
||||||
self.euph_rooms.remove(&room);
|
self.euph_rooms.remove(name);
|
||||||
logging_unwrap!(self.vault.euph().room(room).delete().await);
|
logging_unwrap!(self.vault.euph().room(name.clone()).delete().await);
|
||||||
self.state = State::ShowList;
|
self.state = State::ShowList;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
DeleteResult::Handled => {
|
if util::handle_editor_input_event(editor, event, keys, Self::room_char) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
DeleteResult::Unhandled => {}
|
}
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_euph_event(&mut self, event: Event) -> bool {
|
pub async fn handle_euph_event(&mut self, event: Event) -> bool {
|
||||||
let config = event.config();
|
let room_name = event.config().room.clone();
|
||||||
let room_id = RoomIdentifier::new(config.server.domain.clone(), config.room.clone());
|
let Some(room) = self.euph_rooms.get_mut(&room_name) else {
|
||||||
let Some(room) = self.euph_rooms.get_mut(&room_id) else {
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let handled = room.handle_event(event).await;
|
let handled = room.handle_event(event).await;
|
||||||
|
|
||||||
let room_visible = match &self.state {
|
let room_visible = match &self.state {
|
||||||
State::ShowRoom(id) => *id == room_id,
|
State::ShowRoom(name) => *name == room_name,
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
handled && room_visible
|
handled && room_visible
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
use cove_config::Keys;
|
|
||||||
use cove_input::InputEvent;
|
|
||||||
use crossterm::style::Stylize;
|
|
||||||
use toss::{
|
|
||||||
Style, Styled, Widget, WidgetExt,
|
|
||||||
widgets::{EditorState, Empty, Join2, Join3, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ui::{UiError, util, widgets::Popup},
|
|
||||||
vault::RoomIdentifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum Focus {
|
|
||||||
Name,
|
|
||||||
Domain,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Focus {
|
|
||||||
fn advance(self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Name => Self::Domain,
|
|
||||||
Self::Domain => Self::Name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectState {
|
|
||||||
focus: Focus,
|
|
||||||
name: EditorState,
|
|
||||||
domain: EditorState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ConnectResult {
|
|
||||||
Close,
|
|
||||||
Connect(RoomIdentifier),
|
|
||||||
Handled,
|
|
||||||
Unhandled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
focus: Focus::Name,
|
|
||||||
name: EditorState::new(),
|
|
||||||
domain: EditorState::with_initial_text("euphoria.leet.nu".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> ConnectResult {
|
|
||||||
if event.matches(&keys.general.abort) {
|
|
||||||
return ConnectResult::Close;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.matches(&keys.general.focus) {
|
|
||||||
self.focus = self.focus.advance();
|
|
||||||
return ConnectResult::Handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.matches(&keys.general.confirm) {
|
|
||||||
let id = RoomIdentifier {
|
|
||||||
domain: self.domain.text().to_string(),
|
|
||||||
name: self.name.text().to_string(),
|
|
||||||
};
|
|
||||||
if !id.domain.is_empty() && !id.name.is_empty() {
|
|
||||||
return ConnectResult::Connect(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handled = match self.focus {
|
|
||||||
Focus::Name => {
|
|
||||||
util::handle_editor_input_event(&mut self.name, event, keys, util::is_room_char)
|
|
||||||
}
|
|
||||||
Focus::Domain => {
|
|
||||||
util::handle_editor_input_event(&mut self.domain, event, keys, |c| c != '\n')
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if handled {
|
|
||||||
return ConnectResult::Handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectResult::Unhandled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn widget(&mut self) -> impl Widget<UiError> {
|
|
||||||
let room_style = Style::new().bold().blue();
|
|
||||||
let domain_style = Style::new().grey();
|
|
||||||
|
|
||||||
let name = Join2::horizontal(
|
|
||||||
Text::new(Styled::new_plain("Room: ").then("&", room_style))
|
|
||||||
.with_wrap(false)
|
|
||||||
.segment()
|
|
||||||
.with_fixed(true),
|
|
||||||
self.name
|
|
||||||
.widget()
|
|
||||||
.with_highlight(|s| Styled::new(s, room_style))
|
|
||||||
.with_focus(self.focus == Focus::Name)
|
|
||||||
.segment(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let domain = Join3::horizontal(
|
|
||||||
Text::new("Domain:")
|
|
||||||
.with_wrap(false)
|
|
||||||
.segment()
|
|
||||||
.with_fixed(true),
|
|
||||||
Empty::new().with_width(1).segment().with_fixed(true),
|
|
||||||
self.domain
|
|
||||||
.widget()
|
|
||||||
.with_highlight(|s| Styled::new(s, domain_style))
|
|
||||||
.with_focus(self.focus == Focus::Domain)
|
|
||||||
.segment(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let inner = Join2::vertical(
|
|
||||||
name.segment().with_fixed(true),
|
|
||||||
domain.segment().with_fixed(true),
|
|
||||||
);
|
|
||||||
|
|
||||||
Popup::new(inner, "Connect to")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
use cove_config::Keys;
|
|
||||||
use cove_input::InputEvent;
|
|
||||||
use crossterm::style::Stylize;
|
|
||||||
use toss::{
|
|
||||||
Style, Styled, Widget, WidgetExt,
|
|
||||||
widgets::{EditorState, Empty, Join2, Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ui::{UiError, util, widgets::Popup},
|
|
||||||
vault::RoomIdentifier,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DeleteState {
|
|
||||||
id: RoomIdentifier,
|
|
||||||
name: EditorState,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum DeleteResult {
|
|
||||||
Close,
|
|
||||||
Delete(RoomIdentifier),
|
|
||||||
Handled,
|
|
||||||
Unhandled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeleteState {
|
|
||||||
pub fn new(id: RoomIdentifier) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
name: EditorState::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_input_event(&mut self, event: &mut InputEvent<'_>, keys: &Keys) -> DeleteResult {
|
|
||||||
if event.matches(&keys.general.abort) {
|
|
||||||
return DeleteResult::Close;
|
|
||||||
}
|
|
||||||
|
|
||||||
if event.matches(&keys.general.confirm) && self.name.text() == self.id.name {
|
|
||||||
return DeleteResult::Delete(self.id.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if util::handle_editor_input_event(&mut self.name, event, keys, util::is_room_char) {
|
|
||||||
return DeleteResult::Handled;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteResult::Unhandled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn widget(&mut self) -> impl Widget<UiError> {
|
|
||||||
let warn_style = Style::new().bold().red();
|
|
||||||
let room_style = Style::new().bold().blue();
|
|
||||||
let text = Styled::new_plain("Are you sure you want to delete ")
|
|
||||||
.then("&", room_style)
|
|
||||||
.then(&self.id.name, room_style)
|
|
||||||
.then_plain(" on the ")
|
|
||||||
.then(&self.id.domain, Style::new().grey())
|
|
||||||
.then_plain(" server?\n\n")
|
|
||||||
.then_plain("This will delete the entire room history from your vault. ")
|
|
||||||
.then_plain("To shrink your vault afterwards, run ")
|
|
||||||
.then("cove gc", Style::new().italic().grey())
|
|
||||||
.then_plain(".\n\n")
|
|
||||||
.then_plain("To confirm the deletion, ")
|
|
||||||
.then_plain("enter the full name of the room and press enter:");
|
|
||||||
|
|
||||||
let inner = Join2::vertical(
|
|
||||||
// The Join prevents the text from filling up the entire available
|
|
||||||
// space if the editor is wider than the text.
|
|
||||||
Join2::horizontal(
|
|
||||||
Text::new(text)
|
|
||||||
.resize()
|
|
||||||
.with_max_width(54)
|
|
||||||
.segment()
|
|
||||||
.with_growing(false),
|
|
||||||
Empty::new().segment(),
|
|
||||||
)
|
|
||||||
.segment(),
|
|
||||||
Join2::horizontal(
|
|
||||||
Text::new(("&", room_style)).segment().with_fixed(true),
|
|
||||||
self.name
|
|
||||||
.widget()
|
|
||||||
.with_highlight(|s| Styled::new(s, room_style))
|
|
||||||
.segment(),
|
|
||||||
)
|
|
||||||
.segment(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Popup::new(inner, "Delete room").with_border_style(warn_style)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,11 +5,6 @@ use toss::widgets::EditorState;
|
||||||
|
|
||||||
use super::widgets::ListState;
|
use super::widgets::ListState;
|
||||||
|
|
||||||
/// Test if a character is allowed to be typed in a room name.
|
|
||||||
pub fn is_room_char(c: char) -> bool {
|
|
||||||
c.is_ascii_alphanumeric() || c == '_'
|
|
||||||
}
|
|
||||||
|
|
||||||
//////////
|
//////////
|
||||||
// List //
|
// List //
|
||||||
//////////
|
//////////
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pub use self::list::*;
|
|
||||||
pub use self::popup::*;
|
|
||||||
|
|
||||||
mod list;
|
mod list;
|
||||||
mod popup;
|
mod popup;
|
||||||
|
|
||||||
|
pub use self::list::*;
|
||||||
|
pub use self::popup::*;
|
||||||
|
|
|
||||||
|
|
@ -239,12 +239,6 @@ impl<Id: Clone + Eq> ListState<Id> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_to_id(&mut self, id: &Id) {
|
|
||||||
if let Some(new_cursor) = self.selectable_of_id(id) {
|
|
||||||
self.move_cursor_to(new_cursor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fix_cursor(&mut self) {
|
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)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
use toss::{
|
use toss::widgets::{Background, Border, Desync, Float, Layer2, Padding, Text};
|
||||||
Frame, Size, Style, Styled, Widget, WidgetExt, WidthDb,
|
use toss::{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>>>>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use std::{convert::Infallible, env};
|
use std::convert::Infallible;
|
||||||
|
|
||||||
use jiff::tz::TimeZone;
|
|
||||||
|
|
||||||
pub trait InfallibleExt {
|
pub trait InfallibleExt {
|
||||||
type Inner;
|
type Inner;
|
||||||
|
|
@ -15,56 +13,3 @@ impl<T> InfallibleExt for Result<T, Infallible> {
|
||||||
self.expect("infallible")
|
self.expect("infallible")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a [`TimeZone`] specified by the `TZ` environment varible, or by the
|
|
||||||
/// provided string if the environment variable does not exist.
|
|
||||||
///
|
|
||||||
/// If a string is provided, it is interpreted in the same format that the `TZ`
|
|
||||||
/// environment variable uses.
|
|
||||||
///
|
|
||||||
/// If no `TZ` environment variable could be found and no string is provided,
|
|
||||||
/// the system local time (or UTC on Windows) is used.
|
|
||||||
pub fn load_time_zone(tz_string: Option<&str>) -> Result<TimeZone, jiff::Error> {
|
|
||||||
let env_string = env::var("TZ").ok();
|
|
||||||
let tz_string = env_string.as_ref().map(|s| s as &str).or(tz_string);
|
|
||||||
|
|
||||||
let Some(tz_string) = tz_string else {
|
|
||||||
return Ok(TimeZone::system());
|
|
||||||
};
|
|
||||||
|
|
||||||
if tz_string == "localtime" {
|
|
||||||
return Ok(TimeZone::system());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(tz_string) = tz_string.strip_prefix(':') {
|
|
||||||
return TimeZone::get(tz_string);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The time zone is either a manually specified string or a file in the tz
|
|
||||||
// database. We'll try to parse it as a manually specified string first
|
|
||||||
// because that doesn't require a fs lookup.
|
|
||||||
if let Ok(tz) = TimeZone::posix(tz_string) {
|
|
||||||
return Ok(tz);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeZone::get(tz_string)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn caesar(text: &str, by: i8) -> String {
|
|
||||||
let by = by.rem_euclid(26) as u8;
|
|
||||||
text.chars()
|
|
||||||
.map(|c| {
|
|
||||||
if c.is_ascii_lowercase() {
|
|
||||||
let c = c as u8 - b'a';
|
|
||||||
let c = (c + by) % 26;
|
|
||||||
(c + b'a') as char
|
|
||||||
} else if c.is_ascii_uppercase() {
|
|
||||||
let c = c as u8 - b'A';
|
|
||||||
let c = (c + by) % 26;
|
|
||||||
(c + b'A') as char
|
|
||||||
} else {
|
|
||||||
c
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,16 @@
|
||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
use rusqlite::Connection;
|
|
||||||
use vault::{Action, tokio::TokioVault};
|
|
||||||
|
|
||||||
pub use self::euph::{EuphRoomVault, EuphVault, RoomIdentifier};
|
|
||||||
|
|
||||||
mod euph;
|
mod 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};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Vault {
|
pub struct Vault {
|
||||||
tokio_vault: TokioVault,
|
tokio_vault: TokioVault,
|
||||||
|
|
@ -48,6 +50,8 @@ fn launch_from_connection(conn: Connection, ephemeral: bool) -> rusqlite::Result
|
||||||
conn.pragma_update(None, "foreign_keys", true)?;
|
conn.pragma_update(None, "foreign_keys", true)?;
|
||||||
conn.pragma_update(None, "trusted_schema", false)?;
|
conn.pragma_update(None, "trusted_schema", false)?;
|
||||||
|
|
||||||
|
eprintln!("Opening vault");
|
||||||
|
|
||||||
let tokio_vault = TokioVault::launch_and_prepare(conn, &migrate::MIGRATIONS, prepare::prepare)?;
|
let tokio_vault = TokioVault::launch_and_prepare(conn, &migrate::MIGRATIONS, prepare::prepare)?;
|
||||||
Ok(Vault {
|
Ok(Vault {
|
||||||
tokio_vault,
|
tokio_vault,
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
use std::{fmt, mem, str::FromStr};
|
use std::mem;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
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::{
|
use rusqlite::types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef};
|
||||||
Connection, OptionalExtension, Row, ToSql, Transaction, named_params, params,
|
use rusqlite::{named_params, params, Connection, OptionalExtension, Row, ToSql, Transaction};
|
||||||
types::{FromSql, FromSqlError, ToSqlOutput, Value, ValueRef},
|
use time::OffsetDateTime;
|
||||||
};
|
|
||||||
use vault::Action;
|
use vault::Action;
|
||||||
|
|
||||||
use crate::{
|
use crate::euph::SmallMessage;
|
||||||
euph::SmallMessage,
|
use crate::store::{MsgStore, Path, Tree};
|
||||||
store::{MsgStore, Path, Tree},
|
|
||||||
};
|
///////////////////
|
||||||
|
// Wrapper types //
|
||||||
|
///////////////////
|
||||||
|
|
||||||
/// 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +36,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.unix_timestamp();
|
||||||
Ok(ToSqlOutput::Owned(Value::Integer(timestamp)))
|
Ok(ToSqlOutput::Owned(Value::Integer(timestamp)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,25 +44,9 @@ impl ToSql for WTime {
|
||||||
impl FromSql for WTime {
|
impl FromSql for WTime {
|
||||||
fn column_result(value: ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
fn column_result(value: ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||||
let timestamp = i64::column_result(value)?;
|
let timestamp = i64::column_result(value)?;
|
||||||
Ok(Self(Time(timestamp)))
|
Ok(Self(Time(
|
||||||
}
|
OffsetDateTime::from_unix_timestamp(timestamp).expect("timestamp in range"),
|
||||||
}
|
)))
|
||||||
|
|
||||||
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub struct RoomIdentifier {
|
|
||||||
pub domain: String,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for RoomIdentifier {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "&{}@{}", self.name, self.domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RoomIdentifier {
|
|
||||||
pub fn new(domain: String, name: String) -> Self {
|
|
||||||
Self { domain, name }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,10 +68,10 @@ impl EuphVault {
|
||||||
&self.vault
|
&self.vault
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room(&self, room: RoomIdentifier) -> EuphRoomVault {
|
pub fn room(&self, name: String) -> EuphRoomVault {
|
||||||
EuphRoomVault {
|
EuphRoomVault {
|
||||||
vault: self.clone(),
|
vault: self.clone(),
|
||||||
room,
|
room: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,11 +97,9 @@ macro_rules! euph_vault_actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
euph_vault_actions! {
|
euph_vault_actions! {
|
||||||
GetCookies : cookies(domain: String) -> CookieJar;
|
GetCookies : cookies() -> CookieJar;
|
||||||
SetCookies : set_cookies(domain: String, cookies: CookieJar) -> ();
|
SetCookies : set_cookies(cookies: CookieJar) -> ();
|
||||||
ClearCookies : clear_cookies(domain: Option<String>) -> ();
|
GetRooms : rooms() -> Vec<String>;
|
||||||
GetRooms : rooms() -> Vec<RoomIdentifier>;
|
|
||||||
GetTotalUnseenMsgsCount : total_unseen_msgs_count() -> usize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action for GetCookies {
|
impl Action for GetCookies {
|
||||||
|
|
@ -128,10 +112,9 @@ impl Action for GetCookies {
|
||||||
"
|
"
|
||||||
SELECT cookie
|
SELECT cookie
|
||||||
FROM euph_cookies
|
FROM euph_cookies
|
||||||
WHERE domain = ?
|
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_map([self.domain], |row| {
|
.query_map([], |row| {
|
||||||
let cookie_str: String = row.get(0)?;
|
let cookie_str: String = row.get(0)?;
|
||||||
Ok(Cookie::from_str(&cookie_str).expect("cookie in db is valid"))
|
Ok(Cookie::from_str(&cookie_str).expect("cookie in db is valid"))
|
||||||
})?
|
})?
|
||||||
|
|
@ -154,21 +137,16 @@ impl Action for SetCookies {
|
||||||
|
|
||||||
// Since euphoria sets all cookies on every response, we can just delete
|
// Since euphoria sets all cookies on every response, we can just delete
|
||||||
// all previous cookies.
|
// all previous cookies.
|
||||||
tx.execute(
|
tx.execute_batch("DELETE FROM euph_cookies")?;
|
||||||
"
|
|
||||||
DELETE FROM euph_cookies
|
|
||||||
WHERE domain = ?",
|
|
||||||
[&self.domain],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut insert_cookie = tx.prepare(
|
let mut insert_cookie = tx.prepare(
|
||||||
"
|
"
|
||||||
INSERT INTO euph_cookies (domain, cookie)
|
INSERT INTO euph_cookies (cookie)
|
||||||
VALUES (?, ?)
|
VALUES (?)
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
for cookie in self.cookies.iter() {
|
for cookie in self.cookies.iter() {
|
||||||
insert_cookie.execute(params![self.domain, format!("{cookie}")])?;
|
insert_cookie.execute([format!("{cookie}")])?;
|
||||||
}
|
}
|
||||||
drop(insert_cookie);
|
drop(insert_cookie);
|
||||||
|
|
||||||
|
|
@ -177,57 +155,22 @@ impl Action for SetCookies {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action for ClearCookies {
|
|
||||||
type Output = ();
|
|
||||||
type Error = rusqlite::Error;
|
|
||||||
|
|
||||||
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error> {
|
|
||||||
if let Some(domain) = self.domain {
|
|
||||||
conn.execute("DELETE FROM euph_cookies WHERE domain = ?", [domain])?;
|
|
||||||
} else {
|
|
||||||
conn.execute_batch("DELETE FROM euph_cookies")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Action for GetRooms {
|
impl Action for GetRooms {
|
||||||
type Output = Vec<RoomIdentifier>;
|
type Output = Vec<String>;
|
||||||
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> {
|
||||||
conn.prepare(
|
conn.prepare(
|
||||||
"
|
"
|
||||||
SELECT domain, room
|
SELECT room
|
||||||
FROM euph_rooms
|
FROM euph_rooms
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_map([], |row| {
|
.query_map([], |row| row.get(0))?
|
||||||
Ok(RoomIdentifier {
|
|
||||||
domain: row.get(0)?,
|
|
||||||
name: row.get(1)?,
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.collect::<rusqlite::Result<_>>()
|
.collect::<rusqlite::Result<_>>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action for GetTotalUnseenMsgsCount {
|
|
||||||
type Output = usize;
|
|
||||||
type Error = rusqlite::Error;
|
|
||||||
|
|
||||||
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error> {
|
|
||||||
conn.prepare(
|
|
||||||
"
|
|
||||||
SELECT COALESCE(SUM(amount), 0)
|
|
||||||
FROM euph_unseen_counts
|
|
||||||
",
|
|
||||||
)?
|
|
||||||
.query_row([], |row| row.get(0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// EuphRoomVault //
|
// EuphRoomVault //
|
||||||
///////////////////
|
///////////////////
|
||||||
|
|
@ -235,7 +178,7 @@ impl Action for GetTotalUnseenMsgsCount {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct EuphRoomVault {
|
pub struct EuphRoomVault {
|
||||||
vault: EuphVault,
|
vault: EuphVault,
|
||||||
room: RoomIdentifier,
|
room: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EuphRoomVault {
|
impl EuphRoomVault {
|
||||||
|
|
@ -243,7 +186,7 @@ impl EuphRoomVault {
|
||||||
&self.vault
|
&self.vault
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn room(&self) -> &RoomIdentifier {
|
pub fn room(&self) -> &str {
|
||||||
&self.room
|
&self.room
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -254,7 +197,7 @@ macro_rules! euph_room_vault_actions {
|
||||||
)* ) => {
|
)* ) => {
|
||||||
$(
|
$(
|
||||||
struct $struct {
|
struct $struct {
|
||||||
room: RoomIdentifier,
|
room: String,
|
||||||
$( $arg: $arg_ty, )*
|
$( $arg: $arg_ty, )*
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
|
|
@ -310,16 +253,12 @@ impl Action for Join {
|
||||||
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error> {
|
fn run(self, conn: &mut Connection) -> Result<Self::Output, Self::Error> {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"
|
"
|
||||||
INSERT INTO euph_rooms (domain, room, first_joined, last_joined)
|
INSERT INTO euph_rooms (room, first_joined, last_joined)
|
||||||
VALUES (:domain, :room, :time, :time)
|
VALUES (:room, :time, :time)
|
||||||
ON CONFLICT (domain, room) DO UPDATE
|
ON CONFLICT (room) DO UPDATE
|
||||||
SET last_joined = :time
|
SET last_joined = :time
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! {":room": self.room, ":time": WTime(self.time)},
|
||||||
":domain": self.room.domain,
|
|
||||||
":room": self.room.name,
|
|
||||||
":time": WTime(self.time),
|
|
||||||
},
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -333,10 +272,9 @@ impl Action for Delete {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"
|
"
|
||||||
DELETE FROM euph_rooms
|
DELETE FROM euph_rooms
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
",
|
",
|
||||||
[&self.room.domain, &self.room.name],
|
[&self.room],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -344,33 +282,29 @@ impl Action for Delete {
|
||||||
|
|
||||||
fn insert_msgs(
|
fn insert_msgs(
|
||||||
tx: &Transaction<'_>,
|
tx: &Transaction<'_>,
|
||||||
room: &RoomIdentifier,
|
room: &str,
|
||||||
own_user_id: &Option<UserId>,
|
own_user_id: &Option<UserId>,
|
||||||
msgs: Vec<Message>,
|
msgs: Vec<Message>,
|
||||||
) -> rusqlite::Result<()> {
|
) -> rusqlite::Result<()> {
|
||||||
let mut insert_msg = tx.prepare(
|
let mut insert_msg = tx.prepare(
|
||||||
"
|
"
|
||||||
INSERT INTO euph_msgs (
|
INSERT INTO euph_msgs (
|
||||||
domain, room,
|
room, id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address,
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address,
|
||||||
seen
|
seen
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
:domain, :room,
|
:room, :id, :parent, :previous_edit_id, :time, :content, :encryption_key_id, :edited, :deleted, :truncated,
|
||||||
:id, :parent, :previous_edit_id, :time, :content, :encryption_key_id, :edited, :deleted, :truncated,
|
|
||||||
:user_id, :name, :server_id, :server_era, :session_id, :is_staff, :is_manager, :client_address, :real_client_address,
|
:user_id, :name, :server_id, :server_era, :session_id, :is_staff, :is_manager, :client_address, :real_client_address,
|
||||||
(:user_id == :own_user_id OR EXISTS(
|
(:user_id == :own_user_id OR EXISTS(
|
||||||
SELECT 1
|
SELECT 1
|
||||||
FROM euph_rooms
|
FROM euph_rooms
|
||||||
WHERE domain = :domain
|
WHERE room = :room
|
||||||
AND room = :room
|
|
||||||
AND :time < first_joined
|
AND :time < first_joined
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
ON CONFLICT (domain, room, id) DO UPDATE
|
ON CONFLICT (room, id) DO UPDATE
|
||||||
SET
|
SET
|
||||||
domain = :domain,
|
|
||||||
room = :room,
|
room = :room,
|
||||||
id = :id,
|
id = :id,
|
||||||
parent = :parent,
|
parent = :parent,
|
||||||
|
|
@ -397,8 +331,7 @@ fn insert_msgs(
|
||||||
let own_user_id = own_user_id.as_ref().map(|u| &u.0);
|
let own_user_id = own_user_id.as_ref().map(|u| &u.0);
|
||||||
for msg in msgs {
|
for msg in msgs {
|
||||||
insert_msg.execute(named_params! {
|
insert_msg.execute(named_params! {
|
||||||
":domain": room.domain,
|
":room": room,
|
||||||
":room": room.name,
|
|
||||||
":id": WSnowflake(msg.id.0),
|
":id": WSnowflake(msg.id.0),
|
||||||
":parent": msg.parent.map(|id| WSnowflake(id.0)),
|
":parent": msg.parent.map(|id| WSnowflake(id.0)),
|
||||||
":previous_edit_id": msg.previous_edit_id.map(WSnowflake),
|
":previous_edit_id": msg.previous_edit_id.map(WSnowflake),
|
||||||
|
|
@ -426,7 +359,7 @@ fn insert_msgs(
|
||||||
|
|
||||||
fn add_span(
|
fn add_span(
|
||||||
tx: &Transaction<'_>,
|
tx: &Transaction<'_>,
|
||||||
room: &RoomIdentifier,
|
room: &str,
|
||||||
start: Option<MessageId>,
|
start: Option<MessageId>,
|
||||||
end: Option<MessageId>,
|
end: Option<MessageId>,
|
||||||
) -> rusqlite::Result<()> {
|
) -> rusqlite::Result<()> {
|
||||||
|
|
@ -436,11 +369,10 @@ fn add_span(
|
||||||
"
|
"
|
||||||
SELECT start, end
|
SELECT start, end
|
||||||
FROM euph_spans
|
FROM euph_spans
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_map([&room.domain, &room.name], |row| {
|
.query_map([room], |row| {
|
||||||
let start = row.get::<_, Option<WSnowflake>>(0)?.map(|s| MessageId(s.0));
|
let start = row.get::<_, Option<WSnowflake>>(0)?.map(|s| MessageId(s.0));
|
||||||
let end = row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0));
|
let end = row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0));
|
||||||
Ok((start, end))
|
Ok((start, end))
|
||||||
|
|
@ -480,23 +412,21 @@ fn add_span(
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"
|
"
|
||||||
DELETE FROM euph_spans
|
DELETE FROM euph_spans
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
",
|
",
|
||||||
[&room.domain, &room.name],
|
[room],
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Re-insert combined spans for the room
|
// Re-insert combined spans for the room
|
||||||
let mut stmt = tx.prepare(
|
let mut stmt = tx.prepare(
|
||||||
"
|
"
|
||||||
INSERT INTO euph_spans (domain, room, start, end)
|
INSERT INTO euph_spans (room, start, end)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
for (start, end) in result {
|
for (start, end) in result {
|
||||||
stmt.execute(params![
|
stmt.execute(params![
|
||||||
room.domain,
|
room,
|
||||||
room.name,
|
|
||||||
start.map(|id| WSnowflake(id.0)),
|
start.map(|id| WSnowflake(id.0)),
|
||||||
end.map(|id| WSnowflake(id.0))
|
end.map(|id| WSnowflake(id.0))
|
||||||
])?;
|
])?;
|
||||||
|
|
@ -555,13 +485,12 @@ impl Action for GetLastSpan {
|
||||||
"
|
"
|
||||||
SELECT start, end
|
SELECT start, end
|
||||||
FROM euph_spans
|
FROM euph_spans
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY start DESC
|
ORDER BY start DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
Ok((
|
Ok((
|
||||||
row.get::<_, Option<WSnowflake>>(0)?.map(|s| MessageId(s.0)),
|
row.get::<_, Option<WSnowflake>>(0)?.map(|s| MessageId(s.0)),
|
||||||
row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
||||||
|
|
@ -581,12 +510,12 @@ impl Action for GetPath {
|
||||||
.prepare(
|
.prepare(
|
||||||
"
|
"
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
path (domain, room, id) AS (
|
path (room, id) AS (
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?)
|
||||||
UNION
|
UNION
|
||||||
SELECT domain, room, parent
|
SELECT room, parent
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
JOIN path USING (domain, room, id)
|
JOIN path USING (room, id)
|
||||||
)
|
)
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM path
|
FROM path
|
||||||
|
|
@ -594,10 +523,9 @@ impl Action for GetPath {
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_map(
|
.query_map(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})?
|
||||||
)?
|
|
||||||
.collect::<rusqlite::Result<_>>()?;
|
.collect::<rusqlite::Result<_>>()?;
|
||||||
Ok(Path::new(path))
|
Ok(Path::new(path))
|
||||||
}
|
}
|
||||||
|
|
@ -611,22 +539,20 @@ impl Action for GetMsg {
|
||||||
let msg = conn
|
let msg = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"
|
"
|
||||||
SELECT id, parent, time, user_id, name, content, seen
|
SELECT id, parent, time, name, content, seen
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id = ?
|
AND id = ?
|
||||||
",
|
",
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
params![self.room, WSnowflake(self.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,
|
||||||
user_id: UserId(row.get(3)?),
|
nick: row.get(3)?,
|
||||||
nick: row.get(4)?,
|
content: row.get(4)?,
|
||||||
content: row.get(5)?,
|
seen: row.get(5)?,
|
||||||
seen: row.get(6)?,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -646,40 +572,36 @@ impl Action for GetFullMsg {
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id = ?
|
AND id = ?
|
||||||
"
|
"
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let msg = query
|
let msg = query
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
Ok(Message {
|
||||||
|row| {
|
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
||||||
Ok(Message {
|
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
||||||
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
previous_edit_id: row.get::<_, Option<WSnowflake>>(2)?.map(|s| s.0),
|
||||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
time: row.get::<_, WTime>(3)?.0,
|
||||||
previous_edit_id: row.get::<_, Option<WSnowflake>>(2)?.map(|s| s.0),
|
content: row.get(4)?,
|
||||||
time: row.get::<_, WTime>(3)?.0,
|
encryption_key_id: row.get(5)?,
|
||||||
content: row.get(4)?,
|
edited: row.get::<_, Option<WTime>>(6)?.map(|t| t.0),
|
||||||
encryption_key_id: row.get(5)?,
|
deleted: row.get::<_, Option<WTime>>(7)?.map(|t| t.0),
|
||||||
edited: row.get::<_, Option<WTime>>(6)?.map(|t| t.0),
|
truncated: row.get(8)?,
|
||||||
deleted: row.get::<_, Option<WTime>>(7)?.map(|t| t.0),
|
sender: SessionView {
|
||||||
truncated: row.get(8)?,
|
id: UserId(row.get(9)?),
|
||||||
sender: SessionView {
|
name: row.get(10)?,
|
||||||
id: UserId(row.get(9)?),
|
server_id: row.get(11)?,
|
||||||
name: row.get(10)?,
|
server_era: row.get(12)?,
|
||||||
server_id: row.get(11)?,
|
session_id: SessionId(row.get(13)?),
|
||||||
server_era: row.get(12)?,
|
is_staff: row.get(14)?,
|
||||||
session_id: SessionId(row.get(13)?),
|
is_manager: row.get(15)?,
|
||||||
is_staff: row.get(14)?,
|
client_address: row.get(16)?,
|
||||||
is_manager: row.get(15)?,
|
real_client_address: row.get(17)?,
|
||||||
client_address: row.get(16)?,
|
},
|
||||||
real_client_address: row.get(17)?,
|
})
|
||||||
},
|
})
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(msg)
|
Ok(msg)
|
||||||
}
|
}
|
||||||
|
|
@ -694,36 +616,31 @@ impl Action for GetTree {
|
||||||
.prepare(
|
.prepare(
|
||||||
"
|
"
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
tree (domain, room, id) AS (
|
tree (room, id) AS (
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?)
|
||||||
UNION
|
UNION
|
||||||
SELECT euph_msgs.domain, euph_msgs.room, euph_msgs.id
|
SELECT euph_msgs.room, euph_msgs.id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
JOIN tree
|
JOIN tree
|
||||||
ON tree.domain = euph_msgs.domain
|
ON 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, user_id, name, content, seen
|
SELECT id, parent, time, name, content, seen
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
JOIN tree USING (domain, room, id)
|
JOIN tree USING (room, id)
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_map(
|
.query_map(params![self.room, WSnowflake(self.root_id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.root_id.0)],
|
Ok(SmallMessage {
|
||||||
|row| {
|
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
||||||
Ok(SmallMessage {
|
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
||||||
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
time: row.get::<_, WTime>(2)?.0,
|
||||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
nick: row.get(3)?,
|
||||||
time: row.get::<_, WTime>(2)?.0,
|
content: row.get(4)?,
|
||||||
user_id: UserId(row.get(3)?),
|
seen: row.get(5)?,
|
||||||
nick: row.get(4)?,
|
})
|
||||||
content: row.get(5)?,
|
})?
|
||||||
seen: row.get(6)?,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)?
|
|
||||||
.collect::<rusqlite::Result<_>>()?;
|
.collect::<rusqlite::Result<_>>()?;
|
||||||
Ok(Tree::new(self.root_id, msgs))
|
Ok(Tree::new(self.root_id, msgs))
|
||||||
}
|
}
|
||||||
|
|
@ -739,13 +656,12 @@ impl Action for GetFirstRootId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_trees
|
FROM euph_trees
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -763,13 +679,12 @@ impl Action for GetLastRootId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_trees
|
FROM euph_trees
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -787,17 +702,15 @@ impl Action for GetPrevRootId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_trees
|
FROM euph_trees
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id < ?
|
AND id < ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.root_id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.root_id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(root_id)
|
Ok(root_id)
|
||||||
}
|
}
|
||||||
|
|
@ -813,17 +726,15 @@ impl Action for GetNextRootId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_trees
|
FROM euph_trees
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id > ?
|
AND id > ?
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.root_id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.root_id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(root_id)
|
Ok(root_id)
|
||||||
}
|
}
|
||||||
|
|
@ -839,13 +750,12 @@ impl Action for GetOldestMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -863,13 +773,12 @@ impl Action for GetNewestMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -887,17 +796,15 @@ impl Action for GetOlderMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id < ?
|
AND id < ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
@ -912,17 +819,15 @@ impl Action for GetNewerMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id > ?
|
AND id > ?
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
@ -938,14 +843,13 @@ impl Action for GetOldestUnseenMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND NOT seen
|
AND NOT seen
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -963,14 +867,13 @@ impl Action for GetNewestUnseenMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND NOT seen
|
AND NOT seen
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row([&self.room.domain, &self.room.name], |row| {
|
.query_row([self.room], |row| {
|
||||||
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
})
|
})
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
@ -988,18 +891,16 @@ impl Action for GetOlderUnseenMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND NOT seen
|
AND NOT seen
|
||||||
AND id < ?
|
AND id < ?
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
@ -1015,18 +916,16 @@ impl Action for GetNewerUnseenMsgId {
|
||||||
"
|
"
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND NOT seen
|
AND NOT seen
|
||||||
AND id > ?
|
AND id > ?
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![self.room, WSnowflake(self.id.0)], |row| {
|
||||||
params![self.room.domain, self.room.name, WSnowflake(self.id.0)],
|
row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0))
|
||||||
|row| row.get::<_, WSnowflake>(0).map(|s| MessageId(s.0)),
|
})
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
Ok(msg_id)
|
Ok(msg_id)
|
||||||
}
|
}
|
||||||
|
|
@ -1042,11 +941,10 @@ impl Action for GetUnseenMsgsCount {
|
||||||
"
|
"
|
||||||
SELECT amount
|
SELECT amount
|
||||||
FROM euph_unseen_counts
|
FROM euph_unseen_counts
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
",
|
",
|
||||||
)?
|
)?
|
||||||
.query_row(params![self.room.domain, self.room.name], |row| row.get(0))
|
.query_row(params![self.room], |row| row.get(0))
|
||||||
.optional()?
|
.optional()?
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
Ok(amount)
|
Ok(amount)
|
||||||
|
|
@ -1062,16 +960,10 @@ impl Action for SetSeen {
|
||||||
"
|
"
|
||||||
UPDATE euph_msgs
|
UPDATE euph_msgs
|
||||||
SET seen = :seen
|
SET seen = :seen
|
||||||
WHERE domain = :domain
|
WHERE room = :room
|
||||||
AND room = :room
|
|
||||||
AND id = :id
|
AND id = :id
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! { ":room": self.room, ":id": WSnowflake(self.id.0), ":seen": self.seen },
|
||||||
":domain": self.room.domain,
|
|
||||||
":room": self.room.name,
|
|
||||||
":id": WSnowflake(self.id.0),
|
|
||||||
":seen": self.seen,
|
|
||||||
},
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1086,17 +978,11 @@ impl Action for SetOlderSeen {
|
||||||
"
|
"
|
||||||
UPDATE euph_msgs
|
UPDATE euph_msgs
|
||||||
SET seen = :seen
|
SET seen = :seen
|
||||||
WHERE domain = :domain
|
WHERE room = :room
|
||||||
AND room = :room
|
|
||||||
AND id <= :id
|
AND id <= :id
|
||||||
AND seen != :seen
|
AND seen != :seen
|
||||||
",
|
",
|
||||||
named_params! {
|
named_params! { ":room": self.room, ":id": WSnowflake(self.id.0), ":seen": self.seen },
|
||||||
":domain": self.room.domain,
|
|
||||||
":room": self.room.name,
|
|
||||||
":id": WSnowflake(self.id.0),
|
|
||||||
":seen": self.seen,
|
|
||||||
},
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -1138,13 +1024,12 @@ impl Action for GetChunkAfter {
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
AND id > ?
|
AND id > ?
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
")?
|
")?
|
||||||
.query_map(params![self.room.domain, self.room.name, WSnowflake(id.0), self.amount], row2msg)?
|
.query_map(params![self.room, WSnowflake(id.0), self.amount], row2msg)?
|
||||||
.collect::<rusqlite::Result<_>>()?
|
.collect::<rusqlite::Result<_>>()?
|
||||||
} else {
|
} else {
|
||||||
conn.prepare("
|
conn.prepare("
|
||||||
|
|
@ -1152,12 +1037,11 @@ impl Action for GetChunkAfter {
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = ?
|
WHERE room = ?
|
||||||
AND room = ?
|
|
||||||
ORDER BY id ASC
|
ORDER BY id ASC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
")?
|
")?
|
||||||
.query_map(params![self.room.domain, self.room.name, self.amount], row2msg)?
|
.query_map(params![self.room, self.amount], row2msg)?
|
||||||
.collect::<rusqlite::Result<_>>()?
|
.collect::<rusqlite::Result<_>>()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,10 @@
|
||||||
use rusqlite::Transaction;
|
use rusqlite::Transaction;
|
||||||
use vault::Migration;
|
use vault::Migration;
|
||||||
|
|
||||||
pub const MIGRATIONS: [Migration; 3] = [m1, m2, m3];
|
pub const MIGRATIONS: [Migration; 2] = [m1, m2];
|
||||||
|
|
||||||
fn eprint_status(nr: usize, total: usize) {
|
|
||||||
eprintln!("Migrating vault from {} to {} (out of {total})", nr, nr + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn m1(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()> {
|
fn m1(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()> {
|
||||||
eprint_status(nr, total);
|
eprintln!("Migrating vault from {} to {} (out of {total})", nr, nr + 1);
|
||||||
tx.execute_batch(
|
tx.execute_batch(
|
||||||
"
|
"
|
||||||
CREATE TABLE euph_rooms (
|
CREATE TABLE euph_rooms (
|
||||||
|
|
@ -71,7 +67,7 @@ fn m1(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn m2(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()> {
|
fn m2(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()> {
|
||||||
eprint_status(nr, total);
|
eprintln!("Migrating vault from {} to {} (out of {total})", nr, nr + 1);
|
||||||
tx.execute_batch(
|
tx.execute_batch(
|
||||||
"
|
"
|
||||||
ALTER TABLE euph_msgs
|
ALTER TABLE euph_msgs
|
||||||
|
|
@ -82,143 +78,3 @@ fn m2(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()>
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn m3(tx: &mut Transaction<'_>, nr: usize, total: usize) -> rusqlite::Result<()> {
|
|
||||||
eprint_status(nr, total);
|
|
||||||
println!(" This migration might take quite a while.");
|
|
||||||
println!(" Aborting it will not corrupt your vault.");
|
|
||||||
|
|
||||||
// Rooms should be identified not just via their name but also their domain.
|
|
||||||
// The domain should be required but there should be no default value.
|
|
||||||
//
|
|
||||||
// To accomplish this, we need to recreate and repopulate all euph related
|
|
||||||
// tables because SQLite's ALTER TABLE is not powerful enough.
|
|
||||||
|
|
||||||
eprintln!(" Preparing tables...");
|
|
||||||
tx.execute_batch(
|
|
||||||
"
|
|
||||||
DROP INDEX euph_idx_msgs_room_id_parent;
|
|
||||||
DROP INDEX euph_idx_msgs_room_parent_id;
|
|
||||||
DROP INDEX euph_idx_msgs_room_id_seen;
|
|
||||||
|
|
||||||
ALTER TABLE euph_rooms RENAME TO old_euph_rooms;
|
|
||||||
ALTER TABLE euph_msgs RENAME TO old_euph_msgs;
|
|
||||||
ALTER TABLE euph_spans RENAME TO old_euph_spans;
|
|
||||||
ALTER TABLE euph_cookies RENAME TO old_euph_cookies;
|
|
||||||
|
|
||||||
CREATE TABLE euph_rooms (
|
|
||||||
domain TEXT NOT NULL,
|
|
||||||
room TEXT NOT NULL,
|
|
||||||
first_joined INT NOT NULL,
|
|
||||||
last_joined INT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (domain, room)
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE euph_msgs (
|
|
||||||
domain TEXT NOT NULL,
|
|
||||||
room TEXT NOT NULL,
|
|
||||||
seen INT NOT NULL,
|
|
||||||
|
|
||||||
-- Message
|
|
||||||
id INT NOT NULL,
|
|
||||||
parent INT,
|
|
||||||
previous_edit_id INT,
|
|
||||||
time INT NOT NULL,
|
|
||||||
content TEXT NOT NULL,
|
|
||||||
encryption_key_id TEXT,
|
|
||||||
edited INT,
|
|
||||||
deleted INT,
|
|
||||||
truncated INT NOT NULL,
|
|
||||||
|
|
||||||
-- SessionView
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
name TEXT,
|
|
||||||
server_id TEXT NOT NULL,
|
|
||||||
server_era TEXT NOT NULL,
|
|
||||||
session_id TEXT NOT NULL,
|
|
||||||
is_staff INT NOT NULL,
|
|
||||||
is_manager INT NOT NULL,
|
|
||||||
client_address TEXT,
|
|
||||||
real_client_address TEXT,
|
|
||||||
|
|
||||||
PRIMARY KEY (domain, room, id),
|
|
||||||
FOREIGN KEY (domain, room) REFERENCES euph_rooms (domain, room)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE euph_spans (
|
|
||||||
domain TEXT NOT NULL,
|
|
||||||
room TEXT NOT NULL,
|
|
||||||
start INT,
|
|
||||||
end INT,
|
|
||||||
|
|
||||||
UNIQUE (domain, room, start, end),
|
|
||||||
FOREIGN KEY (domain, room) REFERENCES euph_rooms (domain, room)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CHECK (start IS NULL OR end IS NOT NULL)
|
|
||||||
) STRICT;
|
|
||||||
|
|
||||||
CREATE TABLE euph_cookies (
|
|
||||||
domain TEXT NOT NULL,
|
|
||||||
cookie TEXT NOT NULL
|
|
||||||
) STRICT;
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
eprintln!(" Migrating data...");
|
|
||||||
tx.execute_batch(
|
|
||||||
"
|
|
||||||
INSERT INTO euph_rooms (domain, room, first_joined, last_joined)
|
|
||||||
SELECT 'euphoria.io', room, first_joined, last_joined
|
|
||||||
FROM old_euph_rooms;
|
|
||||||
|
|
||||||
INSERT INTO euph_msgs (
|
|
||||||
domain, room, seen,
|
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
'euphoria.io', room, seen,
|
|
||||||
id, parent, previous_edit_id, time, content, encryption_key_id, edited, deleted, truncated,
|
|
||||||
user_id, name, server_id, server_era, session_id, is_staff, is_manager, client_address, real_client_address
|
|
||||||
FROM old_euph_msgs;
|
|
||||||
|
|
||||||
INSERT INTO euph_spans (domain, room, start, end)
|
|
||||||
SELECT 'euphoria.io', room, start, end
|
|
||||||
FROM old_euph_spans;
|
|
||||||
|
|
||||||
INSERT INTO euph_cookies (domain, cookie)
|
|
||||||
SELECT 'euphoria.io', cookie
|
|
||||||
FROM old_euph_cookies;
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
eprintln!(" Recreating indexes...");
|
|
||||||
tx.execute_batch(
|
|
||||||
"
|
|
||||||
CREATE INDEX euph_idx_msgs_domain_room_id_parent
|
|
||||||
ON euph_msgs (domain, room, id, parent);
|
|
||||||
|
|
||||||
CREATE INDEX euph_idx_msgs_domain_room_parent_id
|
|
||||||
ON euph_msgs (domain, room, parent, id);
|
|
||||||
|
|
||||||
CREATE INDEX euph_idx_msgs_domain_room_id_seen
|
|
||||||
ON euph_msgs (domain, room, id, seen);
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
eprintln!(" Cleaning up loose ends...");
|
|
||||||
tx.execute_batch(
|
|
||||||
"
|
|
||||||
DROP TABLE old_euph_rooms;
|
|
||||||
DROP TABLE old_euph_msgs;
|
|
||||||
DROP TABLE old_euph_spans;
|
|
||||||
DROP TABLE old_euph_cookies;
|
|
||||||
|
|
||||||
ANALYZE;
|
|
||||||
",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,28 @@
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
eprintln!("Preparing vault");
|
|
||||||
|
|
||||||
// Cache ids of tree roots.
|
// Cache ids of tree roots.
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
CREATE TEMPORARY TABLE euph_trees (
|
CREATE TEMPORARY TABLE euph_trees (
|
||||||
domain TEXT NOT NULL,
|
|
||||||
room TEXT NOT NULL,
|
room TEXT NOT NULL,
|
||||||
id INT NOT NULL,
|
id INT NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (domain, room, id)
|
PRIMARY KEY (room, id)
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
INSERT INTO euph_trees (domain, room, id)
|
INSERT INTO euph_trees (room, id)
|
||||||
SELECT domain, room, id
|
SELECT room, id
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE parent IS NULL
|
WHERE parent IS NULL
|
||||||
UNION
|
UNION
|
||||||
SELECT domain, room, parent
|
SELECT room, parent
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE parent IS NOT NULL
|
WHERE parent IS NOT NULL
|
||||||
AND NOT EXISTS(
|
AND NOT EXISTS(
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM euph_msgs AS parents
|
FROM euph_msgs AS parents
|
||||||
WHERE parents.domain = euph_msgs.domain
|
WHERE parents.room = euph_msgs.room
|
||||||
AND parents.room = euph_msgs.room
|
|
||||||
AND parents.id = euph_msgs.parent
|
AND parents.id = euph_msgs.parent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -34,16 +30,15 @@ pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
AFTER DELETE ON main.euph_rooms
|
AFTER DELETE ON main.euph_rooms
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM euph_trees
|
DELETE FROM euph_trees
|
||||||
WHERE domain = old.domain
|
WHERE room = old.room;
|
||||||
AND room = old.room;
|
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER et_insert_msg_without_parent
|
CREATE TEMPORARY TRIGGER et_insert_msg_without_parent
|
||||||
AFTER INSERT ON main.euph_msgs
|
AFTER INSERT ON main.euph_msgs
|
||||||
WHEN new.parent IS NULL
|
WHEN new.parent IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT OR IGNORE INTO euph_trees (domain, room, id)
|
INSERT OR IGNORE INTO euph_trees (room, id)
|
||||||
VALUES (new.domain, new.room, new.id);
|
VALUES (new.room, new.id);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER et_insert_msg_with_parent
|
CREATE TEMPORARY TRIGGER et_insert_msg_with_parent
|
||||||
|
|
@ -51,18 +46,16 @@ pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
WHEN new.parent IS NOT NULL
|
WHEN new.parent IS NOT NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM euph_trees
|
DELETE FROM euph_trees
|
||||||
WHERE domain = new.domain
|
WHERE room = new.room
|
||||||
AND room = new.room
|
|
||||||
AND id = new.id;
|
AND id = new.id;
|
||||||
|
|
||||||
INSERT OR IGNORE INTO euph_trees (domain, room, id)
|
INSERT OR IGNORE INTO euph_trees (room, id)
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM (VALUES (new.domain, new.room, new.parent))
|
FROM (VALUES (new.room, new.parent))
|
||||||
WHERE NOT EXISTS(
|
WHERE NOT EXISTS(
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE domain = new.domain
|
WHERE room = new.room
|
||||||
AND room = new.room
|
|
||||||
AND id = new.parent
|
AND id = new.parent
|
||||||
AND parent IS NOT NULL
|
AND parent IS NOT NULL
|
||||||
);
|
);
|
||||||
|
|
@ -74,37 +67,35 @@ pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
CREATE TEMPORARY TABLE euph_unseen_counts (
|
CREATE TEMPORARY TABLE euph_unseen_counts (
|
||||||
domain TEXT NOT NULL,
|
|
||||||
room TEXT NOT NULL,
|
room TEXT NOT NULL,
|
||||||
amount INTEGER NOT NULL,
|
amount INTEGER NOT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (domain, room)
|
PRIMARY KEY (room)
|
||||||
) STRICT;
|
) STRICT;
|
||||||
|
|
||||||
-- There must be an entry for every existing room.
|
-- There must be an entry for every existing room.
|
||||||
INSERT INTO euph_unseen_counts (domain, room, amount)
|
INSERT INTO euph_unseen_counts (room, amount)
|
||||||
SELECT domain, room, 0
|
SELECT room, 0
|
||||||
FROM euph_rooms;
|
FROM euph_rooms;
|
||||||
|
|
||||||
INSERT OR REPLACE INTO euph_unseen_counts (domain, room, amount)
|
INSERT OR REPLACE INTO euph_unseen_counts (room, amount)
|
||||||
SELECT domain, room, COUNT(*)
|
SELECT room, COUNT(*)
|
||||||
FROM euph_msgs
|
FROM euph_msgs
|
||||||
WHERE NOT seen
|
WHERE NOT seen
|
||||||
GROUP BY domain, room;
|
GROUP BY room;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER euc_insert_room
|
CREATE TEMPORARY TRIGGER euc_insert_room
|
||||||
AFTER INSERT ON main.euph_rooms
|
AFTER INSERT ON main.euph_rooms
|
||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO euph_unseen_counts (domain, room, amount)
|
INSERT INTO euph_unseen_counts (room, amount)
|
||||||
VALUES (new.domain, new.room, 0);
|
VALUES (new.room, 0);
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER euc_delete_room
|
CREATE TEMPORARY TRIGGER euc_delete_room
|
||||||
AFTER DELETE ON main.euph_rooms
|
AFTER DELETE ON main.euph_rooms
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM euph_unseen_counts
|
DELETE FROM euph_unseen_counts
|
||||||
WHERE domain = old.domain
|
WHERE room = old.room;
|
||||||
AND room = old.room;
|
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER euc_insert_msg
|
CREATE TEMPORARY TRIGGER euc_insert_msg
|
||||||
|
|
@ -113,8 +104,7 @@ pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE euph_unseen_counts
|
UPDATE euph_unseen_counts
|
||||||
SET amount = amount + 1
|
SET amount = amount + 1
|
||||||
WHERE domain = new.domain
|
WHERE room = new.room;
|
||||||
AND room = new.room;
|
|
||||||
END;
|
END;
|
||||||
|
|
||||||
CREATE TEMPORARY TRIGGER euc_update_msg
|
CREATE TEMPORARY TRIGGER euc_update_msg
|
||||||
|
|
@ -123,8 +113,7 @@ pub fn prepare(conn: &mut Connection) -> rusqlite::Result<()> {
|
||||||
BEGIN
|
BEGIN
|
||||||
UPDATE euph_unseen_counts
|
UPDATE euph_unseen_counts
|
||||||
SET amount = CASE WHEN new.seen THEN amount - 1 ELSE amount + 1 END
|
SET amount = CASE WHEN new.seen THEN amount - 1 ELSE amount + 1 END
|
||||||
WHERE domain = new.domain
|
WHERE room = new.room;
|
||||||
AND room = new.room;
|
|
||||||
END;
|
END;
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub const NAME: &str = env!("CARGO_PKG_NAME");
|
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
||||||
47
flake.lock
generated
Normal file
47
flake.lock
generated
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"naersk": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1679567394,
|
||||||
|
"narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"rev": "88cd22380154a2c36799fe8098888f0f59861a15",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "naersk",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1680643271,
|
||||||
|
"narHash": "sha256-m76rYcvqs+NzTyETfxh1o/9gKdBuJ/Hl+PI/kp73mDw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "246567a3ad88e3119c2001e2fe78be233474cde0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"naersk": "naersk",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
29
flake.nix
Normal file
29
flake.nix
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
description = "TUI client for euphoria.io, 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 = ./.;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue