aboutsummaryrefslogtreecommitdiffstats
path: root/teleterm/src/cmd/watch.rs
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2019-11-15 13:11:07 -0500
committerJesse Luehrs <doy@tozt.net>2019-11-15 13:11:07 -0500
commitbbf15cfef8134da720a27bd71a93efcb8467025b (patch)
treeaa58a5d7c1862fcdd6c8629651f664aa12c70f66 /teleterm/src/cmd/watch.rs
parentfe4fa53dbbb6030beae2094e33d1db008532ae3c (diff)
downloadteleterm-bbf15cfef8134da720a27bd71a93efcb8467025b.tar.gz
teleterm-bbf15cfef8134da720a27bd71a93efcb8467025b.zip
use workspaces
Diffstat (limited to 'teleterm/src/cmd/watch.rs')
-rw-r--r--teleterm/src/cmd/watch.rs776
1 files changed, 776 insertions, 0 deletions
diff --git a/teleterm/src/cmd/watch.rs b/teleterm/src/cmd/watch.rs
new file mode 100644
index 0000000..b872706
--- /dev/null
+++ b/teleterm/src/cmd/watch.rs
@@ -0,0 +1,776 @@
+use crate::prelude::*;
+use std::io::Write as _;
+
+#[derive(serde::Deserialize, Debug, Default)]
+pub struct Config {
+ #[serde(default)]
+ client: crate::config::Client,
+}
+
+impl crate::config::Config for Config {
+ fn merge_args<'a>(
+ &mut self,
+ matches: &clap::ArgMatches<'a>,
+ ) -> Result<()> {
+ self.client.merge_args(matches)
+ }
+
+ fn run(
+ &self,
+ ) -> Box<dyn futures::future::Future<Item = (), Error = Error> + Send>
+ {
+ let auth = match self.client.auth {
+ crate::protocol::AuthType::Plain => {
+ let username = self
+ .client
+ .username
+ .clone()
+ .context(crate::error::CouldntFindUsername);
+ match username {
+ Ok(username) => crate::protocol::Auth::plain(&username),
+ Err(e) => return Box::new(futures::future::err(e)),
+ }
+ }
+ crate::protocol::AuthType::RecurseCenter => {
+ let id = crate::oauth::load_client_auth_id(self.client.auth);
+ crate::protocol::Auth::recurse_center(
+ id.as_ref().map(std::string::String::as_str),
+ )
+ }
+ };
+
+ let host = self.client.host().to_string();
+ let address = *self.client.addr();
+ if self.client.tls {
+ let connector = match native_tls::TlsConnector::new()
+ .context(crate::error::CreateConnector)
+ {
+ Ok(connector) => connector,
+ Err(e) => return Box::new(futures::future::err(e)),
+ };
+ let make_connector: Box<
+ dyn Fn() -> crate::client::Connector<_> + Send,
+ > = Box::new(move || {
+ let host = host.clone();
+ let connector = connector.clone();
+ Box::new(move || {
+ let host = host.clone();
+ let connector = connector.clone();
+ let connector = tokio_tls::TlsConnector::from(connector);
+ let stream =
+ tokio::net::tcp::TcpStream::connect(&address);
+ Box::new(
+ stream
+ .context(crate::error::Connect { address })
+ .and_then(move |stream| {
+ connector.connect(&host, stream).context(
+ crate::error::ConnectTls { host },
+ )
+ }),
+ )
+ })
+ });
+ Box::new(WatchSession::new(make_connector, &auth))
+ } else {
+ let make_connector: Box<
+ dyn Fn() -> crate::client::Connector<_> + Send,
+ > = Box::new(move || {
+ Box::new(move || {
+ Box::new(
+ tokio::net::tcp::TcpStream::connect(&address)
+ .context(crate::error::Connect { address }),
+ )
+ })
+ });
+ Box::new(WatchSession::new(make_connector, &auth))
+ }
+ }
+}
+
+pub fn cmd<'a, 'b>(app: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
+ crate::config::Client::cmd(app.about("Watch teleterm streams"))
+}
+
+pub fn config(
+ mut config: Option<config::Config>,
+) -> Result<Box<dyn crate::config::Config>> {
+ if config.is_none() {
+ config = crate::config::wizard::run()?;
+ }
+ let config: Config = if let Some(config) = config {
+ config
+ .try_into()
+ .context(crate::error::CouldntParseConfig)?
+ } else {
+ Config::default()
+ };
+ Ok(Box::new(config))
+}
+
+// XXX https://github.com/rust-lang/rust/issues/64362
+#[allow(dead_code)]
+enum State<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static> {
+ Temporary,
+ LoggingIn {
+ alternate_screen: crossterm::screen::AlternateScreen,
+ },
+ Choosing {
+ sessions: crate::session_list::SessionList,
+ alternate_screen: crossterm::screen::AlternateScreen,
+ },
+ Watching {
+ client: Box<crate::client::Client<S>>,
+ },
+}
+
+impl<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static>
+ State<S>
+{
+ fn new() -> Self {
+ Self::Temporary
+ }
+
+ fn logging_in(&mut self) -> Result<()> {
+ let prev_state = std::mem::replace(self, Self::Temporary);
+ *self = match prev_state {
+ Self::Temporary => unreachable!(),
+ Self::LoggingIn { alternate_screen } => {
+ Self::LoggingIn { alternate_screen }
+ }
+ Self::Choosing {
+ alternate_screen, ..
+ } => Self::LoggingIn { alternate_screen },
+ _ => Self::LoggingIn {
+ alternate_screen: new_alternate_screen()?,
+ },
+ };
+ Ok(())
+ }
+
+ fn choosing(
+ &mut self,
+ sessions: crate::session_list::SessionList,
+ ) -> Result<()> {
+ let prev_state = std::mem::replace(self, Self::Temporary);
+ *self = match prev_state {
+ Self::Temporary => unreachable!(),
+ Self::LoggingIn { alternate_screen } => Self::Choosing {
+ alternate_screen,
+ sessions,
+ },
+ Self::Choosing {
+ alternate_screen, ..
+ } => Self::Choosing {
+ alternate_screen,
+ sessions,
+ },
+ _ => Self::Choosing {
+ alternate_screen: new_alternate_screen()?,
+ sessions,
+ },
+ };
+ Ok(())
+ }
+
+ fn watching(&mut self, client: crate::client::Client<S>) {
+ if let Self::Temporary = self {
+ unreachable!()
+ }
+ *self = Self::Watching {
+ client: Box::new(client),
+ }
+ }
+}
+
+struct WatchSession<
+ S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static,
+> {
+ make_connector: Box<dyn Fn() -> crate::client::Connector<S> + Send>,
+ auth: crate::protocol::Auth,
+
+ key_reader: crate::key_reader::KeyReader,
+ list_client: crate::client::Client<S>,
+ resizer: Box<
+ dyn futures::stream::Stream<
+ Item = (u16, u16),
+ Error = crate::error::Error,
+ > + Send,
+ >,
+ state: State<S>,
+ raw_screen: Option<crossterm::screen::RawScreen>,
+ needs_redraw: bool,
+}
+
+impl<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static>
+ WatchSession<S>
+{
+ fn new(
+ make_connector: Box<dyn Fn() -> crate::client::Connector<S> + Send>,
+ auth: &crate::protocol::Auth,
+ ) -> Self {
+ let list_client = crate::client::Client::list(make_connector(), auth);
+
+ Self {
+ make_connector,
+ auth: auth.clone(),
+
+ key_reader: crate::key_reader::KeyReader::new(),
+ list_client,
+ resizer: Box::new(
+ tokio_terminal_resize::resizes()
+ .flatten_stream()
+ .context(crate::error::Resize),
+ ),
+ state: State::new(),
+ raw_screen: None,
+ needs_redraw: true,
+ }
+ }
+
+ fn reconnect(&mut self, hard: bool) -> Result<()> {
+ self.state.logging_in()?;
+ self.needs_redraw = true;
+ if hard {
+ self.list_client.reconnect();
+ } else {
+ self.list_client
+ .send_message(crate::protocol::Message::list_sessions());
+ }
+ Ok(())
+ }
+
+ fn loading_keypress(
+ &mut self,
+ e: &crossterm::input::InputEvent,
+ ) -> Result<bool> {
+ match e {
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char('q'),
+ ) => {
+ return Ok(true);
+ }
+ _ => {}
+ }
+ Ok(false)
+ }
+
+ fn list_server_message(
+ &mut self,
+ msg: crate::protocol::Message,
+ ) -> Result<()> {
+ match msg {
+ crate::protocol::Message::Sessions { sessions } => {
+ self.state.choosing(
+ crate::session_list::SessionList::new(
+ sessions,
+ crate::term::Size::get()?,
+ ),
+ )?;
+ self.needs_redraw = true;
+ }
+ crate::protocol::Message::Disconnected => {
+ self.reconnect(true)?;
+ }
+ crate::protocol::Message::Error { msg } => {
+ return Err(Error::Server { message: msg });
+ }
+ msg => {
+ return Err(crate::error::Error::UnexpectedMessage {
+ message: msg,
+ });
+ }
+ }
+ Ok(())
+ }
+
+ fn list_keypress(
+ &mut self,
+ e: &crossterm::input::InputEvent,
+ ) -> Result<bool> {
+ let sessions =
+ if let State::Choosing { sessions, .. } = &mut self.state {
+ sessions
+ } else {
+ unreachable!()
+ };
+
+ match e {
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char(' '),
+ ) => {
+ self.list_client
+ .send_message(crate::protocol::Message::list_sessions());
+ }
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char('q'),
+ ) => {
+ return Ok(true);
+ }
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char('<'),
+ ) => {
+ sessions.prev_page();
+ self.needs_redraw = true;
+ }
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char('>'),
+ ) => {
+ sessions.next_page();
+ self.needs_redraw = true;
+ }
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char(c),
+ ) => {
+ if let Some(id) = sessions.id_for(*c) {
+ let client = crate::client::Client::watch(
+ (self.make_connector)(),
+ &self.auth,
+ id,
+ );
+ self.state.watching(client);
+ clear()?;
+ }
+ }
+ _ => {}
+ }
+ Ok(false)
+ }
+
+ fn watch_server_message(
+ &mut self,
+ msg: crate::protocol::Message,
+ ) -> Result<()> {
+ match msg {
+ crate::protocol::Message::TerminalOutput { data } => {
+ let data: Vec<_> = data
+ .iter()
+ // replace \n with \r\n since we're writing to a
+ // raw terminal
+ .fold(vec![], |mut acc, &c| {
+ if c == b'\n' {
+ acc.push(b'\r');
+ acc.push(b'\n');
+ } else {
+ acc.push(c);
+ }
+ acc
+ });
+ // TODO async
+ let stdout = std::io::stdout();
+ let mut stdout = stdout.lock();
+ stdout.write(&data).context(crate::error::WriteTerminal)?;
+ stdout.flush().context(crate::error::FlushTerminal)?;
+ }
+ crate::protocol::Message::Disconnected => {
+ self.reconnect(false)?;
+ }
+ crate::protocol::Message::Error { msg } => {
+ return Err(Error::Server { message: msg });
+ }
+ msg => {
+ return Err(crate::error::Error::UnexpectedMessage {
+ message: msg,
+ });
+ }
+ }
+ Ok(())
+ }
+
+ fn watch_keypress(
+ &mut self,
+ e: &crossterm::input::InputEvent,
+ ) -> Result<bool> {
+ match e {
+ crossterm::input::InputEvent::Keyboard(
+ crossterm::input::KeyEvent::Char('q'),
+ ) => {
+ self.reconnect(false)?;
+ }
+ _ => {}
+ }
+ Ok(false)
+ }
+
+ fn resize(&mut self, size: crate::term::Size) -> Result<()> {
+ if let State::Choosing { sessions, .. } = &mut self.state {
+ sessions.resize(size);
+ self.needs_redraw = true;
+ }
+ Ok(())
+ }
+
+ fn redraw(&self) -> Result<()> {
+ match &self.state {
+ State::Temporary => unreachable!(),
+ State::LoggingIn { .. } => {
+ self.display_loading_screen()?;
+ }
+ State::Choosing { .. } => {
+ self.display_choosing_screen()?;
+ }
+ State::Watching { .. } => {}
+ }
+ Ok(())
+ }
+
+ fn display_loading_screen(&self) -> Result<()> {
+ clear()?;
+
+ println!("loading...\r");
+ if let Some(err) = self.list_client.last_error() {
+ println!("error: {}\r", err);
+ }
+ print!("q: quit --> ");
+
+ std::io::stdout()
+ .flush()
+ .context(crate::error::FlushTerminal)?;
+
+ Ok(())
+ }
+
+ fn display_choosing_screen(&self) -> Result<()> {
+ let sessions = if let State::Choosing { sessions, .. } = &self.state {
+ sessions
+ } else {
+ unreachable!()
+ };
+
+ let char_width = 2;
+
+ let max_name_width = (sessions.size().cols / 3) as usize;
+ let name_width = sessions
+ .visible_sessions()
+ .iter()
+ .map(|s| s.username.len())
+ .max()
+ .unwrap_or(4);
+ // XXX unstable
+ // let name_width = name_width.clamp(4, max_name_width);
+ let name_width = if name_width < 4 {
+ 4
+ } else if name_width > max_name_width {
+ max_name_width
+ } else {
+ name_width
+ };
+
+ let size_width = 7;
+
+ let max_idle_time = sessions
+ .visible_sessions()
+ .iter()
+ .map(|s| s.idle_time)
+ .max()
+ .unwrap_or(4);
+ let idle_width = format_time(max_idle_time).len();
+ let idle_width = if idle_width < 4 { 4 } else { idle_width };
+
+ let watch_width = 5;
+
+ let max_title_width = (sessions.size().cols as usize)
+ - char_width
+ - 3
+ - name_width
+ - 3
+ - size_width
+ - 3
+ - idle_width
+ - 3
+ - watch_width
+ - 3;
+
+ clear()?;
+ println!("welcome to teleterm\r");
+ println!("available sessions:\r");
+ println!("\r");
+ println!(
+ "{:5$} | {:6$} | {:7$} | {:8$} | {:9$} | title\r",
+ "",
+ "name",
+ "size",
+ "idle",
+ "watch",
+ char_width,
+ name_width,
+ size_width,
+ idle_width,
+ watch_width,
+ );
+ println!(
+ "{}+{}+{}+{}+{}+{}\r",
+ "-".repeat(char_width + 1),
+ "-".repeat(name_width + 2),
+ "-".repeat(size_width + 2),
+ "-".repeat(idle_width + 2),
+ "-".repeat(watch_width + 2),
+ "-".repeat(max_title_width + 1)
+ );
+
+ let mut prev_name: Option<&str> = None;
+ for (c, session) in sessions.visible_sessions_with_chars() {
+ let first = if let Some(name) = prev_name {
+ name != session.username
+ } else {
+ true
+ };
+
+ let display_char = format!("{})", c);
+ let display_name = if first {
+ truncate(&session.username, max_name_width)
+ } else {
+ "".to_string()
+ };
+ let display_size_plain = format!("{}", &session.size);
+ let display_size_full = if session.size == sessions.size() {
+ // XXX i should be able to use crossterm::style here, but
+ // it has bugs
+ format!("\x1b[32m{}\x1b[m", display_size_plain)
+ } else if session.size.fits_in(sessions.size()) {
+ display_size_plain.clone()
+ } else {
+ // XXX i should be able to use crossterm::style here, but
+ // it has bugs
+ format!("\x1b[31m{}\x1b[m", display_size_plain)
+ };
+ let display_idle = format_time(session.idle_time);
+ let display_title = truncate(&session.title, max_title_width);
+ let display_watch = session.watchers;
+
+ println!(
+ "{:6$} | {:7$} | {:8$} | {:9$} | {:10$} | {}\r",
+ display_char,
+ display_name,
+ display_size_full,
+ display_idle,
+ display_watch,
+ display_title,
+ char_width,
+ name_width,
+ size_width
+ + (display_size_full.len() - display_size_plain.len()),
+ idle_width,
+ watch_width,
+ );
+
+ prev_name = Some(&session.username);
+ }
+ print!(
+ "({}/{}) space: refresh, q: quit, <: prev page, >: next page --> ",
+ sessions.current_page(),
+ sessions.total_pages(),
+ );
+ std::io::stdout()
+ .flush()
+ .context(crate::error::FlushTerminal)?;
+
+ Ok(())
+ }
+}
+
+impl<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static>
+ WatchSession<S>
+{
+ const POLL_FNS:
+ &'static [&'static dyn for<'a> Fn(
+ &'a mut Self,
+ )
+ -> component_future::Poll<
+ (),
+ Error,
+ >] = &[
+ &Self::poll_resizer,
+ &Self::poll_input,
+ &Self::poll_list_client,
+ &Self::poll_watch_client,
+ ];
+
+ fn poll_resizer(&mut self) -> component_future::Poll<(), Error> {
+ let (rows, cols) =
+ component_future::try_ready!(self.resizer.poll()).unwrap();
+ self.resize(crate::term::Size { rows, cols })?;
+ Ok(component_future::Async::DidWork)
+ }
+
+ fn poll_input(&mut self) -> component_future::Poll<(), Error> {
+ if self.raw_screen.is_none() {
+ self.raw_screen = Some(
+ crossterm::screen::RawScreen::into_raw_mode()
+ .context(crate::error::ToRawMode)?,
+ );
+ }
+ if let State::Temporary = self.state {
+ self.state = State::LoggingIn {
+ alternate_screen: new_alternate_screen()?,
+ }
+ }
+
+ let e = component_future::try_ready!(self.key_reader.poll()).unwrap();
+ let quit = match &mut self.state {
+ State::Temporary => unreachable!(),
+ State::LoggingIn { .. } => self.loading_keypress(&e)?,
+ State::Choosing { .. } => self.list_keypress(&e)?,
+ State::Watching { .. } => self.watch_keypress(&e)?,
+ };
+ if quit {
+ Ok(component_future::Async::Ready(()))
+ } else {
+ Ok(component_future::Async::DidWork)
+ }
+ }
+
+ fn poll_list_client(&mut self) -> component_future::Poll<(), Error> {
+ match component_future::try_ready!(self.list_client.poll()).unwrap() {
+ crate::client::Event::Disconnect => {
+ self.reconnect(true)?;
+ }
+ crate::client::Event::Connect => {
+ self.list_client
+ .send_message(crate::protocol::Message::list_sessions());
+ }
+ crate::client::Event::ServerMessage(msg) => {
+ self.list_server_message(msg)?;
+ }
+ }
+ Ok(component_future::Async::DidWork)
+ }
+
+ fn poll_watch_client(&mut self) -> component_future::Poll<(), Error> {
+ let client = if let State::Watching { client } = &mut self.state {
+ client
+ } else {
+ return Ok(component_future::Async::NothingToDo);
+ };
+
+ match component_future::try_ready!(client.poll()).unwrap() {
+ crate::client::Event::Disconnect => {
+ self.reconnect(true)?;
+ }
+ crate::client::Event::Connect => {}
+ crate::client::Event::ServerMessage(msg) => {
+ self.watch_server_message(msg)?;
+ }
+ }
+ Ok(component_future::Async::DidWork)
+ }
+}
+
+#[must_use = "futures do nothing unless polled"]
+impl<S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Send + 'static>
+ futures::future::Future for WatchSession<S>
+{
+ type Item = ();
+ type Error = Error;
+
+ fn poll(&mut self) -> futures::Poll<Self::Item, Self::Error> {
+ let res = component_future::poll_future(self, Self::POLL_FNS);
+ if res.is_err() {
+ self.state = State::Temporary; // drop alternate screen
+ self.raw_screen = None;
+ } else if self.needs_redraw {
+ self.redraw()?;
+ self.needs_redraw = false;
+ }
+ res
+ }
+}
+
+fn new_alternate_screen() -> Result<crossterm::screen::AlternateScreen> {
+ crossterm::screen::AlternateScreen::to_alternate(false)
+ .context(crate::error::ToAlternateScreen)
+}
+
+fn format_time(dur: u32) -> String {
+ let secs = dur % 60;
+ let dur = dur / 60;
+ if dur == 0 {
+ return format!("{}s", secs);
+ }
+
+ let mins = dur % 60;
+ let dur = dur / 60;
+ if dur == 0 {
+ return format!("{}m{:02}s", mins, secs);
+ }
+
+ let hours = dur % 24;
+ let dur = dur / 24;
+ if dur == 0 {
+ return format!("{}h{:02}m{:02}s", hours, mins, secs);
+ }
+
+ let days = dur;
+ format!("{}d{:02}h{:02}m{:02}s", days, hours, mins, secs)
+}
+
+fn truncate(s: &str, len: usize) -> String {
+ if s.len() <= len {
+ s.to_string()
+ } else {
+ format!("{}...", &s[..(len - 3)])
+ }
+}
+
+fn clear() -> Result<()> {
+ crossterm::execute!(
+ std::io::stdout(),
+ crossterm::cursor::MoveTo(0, 0),
+ crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
+ )
+ .context(crate::error::WriteTerminalCrossterm)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_truncate() {
+ assert_eq!(truncate("abcdefghij", 12), "abcdefghij");
+ assert_eq!(truncate("abcdefghij", 11), "abcdefghij");
+ assert_eq!(truncate("abcdefghij", 10), "abcdefghij");
+ assert_eq!(truncate("abcdefghij", 9), "abcdef...");
+ assert_eq!(truncate("abcdefghij", 8), "abcde...");
+ assert_eq!(truncate("abcdefghij", 7), "abcd...");
+
+ assert_eq!(truncate("", 7), "");
+ assert_eq!(truncate("a", 7), "a");
+ assert_eq!(truncate("ab", 7), "ab");
+ assert_eq!(truncate("abc", 7), "abc");
+ assert_eq!(truncate("abcd", 7), "abcd");
+ assert_eq!(truncate("abcde", 7), "abcde");
+ assert_eq!(truncate("abcdef", 7), "abcdef");
+ assert_eq!(truncate("abcdefg", 7), "abcdefg");
+ assert_eq!(truncate("abcdefgh", 7), "abcd...");
+ assert_eq!(truncate("abcdefghi", 7), "abcd...");
+ assert_eq!(truncate("abcdefghij", 7), "abcd...");
+ }
+
+ #[test]
+ fn test_format_time() {
+ assert_eq!(format_time(0), "0s");
+ assert_eq!(format_time(5), "5s");
+ assert_eq!(format_time(10), "10s");
+ assert_eq!(format_time(60), "1m00s");
+ assert_eq!(format_time(61), "1m01s");
+ assert_eq!(format_time(601), "10m01s");
+ assert_eq!(format_time(610), "10m10s");
+ assert_eq!(format_time(3599), "59m59s");
+ assert_eq!(format_time(3600), "1h00m00s");
+ assert_eq!(format_time(3601), "1h00m01s");
+ assert_eq!(format_time(3610), "1h00m10s");
+ assert_eq!(format_time(3660), "1h01m00s");
+ assert_eq!(format_time(3661), "1h01m01s");
+ assert_eq!(format_time(3670), "1h01m10s");
+ assert_eq!(format_time(4200), "1h10m00s");
+ assert_eq!(format_time(4201), "1h10m01s");
+ assert_eq!(format_time(4210), "1h10m10s");
+ assert_eq!(format_time(36000), "10h00m00s");
+ assert_eq!(format_time(86399), "23h59m59s");
+ assert_eq!(format_time(86400), "1d00h00m00s");
+ assert_eq!(format_time(86401), "1d00h00m01s");
+ assert_eq!(format_time(864_000), "10d00h00m00s");
+ assert_eq!(format_time(8_640_000), "100d00h00m00s");
+ assert_eq!(format_time(86_400_000), "1000d00h00m00s");
+ }
+}