Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10214f3369 | |||
| 2ca6190d97 | |||
| 67e77c8880 | |||
| b70d7548da | |||
| 732d462775 | |||
| 40de073799 | |||
| 8b928184e8 |
19 changed files with 176 additions and 35 deletions
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- ubuntu-22.04
|
||||
- windows-latest
|
||||
- macos-latest
|
||||
- macos-13
|
||||
|
|
@ -59,11 +59,11 @@ jobs:
|
|||
|
||||
- name: Zip artifacts
|
||||
run: |
|
||||
chmod +x cove-ubuntu-latest/cove
|
||||
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-latest/host).zip" cove-ubuntu-latest/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
|
||||
|
|
|
|||
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -15,6 +15,21 @@ Procedure when bumping the version number:
|
|||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -537,6 +537,14 @@ Reply to message, inline if possible.
|
|||
|
||||
Reply opposite to normal reply.
|
||||
|
||||
### `keys.tree.action.toggle_nick_emoji`
|
||||
|
||||
**Required:** yes
|
||||
**Type:** key binding
|
||||
**Default:** `"e"`
|
||||
|
||||
Toggle agent id based nick emoji.
|
||||
|
||||
### `keys.tree.action.toggle_seen`
|
||||
|
||||
**Required:** yes
|
||||
|
|
|
|||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -322,7 +322,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||
|
||||
[[package]]
|
||||
name = "cove"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
|
|
@ -350,7 +350,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cove-config"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"cove-input",
|
||||
"cove-macro",
|
||||
|
|
@ -361,7 +361,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cove-input"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"cove-macro",
|
||||
"crossterm",
|
||||
|
|
@ -375,7 +375,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cove-macro"
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ resolver = "3"
|
|||
members = ["cove", "cove-*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.2"
|
||||
version = "0.9.3"
|
||||
edition = "2024"
|
||||
|
||||
[workspace.dependencies]
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ default_bindings! {
|
|||
pub fn mark_older_seen => ["ctrl+s"];
|
||||
pub fn info => ["i"];
|
||||
pub fn links => ["I"];
|
||||
pub fn toggle_nick_emoji => ["e"];
|
||||
pub fn increase_caesar => ["c"];
|
||||
pub fn decrease_caesar => ["C"];
|
||||
}
|
||||
|
|
@ -356,6 +357,9 @@ pub struct TreeAction {
|
|||
/// List links found in message.
|
||||
#[serde(default = "default::tree_action::links")]
|
||||
pub links: KeyBinding,
|
||||
/// Toggle agent id based nick emoji.
|
||||
#[serde(default = "default::tree_action::toggle_nick_emoji")]
|
||||
pub toggle_nick_emoji: KeyBinding,
|
||||
/// Increase caesar cipher rotation.
|
||||
#[serde(default = "default::tree_action::increase_caesar")]
|
||||
pub increase_caesar: KeyBinding,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
use crossterm::style::Stylize;
|
||||
use euphoxide::api::{MessageId, Snowflake, Time};
|
||||
use euphoxide::api::{MessageId, Snowflake, Time, UserId};
|
||||
use jiff::Timestamp;
|
||||
use toss::{Style, Styled};
|
||||
|
||||
use crate::{store::Msg, ui::ChatMsg};
|
||||
|
||||
use super::util;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmallMessage {
|
||||
pub id: MessageId,
|
||||
pub parent: Option<MessageId>,
|
||||
pub time: Time,
|
||||
pub user_id: UserId,
|
||||
pub nick: String,
|
||||
pub content: String,
|
||||
pub seen: bool,
|
||||
|
|
@ -70,6 +73,10 @@ impl Msg for SmallMessage {
|
|||
fn last_possible_id() -> Self::Id {
|
||||
MessageId(Snowflake::MAX)
|
||||
}
|
||||
|
||||
fn nick_emoji(&self) -> Option<String> {
|
||||
Some(util::user_id_emoji(&self.user_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatMsg for SmallMessage {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,28 @@
|
|||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use crossterm::style::{Color, Stylize};
|
||||
use euphoxide::Emoji;
|
||||
use euphoxide::{Emoji, api::UserId};
|
||||
use toss::{Style, Styled};
|
||||
|
||||
pub static EMOJI: LazyLock<Emoji> = LazyLock::new(Emoji::load);
|
||||
|
||||
pub static EMOJI_LIST: LazyLock<Vec<String>> = LazyLock::new(|| {
|
||||
let mut list = EMOJI
|
||||
.0
|
||||
.values()
|
||||
.flatten()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
list.sort_unstable();
|
||||
list
|
||||
});
|
||||
|
||||
/// Convert HSL to RGB following [this approach from wikipedia][1].
|
||||
///
|
||||
/// `h` must be in the range `[0, 360]`, `s` and `l` in the range `[0, 1]`.
|
||||
|
|
@ -69,3 +86,11 @@ pub fn style_mention_exact(mention: &str, base: Style) -> Styled {
|
|||
.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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ pub trait Msg {
|
|||
fn parent(&self) -> Option<Self::Id>;
|
||||
fn seen(&self) -> bool;
|
||||
|
||||
fn nick_emoji(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn last_possible_id() -> Self::Id;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ impl From<Infallible> for UiError {
|
|||
}
|
||||
}
|
||||
|
||||
#[expect(clippy::large_enum_variant)]
|
||||
pub enum UiEvent {
|
||||
GraphemeWidthsChanged,
|
||||
LogChanged,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ pub struct ChatState<M: Msg, S: MsgStore<M>> {
|
|||
|
||||
cursor: Cursor<M::Id>,
|
||||
editor: EditorState,
|
||||
nick_emoji: bool,
|
||||
caesar: i8,
|
||||
|
||||
mode: Mode,
|
||||
|
|
@ -48,6 +49,7 @@ impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
|
|||
Self {
|
||||
cursor: Cursor::Bottom,
|
||||
editor: EditorState::new(),
|
||||
nick_emoji: false,
|
||||
caesar: 0,
|
||||
|
||||
mode: Mode::Tree,
|
||||
|
|
@ -56,6 +58,10 @@ impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
|
|||
store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nick_emoji(&self) -> bool {
|
||||
self.nick_emoji
|
||||
}
|
||||
}
|
||||
|
||||
impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
||||
|
|
@ -79,6 +85,7 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
|||
&mut self.editor,
|
||||
nick,
|
||||
focused,
|
||||
self.nick_emoji,
|
||||
self.caesar,
|
||||
)
|
||||
.boxed_async(),
|
||||
|
|
@ -117,6 +124,11 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
|
|||
Reaction::Composed { parent, content }
|
||||
}
|
||||
|
||||
Reaction::NotHandled if event.matches(&keys.tree.action.toggle_nick_emoji) => {
|
||||
self.nick_emoji = !self.nick_emoji;
|
||||
Reaction::Handled
|
||||
}
|
||||
|
||||
Reaction::NotHandled if event.matches(&keys.tree.action.increase_caesar) => {
|
||||
self.caesar = (self.caesar + 1).rem_euclid(26);
|
||||
Reaction::Handled
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
|||
editor: &'a mut EditorState,
|
||||
nick: String,
|
||||
focused: bool,
|
||||
nick_emoji: bool,
|
||||
caesar: i8,
|
||||
) -> TreeView<'a, M, S> {
|
||||
TreeView {
|
||||
|
|
@ -397,6 +398,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
|
|||
editor,
|
||||
nick,
|
||||
focused,
|
||||
nick_emoji,
|
||||
caesar,
|
||||
}
|
||||
}
|
||||
|
|
@ -410,6 +412,8 @@ pub struct TreeView<'a, M: Msg, S: MsgStore<M>> {
|
|||
|
||||
nick: String,
|
||||
focused: bool,
|
||||
|
||||
nick_emoji: bool,
|
||||
caesar: i8,
|
||||
}
|
||||
|
||||
|
|
@ -438,6 +442,7 @@ where
|
|||
size,
|
||||
nick: self.nick.clone(),
|
||||
focused: self.focused,
|
||||
nick_emoji: self.nick_emoji,
|
||||
caesar: self.caesar,
|
||||
last_cursor: self.state.last_cursor.clone(),
|
||||
last_cursor_top: self.state.last_cursor_top,
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ pub struct TreeContext<Id> {
|
|||
pub size: Size,
|
||||
pub nick: String,
|
||||
pub focused: bool,
|
||||
pub nick_emoji: bool,
|
||||
pub caesar: i8,
|
||||
pub last_cursor: Cursor<Id>,
|
||||
pub last_cursor_top: i32,
|
||||
|
|
@ -207,6 +208,7 @@ where
|
|||
self.tz.clone(),
|
||||
indent,
|
||||
msg,
|
||||
self.context.nick_emoji,
|
||||
self.context.caesar,
|
||||
folded_info,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ where
|
|||
size: self.last_size,
|
||||
nick: self.last_nick.clone(),
|
||||
focused: true,
|
||||
nick_emoji: false,
|
||||
caesar: 0,
|
||||
last_cursor: self.last_cursor.clone(),
|
||||
last_cursor_top: self.last_cursor_top,
|
||||
|
|
|
|||
|
|
@ -59,10 +59,17 @@ pub fn msg<M: Msg + ChatMsg>(
|
|||
tz: TimeZone,
|
||||
indent: usize,
|
||||
msg: &M,
|
||||
nick_emoji: bool,
|
||||
caesar: i8,
|
||||
folded_info: Option<usize>,
|
||||
) -> Boxed<'static, Infallible> {
|
||||
let (nick, mut content) = msg.styled();
|
||||
let (mut nick, mut content) = msg.styled();
|
||||
|
||||
if nick_emoji {
|
||||
if let Some(emoji) = msg.nick_emoji() {
|
||||
nick = nick.then_plain("(").then_plain(emoji).then_plain(")");
|
||||
}
|
||||
}
|
||||
|
||||
if caesar != 0 {
|
||||
// Apply caesar in inverse because we're decoding
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ pub fn widget<'a>(
|
|||
list: &'a mut ListState<SessionId>,
|
||||
joined: &Joined,
|
||||
focused: bool,
|
||||
nick_emoji: bool,
|
||||
) -> impl Widget<UiError> + use<'a> {
|
||||
let mut list_builder = ListBuilder::new();
|
||||
render_rows(&mut list_builder, joined, focused);
|
||||
render_rows(&mut list_builder, joined, focused, nick_emoji);
|
||||
list_builder.build(list)
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +71,7 @@ fn render_rows(
|
|||
list_builder: &mut ListBuilder<'_, SessionId, Background<Text>>,
|
||||
joined: &Joined,
|
||||
focused: bool,
|
||||
nick_emoji: bool,
|
||||
) {
|
||||
let mut people = vec![];
|
||||
let mut bots = vec![];
|
||||
|
|
@ -95,10 +97,38 @@ fn render_rows(
|
|||
lurkers.sort_unstable();
|
||||
nurkers.sort_unstable();
|
||||
|
||||
render_section(list_builder, "People", &people, &joined.session, focused);
|
||||
render_section(list_builder, "Bots", &bots, &joined.session, focused);
|
||||
render_section(list_builder, "Lurkers", &lurkers, &joined.session, focused);
|
||||
render_section(list_builder, "Nurkers", &nurkers, &joined.session, focused);
|
||||
render_section(
|
||||
list_builder,
|
||||
"People",
|
||||
&people,
|
||||
&joined.session,
|
||||
focused,
|
||||
nick_emoji,
|
||||
);
|
||||
render_section(
|
||||
list_builder,
|
||||
"Bots",
|
||||
&bots,
|
||||
&joined.session,
|
||||
focused,
|
||||
nick_emoji,
|
||||
);
|
||||
render_section(
|
||||
list_builder,
|
||||
"Lurkers",
|
||||
&lurkers,
|
||||
&joined.session,
|
||||
focused,
|
||||
nick_emoji,
|
||||
);
|
||||
render_section(
|
||||
list_builder,
|
||||
"Nurkers",
|
||||
&nurkers,
|
||||
&joined.session,
|
||||
focused,
|
||||
nick_emoji,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_section(
|
||||
|
|
@ -107,6 +137,7 @@ fn render_section(
|
|||
sessions: &[HalfSession],
|
||||
own_session: &SessionView,
|
||||
focused: bool,
|
||||
nick_emoji: bool,
|
||||
) {
|
||||
if sessions.is_empty() {
|
||||
return;
|
||||
|
|
@ -124,7 +155,7 @@ fn render_section(
|
|||
list_builder.add_unsel(Text::new(row).background());
|
||||
|
||||
for session in sessions {
|
||||
render_row(list_builder, session, own_session, focused);
|
||||
render_row(list_builder, session, own_session, focused, nick_emoji);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +164,7 @@ fn render_row(
|
|||
session: &HalfSession,
|
||||
own_session: &SessionView,
|
||||
focused: bool,
|
||||
nick_emoji: bool,
|
||||
) {
|
||||
let (name, style, style_inv, perms_style_inv) = if session.name.is_empty() {
|
||||
let name = "lurk".to_string();
|
||||
|
|
@ -166,16 +198,24 @@ fn render_row(
|
|||
" "
|
||||
};
|
||||
|
||||
let emoji = if nick_emoji {
|
||||
format!(" ({})", euph::user_id_emoji(&session.id))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
list_builder.add_sel(session.session_id.clone(), move |selected| {
|
||||
if focused && selected {
|
||||
let text = Styled::new_plain(owner)
|
||||
.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)
|
||||
} else {
|
||||
let text = Styled::new_plain(owner)
|
||||
.then(&name, style)
|
||||
.then_plain(perms);
|
||||
.then_plain(perms)
|
||||
.then_plain(emoji);
|
||||
Text::new(text).background()
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ impl EuphRoom {
|
|||
.server_config
|
||||
.clone()
|
||||
.room(self.vault().room().name.clone())
|
||||
.name(format!("{room:?}-{}", next_instance_id))
|
||||
.name(format!("{room:?}-{next_instance_id}"))
|
||||
.human(true)
|
||||
.username(self.room_config.username.clone())
|
||||
.force_username(self.room_config.force_username)
|
||||
|
|
@ -291,11 +291,16 @@ impl EuphRoom {
|
|||
joined: &Joined,
|
||||
focus: Focus,
|
||||
) -> BoxedAsync<'a, UiError> {
|
||||
let nick_list_widget = nick_list::widget(nick_list, joined, focus == Focus::NickList)
|
||||
.padding()
|
||||
.with_right(1)
|
||||
.border()
|
||||
.desync();
|
||||
let nick_list_widget = nick_list::widget(
|
||||
nick_list,
|
||||
joined,
|
||||
focus == Focus::NickList,
|
||||
chat.nick_emoji(),
|
||||
)
|
||||
.padding()
|
||||
.with_right(1)
|
||||
.border()
|
||||
.desync();
|
||||
|
||||
let chat_widget = chat.widget(joined.session.name.clone(), focus == Focus::Chat);
|
||||
|
||||
|
|
|
|||
|
|
@ -536,7 +536,10 @@ impl Rooms {
|
|||
}
|
||||
if event.matches(&keys.rooms.action.connect_autojoin) {
|
||||
for (domain, server) in &self.config.euph.servers {
|
||||
for name in server.rooms.keys() {
|
||||
for (name, room) in &server.rooms {
|
||||
if !room.autojoin {
|
||||
continue;
|
||||
}
|
||||
let id = RoomIdentifier::new(domain.clone(), name.clone());
|
||||
self.connect_to_room(id).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -611,7 +611,7 @@ impl Action for GetMsg {
|
|||
let msg = conn
|
||||
.query_row(
|
||||
"
|
||||
SELECT id, parent, time, name, content, seen
|
||||
SELECT id, parent, time, user_id, name, content, seen
|
||||
FROM euph_msgs
|
||||
WHERE domain = ?
|
||||
AND room = ?
|
||||
|
|
@ -623,9 +623,10 @@ impl Action for GetMsg {
|
|||
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
||||
time: row.get::<_, WTime>(2)?.0,
|
||||
nick: row.get(3)?,
|
||||
content: row.get(4)?,
|
||||
seen: row.get(5)?,
|
||||
user_id: UserId(row.get(3)?),
|
||||
nick: row.get(4)?,
|
||||
content: row.get(5)?,
|
||||
seen: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
@ -703,7 +704,7 @@ impl Action for GetTree {
|
|||
AND tree.room = euph_msgs.room
|
||||
AND tree.id = euph_msgs.parent
|
||||
)
|
||||
SELECT id, parent, time, name, content, seen
|
||||
SELECT id, parent, time, user_id, name, content, seen
|
||||
FROM euph_msgs
|
||||
JOIN tree USING (domain, room, id)
|
||||
ORDER BY id ASC
|
||||
|
|
@ -716,9 +717,10 @@ impl Action for GetTree {
|
|||
id: MessageId(row.get::<_, WSnowflake>(0)?.0),
|
||||
parent: row.get::<_, Option<WSnowflake>>(1)?.map(|s| MessageId(s.0)),
|
||||
time: row.get::<_, WTime>(2)?.0,
|
||||
nick: row.get(3)?,
|
||||
content: row.get(4)?,
|
||||
seen: row.get(5)?,
|
||||
user_id: UserId(row.get(3)?),
|
||||
nick: row.get(4)?,
|
||||
content: row.get(5)?,
|
||||
seen: row.get(6)?,
|
||||
})
|
||||
},
|
||||
)?
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue