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
153
src/base.rs
153
src/base.rs
|
@ -1,12 +1,23 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Display};
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use emojis::Emoji;
|
||||
use serde::{
|
||||
de::Error as SerdeError,
|
||||
de::Visitor,
|
||||
Deserialize,
|
||||
Deserializer,
|
||||
Serialize,
|
||||
Serializer,
|
||||
};
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
encryption::verification::SasVerification,
|
||||
|
@ -715,17 +726,159 @@ impl ApplicationStore for ChatStore {}
|
|||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum IambId {
|
||||
/// A Matrix room.
|
||||
Room(OwnedRoomId),
|
||||
|
||||
/// The `:rooms` window.
|
||||
DirectList,
|
||||
|
||||
/// The `:members` window for a given Matrix room.
|
||||
MemberList(OwnedRoomId),
|
||||
|
||||
/// The `:rooms` window.
|
||||
RoomList,
|
||||
|
||||
/// The `:spaces` window.
|
||||
SpaceList,
|
||||
|
||||
/// The `:verify` window.
|
||||
VerifyList,
|
||||
|
||||
/// The `:welcome` window.
|
||||
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 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)]
|
||||
pub enum RoomFocus {
|
||||
Scrollback,
|
||||
|
|
117
src/config.rs
117
src/config.rs
|
@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process;
|
||||
|
||||
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 tracing::Level;
|
||||
use url::Url;
|
||||
|
@ -19,6 +19,8 @@ use modalkit::tui::{
|
|||
text::Span,
|
||||
};
|
||||
|
||||
use super::base::IambId;
|
||||
|
||||
macro_rules! usage {
|
||||
( $($args: tt)* ) => {
|
||||
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)]
|
||||
pub struct ProfileConfig {
|
||||
pub user_id: OwnedUserId,
|
||||
pub url: Url,
|
||||
pub settings: Option<Tunables>,
|
||||
pub dirs: Option<Directories>,
|
||||
pub layout: Option<Layout>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
|
@ -353,6 +386,7 @@ pub struct IambConfig {
|
|||
pub default_profile: Option<String>,
|
||||
pub settings: Option<Tunables>,
|
||||
pub dirs: Option<Directories>,
|
||||
pub layout: Option<Layout>,
|
||||
}
|
||||
|
||||
impl IambConfig {
|
||||
|
@ -376,11 +410,13 @@ impl IambConfig {
|
|||
#[derive(Clone)]
|
||||
pub struct ApplicationSettings {
|
||||
pub matrix_dir: PathBuf,
|
||||
pub layout_json: PathBuf,
|
||||
pub session_json: PathBuf,
|
||||
pub profile_name: String,
|
||||
pub profile: ProfileConfig,
|
||||
pub tunables: TunableValues,
|
||||
pub dirs: DirectoryValues,
|
||||
pub layout: Layout,
|
||||
}
|
||||
|
||||
impl ApplicationSettings {
|
||||
|
@ -401,6 +437,7 @@ impl ApplicationSettings {
|
|||
default_profile,
|
||||
dirs,
|
||||
settings: global,
|
||||
layout,
|
||||
} = IambConfig::load(config_json.as_path())?;
|
||||
|
||||
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 = profile.settings.take().unwrap_or_default().merge(tunables);
|
||||
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();
|
||||
profile_dir.push("profiles");
|
||||
profile_dir.push(profile_name.as_str());
|
||||
|
@ -438,17 +482,23 @@ impl ApplicationSettings {
|
|||
let mut session_json = profile_dir;
|
||||
session_json.push("session.json");
|
||||
|
||||
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 cache directory.
|
||||
let mut cache_dir = dirs.cache.clone();
|
||||
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 {
|
||||
matrix_dir,
|
||||
layout_json,
|
||||
session_json,
|
||||
profile_name,
|
||||
profile,
|
||||
tunables,
|
||||
dirs,
|
||||
layout,
|
||||
};
|
||||
|
||||
Ok(settings)
|
||||
|
@ -496,6 +546,7 @@ impl ApplicationSettings {
|
|||
mod tests {
|
||||
use super::*;
|
||||
use matrix_sdk::ruma::user_id;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[test]
|
||||
fn test_profile_name_invalid() {
|
||||
|
@ -589,4 +640,62 @@ mod tests {
|
|||
})];
|
||||
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::fmt::Display;
|
||||
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::process;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
@ -89,6 +89,7 @@ use modalkit::{
|
|||
Jumpable,
|
||||
Promptable,
|
||||
Scrollable,
|
||||
TabAction,
|
||||
TabContainer,
|
||||
TabCount,
|
||||
UIError,
|
||||
|
@ -103,21 +104,120 @@ use modalkit::{
|
|||
input::{bindings::BindingMachine, dialog::Pager, key::TerminalKey},
|
||||
widgets::{
|
||||
cmdbar::CommandBarState,
|
||||
screen::{Screen, ScreenState},
|
||||
screen::{FocusList, Screen, ScreenState, TabLayoutDescription},
|
||||
windows::WindowLayoutDescription,
|
||||
TerminalCursor,
|
||||
TerminalExtOps,
|
||||
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 {
|
||||
store: AsyncProgramStore,
|
||||
worker: Requester,
|
||||
/// Terminal backend.
|
||||
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>,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// The tab layout before the last executed [TabAction].
|
||||
last_layout: Option<TabLayoutDescription<IambInfo>>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
|
@ -141,16 +241,7 @@ impl Application {
|
|||
let bindings = KeyManager::new(bindings);
|
||||
|
||||
let mut locked = store.lock().await;
|
||||
|
||||
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 screen = setup_screen(settings, locked.deref_mut())?;
|
||||
|
||||
let worker = locked.application.worker.clone();
|
||||
drop(locked);
|
||||
|
@ -165,6 +256,7 @@ impl Application {
|
|||
actstack,
|
||||
screen,
|
||||
focused: true,
|
||||
last_layout: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -312,7 +404,6 @@ impl Application {
|
|||
Action::Macro(act) => self.bindings.macro_command(&act, &ctx, store)?,
|
||||
Action::Scroll(style) => self.screen.scroll(&style, &ctx, store)?,
|
||||
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::Jump(l, dir, count) => {
|
||||
|
@ -328,6 +419,13 @@ impl Application {
|
|||
},
|
||||
|
||||
// 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 => {
|
||||
self.screen.clear_message();
|
||||
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()?;
|
||||
execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
self.terminal.show_cursor()?;
|
||||
|
|
|
@ -156,6 +156,10 @@ impl<'a> TextPrinter<'a> {
|
|||
pub fn push_str(&mut self, s: &'a str, style: Style) {
|
||||
let style = self.base_style.patch(style);
|
||||
|
||||
if self.width == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
for mut word in UnicodeSegmentation::split_word_bounds(s) {
|
||||
if let "\n" | "\r\n" = word {
|
||||
// Render embedded newlines as spaces.
|
||||
|
|
|
@ -195,16 +195,20 @@ pub fn mock_tunables() -> TunableValues {
|
|||
pub fn mock_settings() -> ApplicationSettings {
|
||||
ApplicationSettings {
|
||||
matrix_dir: PathBuf::new(),
|
||||
layout_json: PathBuf::new(),
|
||||
session_json: PathBuf::new(),
|
||||
|
||||
profile_name: "test".into(),
|
||||
profile: ProfileConfig {
|
||||
user_id: user_id!("@user:example.com").to_owned(),
|
||||
url: Url::parse("https://example.com").unwrap(),
|
||||
settings: None,
|
||||
dirs: None,
|
||||
layout: None,
|
||||
},
|
||||
tunables: mock_tunables(),
|
||||
dirs: mock_dirs(),
|
||||
layout: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue