Compare commits

...

5 commits

8 changed files with 133 additions and 68 deletions

48
Cargo.lock generated
View file

@ -1442,6 +1442,41 @@ dependencies = [
"which",
]
[[package]]
name = "editor-types"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e99679670f67825fcd24a23cb4eb655a0f92c82bd4d1c1a1357c0cd71e87"
dependencies = [
"bitflags 2.9.1",
"editor-types-macros",
"keybindings",
"regex",
]
[[package]]
name = "editor-types-macros"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42680de76cf91f231abd90cc623750d39077f7d2fadb7962325fb082871f4c66"
dependencies = [
"editor-types-parser",
"nom",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "editor-types-parser"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cac4b91fe830fbbe0a60c37ba0264b6e9ffc70e3664c028234dac59e79299ad4"
dependencies = [
"nom",
"thiserror 1.0.69",
]
[[package]]
name = "either"
version = "1.15.0"
@ -2706,9 +2741,9 @@ dependencies = [
[[package]]
name = "keybindings"
version = "0.0.1"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "680e4699c91c0622dd70da32c274881aadb1ac86252d738c3641266e90e4ca15"
checksum = "19a726307ed87e05155c31329676130e6a237e62dda80211f7e1ed811e47630f"
dependencies = [
"textwrap",
"unicode-segmentation",
@ -3292,15 +3327,16 @@ dependencies = [
[[package]]
name = "modalkit"
version = "0.0.21"
version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc7599fc1bcd2f0a0b4598f23433b45613345a46419ab27d3a9adecb57acd648"
checksum = "6ed06f32b03a7504acadcb0d95d06f3d55258934c34b620ed95e3dae24f081a5"
dependencies = [
"anymap2",
"arboard",
"bitflags 2.9.1",
"crossterm",
"derive_more",
"editor-types",
"intervaltree",
"keybindings",
"nom",
@ -3314,9 +3350,9 @@ dependencies = [
[[package]]
name = "modalkit-ratatui"
version = "0.0.21"
version = "0.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd3d88c86435d4b1fb22c7c0f978b09cb888338cafc10336715aa5070e92b6f6"
checksum = "a33bd64f6dd0011ee88f4f12b28108d3e63df0a5c86fe595d24561be9522f6ea"
dependencies = [
"crossterm",
"intervaltree",

View file

@ -77,13 +77,13 @@ features = ["zbus", "serde"]
optional = true
[dependencies.modalkit]
version = "0.0.21"
version = "0.0.23"
default-features = false
#git = "https://github.com/ulyssa/modalkit"
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"
[dependencies.modalkit-ratatui]
version = "0.0.21"
version = "0.0.23"
#git = "https://github.com/ulyssa/modalkit"
#rev = "e40dbb0bfeabe4cfd08facd2acb446080a330d75"

View file

@ -74,7 +74,7 @@ use modalkit::{
ApplicationStore,
ApplicationWindowId,
},
completion::{complete_path, CompletionMap},
completion::{complete_path, Completer, CompletionMap},
context::EditContext,
cursor::Cursor,
rope::EditRope,
@ -1914,11 +1914,20 @@ impl ApplicationInfo for IambInfo {
type WindowId = IambId;
type ContentId = IambBufferId;
fn content_of_command(ct: CommandType) -> IambBufferId {
IambBufferId::Command(ct)
}
}
pub struct IambCompleter;
impl Completer<IambInfo> for IambCompleter {
fn complete(
&mut self,
text: &EditRope,
cursor: &mut Cursor,
content: &IambBufferId,
store: &mut ProgramStore,
store: &mut ChatStore,
) -> Vec<String> {
match content {
IambBufferId::Command(CommandType::Command) => complete_cmdbar(text, cursor, store),
@ -1936,21 +1945,16 @@ impl ApplicationInfo for IambInfo {
IambBufferId::UnreadList => vec![],
}
}
fn content_of_command(ct: CommandType) -> IambBufferId {
IambBufferId::Command(ct)
}
}
/// Tab completion for user IDs.
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
let id = text
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
.unwrap_or_else(EditRope::empty);
let id = Cow::from(&id);
store
.application
.presences
.complete(id.as_ref())
.into_iter()
@ -1959,7 +1963,7 @@ fn complete_users(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) ->
}
/// Tab completion within the message bar.
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
let id = text
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
.unwrap_or_else(EditRope::empty);
@ -1968,13 +1972,12 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
match id.chars().next() {
// Complete room aliases.
Some('#') => {
return store.application.names.complete(id.as_ref());
return store.names.complete(id.as_ref());
},
// Complete room identifiers.
Some('!') => {
return store
.application
.rooms
.complete(id.as_ref())
.into_iter()
@ -1984,7 +1987,7 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
// Complete Emoji shortcodes.
Some(':') => {
let list = store.application.emojis.complete(&id[1..]);
let list = store.emojis.complete(&id[1..]);
let iter = list.into_iter().take(200).map(|s| format!(":{}:", s));
return iter.collect();
@ -1993,7 +1996,6 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
// Complete usernames for @ and empty strings.
Some('@') | None => {
return store
.application
.presences
.complete(id.as_ref())
.into_iter()
@ -2007,28 +2009,23 @@ fn complete_msgbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -
}
/// Tab completion for Matrix identifiers (usernames, room aliases, etc.)
fn complete_matrix_names(
text: &EditRope,
cursor: &mut Cursor,
store: &ProgramStore,
) -> Vec<String> {
fn complete_matrix_names(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
let id = text
.get_prefix_word_mut(cursor, &MATRIX_ID_WORD)
.unwrap_or_else(EditRope::empty);
let id = Cow::from(&id);
let list = store.application.names.complete(id.as_ref());
let list = store.names.complete(id.as_ref());
if !list.is_empty() {
return list;
}
let list = store.application.presences.complete(id.as_ref());
let list = store.presences.complete(id.as_ref());
if !list.is_empty() {
return list.into_iter().map(|i| i.to_string()).collect();
}
store
.application
.rooms
.complete(id.as_ref())
.into_iter()
@ -2037,12 +2034,12 @@ fn complete_matrix_names(
}
/// Tab completion for Emoji shortcode names.
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
fn complete_emoji(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
let sc = text.get_prefix_word_mut(cursor, &WordStyle::Little);
let sc = sc.unwrap_or_else(EditRope::empty);
let sc = Cow::from(&sc);
store.application.emojis.complete(sc.as_ref())
store.emojis.complete(sc.as_ref())
}
/// Tab completion for command names.
@ -2050,11 +2047,11 @@ fn complete_cmdname(
desc: CommandDescription,
text: &EditRope,
cursor: &mut Cursor,
store: &ProgramStore,
store: &ChatStore,
) -> Vec<String> {
// Complete command name and set cursor position.
let _ = text.get_prefix_word_mut(cursor, &WordStyle::Little);
store.application.cmds.complete_name(desc.command.as_str())
store.cmds.complete_name(desc.command.as_str())
}
/// Tab completion for command arguments.
@ -2062,9 +2059,9 @@ fn complete_cmdarg(
desc: CommandDescription,
text: &EditRope,
cursor: &mut Cursor,
store: &ProgramStore,
store: &ChatStore,
) -> Vec<String> {
let cmd = match store.application.cmds.get(desc.command.as_str()) {
let cmd = match store.cmds.get(desc.command.as_str()) {
Ok(cmd) => cmd,
Err(_) => return vec![],
};
@ -2087,12 +2084,7 @@ fn complete_cmdarg(
}
/// Tab completion for commands.
fn complete_cmd(
cmd: &str,
text: &EditRope,
cursor: &mut Cursor,
store: &ProgramStore,
) -> Vec<String> {
fn complete_cmd(cmd: &str, text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
match CommandDescription::from_str(cmd) {
Ok(desc) => {
if desc.arg.untrimmed.is_empty() {
@ -2109,7 +2101,7 @@ fn complete_cmd(
}
/// Tab completion for the command bar.
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ProgramStore) -> Vec<String> {
fn complete_cmdbar(text: &EditRope, cursor: &mut Cursor, store: &ChatStore) -> Vec<String> {
let eo = text.cursor_to_offset(cursor);
let slice = text.slice(..eo);
let cow = Cow::from(&slice);
@ -2289,6 +2281,7 @@ pub mod tests {
#[tokio::test]
async fn test_complete_msgbar() {
let store = mock_store().await;
let store = store.application;
let text = EditRope::from("going for a walk :walk ");
let mut cursor = Cursor::new(0, 22);
@ -2312,6 +2305,7 @@ pub mod tests {
#[tokio::test]
async fn test_complete_cmdbar() {
let store = mock_store().await;
let store = store.application;
let users = vec![
"@user1:example.com",
"@user2:example.com",

View file

@ -353,29 +353,31 @@ pub struct UserDisplayTunables {
pub type UserOverrides = HashMap<OwnedUserId, UserDisplayTunables>;
fn merge_sorts(a: SortOverrides, b: SortOverrides) -> SortOverrides {
fn merge_sorts(profile: SortOverrides, global: SortOverrides) -> SortOverrides {
SortOverrides {
chats: b.chats.or(a.chats),
dms: b.dms.or(a.dms),
rooms: b.rooms.or(a.rooms),
spaces: b.spaces.or(a.spaces),
members: b.members.or(a.members),
chats: profile.chats.or(global.chats),
dms: profile.dms.or(global.dms),
rooms: profile.rooms.or(global.rooms),
spaces: profile.spaces.or(global.spaces),
members: profile.members.or(global.members),
}
}
fn merge_maps<K, V>(a: Option<HashMap<K, V>>, b: Option<HashMap<K, V>>) -> Option<HashMap<K, V>>
fn merge_maps<K, V>(
profile: Option<HashMap<K, V>>,
global: Option<HashMap<K, V>>,
) -> Option<HashMap<K, V>>
where
K: Eq + Hash,
{
match (a, b) {
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(Some(mut a), Some(b)) => {
for (k, v) in b {
a.insert(k, v);
match (global, profile) {
(Some(m), None) | (None, Some(m)) => Some(m),
(Some(mut global), Some(profile)) => {
for (k, v) in profile {
global.insert(k, v);
}
Some(a)
Some(global)
},
(None, None) => None,
}
@ -397,6 +399,9 @@ pub enum UserDisplayStyle {
// it can wind up being the Matrix username if there are display name collisions in the room,
// in order to avoid any confusion.
DisplayName,
// Acts like Username, except when the username matches given regex, then acts like DisplayName
Regex,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -566,6 +571,7 @@ pub struct TunableValues {
pub typing_notice_display: bool,
pub users: UserOverrides,
pub username_display: UserDisplayStyle,
pub username_display_regex: Option<String>,
pub message_user_color: bool,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
@ -593,6 +599,7 @@ pub struct Tunables {
pub typing_notice_display: Option<bool>,
pub users: Option<UserOverrides>,
pub username_display: Option<UserDisplayStyle>,
pub username_display_regex: Option<String>,
pub message_user_color: Option<bool>,
pub default_room: Option<String>,
pub open_command: Option<Vec<String>>,
@ -624,6 +631,7 @@ impl Tunables {
typing_notice_display: self.typing_notice_display.or(other.typing_notice_display),
users: merge_maps(self.users, other.users),
username_display: self.username_display.or(other.username_display),
username_display_regex: self.username_display_regex.or(other.username_display_regex),
message_user_color: self.message_user_color.or(other.message_user_color),
default_room: self.default_room.or(other.default_room),
open_command: self.open_command.or(other.open_command),
@ -653,6 +661,7 @@ impl Tunables {
typing_notice_display: self.typing_notice_display.unwrap_or(true),
users: self.users.unwrap_or_default(),
username_display: self.username_display.unwrap_or_default(),
username_display_regex: self.username_display_regex,
message_user_color: self.message_user_color.unwrap_or(false),
default_room: self.default_room,
open_command: self.open_command,
@ -911,7 +920,7 @@ impl ApplicationSettings {
}
};
let macros = merge_maps(macros, profile.macros.take()).unwrap_or_default();
let macros = merge_maps(profile.macros.take(), macros).unwrap_or_default();
let layout = profile.layout.take().or(layout).unwrap_or_default();
let tunables = global.unwrap_or_default();
@ -1046,6 +1055,20 @@ impl ApplicationSettings {
Cow::Borrowed(user_id.as_str())
}
},
(None, UserDisplayStyle::Regex) => {
let re = regex::Regex::new(
&self.tunables.username_display_regex.clone().unwrap_or("*".into()),
)
.unwrap();
if !re.is_match(user_id.as_str()) {
Cow::Borrowed(user_id.as_str())
} else if let Some(display) = info.display_names.get(user_id) {
Cow::Borrowed(display.as_str())
} else {
Cow::Borrowed(user_id.as_str())
}
},
};
Span::styled(name, style)
@ -1110,10 +1133,10 @@ mod tests {
assert_eq!(res, Some(b.clone()));
let res = merge_maps(Some(b.clone()), Some(c.clone()));
assert_eq!(res, Some(c.clone()));
assert_eq!(res, Some(b.clone()));
let res = merge_maps(Some(c.clone()), Some(b.clone()));
assert_eq!(res, Some(b.clone()));
assert_eq!(res, Some(c.clone()));
}
#[test]
@ -1162,6 +1185,13 @@ mod tests {
let res: Tunables =
serde_json::from_str("{\"username_display\": \"displayname\"}").unwrap();
assert_eq!(res.username_display, Some(UserDisplayStyle::DisplayName));
let res: Tunables = serde_json::from_str(
"{\"username_display\": \"regex\",\n\"username_display_regex\": \"foo\"}",
)
.unwrap();
assert_eq!(res.username_display, Some(UserDisplayStyle::Regex));
assert_eq!(res.username_display_regex.unwrap_or("FAILED".into()), "foo".to_string());
}
#[test]

View file

@ -62,7 +62,7 @@ use modalkit::crossterm::{
use ratatui::{
backend::CrosstermBackend,
layout::Rect,
style::{Color, Style},
style::{Color, Modifier, Style},
text::Span,
widgets::Paragraph,
Terminal,
@ -89,6 +89,7 @@ use crate::{
ChatStore,
HomeserverAction,
IambAction,
IambCompleter,
IambError,
IambId,
IambInfo,
@ -327,6 +328,9 @@ impl Application {
.show_dialog(dialogstr)
.show_mode(modestr)
.borders(true)
.border_style(Style::default().add_modifier(Modifier::DIM))
.tab_style(Style::default().add_modifier(Modifier::DIM))
.tab_style_focused(Style::default().remove_modifier(Modifier::DIM))
.focus(focused);
f.render_stateful_widget(screen, area, sstate);
@ -529,7 +533,7 @@ impl Application {
},
// Unimplemented.
Action::KeywordLookup => {
Action::KeywordLookup(_) => {
// XXX: implement
None
},
@ -1011,7 +1015,9 @@ async fn run(settings: ApplicationSettings) -> IambResult<()> {
// Set up the async worker thread and global store.
let worker = ClientWorker::spawn(client.clone(), settings.clone()).await;
let store = ChatStore::new(worker.clone(), settings.clone());
let store = Store::new(store);
let mut store = Store::new(store);
store.completer = Box::new(IambCompleter);
let store = Arc::new(AsyncMutex::new(store));
worker.init(store.clone());

View file

@ -189,6 +189,7 @@ pub fn mock_tunables() -> TunableValues {
open_command: None,
external_edit_file_suffix: String::from(".md"),
username_display: UserDisplayStyle::Username,
username_display_regex: Some(String::from(".*")),
message_user_color: false,
mouse: Default::default(),
notifications: Notifications {

View file

@ -864,16 +864,16 @@ impl PromptActions<ProgramContext, ProgramStore, IambInfo> for ChatState {
fn recall(
&mut self,
filter: &RecallFilter,
dir: &MoveDir1D,
count: &Count,
prefixed: bool,
ctx: &ProgramContext,
_: &mut ProgramStore,
) -> EditResult<Vec<(ProgramAction, ProgramContext)>, IambInfo> {
let count = ctx.resolve(count);
let rope = self.tbox.get();
let text = self.sent.recall(&rope, &mut self.sent_scrollback, *dir, prefixed, count);
let text = self.sent.recall(&rope, &mut self.sent_scrollback, filter, *dir, count);
if let Some(text) = text {
self.tbox.set_text(text);
@ -897,9 +897,7 @@ impl Promptable<ProgramContext, ProgramStore, IambInfo> for ChatState {
match act {
PromptAction::Submit => self.submit(ctx, store),
PromptAction::Abort(empty) => self.abort(*empty, ctx, store),
PromptAction::Recall(dir, count, prefixed) => {
self.recall(dir, count, *prefixed, ctx, store)
},
PromptAction::Recall(filter, dir, count) => self.recall(filter, dir, count, ctx, store),
}
}
}

View file

@ -840,8 +840,8 @@ impl EditorActions<ProgramContext, ProgramStore, IambInfo> for ScrollbackState {
fn complete(
&mut self,
_: &CompletionStyle,
_: &CompletionType,
_: &CompletionSelection,
_: &CompletionDisplay,
_: &ProgramContext,
_: &mut ProgramStore,