Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 7 additions & 22 deletions .github/workflows/bin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name: Binaries Publisher
run-name: Binaries Publisher | ${{ github.event_name }} by ${{ github.actor }} on '${{ github.ref_name }}'

on:
release:
types: [published]
push:
tags:
- "v*"
Expand Down Expand Up @@ -34,67 +32,61 @@ jobs:
architecture: x86_64
binary-postfix: ""
binary-name: vimstoat
use-cross: false

- os: macos-latest
os-name: macos
target: aarch64-apple-darwin
architecture: arm64
binary-postfix: ""
binary-name: vimstoat
use-cross: false

- os: ubuntu-latest
os-name: linux
target: x86_64-unknown-linux-gnu
architecture: x86_64
binary-postfix: ""
binary-name: vimstoat
use-cross: false

- os: ubuntu-latest
os-name: linux
target: x86_64-unknown-linux-musl
architecture: x86_64-musl
binary-postfix: ""
binary-name: vimstoat
use-cross: true

- os: ubuntu-24.04-arm
os-name: linux
target: aarch64-unknown-linux-gnu
architecture: arm64
binary-postfix: ""
binary-name: vimstoat
use-cross: false

- os: ubuntu-24.04-arm
os-name: linux
target: aarch64-unknown-linux-musl
architecture: arm64-musl
binary-postfix: ""
binary-name: vimstoat
use-cross: true

- os: windows-latest
os-name: windows
target: x86_64-pc-windows-msvc
architecture: x86_64
binary-postfix: ".exe"
binary-name: vimstoat
use-cross: false

- os: windows-latest
os-name: windows
target: aarch64-pc-windows-msvc
architecture: arm64
binary-postfix: ".exe"
binary-name: vimstoat
use-cross: true

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.tag || github.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand All @@ -103,28 +95,21 @@ jobs:

- uses: Swatinem/rust-cache@v2

- name: Install cross
if: matrix.use-cross
uses: taiki-e/install-action@v2
with:
tool: cross
- name: Install musl-tools
if: matrix.os-name == 'linux' && contains(matrix.target, 'musl')
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Cargo build
shell: bash
run: |
if [ "${{ matrix.use-cross }}" = "true" ]; then
cross build --release --target ${{ matrix.target }}
else
cargo build --release --target ${{ matrix.target }}
fi
run: cargo build --release --target ${{ matrix.target }}

- name: Packaging final binary
shell: bash
run: |
cd target/${{ matrix.target }}/release

BINARY_NAME="${{ matrix.binary-name }}${{ matrix.binary-postfix }}"
RELEASE_NAME="${{ matrix.binary-name }}-${{ github.ref_name }}-${{ matrix.os-name }}-${{ matrix.architecture }}"
RELEASE_NAME="${{ matrix.binary-name }}-${{ inputs.tag || github.ref_name }}-${{ matrix.os-name }}-${{ matrix.architecture }}"

# Create archive with binary, README, and LICENSE
cp ../../../README.md . 2>/dev/null || true
Expand Down
20 changes: 18 additions & 2 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,24 @@ jobs:
- name: Lint with Clippy
run: cargo clippy -- -D warnings

- name: Run Tests
run: cargo test
- name: Install Keyring & D-Bus dependencies
run: |
sudo apt-get update
sudo apt-get install -y gnome-keyring dbus dbus-x11 libsecret-tools

- name: Run Tests with Keyring
env:
STOAT_TEST_TOKEN: ${{ secrets.STOAT_TEST_TOKEN }}
run: |
dbus-run-session -- bash -c '
set -e
echo -n "ci-keyring-pwd" | gnome-keyring-daemon --components=secrets --unlock &
sleep 1
test -n "$STOAT_TEST_TOKEN" || { echo "STOAT_TEST_TOKEN secret is not set"; exit 1; }
printf "%s" "$STOAT_TEST_TOKEN" | secret-tool store --label=vimstoat \
service keyring-lib username vimstoat target default application rust-keyring
cargo test
'

publish-cargo:
name: Publishing to Cargo
Expand Down
97 changes: 84 additions & 13 deletions docs/vimstoat/src/features.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,91 @@
# Features

VimStoat is currently in early development. Here are the features implemented so far:
VimStoat is currently in early development. Here are the features implemented so far. Anything described as **Planned** is not implemented yet.

## Real-time Messaging
- **WebSocket Integration**: Listens for real-time `Message`, `MessageUpdate`, and `MessageDelete` events from the server.
- **Dynamic Author Resolution**: Transparently fetches and caches unknown user profiles to display real usernames instead of raw IDs in the UI.
- **In-Memory Cache**: Uses an in-memory cache synchronized to disk upon application exit for maximum performance.
## Authentication
- **Session token**: VimStoat logs in with a Stoat session token rather than a username and password.
- **OS keyring storage**: The token is stored in your operating system's keyring (under the name `vimstoat`). It is never written to the cache or the log file.
- **First run**: If no token is stored, VimStoat asks you to paste one and press `Enter`. The input is masked (`•`). The token is checked against the Stoat API *before* it is saved, so a bad token is never stored. `Esc` quits.
- **Later runs**: The stored token is read from the keyring and validated against the API on startup, so you go straight to the server list.
- **Errors**: If the keyring is unavailable, the server can't be reached, or the token is rejected, an error screen is shown (titled *Keyring*, *Connection*, or *Authentication* error). Press a character key, `Enter`, or `Esc` to return to the token prompt. A rejected token stays in the keyring until you enter a valid one, which replaces it.

## Server List
- **Landing screen**: After logging in you see the server list. The first row is always **Direct Messages**; the servers you are a member of follow it.
- **Relative line numbers**: Like Vim's `relativenumber`, the selected row shows its own index and every other row shows its distance from the selection.
- **Navigation**: `j`/`k` (or the arrow keys) move the selection, `gg` jumps to the top, `G` jumps to the bottom, and `Enter` opens the selected item.
- **Servers are not browsable yet**: Servers are listed, but opening one does nothing. Server channels are **Planned**.

## Direct Messages
- **DM List View**: Displays all active Direct Message channels.
- **Unread Indicators**: Channels with new messages are automatically marked with a red `[*]` unread indicator.
- **Message Previews**: The DM list shows a truncated preview of the latest message for each channel.
- **Chat Interface**: View message history and send new messages seamlessly via the REST API.
- **DM List View**: Displays all active Direct Message channels, including Saved Messages and group DMs.
- **Chat Interface**: View message history (the latest 50 messages) and send new messages via the REST API.
- **Message content**: System messages are shown as `[System message: <type>]`, and anything VimStoat can't display as `[Unsupported message]`.

## Presence
- **Status bubble**: Each DM shows a coloured bubble after the name, in both the DM list and the conversation title:

| Bubble | Status |
| ------ | -------------- |
| `●` | Online |
| `◐` | Idle |
| `◆` | Focus |
| `■` | Do Not Disturb |
| `○` | Offline / Invisible |

- **Status text**: A user's custom status text is shown after the bubble (` - text`), unless they are offline.
- **Always live**: Presence comes from the real-time connection. The `Ready` event sets everyone's status when VimStoat connects, and `UserUpdate` events keep it current from then on, so the bubble reflects the latest state. A user who disconnects is shown as offline.
- **Group DMs**: Group DMs have no single recipient, so they don't show a bubble.

## Typing Indicators
- **Incoming typing**: When someone in the open conversation is typing, the title of the message box reads `… - <name> is typing...`. Several people are listed separated by commas.
- **Not sent yet**: VimStoat does not yet tell other people when *you* are typing (**Planned**).

## Unread Indicators
- **Marker**: Channels with unread messages are marked with a red `[*]` before the name.
- **Synced with the server**: On startup VimStoat fetches your unread state from Stoat. A channel is unread if you were mentioned in it or its latest message is newer than the last one you read. If the unread state can't be fetched, nothing is marked unread.
- **Live**: A message arriving in a conversation you are not viewing sets the marker.
- **Acknowledged on read**: Opening a conversation clears its marker and tells the server you've read it, and messages that arrive while it is open are acknowledged as they come in. Reading a channel on another device clears the marker here too.
- **No mention marker**: Mentions and ordinary unread messages look the same.

## Real-time Events
VimStoat keeps a WebSocket connection to Stoat open (a ping is sent every 20 seconds to keep it alive) and reacts to these events:

| Event | Effect |
| ------------------------------------- | ---------------------------------------------------------------- |
| `Ready` | Loads the initial users (and their presence) and your servers |
| `Message` | Appends the message if the conversation is open; otherwise marks the channel unread |
| `MessageUpdate` | Applies the edited text (open conversation only) |
| `MessageDelete` | Removes the message (open conversation only) |
| `UserUpdate` | Updates a user's presence and status text |
| `ChannelStartTyping` / `ChannelStopTyping` | Shows or hides the typing indicator |
| `ChannelAck` | Clears the unread marker when another client read the channel |

Every other event (reactions, channel, server and member changes, …) is received but ignored; it is only recorded in the log file. This means a few things are **not** reflected until you restart: DMs created after startup, username changes, and reactions.

If the connection to Stoat drops, VimStoat does not reconnect yet. The app keeps running but stops receiving updates, so restart it to resume live updates.

## Caching
- **Dynamic Author Resolution**: Transparently fetches and caches unknown user profiles to display real usernames instead of raw IDs in the UI.
- **Persistent cache**: User profiles and DM message history are kept in a cache file in your OS cache directory (on Linux, `~/.cache/vimstoat/cache.db`). The cache is held in memory while the app runs and is written to disk every 10 minutes and when you quit, so a crash can lose up to 10 minutes of cache updates. It is safe to delete the file; it is rebuilt as you use the app.
- **Instant conversations**: Opening a DM shows the cached history immediately while the latest 50 messages are fetched, then replaces it with the fresh copy.

### The cache is only a starting point
What you see in the UI is the live state built from the server and the real-time events above, not the cache file. The cache only fills in the first screen while fresh data loads:

- **Message history** is fetched from the server every time you open a conversation, so the cached copy is on screen only until that request completes.
- **Presence** is replaced by the `Ready` event as soon as VimStoat connects.
- **Failed fetches**: if fetching a conversation's history fails (for example, you are offline), the conversation shows as empty rather than falling back to the cached copy.

## Vim-Native Interface
- **Mode-Specific UI Themes**: Border colors dynamically shift based on the current mode (e.g., Yellow for Insert mode, Blue for UI/Normal mode).
- **Cursor Shaping**: Hardware cursor shape changes depending on mode (e.g., blinking bar in Insert mode, block in Normal mode).
- **Input Motions**: Features robust vim-like text composition motions like `i`, `I`, `a`, `A`, `o`, `O`, `dd` (delete line), and an in-memory yank buffer.
- **Screen Navigation**: Use `q` or `:q` to navigate back through screens, and `:qa` to quit globally.
- **Mode-Specific UI Themes**: Border colors dynamically shift based on the current mode (Blue for UI/Normal, Yellow for Insert, Green for Command).
- **Cursor Shaping**: Hardware cursor shape changes depending on mode (blinking bar in Insert and Command mode, block otherwise).
- **Input Motions**: Features vim-like text composition motions like `i`, `I`, `a`, `A`, `o`, `O`, and `dd` (delete line), with an in-memory yank buffer. Pasting from the yank buffer is **Planned**.
- **Multi-line messages**: `Shift+Enter` or `Alt+Enter` inserts a newline; `Enter` sends.
- **Screen Navigation**: `:q` goes back one screen at a time, `q` goes back from a conversation, and `:qa` quits globally. See [Keybinds](./keybinds.md).

## Planned
- Fuzzy finder (`fs`, `fc`, `ff`, `fm`)
- Search (`/`)
- Visual mode (`v`)
- Pasting the yank buffer
- Browsing server channels
- Broadcasting your own typing indicator
70 changes: 53 additions & 17 deletions docs/vimstoat/src/keybinds.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
# Keybinds

VimStoat has three screens you navigate with the keyboard: the **server list**, the **DM list**, and a **conversation**. The list screens use *UI mode*; a conversation starts in *Normal mode* so you can edit your message like a Vim buffer. Both look the same (blue border).

Keys marked **(Planned)** are not implemented yet.

## Basic Navigation

| Key | Action |
| --- | ---------- |
| `h` | Move left |
| `j` | Move down |
| `k` | Move up |
| `l` | Move right |
| Key | Action |
| --------- | --------------------------------------------------------------- |
| `h` / `←` | Move left (in the message box) |
| `j` / `↓` | Move down (in lists, or down a line in the message box) |
| `k` / `↑` | Move up (in lists, or up a line in the message box) |
| `l` / `→` | Move right (in the message box) |
| `gg` | Go to the top of the list (server list and DM list only) |
| `G` | Go to the bottom of the list (server list and DM list only) |
| `Enter` | Open the selected item in a list; send the message in a conversation |

## Mode Switching

Expand All @@ -19,22 +26,51 @@
| `A` | Enter Insert mode, at the end of the line |
| `o` | Enter Insert mode, and create a new line below the current line. |
| `O` | Enter Insert mode, and create a new line above the current line. |
| `dd` | Delete the current line |
| `gg` | Go to the top of the UI list |
| `q` | Go back or Quit |
| `:` | Enter Command mode |
| `/` | Search (enters Command mode with `/` prefix) |
| `v` | Enter Visual mode (future) |
| `Esc` | Enter Normal mode |
| `Esc` | Leave Insert or Command mode |
| `/` | Search **(Planned)** |
| `v` | Enter Visual mode **(Planned)** |

## Editing (Normal mode)

| Key | Action |
| ---- | -------------------------------------------------------------------------------------------- |
| `dd` | Delete the current line of the message. The line is saved to the yank buffer (`p` to paste it back is **Planned**). |

## Insert Mode

| Key | Action |
| ------------------------ | ------------------------------------------- |
| any character | Insert it at the cursor |
| `Backspace` | Delete the character before the cursor |
| `Enter` | Send the message |
| `Shift+Enter`, `Alt+Enter` | Insert a newline without sending |
| `←` `→` `↑` `↓` | Move the cursor |
| `Esc` | Return to Normal mode |

## Going Back and Quitting

| Key | Where | Action |
| ---- | ---------------------- | --------------------------------------------- |
| `q` | Conversation (Normal) | Go back to the DM list |
| `q` | Server list, DM list | Quit the application |

`:q` always goes back exactly one screen (conversation → DM list → server list), and quits from the server list. See the commands below.

## Commands (Command Mode)

| Command | Action |
| -------------- | ---------------------------------------------------------------- |
| `:q`, `:quit` | Go back to the previous screen, or quit if at the top level |
| `:qa`, `:qall` | Quit the application immediately |
Press `:` to enter Command mode, type a command, then press `Enter`. `Esc` cancels and `Backspace` edits the command.

| Command | Action |
| ------------------------------------ | ----------------------------------------------------------- |
| `:q`, `:quit`, `:q!` | Go back to the previous screen, or quit if at the top level |
| `:qa`, `:qall`, `:qa!`, `:qall!` | Quit the application immediately |

Unknown commands are ignored (a warning is written to the log file).

## Fuzzy Finder (Planned)

## Fuzzy Finder
None of these are implemented yet.

| Key | Action |
| ---- | ------------ |
Expand Down
4 changes: 1 addition & 3 deletions src/action.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Action {
#[allow(unused)]
GoToTopUI,
#[allow(unused)]
GoToBottomUI,
AppendCharacter(char),
RemoveCharacter,
EnterCommandMode,
Expand All @@ -19,6 +18,5 @@ pub enum Action {
CursorUp,
CursorDown,
Escape,
#[allow(unused)]
Quit,
}
Loading
Loading