mirror of
https://github.com/youwen5/iamb.git
synced 2025-06-20 13:49:52 -07:00
Restore opened tabs and windows upon restart (#72)
This commit is contained in:
parent
758397eb5a
commit
7c39e88ba2
8 changed files with 428 additions and 25 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
@ -2136,8 +2136,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "modalkit"
|
name = "modalkit"
|
||||||
version = "0.0.15"
|
version = "0.0.16"
|
||||||
source = "git+https://github.com/ulyssa/modalkit?rev=f641162#f6411625caa1ddb764c91a3fc47e052cfdc405b5"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f38eef0b4f6377e73d1082d508cae1df4d99a94d25361538131d839f292aa49"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anymap2",
|
"anymap2",
|
||||||
"arboard",
|
"arboard",
|
||||||
|
@ -2151,6 +2152,7 @@ dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"regex",
|
"regex",
|
||||||
"ropey",
|
"ropey",
|
||||||
|
"serde",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
|
|
|
@ -51,8 +51,7 @@ unicode-width = "0.1.10"
|
||||||
url = {version = "^2.2.2", features = ["serde"]}
|
url = {version = "^2.2.2", features = ["serde"]}
|
||||||
|
|
||||||
[dependencies.modalkit]
|
[dependencies.modalkit]
|
||||||
git = "https://github.com/ulyssa/modalkit"
|
version = "0.0.16"
|
||||||
rev = "f641162"
|
|
||||||
|
|
||||||
[dependencies.matrix-sdk]
|
[dependencies.matrix-sdk]
|
||||||
version = "0.6"
|
version = "0.6"
|
||||||
|
|
|
@ -36,6 +36,10 @@ These options are configured as a map under the profiles name.
|
||||||
> Overwrite general settings for this account. The fields are identical to
|
> Overwrite general settings for this account. The fields are identical to
|
||||||
> those in *TUNABLES*.
|
> those in *TUNABLES*.
|
||||||
|
|
||||||
|
**layout** (type: startup layout object)
|
||||||
|
> Overwrite general settings for this account. The fields are identical to
|
||||||
|
> those in *STARTUP LAYOUT*.
|
||||||
|
|
||||||
**dirs** (type: XDG overrides object)
|
**dirs** (type: XDG overrides object)
|
||||||
> Overwrite general settings for this account. The fields are identical to
|
> Overwrite general settings for this account. The fields are identical to
|
||||||
> those in *DIRECTORIES*.
|
> those in *DIRECTORIES*.
|
||||||
|
@ -92,6 +96,23 @@ maps containing the following key value pairs.
|
||||||
> _light-cyan_, _light-green_, _light-magenta_, _light-red_,
|
> _light-cyan_, _light-green_, _light-magenta_, _light-red_,
|
||||||
> _light-yellow_, _magenta_, _none_, _red_, _white_, _yellow_
|
> _light-yellow_, _magenta_, _none_, _red_, _white_, _yellow_
|
||||||
|
|
||||||
|
# STARTUP LAYOUT
|
||||||
|
|
||||||
|
Specifies what initial set of tabs and windows to show when starting the
|
||||||
|
client. Configured as an object under the key *layout*.
|
||||||
|
|
||||||
|
**style** (type: string)
|
||||||
|
> Specifies what window layout to load when starting. Valid values are
|
||||||
|
> _restore_ to restore the layout from the last time the client was exited,
|
||||||
|
> _new_ to open a single window (uses the value of _default\_room_ if set), or
|
||||||
|
> _config_ to open the layout described under _tabs_.
|
||||||
|
|
||||||
|
**tabs** (type: array of window objects)
|
||||||
|
> If **style** is set to _config_, then this value will be used to open a set
|
||||||
|
> of tabs and windows at startup. Each object can contain either a **window**
|
||||||
|
> key specifying a username, room identifier or room alias to show, or a
|
||||||
|
> **split** key specifying an array of window objects.
|
||||||
|
|
||||||
# DIRECTORIES
|
# DIRECTORIES
|
||||||
|
|
||||||
Specifies the directories to save data in. Configured as a map under the key
|
Specifies the directories to save data in. Configured as a map under the key
|
||||||
|
|
153
src/base.rs
153
src/base.rs
|
@ -1,12 +1,23 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::fmt::{self, Display};
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use emojis::Emoji;
|
use emojis::Emoji;
|
||||||
|
use serde::{
|
||||||
|
de::Error as SerdeError,
|
||||||
|
de::Visitor,
|
||||||
|
Deserialize,
|
||||||
|
Deserializer,
|
||||||
|
Serialize,
|
||||||
|
Serializer,
|
||||||
|
};
|
||||||
use tokio::sync::Mutex as AsyncMutex;
|
use tokio::sync::Mutex as AsyncMutex;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use matrix_sdk::{
|
use matrix_sdk::{
|
||||||
encryption::verification::SasVerification,
|
encryption::verification::SasVerification,
|
||||||
|
@ -715,17 +726,159 @@ impl ApplicationStore for ChatStore {}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum IambId {
|
pub enum IambId {
|
||||||
|
/// A Matrix room.
|
||||||
Room(OwnedRoomId),
|
Room(OwnedRoomId),
|
||||||
|
|
||||||
|
/// The `:rooms` window.
|
||||||
DirectList,
|
DirectList,
|
||||||
|
|
||||||
|
/// The `:members` window for a given Matrix room.
|
||||||
MemberList(OwnedRoomId),
|
MemberList(OwnedRoomId),
|
||||||
|
|
||||||
|
/// The `:rooms` window.
|
||||||
RoomList,
|
RoomList,
|
||||||
|
|
||||||
|
/// The `:spaces` window.
|
||||||
SpaceList,
|
SpaceList,
|
||||||
|
|
||||||
|
/// The `:verify` window.
|
||||||
VerifyList,
|
VerifyList,
|
||||||
|
|
||||||
|
/// The `:welcome` window.
|
||||||
Welcome,
|
Welcome,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for IambId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
IambId::Room(room_id) => {
|
||||||
|
write!(f, "iamb://room/{room_id}")
|
||||||
|
},
|
||||||
|
IambId::MemberList(room_id) => {
|
||||||
|
write!(f, "iamb://members/{room_id}")
|
||||||
|
},
|
||||||
|
IambId::DirectList => f.write_str("iamb://dms"),
|
||||||
|
IambId::RoomList => f.write_str("iamb://rooms"),
|
||||||
|
IambId::SpaceList => f.write_str("iamb://spaces"),
|
||||||
|
IambId::VerifyList => f.write_str("iamb://verify"),
|
||||||
|
IambId::Welcome => f.write_str("iamb://welcome"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ApplicationWindowId for IambId {}
|
impl ApplicationWindowId for IambId {}
|
||||||
|
|
||||||
|
impl Serialize for IambId {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for IambId {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
deserializer.deserialize_str(IambIdVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IambIdVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for IambIdVisitor {
|
||||||
|
type Value = IambId;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("a valid window URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: SerdeError,
|
||||||
|
{
|
||||||
|
let Ok(url) = Url::parse(value) else {
|
||||||
|
return Err(E::custom("Invalid iamb window URL"));
|
||||||
|
};
|
||||||
|
|
||||||
|
if url.scheme() != "iamb" {
|
||||||
|
return Err(E::custom("Invalid iamb window URL"));
|
||||||
|
}
|
||||||
|
|
||||||
|
match url.domain() {
|
||||||
|
Some("room") => {
|
||||||
|
let Some(path) = url.path_segments() else {
|
||||||
|
return Err(E::custom("Invalid members window URL"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let &[room_id] = path.collect::<Vec<_>>().as_slice() else {
|
||||||
|
return Err(E::custom("Invalid members window URL"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
||||||
|
return Err(E::custom("Invalid room identifier"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(IambId::Room(room_id))
|
||||||
|
},
|
||||||
|
Some("members") => {
|
||||||
|
let Some(path) = url.path_segments() else {
|
||||||
|
return Err(E::custom( "Invalid members window URL"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let &[room_id] = path.collect::<Vec<_>>().as_slice() else {
|
||||||
|
return Err(E::custom( "Invalid members window URL"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(room_id) = OwnedRoomId::try_from(room_id) else {
|
||||||
|
return Err(E::custom("Invalid room identifier"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(IambId::MemberList(room_id))
|
||||||
|
},
|
||||||
|
Some("dms") => {
|
||||||
|
if url.path() != "" {
|
||||||
|
return Err(E::custom("iamb://dms takes no path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IambId::DirectList)
|
||||||
|
},
|
||||||
|
Some("rooms") => {
|
||||||
|
if url.path() != "" {
|
||||||
|
return Err(E::custom("iamb://rooms takes no path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IambId::RoomList)
|
||||||
|
},
|
||||||
|
Some("spaces") => {
|
||||||
|
if url.path() != "" {
|
||||||
|
return Err(E::custom("iamb://spaces takes no path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IambId::SpaceList)
|
||||||
|
},
|
||||||
|
Some("verify") => {
|
||||||
|
if url.path() != "" {
|
||||||
|
return Err(E::custom("iamb://verify takes no path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IambId::VerifyList)
|
||||||
|
},
|
||||||
|
Some("welcome") => {
|
||||||
|
if url.path() != "" {
|
||||||
|
return Err(E::custom("iamb://welcome takes no path"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IambId::Welcome)
|
||||||
|
},
|
||||||
|
Some(s) => Err(E::custom(format!("{s:?} is not a valid window"))),
|
||||||
|
None => Err(E::custom("Invalid iamb window URL")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub enum RoomFocus {
|
pub enum RoomFocus {
|
||||||
Scrollback,
|
Scrollback,
|
||||||
|
|
117
src/config.rs
117
src/config.rs
|
@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use matrix_sdk::ruma::{OwnedUserId, UserId};
|
use matrix_sdk::ruma::{OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UserId};
|
||||||
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
use serde::{de::Error as SerdeError, de::Visitor, Deserialize, Deserializer};
|
||||||
use tracing::Level;
|
use tracing::Level;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -19,6 +19,8 @@ use modalkit::tui::{
|
||||||
text::Span,
|
text::Span,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::base::IambId;
|
||||||
|
|
||||||
macro_rules! usage {
|
macro_rules! usage {
|
||||||
( $($args: tt)* ) => {
|
( $($args: tt)* ) => {
|
||||||
println!($($args)*);
|
println!($($args)*);
|
||||||
|
@ -339,12 +341,43 @@ impl Directories {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum WindowPath {
|
||||||
|
AliasId(OwnedRoomAliasId),
|
||||||
|
RoomId(OwnedRoomId),
|
||||||
|
UserId(OwnedUserId),
|
||||||
|
Window(IambId),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(untagged, deny_unknown_fields)]
|
||||||
|
pub enum WindowLayout {
|
||||||
|
Window { window: WindowPath },
|
||||||
|
Split { split: Vec<WindowLayout> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase", tag = "style")]
|
||||||
|
pub enum Layout {
|
||||||
|
/// Restore the layout from the previous session.
|
||||||
|
#[default]
|
||||||
|
Restore,
|
||||||
|
|
||||||
|
/// Open a single window using the `default_room` value.
|
||||||
|
New,
|
||||||
|
|
||||||
|
/// Open the window layouts described under `tabs`.
|
||||||
|
Config { tabs: Vec<WindowLayout> },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
pub struct ProfileConfig {
|
pub struct ProfileConfig {
|
||||||
pub user_id: OwnedUserId,
|
pub user_id: OwnedUserId,
|
||||||
pub url: Url,
|
pub url: Url,
|
||||||
pub settings: Option<Tunables>,
|
pub settings: Option<Tunables>,
|
||||||
pub dirs: Option<Directories>,
|
pub dirs: Option<Directories>,
|
||||||
|
pub layout: Option<Layout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize)]
|
#[derive(Clone, Deserialize)]
|
||||||
|
@ -353,6 +386,7 @@ pub struct IambConfig {
|
||||||
pub default_profile: Option<String>,
|
pub default_profile: Option<String>,
|
||||||
pub settings: Option<Tunables>,
|
pub settings: Option<Tunables>,
|
||||||
pub dirs: Option<Directories>,
|
pub dirs: Option<Directories>,
|
||||||
|
pub layout: Option<Layout>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IambConfig {
|
impl IambConfig {
|
||||||
|
@ -376,11 +410,13 @@ impl IambConfig {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ApplicationSettings {
|
pub struct ApplicationSettings {
|
||||||
pub matrix_dir: PathBuf,
|
pub matrix_dir: PathBuf,
|
||||||
|
pub layout_json: PathBuf,
|
||||||
pub session_json: PathBuf,
|
pub session_json: PathBuf,
|
||||||
pub profile_name: String,
|
pub profile_name: String,
|
||||||
pub profile: ProfileConfig,
|
pub profile: ProfileConfig,
|
||||||
pub tunables: TunableValues,
|
pub tunables: TunableValues,
|
||||||
pub dirs: DirectoryValues,
|
pub dirs: DirectoryValues,
|
||||||
|
pub layout: Layout,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApplicationSettings {
|
impl ApplicationSettings {
|
||||||
|
@ -401,6 +437,7 @@ impl ApplicationSettings {
|
||||||
default_profile,
|
default_profile,
|
||||||
dirs,
|
dirs,
|
||||||
settings: global,
|
settings: global,
|
||||||
|
layout,
|
||||||
} = IambConfig::load(config_json.as_path())?;
|
} = IambConfig::load(config_json.as_path())?;
|
||||||
|
|
||||||
validate_profile_names(&profiles);
|
validate_profile_names(&profiles);
|
||||||
|
@ -424,10 +461,17 @@ impl ApplicationSettings {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let layout = profile.layout.take().or(layout).unwrap_or_default();
|
||||||
|
|
||||||
let tunables = global.unwrap_or_default();
|
let tunables = global.unwrap_or_default();
|
||||||
let tunables = profile.settings.take().unwrap_or_default().merge(tunables);
|
let tunables = profile.settings.take().unwrap_or_default().merge(tunables);
|
||||||
let tunables = tunables.values();
|
let tunables = tunables.values();
|
||||||
|
|
||||||
|
let dirs = dirs.unwrap_or_default();
|
||||||
|
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
|
||||||
|
let dirs = dirs.values();
|
||||||
|
|
||||||
|
// Set up paths that live inside the profile's data directory.
|
||||||
let mut profile_dir = config_dir.clone();
|
let mut profile_dir = config_dir.clone();
|
||||||
profile_dir.push("profiles");
|
profile_dir.push("profiles");
|
||||||
profile_dir.push(profile_name.as_str());
|
profile_dir.push(profile_name.as_str());
|
||||||
|
@ -438,17 +482,23 @@ impl ApplicationSettings {
|
||||||
let mut session_json = profile_dir;
|
let mut session_json = profile_dir;
|
||||||
session_json.push("session.json");
|
session_json.push("session.json");
|
||||||
|
|
||||||
let dirs = dirs.unwrap_or_default();
|
// Set up paths that live inside the profile's cache directory.
|
||||||
let dirs = profile.dirs.take().unwrap_or_default().merge(dirs);
|
let mut cache_dir = dirs.cache.clone();
|
||||||
let dirs = dirs.values();
|
cache_dir.push("profiles");
|
||||||
|
cache_dir.push(profile_name.as_str());
|
||||||
|
|
||||||
|
let mut layout_json = cache_dir.clone();
|
||||||
|
layout_json.push("layout.json");
|
||||||
|
|
||||||
let settings = ApplicationSettings {
|
let settings = ApplicationSettings {
|
||||||
matrix_dir,
|
matrix_dir,
|
||||||
|
layout_json,
|
||||||
session_json,
|
session_json,
|
||||||
profile_name,
|
profile_name,
|
||||||
profile,
|
profile,
|
||||||
tunables,
|
tunables,
|
||||||
dirs,
|
dirs,
|
||||||
|
layout,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
@ -496,6 +546,7 @@ impl ApplicationSettings {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use matrix_sdk::ruma::user_id;
|
use matrix_sdk::ruma::user_id;
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_profile_name_invalid() {
|
fn test_profile_name_invalid() {
|
||||||
|
@ -589,4 +640,62 @@ mod tests {
|
||||||
})];
|
})];
|
||||||
assert_eq!(res.users, Some(users.into_iter().collect()));
|
assert_eq!(res.users, Some(users.into_iter().collect()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_layout() {
|
||||||
|
let user = WindowPath::UserId(user_id!("@user:example.com").to_owned());
|
||||||
|
let alias = WindowPath::AliasId(OwnedRoomAliasId::try_from("#room:example.com").unwrap());
|
||||||
|
let room = WindowPath::RoomId(OwnedRoomId::try_from("!room:example.com").unwrap());
|
||||||
|
let dms = WindowPath::Window(IambId::DirectList);
|
||||||
|
let welcome = WindowPath::Window(IambId::Welcome);
|
||||||
|
|
||||||
|
let res: Layout = serde_json::from_str("{\"style\": \"restore\"}").unwrap();
|
||||||
|
assert_eq!(res, Layout::Restore);
|
||||||
|
|
||||||
|
let res: Layout = serde_json::from_str("{\"style\": \"new\"}").unwrap();
|
||||||
|
assert_eq!(res, Layout::New);
|
||||||
|
|
||||||
|
let res: Layout = serde_json::from_str(
|
||||||
|
"{\"style\": \"config\", \"tabs\": [{\"window\":\"@user:example.com\"}]}",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(res, Layout::Config {
|
||||||
|
tabs: vec![WindowLayout::Window { window: user.clone() }]
|
||||||
|
});
|
||||||
|
|
||||||
|
let res: Layout = serde_json::from_str(
|
||||||
|
"{\
|
||||||
|
\"style\": \"config\",\
|
||||||
|
\"tabs\": [\
|
||||||
|
{\"split\":[\
|
||||||
|
{\"window\":\"@user:example.com\"},\
|
||||||
|
{\"window\":\"#room:example.com\"}\
|
||||||
|
]},\
|
||||||
|
{\"split\":[\
|
||||||
|
{\"window\":\"!room:example.com\"},\
|
||||||
|
{\"split\":[\
|
||||||
|
{\"window\":\"iamb://dms\"},\
|
||||||
|
{\"window\":\"iamb://welcome\"}\
|
||||||
|
]}\
|
||||||
|
]}\
|
||||||
|
]}",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let split1 = WindowLayout::Split {
|
||||||
|
split: vec![
|
||||||
|
WindowLayout::Window { window: user.clone() },
|
||||||
|
WindowLayout::Window { window: alias },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let split2 = WindowLayout::Split {
|
||||||
|
split: vec![WindowLayout::Window { window: dms }, WindowLayout::Window {
|
||||||
|
window: welcome,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let split3 = WindowLayout::Split {
|
||||||
|
split: vec![WindowLayout::Window { window: room }, split2],
|
||||||
|
};
|
||||||
|
let tabs = vec![split1, split3];
|
||||||
|
assert_eq!(res, Layout::Config { tabs });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
145
src/main.rs
145
src/main.rs
|
@ -6,7 +6,7 @@ use std::collections::VecDeque;
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
use std::io::{stdout, BufReader, Stdout};
|
use std::io::{stdout, BufReader, BufWriter, Stdout};
|
||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
@ -89,6 +89,7 @@ use modalkit::{
|
||||||
Jumpable,
|
Jumpable,
|
||||||
Promptable,
|
Promptable,
|
||||||
Scrollable,
|
Scrollable,
|
||||||
|
TabAction,
|
||||||
TabContainer,
|
TabContainer,
|
||||||
TabCount,
|
TabCount,
|
||||||
UIError,
|
UIError,
|
||||||
|
@ -103,21 +104,120 @@ use modalkit::{
|
||||||
input::{bindings::BindingMachine, dialog::Pager, key::TerminalKey},
|
input::{bindings::BindingMachine, dialog::Pager, key::TerminalKey},
|
||||||
widgets::{
|
widgets::{
|
||||||
cmdbar::CommandBarState,
|
cmdbar::CommandBarState,
|
||||||
screen::{Screen, ScreenState},
|
screen::{FocusList, Screen, ScreenState, TabLayoutDescription},
|
||||||
|
windows::WindowLayoutDescription,
|
||||||
TerminalCursor,
|
TerminalCursor,
|
||||||
TerminalExtOps,
|
TerminalExtOps,
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn config_tab_to_desc(
|
||||||
|
layout: config::WindowLayout,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<WindowLayoutDescription<IambInfo>> {
|
||||||
|
let desc = match layout {
|
||||||
|
config::WindowLayout::Window { window } => {
|
||||||
|
let ChatStore { names, worker, .. } = &mut store.application;
|
||||||
|
|
||||||
|
let window = match window {
|
||||||
|
config::WindowPath::UserId(user_id) => {
|
||||||
|
let name = user_id.to_string();
|
||||||
|
let room_id = worker.join_room(name.clone())?;
|
||||||
|
names.insert(name, room_id.clone());
|
||||||
|
IambId::Room(room_id)
|
||||||
|
},
|
||||||
|
config::WindowPath::RoomId(room_id) => IambId::Room(room_id),
|
||||||
|
config::WindowPath::AliasId(alias) => {
|
||||||
|
let name = alias.to_string();
|
||||||
|
let room_id = worker.join_room(name.clone())?;
|
||||||
|
names.insert(name, room_id.clone());
|
||||||
|
IambId::Room(room_id)
|
||||||
|
},
|
||||||
|
config::WindowPath::Window(id) => id,
|
||||||
|
};
|
||||||
|
|
||||||
|
WindowLayoutDescription::Window { window, length: None }
|
||||||
|
},
|
||||||
|
config::WindowLayout::Split { split } => {
|
||||||
|
let children = split
|
||||||
|
.into_iter()
|
||||||
|
.map(|child| config_tab_to_desc(child, store))
|
||||||
|
.collect::<IambResult<Vec<_>>>()?;
|
||||||
|
|
||||||
|
WindowLayoutDescription::Split { children, length: None }
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_screen(
|
||||||
|
settings: ApplicationSettings,
|
||||||
|
store: &mut ProgramStore,
|
||||||
|
) -> IambResult<ScreenState<IambWindow, IambInfo>> {
|
||||||
|
let cmd = CommandBarState::new(store);
|
||||||
|
let dims = crossterm::terminal::size()?;
|
||||||
|
let area = Rect::new(0, 0, dims.0, dims.1);
|
||||||
|
|
||||||
|
match settings.layout {
|
||||||
|
config::Layout::Restore => {
|
||||||
|
if let Ok(layout) = std::fs::read(&settings.layout_json) {
|
||||||
|
let tabs: TabLayoutDescription<IambInfo> =
|
||||||
|
serde_json::from_slice(&layout).map_err(IambError::from)?;
|
||||||
|
let tabs = tabs.to_layout(area.into(), store)?;
|
||||||
|
|
||||||
|
return Ok(ScreenState::from_list(tabs, cmd));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config::Layout::New => {},
|
||||||
|
config::Layout::Config { tabs } => {
|
||||||
|
let mut list = FocusList::default();
|
||||||
|
|
||||||
|
for tab in tabs.into_iter() {
|
||||||
|
let tab = config_tab_to_desc(tab, store)?;
|
||||||
|
let tab = tab.to_layout(area.into(), store)?;
|
||||||
|
list.push(tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ScreenState::from_list(list, cmd));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let win = settings
|
||||||
|
.tunables
|
||||||
|
.default_room
|
||||||
|
.and_then(|room| IambWindow::find(room, store).ok())
|
||||||
|
.or_else(|| IambWindow::open(IambId::Welcome, store).ok())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
return Ok(ScreenState::new(win, cmd));
|
||||||
|
}
|
||||||
|
|
||||||
struct Application {
|
struct Application {
|
||||||
store: AsyncProgramStore,
|
/// Terminal backend.
|
||||||
worker: Requester,
|
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
bindings: KeyManager<TerminalKey, ProgramAction, RepeatType, ProgramContext>,
|
|
||||||
actstack: VecDeque<(ProgramAction, ProgramContext)>,
|
/// State for the Matrix client, editing, etc.
|
||||||
|
store: AsyncProgramStore,
|
||||||
|
|
||||||
|
/// UI state (open tabs, command bar, etc.) to use when rendering.
|
||||||
screen: ScreenState<IambWindow, IambInfo>,
|
screen: ScreenState<IambWindow, IambInfo>,
|
||||||
|
|
||||||
|
/// Handle to communicate synchronously with the Matrix worker task.
|
||||||
|
worker: Requester,
|
||||||
|
|
||||||
|
/// Mapped keybindings.
|
||||||
|
bindings: KeyManager<TerminalKey, ProgramAction, RepeatType, ProgramContext>,
|
||||||
|
|
||||||
|
/// Pending actions to run.
|
||||||
|
actstack: VecDeque<(ProgramAction, ProgramContext)>,
|
||||||
|
|
||||||
|
/// Whether or not the terminal is currently focused.
|
||||||
focused: bool,
|
focused: bool,
|
||||||
|
|
||||||
|
/// The tab layout before the last executed [TabAction].
|
||||||
|
last_layout: Option<TabLayoutDescription<IambInfo>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Application {
|
impl Application {
|
||||||
|
@ -141,16 +241,7 @@ impl Application {
|
||||||
let bindings = KeyManager::new(bindings);
|
let bindings = KeyManager::new(bindings);
|
||||||
|
|
||||||
let mut locked = store.lock().await;
|
let mut locked = store.lock().await;
|
||||||
|
let screen = setup_screen(settings, locked.deref_mut())?;
|
||||||
let win = settings
|
|
||||||
.tunables
|
|
||||||
.default_room
|
|
||||||
.and_then(|room| IambWindow::find(room, locked.deref_mut()).ok())
|
|
||||||
.or_else(|| IambWindow::open(IambId::Welcome, locked.deref_mut()).ok())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let cmd = CommandBarState::new(locked.deref_mut());
|
|
||||||
let screen = ScreenState::new(win, cmd);
|
|
||||||
|
|
||||||
let worker = locked.application.worker.clone();
|
let worker = locked.application.worker.clone();
|
||||||
drop(locked);
|
drop(locked);
|
||||||
|
@ -165,6 +256,7 @@ impl Application {
|
||||||
actstack,
|
actstack,
|
||||||
screen,
|
screen,
|
||||||
focused: true,
|
focused: true,
|
||||||
|
last_layout: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -312,7 +404,6 @@ impl Application {
|
||||||
Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?,
|
Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?,
|
||||||
Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?,
|
Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?,
|
||||||
Action::ShowInfoMessage(info) => Some(info),
|
Action::ShowInfoMessage(info) => Some(info),
|
||||||
Action::Tab(cmd) => self.screen.tab_command(&cmd, &ctx, store)?,
|
|
||||||
Action::Window(cmd) => self.screen.window_command(&cmd, &ctx, store)?,
|
Action::Window(cmd) => self.screen.window_command(&cmd, &ctx, store)?,
|
||||||
|
|
||||||
Action::Jump(l, dir, count) => {
|
Action::Jump(l, dir, count) => {
|
||||||
|
@ -328,6 +419,13 @@ impl Application {
|
||||||
},
|
},
|
||||||
|
|
||||||
// UI actions.
|
// UI actions.
|
||||||
|
Action::Tab(cmd) => {
|
||||||
|
if let TabAction::Close(_, _) = &cmd {
|
||||||
|
self.last_layout = self.screen.as_description().into();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.screen.tab_command(&cmd, &ctx, store)?
|
||||||
|
},
|
||||||
Action::RedrawScreen => {
|
Action::RedrawScreen => {
|
||||||
self.screen.clear_message();
|
self.screen.clear_message();
|
||||||
self.redraw(true, store)?;
|
self.redraw(true, store)?;
|
||||||
|
@ -494,6 +592,19 @@ impl Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref layout) = self.last_layout {
|
||||||
|
let locked = self.store.lock().await;
|
||||||
|
let path = locked.application.settings.layout_json.as_path();
|
||||||
|
path.parent().map(create_dir_all).transpose()?;
|
||||||
|
|
||||||
|
let file = File::create(path)?;
|
||||||
|
let writer = BufWriter::new(file);
|
||||||
|
|
||||||
|
if let Err(e) = serde_json::to_writer(writer, layout) {
|
||||||
|
tracing::error!("Failed to save window layout while exiting: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
crossterm::terminal::disable_raw_mode()?;
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
|
execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||||
self.terminal.show_cursor()?;
|
self.terminal.show_cursor()?;
|
||||||
|
|
|
@ -156,6 +156,10 @@ impl<'a> TextPrinter<'a> {
|
||||||
pub fn push_str(&mut self, s: &'a str, style: Style) {
|
pub fn push_str(&mut self, s: &'a str, style: Style) {
|
||||||
let style = self.base_style.patch(style);
|
let style = self.base_style.patch(style);
|
||||||
|
|
||||||
|
if self.width == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
||||||
if let "\n" | "\r\n" = word {
|
if let "\n" | "\r\n" = word {
|
||||||
// Render embedded newlines as spaces.
|
// Render embedded newlines as spaces.
|
||||||
|
|
|
@ -195,16 +195,20 @@ pub fn mock_tunables() -> TunableValues {
|
||||||
pub fn mock_settings() -> ApplicationSettings {
|
pub fn mock_settings() -> ApplicationSettings {
|
||||||
ApplicationSettings {
|
ApplicationSettings {
|
||||||
matrix_dir: PathBuf::new(),
|
matrix_dir: PathBuf::new(),
|
||||||
|
layout_json: PathBuf::new(),
|
||||||
session_json: PathBuf::new(),
|
session_json: PathBuf::new(),
|
||||||
|
|
||||||
profile_name: "test".into(),
|
profile_name: "test".into(),
|
||||||
profile: ProfileConfig {
|
profile: ProfileConfig {
|
||||||
user_id: user_id!("@user:example.com").to_owned(),
|
user_id: user_id!("@user:example.com").to_owned(),
|
||||||
url: Url::parse("https://example.com").unwrap(),
|
url: Url::parse("https://example.com").unwrap(),
|
||||||
settings: None,
|
settings: None,
|
||||||
dirs: None,
|
dirs: None,
|
||||||
|
layout: None,
|
||||||
},
|
},
|
||||||
tunables: mock_tunables(),
|
tunables: mock_tunables(),
|
||||||
dirs: mock_dirs(),
|
dirs: mock_dirs(),
|
||||||
|
layout: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue