diff --git a/Cargo.lock b/Cargo.lock index f45d3d9..675007e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,31 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +[[package]] +name = "crossterm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" +dependencies = [ + "bitflags", + "crossterm_winapi", + "lazy_static", + "libc", + "mio", + "parking_lot", + "signal-hook", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" +dependencies = [ + "winapi", +] + [[package]] name = "darling" version = "0.10.2" @@ -327,6 +352,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if", +] + [[package]] name = "jetscii" version = "0.4.4" @@ -371,6 +405,15 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.14" @@ -401,6 +444,28 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +[[package]] +name = "mio" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + [[package]] name = "native-tls" version = "0.2.7" @@ -436,6 +501,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -518,6 +592,31 @@ dependencies = [ "winreg", ] +[[package]] +name = "parking_lot" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + [[package]] name = "pdcurses-sys" version = "0.7.1" @@ -742,6 +841,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.1" @@ -817,6 +922,7 @@ dependencies = [ "anyhow", "chrono", "clap", + "crossterm", "dirs-next", "escaper", "lazy_static", @@ -845,6 +951,32 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "signal-hook" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" +dependencies = [ + "libc", + "mio", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" +dependencies = [ + "libc", +] + +[[package]] +name = "smallvec" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" + [[package]] name = "smawk" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 913882e..fe44c3a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ readme = "README.md" [dependencies] pancurses = "0.16.1" +crossterm = "0.19.0" rss = "1.10.0" rusqlite = "0.21.0" clap = "2.33.1" diff --git a/src/config.rs b/src/config.rs index 8e91012..3063cd2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -25,11 +25,11 @@ pub const EPISODE_PUBDATE_LENGTH: usize = 60; // How many columns we need (total terminal window width) before we // display the details panel -pub const DETAILS_PANEL_LENGTH: i32 = 135; +pub const DETAILS_PANEL_LENGTH: u16 = 135; // How many lines will be scrolled by the big scroll, // in relation to the rows eg: 4 = 1/4 of the screen -pub const BIG_SCROLL_AMOUNT: i32 = 4; +pub const BIG_SCROLL_AMOUNT: u16 = 4; /// Identifies the user's selection for what to do with new episodes diff --git a/src/keymap.rs b/src/keymap.rs index 016d9e1..bcfc6da 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -1,4 +1,4 @@ -use pancurses::Input; +use crossterm::event::KeyCode; use std::collections::HashMap; use crate::config::KeybindingsFromToml; @@ -104,7 +104,7 @@ impl Keybindings { /// Takes an Input object from pancurses and returns the associated /// user action, if one exists. - pub fn get_from_input(&self, input: Input) -> Option<&UserAction> { + pub fn get_from_input(&self, input: KeyCode) -> Option<&UserAction> { match input_to_str(input) { Some(code) => self.0.get(&code), None => None, @@ -185,135 +185,43 @@ impl Keybindings { /// This function is a bit ridiculous, given that 95% of keyboards /// probably don't even have half these special keys, but at any rate... /// they're mapped, if anyone wants them. -pub fn input_to_str(input: Input) -> Option { +pub fn input_to_str(input: KeyCode) -> Option { let mut tmp = [0; 4]; let code = match input { - Input::KeyCodeYes => "CodeYes", - Input::KeyBreak => "Break", - Input::KeyDown => "Down", - Input::KeyUp => "Up", - Input::KeyLeft => "Left", - Input::KeyRight => "Right", - Input::KeyHome => "Home", - Input::KeyBackspace => "Backspace", - Input::KeyF0 => "F0", - Input::KeyF1 => "F1", - Input::KeyF2 => "F2", - Input::KeyF3 => "F3", - Input::KeyF4 => "F4", - Input::KeyF5 => "F5", - Input::KeyF6 => "F6", - Input::KeyF7 => "F7", - Input::KeyF8 => "F8", - Input::KeyF9 => "F9", - Input::KeyF10 => "F10", - Input::KeyF11 => "F11", // F11 triggers KeyResize for me - Input::KeyF12 => "F12", - Input::KeyF13 => "F13", - Input::KeyF14 => "F14", - Input::KeyF15 => "F15", - Input::KeyDL => "DL", - Input::KeyIL => "IL", - Input::KeyDC => "Del", - Input::KeyIC => "Ins", - Input::KeyEIC => "EIC", - Input::KeyClear => "Clear", - Input::KeyEOS => "EOS", - Input::KeyEOL => "EOL", - Input::KeySF => "S_Down", - Input::KeySR => "S_Up", - Input::KeyNPage => "PgDn", - Input::KeyPPage => "PgUp", - Input::KeySTab => "STab", // this doesn't appear to be Shift+Tab - Input::KeyCTab => "C_Tab", - Input::KeyCATab => "CATab", - Input::KeyEnter => "Enter", - Input::KeySReset => "SReset", - Input::KeyReset => "Reset", - Input::KeyPrint => "Print", - Input::KeyLL => "LL", - Input::KeyAbort => "Abort", - Input::KeySHelp => "SHelp", - Input::KeyLHelp => "LHelp", - Input::KeyBTab => "S_Tab", // Shift+Tab - Input::KeyBeg => "Beg", - Input::KeyCancel => "Cancel", - Input::KeyClose => "Close", - Input::KeyCommand => "Command", - Input::KeyCopy => "Copy", - Input::KeyEnd => "End", - Input::KeyExit => "Exit", - Input::KeyFind => "Find", - Input::KeyHelp => "Help", - Input::KeyMark => "Mark", - Input::KeyMessage => "Message", - Input::KeyMove => "Move", - Input::KeyNext => "Next", - Input::KeyOpen => "Open", - Input::KeyOptions => "Options", - Input::KeyPrevious => "Previous", - Input::KeyRedo => "Redo", - Input::KeyReference => "Reference", - Input::KeyRefresh => "Refresh", - Input::KeyResume => "Resume", - Input::KeyRestart => "Restart", - Input::KeySave => "Save", - Input::KeySBeg => "S_Beg", - Input::KeySCancel => "S_Cancel", - Input::KeySCommand => "S_Command", - Input::KeySCopy => "S_Copy", - Input::KeySCreate => "S_Create", - Input::KeySDC => "S_Del", - Input::KeySDL => "S_DL", - Input::KeySelect => "Select", - Input::KeySEnd => "S_End", - Input::KeySEOL => "S_EOL", - Input::KeySExit => "S_Exit", - Input::KeySFind => "S_Find", - Input::KeySHome => "S_Home", - Input::KeySIC => "S_Ins", - Input::KeySLeft => "S_Left", - Input::KeySMessage => "S_Message", - Input::KeySMove => "S_Move", - Input::KeySNext => "S_PgDn", - Input::KeySOptions => "S_Options", - Input::KeySPrevious => "S_PgUp", - Input::KeySPrint => "S_Print", - Input::KeySRedo => "S_Redo", - Input::KeySReplace => "S_Replace", - Input::KeySRight => "S_Right", - Input::KeySResume => "S_Resume", - Input::KeySSave => "S_Save", - Input::KeySSuspend => "S_Suspend", - Input::KeySUndo => "S_Undo", - Input::KeySuspend => "Suspend", - Input::KeyUndo => "Undo", - Input::KeyResize => "F11", // I'm marking this as F11 as well - Input::KeyEvent => "Event", - Input::KeyMouse => "Mouse", - Input::KeyA1 => "A1", - Input::KeyA3 => "A3", - Input::KeyB2 => "B2", - Input::KeyC1 => "C1", - Input::KeyC3 => "C3", - Input::Character(c) => { + KeyCode::Backspace => "Backspace".to_string(), + KeyCode::Enter => "Enter".to_string(), + KeyCode::Left => "Left".to_string(), + KeyCode::Right => "Right".to_string(), + KeyCode::Up => "Up".to_string(), + KeyCode::Down => "Down".to_string(), + KeyCode::Home => "Home".to_string(), + KeyCode::End => "End".to_string(), + KeyCode::PageUp => "PgUp".to_string(), + KeyCode::PageDown => "PgDn".to_string(), + KeyCode::Tab => "Tab".to_string(), + KeyCode::BackTab => "S_Tab".to_string(), + KeyCode::Delete => "Del".to_string(), + KeyCode::Insert => "Ins".to_string(), + KeyCode::Esc => "Esc".to_string(), + KeyCode::F(num) => format!("F{}", num), // Function keys + KeyCode::Char(c) => { if c == '\u{7f}' { - "Backspace" + "Backspace".to_string() } else if c == '\u{1b}' { - "Escape" + "Escape".to_string() } else if c == '\n' { - "Enter" + "Enter".to_string() } else if c == '\t' { - "Tab" + "Tab".to_string() } else { - c.encode_utf8(&mut tmp) + c.encode_utf8(&mut tmp).to_string() } } - _ => "", + _ => "".to_string(), }; if code.is_empty() { return None; } else { - return Some(code.to_string()); + return Some(code); } } diff --git a/src/ui/menu.rs b/src/ui/menu.rs index bf4f559..706d218 100644 --- a/src/ui/menu.rs +++ b/src/ui/menu.rs @@ -6,6 +6,13 @@ use super::ColorType; use super::Panel; use crate::types::*; +/// Holds a value for how much to scroll the menu up or down, without +/// having to deal with positive/negative values. +pub enum Scroll { + Up(u16), + Down(u16), +} + /// Generic struct holding details about a list menu. These menus are /// contained by the UI, and hold the list of podcasts or podcast /// episodes. They also hold the pancurses window used to display the menu @@ -31,9 +38,9 @@ where T: Clone + Menuable pub panel: Panel, pub header: Option, pub items: LockVec, - pub start_row: i32, // beginning of first row of menu - pub top_row: i32, // top row of text shown in window - pub selected: i32, // which line of text is highlighted + pub start_row: u16, // beginning of first row of menu + pub top_row: u16, // top row of text shown in window + pub selected: u16, // which line of text is highlighted } impl Menu { @@ -49,64 +56,61 @@ impl Menu { }; } - /// Prints the list of visible items to the pancurses window and - /// refreshes it. - pub fn init(&mut self) { - self.panel.refresh(); + /// Clears the terminal, and then prints the list of visible items + /// to the terminal. + pub fn redraw(&mut self) { + self.panel.redraw(); self.update_items(); } - /// Prints or reprints the list of visible items to the pancurses - /// window and refreshes it. + /// Prints the list of visible items to the terminal. pub fn update_items(&mut self) { - self.panel.erase(); - self.start_row = self.print_header(); - if self.selected < self.start_row { - self.selected = self.start_row; - } - - let (map, order) = self.items.borrow(); - if !order.is_empty() { - // update selected item if list has gotten shorter - let current_selected = self.get_menu_idx(self.selected) as i32; - let list_len = order.len() as i32; - if current_selected >= list_len { - self.selected = self.selected - (current_selected - list_len) - 1; - } - - // for visible rows, print strings from list - for i in self.start_row..self.panel.get_rows() { - if let Some(elem_id) = order.get(self.get_menu_idx(i)) { - let elem = map.get(&elem_id).expect("Could not retrieve menu item."); - self.panel - .write_line(i, elem.get_title(self.panel.get_cols() as usize)); - - // this is literally the same logic as - // self.set_attrs(), but it's complaining about - // immutable borrows, so... - let attr = if elem.is_played() { - pancurses::A_NORMAL - } else { - pancurses::A_BOLD - }; - self.panel.change_attr( - i, - -1, - self.panel.get_cols() + 3, - attr, - ColorType::Normal, - ); - } else { - break; - } - } - } - self.panel.refresh(); + // self.start_row = self.print_header(); + // if self.selected < self.start_row { + // self.selected = self.start_row; + // } + + // let (map, order) = self.items.borrow(); + // if !order.is_empty() { + // // update selected item if list has gotten shorter + // let current_selected = self.get_menu_idx(self.selected); + // let list_len = order.len(); + // if current_selected >= list_len { + // self.selected = (self.selected as usize - (current_selected - list_len) - 1) as u16; + // } + + // // for visible rows, print strings from list + // for i in self.start_row..self.panel.get_rows() { + // if let Some(elem_id) = order.get(self.get_menu_idx(i)) { + // let elem = map.get(&elem_id).expect("Could not retrieve menu item."); + // self.panel + // .write_line(i, elem.get_title(self.panel.get_cols() as usize)); + + // // this is literally the same logic as + // // self.set_attrs(), but it's complaining about + // // immutable borrows, so... + // let attr = if elem.is_played() { + // pancurses::A_NORMAL + // } else { + // pancurses::A_BOLD + // }; + // self.panel.change_attr( + // i as i16, + // -1, + // self.panel.get_cols() + 3, + // attr, + // ColorType::Normal, + // ); + // } else { + // break; + // } + // } + // } } /// If a header exists, prints lines of text to the panel to appear /// above the menu. - fn print_header(&mut self) -> i32 { + fn print_header(&mut self) -> u16 { if let Some(header) = &self.header { return self.panel.write_wrap_line(0, header) + 2; } else { @@ -114,100 +118,121 @@ impl Menu { } } - /// Scrolls the menu up or down by `lines` lines. Negative values of - /// `lines` will scroll the menu up. + /// Scrolls the menu up or down by `lines` lines. /// /// This function examines the new selected value, ensures it does - /// not fall out of bounds, and then updates the pancurses window to + /// not fall out of bounds, and then updates the panel to /// represent the new visible list. - pub fn scroll(&mut self, lines: i32) { - let mut old_selected; - let checked_lines; - let apply_color_played; - let get_titles; - - let list_len = self.items.len(); - if list_len == 0 { - return; - } - - let n_row = self.panel.get_rows(); - let max_lines = list_len as i32 + self.start_row; - let check_max = |lines| min(lines, max_lines); - - // check the bounds of lines and adjust accordingly - if lines.checked_add(self.top_row + n_row).is_some() { - checked_lines = lines; - } else { - checked_lines = lines - self.top_row - n_row; - } - - old_selected = self.selected; - self.selected += checked_lines; - - // don't allow scrolling past last item in list (if shorter - // than self.panel.get_rows()) - let abs_bottom = min(self.panel.get_rows(), list_len as i32 + self.start_row - 1); - if self.selected > abs_bottom { - self.selected = abs_bottom; - } - - // given a selection, apply correct play status and highlight - apply_color_played = |menu: &mut Menu, selected, color: ColorType| { - let played = menu - .items - .map_single_by_index(menu.get_menu_idx(selected), |el| el.is_played()) - .unwrap_or(false); - menu.set_attrs(selected, played, color); - }; - - // return a vec with sorted titles in range start, end (exclusive) - get_titles = |menu: &mut Menu, start, end| { - menu.items.map_by_range(start, end, |el| { - Some(el.get_title(menu.panel.get_cols() as usize)) - }) - }; - - // scroll list if necessary: - // scroll down - if (self.selected) > (n_row - 1) { - // for scrolls that don't start at the bottom - apply_color_played(self, old_selected, ColorType::Normal); - let delta = n_row - old_selected - 1; - - let titles = get_titles( - self, - (self.top_row + n_row) as usize, - (check_max(checked_lines + self.top_row + n_row - delta)) as usize, - ); - for title in titles.into_iter() { - self.top_row += 1; - self.panel.delete_line(self.start_row); - old_selected -= 1; - self.panel.delete_line(n_row - 1); - self.panel.write_line(n_row - 1, title); - apply_color_played(self, n_row - 1, ColorType::Normal); + pub fn scroll(&mut self, lines: Scroll) { + match lines { + Scroll::Up(v) => { + if v <= self.selected { + self.selected -= v; + } else { + let list_scroll_amount = v - self.selected; + self.top_row = min(0, self.top_row - list_scroll_amount); + self.selected = 0; + } } - self.selected = n_row - 1; - - // scroll up - } else if self.selected < self.start_row { - let titles = get_titles( - self, - max(0, self.top_row + self.selected) as usize, - (self.top_row) as usize, - ); - for title in titles.into_iter().rev() { - self.top_row -= 1; - self.panel.insert_line(self.start_row, title); - apply_color_played(self, 1, ColorType::Normal); - old_selected += 1; + Scroll::Down(v) => { + let n_row = self.panel.get_rows(); + if v < (n_row - self.selected) { + self.selected += v; + } else { + let list_len = self.items.len() as u16; + let list_scroll_amount = v - (n_row - self.selected - 1); + self.top_row = max(list_scroll_amount, list_len - n_row); + self.selected = n_row - 1; + } } - self.selected = self.start_row; } - apply_color_played(self, old_selected, ColorType::Normal); - apply_color_played(self, self.selected, ColorType::HighlightedActive); - self.panel.refresh(); + + // let mut old_selected; + // let checked_lines; + // let apply_color_played; + // let get_titles; + + // let list_len = self.items.len(); + // if list_len == 0 { + // return; + // } + + // let n_row = self.panel.get_rows(); + // let max_lines = list_len as u16 + self.start_row; + // let check_max = |lines| min(lines, max_lines); + + // // check the bounds of lines and adjust accordingly + // if lines.checked_add((self.top_row + n_row) as i32).is_some() { + // checked_lines = lines; + // } else { + // checked_lines = lines - self.top_row - n_row; + // } + + // old_selected = self.selected; + // self.selected = self.selected.checked_add(checked_lines).unwrap(); + + // // don't allow scrolling past last item in list (if shorter + // // than self.panel.get_rows()) + // let abs_bottom = min(self.panel.get_rows(), list_len as u16 + self.start_row - 1); + // if self.selected > abs_bottom { + // self.selected = abs_bottom; + // } + + // // given a selection, apply correct play status and highlight + // apply_color_played = |menu: &mut Menu, selected, color: ColorType| { + // let played = menu + // .items + // .map_single_by_index(menu.get_menu_idx(selected), |el| el.is_played()) + // .unwrap_or(false); + // menu.set_attrs(selected, played, color); + // }; + + // // return a vec with sorted titles in range start, end (exclusive) + // get_titles = |menu: &mut Menu, start, end| { + // menu.items.map_by_range(start, end, |el| { + // Some(el.get_title(menu.panel.get_cols() as usize)) + // }) + // }; + + // // scroll list if necessary: + // // scroll down + // if (self.selected) > (n_row - 1) { + // // for scrolls that don't start at the bottom + // apply_color_played(self, old_selected, ColorType::Normal); + // let delta = n_row - old_selected - 1; + + // let titles = get_titles( + // self, + // (self.top_row + n_row) as usize, + // (check_max(checked_lines + self.top_row + n_row - delta)) as usize, + // ); + // for title in titles.into_iter() { + // self.top_row += 1; + // self.panel.delete_line(self.start_row); + // old_selected -= 1; + // self.panel.delete_line(n_row - 1); + // self.panel.write_line(n_row - 1, title); + // apply_color_played(self, n_row - 1, ColorType::Normal); + // } + // self.selected = n_row - 1; + + // // scroll up + // } else if self.selected < self.start_row { + // let titles = get_titles( + // self, + // max(0, self.top_row + self.selected) as usize, + // (self.top_row) as usize, + // ); + // for title in titles.into_iter().rev() { + // self.top_row -= 1; + // self.panel.insert_line(self.start_row, title); + // apply_color_played(self, 1, ColorType::Normal); + // old_selected += 1; + // } + // self.selected = self.start_row; + // } + // apply_color_played(self, old_selected, ColorType::Normal); + // apply_color_played(self, self.selected, ColorType::HighlightedActive); } /// Sets font style and color of menu item. `index` is the position @@ -215,31 +240,30 @@ impl Menu { /// whether that item has been played or not. `color` is a ColorType /// representing the appropriate state of the item (e.g., Normal, /// Highlighted). - pub fn set_attrs(&mut self, index: i32, played: bool, color: ColorType) { - let attr = if played { - pancurses::A_NORMAL - } else { - pancurses::A_BOLD - }; - self.panel - .change_attr(index, -1, self.panel.get_cols() + 3, attr, color); + pub fn set_attrs(&mut self, index: u16, played: bool, color: ColorType) { + // let attr = if played { + // pancurses::A_NORMAL + // } else { + // pancurses::A_BOLD + // }; + // self.panel + // .change_attr(index, -1, self.panel.get_cols() + 3, attr, color); } /// Highlights the currently selected item in the menu, based on /// whether the menu is currently active or not. pub fn highlight_selected(&mut self, active_menu: bool) { - let is_played = self - .items - .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()); - - if let Some(played) = is_played { - if active_menu { - self.set_attrs(self.selected, played, ColorType::HighlightedActive); - } else { - self.set_attrs(self.selected, played, ColorType::Highlighted); - } - self.panel.refresh(); - } + // let is_played = self + // .items + // .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()); + + // if let Some(played) = is_played { + // if active_menu { + // self.set_attrs(self.selected, played, ColorType::HighlightedActive); + // } else { + // self.set_attrs(self.selected, played, ColorType::Highlighted); + // } + // } } /// Controls how the window changes when it is active (i.e., available @@ -251,12 +275,11 @@ impl Menu { .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::HighlightedActive); - self.panel.refresh(); } } /// Updates window size - pub fn resize(&mut self, n_row: i32, n_col: i32, start_y: i32, start_x: i32) { + pub fn resize(&mut self, n_row: u16, n_col: u16, start_y: u16, start_x: u16) { self.panel.resize(n_row, n_col, start_y, start_x); let n_row = self.panel.get_rows(); @@ -273,7 +296,7 @@ impl Menu { /// do any checks to ensure `screen_y` is between 0 and `n_rows`, /// or that the resulting menu index is between 0 and `n_items`. /// It's merely a straight translation. - pub fn get_menu_idx(&self, screen_y: i32) -> usize { + pub fn get_menu_idx(&self, screen_y: u16) -> usize { return (self.top_row + screen_y - self.start_row) as usize; } } @@ -304,7 +327,6 @@ impl Menu { .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::Highlighted); - self.panel.refresh(); } } } @@ -319,7 +341,6 @@ impl Menu { .map_single_by_index(self.get_menu_idx(self.selected), |el| el.is_played()) { self.set_attrs(self.selected, played, ColorType::Normal); - self.panel.refresh(); } } } @@ -378,169 +399,169 @@ impl Menu { // TESTS ---------------------------------------------------------------- -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - - fn create_menu(n_row: i32, n_col: i32, top_row: i32, selected: i32) -> Menu { - let titles = vec![ - "A Very Cool Episode", - "This is a very long episode title but we'll get through it together", - "An episode with le Unicodé", - "How does an episode with emoji sound? 😉", - "Here's another title", - "Un titre, c'est moi!", - "One more just for good measure", - ]; - let mut items = Vec::new(); - for (i, t) in titles.iter().enumerate() { - let played = i % 2 == 0; - items.push(Episode { - id: i as _, - pod_id: 1, - title: t.to_string(), - url: String::new(), - description: String::new(), - pubdate: Some(Utc::now()), - duration: Some(12345), - path: None, - played: played, - }); - } - - let panel = Panel::new("Episodes".to_string(), 1, n_row, n_col, 0, 0); - return Menu { - panel: panel, - header: None, - items: LockVec::new(items), - start_row: 0, - top_row: top_row, - selected: selected, - }; - } - - #[test] - fn scroll_up() { - let real_rows = 5; - let real_cols = 65; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 2, 0); - menu.update_items(); - - menu.scroll(-1); - - let expected_top = menu - .items - .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) - .unwrap(); - let expected_bot = menu - .items - .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) - .unwrap(); - - assert_eq!(menu.panel.get_row(0).0, expected_top); - assert_eq!(menu.panel.get_row(4).0, expected_bot); - } - - #[test] - fn scroll_down() { - let real_rows = 5; - let real_cols = 65; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); - menu.update_items(); - - menu.scroll(1); - - let expected_top = menu - .items - .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) - .unwrap(); - let expected_bot = menu - .items - .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) - .unwrap(); - - assert_eq!(menu.panel.get_row(0).0, expected_top); - assert_eq!(menu.panel.get_row(4).0, expected_bot); - } - - #[test] - fn resize_bigger() { - let real_rows = 5; - let real_cols = 65; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); - menu.update_items(); - - menu.resize(real_rows + 2 + 5, real_cols + 5 + 5, 0, 0); - menu.update_items(); - - assert_eq!(menu.top_row, 0); - assert_eq!(menu.selected, 4); - - let non_empty: Vec = menu - .panel - .window - .iter() - .filter_map(|x| { - if x.0.is_empty() { - None - } else { - Some(x.0.clone()) - } - }) - .collect(); - assert_eq!(non_empty.len(), menu.items.len()); - } - - #[test] - fn resize_smaller() { - let real_rows = 7; - let real_cols = 65; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 6); - menu.update_items(); - - menu.resize(real_rows + 2 - 2, real_cols + 5 - 5, 0, 0); - menu.update_items(); - - assert_eq!(menu.top_row, 2); - assert_eq!(menu.selected, 4); - - let non_empty: Vec = menu - .panel - .window - .iter() - .filter_map(|x| { - if x.0.is_empty() { - None - } else { - Some(x.0.clone()) - } - }) - .collect(); - assert_eq!(non_empty.len(), (real_rows - 2) as usize); - } - - #[test] - fn chop_accent() { - let real_rows = 5; - let real_cols = 25; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); - menu.update_items(); - - let expected = "An episode with le Unicod".to_string(); - - assert_eq!(menu.panel.get_row(2).0, expected); - } - - #[test] - fn chop_emoji() { - let real_rows = 5; - let real_cols = 38; - let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); - menu.update_items(); - - let expected = "How does an episode with emoji sound? ".to_string(); - - assert_eq!(menu.panel.get_row(3).0, expected); - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use chrono::Utc; + +// fn create_menu(n_row: u16, n_col: u16, top_row: u16, selected: u16) -> Menu { +// let titles = vec![ +// "A Very Cool Episode", +// "This is a very long episode title but we'll get through it together", +// "An episode with le Unicodé", +// "How does an episode with emoji sound? 😉", +// "Here's another title", +// "Un titre, c'est moi!", +// "One more just for good measure", +// ]; +// let mut items = Vec::new(); +// for (i, t) in titles.iter().enumerate() { +// let played = i % 2 == 0; +// items.push(Episode { +// id: i as _, +// pod_id: 1, +// title: t.to_string(), +// url: String::new(), +// description: String::new(), +// pubdate: Some(Utc::now()), +// duration: Some(12345), +// path: None, +// played: played, +// }); +// } + +// let panel = Panel::new("Episodes".to_string(), 1, n_row, n_col, 0); +// return Menu { +// panel: panel, +// header: None, +// items: LockVec::new(items), +// start_row: 0, +// top_row: top_row, +// selected: selected, +// }; +// } + +// #[test] +// fn scroll_up() { +// let real_rows = 5; +// let real_cols = 65; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 2, 0); +// menu.update_items(); + +// menu.scroll(Scroll::Up(1)); + +// let expected_top = menu +// .items +// .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) +// .unwrap(); +// let expected_bot = menu +// .items +// .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) +// .unwrap(); + +// assert_eq!(menu.panel.get_row(0).0, expected_top); +// assert_eq!(menu.panel.get_row(4).0, expected_bot); +// } + +// #[test] +// fn scroll_down() { +// let real_rows = 5; +// let real_cols = 65; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); +// menu.update_items(); + +// menu.scroll(Scroll::Down(1)); + +// let expected_top = menu +// .items +// .map_single_by_index(1, |ep| ep.get_title(real_cols as usize)) +// .unwrap(); +// let expected_bot = menu +// .items +// .map_single_by_index(5, |ep| ep.get_title(real_cols as usize)) +// .unwrap(); + +// assert_eq!(menu.panel.get_row(0).0, expected_top); +// assert_eq!(menu.panel.get_row(4).0, expected_bot); +// } + +// #[test] +// fn resize_bigger() { +// let real_rows = 5; +// let real_cols = 65; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 4); +// menu.update_items(); + +// menu.resize(real_rows + 2 + 5, real_cols + 5 + 5, 0, 0); +// menu.update_items(); + +// assert_eq!(menu.top_row, 0); +// assert_eq!(menu.selected, 4); + +// let non_empty: Vec = menu +// .panel +// .window +// .iter() +// .filter_map(|x| { +// if x.0.is_empty() { +// None +// } else { +// Some(x.0.clone()) +// } +// }) +// .collect(); +// assert_eq!(non_empty.len(), menu.items.len()); +// } + +// #[test] +// fn resize_smaller() { +// let real_rows = 7; +// let real_cols = 65; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 6); +// menu.update_items(); + +// menu.resize(real_rows + 2 - 2, real_cols + 5 - 5, 0, 0); +// menu.update_items(); + +// assert_eq!(menu.top_row, 2); +// assert_eq!(menu.selected, 4); + +// let non_empty: Vec = menu +// .panel +// .window +// .iter() +// .filter_map(|x| { +// if x.0.is_empty() { +// None +// } else { +// Some(x.0.clone()) +// } +// }) +// .collect(); +// assert_eq!(non_empty.len(), (real_rows - 2) as usize); +// } + +// #[test] +// fn chop_accent() { +// let real_rows = 5; +// let real_cols = 25; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); +// menu.update_items(); + +// let expected = "An episode with le Unicod".to_string(); + +// assert_eq!(menu.panel.get_row(2).0, expected); +// } + +// #[test] +// fn chop_emoji() { +// let real_rows = 5; +// let real_cols = 38; +// let mut menu = create_menu(real_rows + 2, real_cols + 5, 0, 0); +// menu.update_items(); + +// let expected = "How does an episode with emoji sound? ".to_string(); + +// assert_eq!(menu.panel.get_row(3).0, expected); +// } +// } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 17809c6..f0229d0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,31 +1,39 @@ +use std::io::{self, Write}; use std::sync::mpsc; use std::thread; use std::time::Duration; -#[cfg_attr(not(test), path = "panel.rs")] -#[cfg_attr(test, path = "mock_panel.rs")] +use crossterm::{ + self, cursor, + event::{self, Event}, + execute, style, terminal, +}; +use lazy_static::lazy_static; +use regex::Regex; + +// #[cfg_attr(not(test), path = "panel.rs")] +// #[cfg_attr(test, path = "mock_panel.rs")] mod panel; pub mod colors; mod menu; mod notification; -mod popup; +// mod popup; use self::colors::ColorType; -use self::menu::Menu; +use self::menu::{Menu, Scroll}; use self::notification::NotifWin; use self::panel::{Details, Panel}; -use self::popup::PopupWin; - -use lazy_static::lazy_static; -use pancurses::{Input, Window}; -use regex::Regex; +// use self::popup::PopupWin; use super::MainMessage; use crate::config::Config; use crate::keymap::{Keybindings, UserAction}; use crate::types::*; +/// Amount of time between ticks in the event loop +const TICK_RATE: u64 = 10; + lazy_static! { /// Regex for finding
tags -- also captures any surrounding /// line breaks @@ -76,16 +84,15 @@ enum ActiveMenu { /// the screen. #[derive(Debug)] pub struct Ui<'a> { - stdscr: Window, - n_row: i32, - n_col: i32, + n_row: u16, + n_col: u16, keymap: &'a Keybindings, podcast_menu: Menu, episode_menu: Menu, active_menu: ActiveMenu, details_panel: Option, notif_win: NotifWin, - popup_win: PopupWin<'a>, + // popup_win: PopupWin<'a>, } impl<'a> Ui<'a> { @@ -105,7 +112,7 @@ impl<'a> Ui<'a> { // any messages at the bottom, check for user input, and // then process any messages from the main thread loop { - ui.notif_win.check_notifs(); + // ui.notif_win.check_notifs(); match ui.getch() { UiMsg::Noop => (), @@ -129,13 +136,15 @@ impl<'a> Ui<'a> { break; } MainMessage::UiSpawnDownloadPopup(episodes, selected) => { - ui.popup_win.spawn_download_win(episodes, selected); + // ui.popup_win.spawn_download_win(episodes, selected); } } } + io::stdout().flush().unwrap(); + // slight delay to avoid excessive CPU usage - thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(TICK_RATE)); } }); } @@ -144,20 +153,17 @@ impl<'a> Ui<'a> { /// creates the pancurses window and draws it to the screen, and /// returns a UI object for future manipulation. pub fn new(config: &'a Config, items: LockVec) -> Ui<'a> { - let stdscr = pancurses::initscr(); - - // set some options - pancurses::cbreak(); // allows characters to be read one by one - pancurses::noecho(); // turns off automatic echoing of characters - // to the screen as they are input - pancurses::curs_set(0); // turn off cursor - stdscr.keypad(true); // returns special characters as single - // key codes - stdscr.nodelay(true); // getch() will not wait for user input + terminal::enable_raw_mode().expect("Terminal can't run in raw mode."); + execute!( + io::stdout(), + terminal::Clear(terminal::ClearType::All), + cursor::Hide + ) + .expect("Can't draw to screen."); self::colors::set_colors(&config.colors); - let (n_row, n_col) = stdscr.get_max_yx(); + let (n_col, n_row) = terminal::size().expect("Can't get terminal size"); let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); let first_pod = match items.borrow_order().get(0) { @@ -168,11 +174,10 @@ impl<'a> Ui<'a> { None => LockVec::new(Vec::new()), }; - let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0, 0); + let podcast_panel = Panel::new("Podcasts".to_string(), 0, n_row - 1, pod_col, 0); let podcast_menu = Menu::new(podcast_panel, None, items); - let episode_panel = - Panel::new("Episodes".to_string(), 1, n_row - 1, ep_col, 0, pod_col - 1); + let episode_panel = Panel::new("Episodes".to_string(), 1, n_row - 1, ep_col, pod_col - 1); let episode_menu = Menu::new(episode_panel, None, first_pod); @@ -180,18 +185,16 @@ impl<'a> Ui<'a> { Some(Self::make_details_panel( n_row - 1, det_col, - 0, pod_col + ep_col - 2, )) } else { None }; - let notif_win = NotifWin::new(n_row, n_col); - let popup_win = PopupWin::new(&config.keybindings, n_row, n_col); + let notif_win = NotifWin::new(n_row - 1, n_row, n_col); + // let popup_win = PopupWin::new(&config.keybindings, n_row, n_col); return Ui { - stdscr: stdscr, n_row: n_row, n_col: n_col, keymap: &config.keybindings, @@ -200,29 +203,29 @@ impl<'a> Ui<'a> { active_menu: ActiveMenu::PodcastMenu, details_panel: details_panel, notif_win: notif_win, - popup_win: popup_win, + // popup_win: popup_win, }; } /// This should be called immediately after creating the UI, in order /// to draw everything to the screen. pub fn init(&mut self) { - self.stdscr.refresh(); - self.podcast_menu.init(); self.podcast_menu.activate(); - self.episode_menu.init(); + self.podcast_menu.redraw(); + self.episode_menu.redraw(); if let Some(ref panel) = self.details_panel { - panel.refresh(); + panel.redraw(); } self.update_details_panel(); - self.notif_win.init(); + self.notif_win.redraw(); // welcome screen if user does not have any podcasts yet if self.podcast_menu.items.is_empty() { - self.popup_win.spawn_welcome_win(); + // self.popup_win.spawn_welcome_win(); } + io::stdout().flush().unwrap(); } /// Waits for user input and, where necessary, provides UiMsgs @@ -234,37 +237,37 @@ impl<'a> Ui<'a> { /// podcast feed spawns a UI window to capture the feed URL, and only /// then passes this data back to the main controller. pub fn getch(&mut self) -> UiMsg { - match self.stdscr.getch() { - Some(Input::KeyResize) => self.resize(), - - Some(input) => { - let (curr_pod_id, curr_ep_id) = self.get_current_ids(); - - // get rid of the "welcome" window once the podcast list - // is no longer empty - if self.popup_win.welcome_win && !self.podcast_menu.items.is_empty() { - self.popup_win.turn_off_welcome_win(); - } - - // if there is a popup window active (apart from the - // welcome window which takes no input), then - // redirect user input there - if self.popup_win.is_non_welcome_popup_active() { - let popup_msg = self.popup_win.handle_input(input); - - // need to check if popup window is still active, as - // handling character input above may involve - // closing the popup window - if !self.popup_win.is_popup_active() { - self.stdscr.refresh(); - self.update_menus(); - if self.details_panel.is_some() { - self.update_details_panel(); - } - } - return popup_msg; - } else { - match self.keymap.get_from_input(input) { + if event::poll(Duration::from_secs(0)).expect("Can't poll for inputs") { + match event::read().expect("Can't read inputs") { + Event::Resize(_, _) => self.resize(), + Event::Key(input) => { + let (curr_pod_id, curr_ep_id) = self.get_current_ids(); + + // get rid of the "welcome" window once the podcast list + // is no longer empty + // if self.popup_win.welcome_win && !self.podcast_menu.items.is_empty() { + // self.popup_win.turn_off_welcome_win(); + // } + + // if there is a popup window active (apart from the + // welcome window which takes no input), then + // redirect user input there + // if self.popup_win.is_non_welcome_popup_active() { + // let popup_msg = self.popup_win.handle_input(input); + + // // need to check if popup window is still active, as + // // handling character input above may involve + // // closing the popup window + // if !self.popup_win.is_popup_active() { + // self.update_menus(); + // if self.details_panel.is_some() { + // self.update_details_panel(); + // } + // io::stdout().flush(); + // } + // return popup_msg; + // } else { + match self.keymap.get_from_input(input.code) { Some(a @ UserAction::Down) | Some(a @ UserAction::Up) | Some(a @ UserAction::Left) @@ -368,66 +371,68 @@ impl<'a> Ui<'a> { } } - Some(UserAction::Help) => self.popup_win.spawn_help_win(), + // Some(UserAction::Help) => self.popup_win.spawn_help_win(), + Some(UserAction::Help) => (), Some(UserAction::Quit) => { return UiMsg::Quit; } None => (), } // end of input match + // } } + _ => (), } - None => (), - }; // end of getch() match + } // end of poll() return UiMsg::Noop; } /// Resize all the windows on the screen and refresh. pub fn resize(&mut self) { - pancurses::resize_term(0, 0); - let (n_row, n_col) = self.stdscr.get_max_yx(); - self.n_row = n_row; - self.n_col = n_col; - - let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); - - self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); - self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); - - if self.details_panel.is_some() { - if det_col > 0 { - let det = self.details_panel.as_mut().unwrap(); - det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); - } else { - self.details_panel = None; - } - } else if det_col > 0 { - self.details_panel = Some(Self::make_details_panel( - n_row - 1, - det_col, - 0, - pod_col + ep_col - 2, - )); - } - - self.stdscr.refresh(); - self.update_menus(); - - match self.active_menu { - ActiveMenu::PodcastMenu => self.podcast_menu.activate(), - ActiveMenu::EpisodeMenu => { - self.podcast_menu.activate(); - self.episode_menu.activate(); - } - } - - if self.details_panel.is_some() { - self.update_details_panel(); - } - - self.popup_win.resize(n_row, n_col); - self.notif_win.resize(n_row, n_col); - self.stdscr.refresh(); + // pancurses::resize_term(0, 0); + // let (n_row, n_col) = self.stdscr.get_max_yx(); + // self.n_row = n_row; + // self.n_col = n_col; + + // let (pod_col, ep_col, det_col) = Self::calculate_sizes(n_col); + + // self.podcast_menu.resize(n_row - 1, pod_col, 0, 0); + // self.episode_menu.resize(n_row - 1, ep_col, 0, pod_col - 1); + + // if self.details_panel.is_some() { + // if det_col > 0 { + // let det = self.details_panel.as_mut().unwrap(); + // det.resize(n_row - 1, det_col, 0, pod_col + ep_col - 2); + // } else { + // self.details_panel = None; + // } + // } else if det_col > 0 { + // self.details_panel = Some(Self::make_details_panel( + // n_row - 1, + // det_col, + // 0, + // pod_col + ep_col - 2, + // )); + // } + + // self.stdscr.refresh(); + // self.update_menus(); + + // match self.active_menu { + // ActiveMenu::PodcastMenu => self.podcast_menu.activate(), + // ActiveMenu::EpisodeMenu => { + // self.podcast_menu.activate(); + // self.episode_menu.activate(); + // } + // } + + // if self.details_panel.is_some() { + // self.update_details_panel(); + // } + + // self.popup_win.resize(n_row, n_col); + // self.notif_win.resize(n_row, n_col); + // self.stdscr.refresh(); } /// Move the menu cursor around and refresh menus when necessary. @@ -439,11 +444,11 @@ impl<'a> Ui<'a> { ) { match action { UserAction::Down => { - self.scroll_current_window(curr_pod_id, 1); + self.scroll_current_window(curr_pod_id, Scroll::Down(1)); } UserAction::Up => { - self.scroll_current_window(curr_pod_id, -1); + self.scroll_current_window(curr_pod_id, Scroll::Up(1)); } UserAction::Left => { @@ -458,7 +463,7 @@ impl<'a> Ui<'a> { } } if let Some(det) = &self.details_panel { - det.refresh(); + det.redraw(); } } @@ -474,38 +479,38 @@ impl<'a> Ui<'a> { } } if let Some(det) = &self.details_panel { - det.refresh(); + det.redraw(); } } UserAction::PageUp => { - self.scroll_current_window(curr_pod_id, -self.n_row + 3); + self.scroll_current_window(curr_pod_id, Scroll::Up(self.n_row + 3)); } UserAction::PageDown => { - self.scroll_current_window(curr_pod_id, self.n_row - 3); + self.scroll_current_window(curr_pod_id, Scroll::Down(self.n_row - 3)); } UserAction::BigUp => { self.scroll_current_window( curr_pod_id, - -self.n_row / crate::config::BIG_SCROLL_AMOUNT, + Scroll::Up(self.n_row / crate::config::BIG_SCROLL_AMOUNT), ); } UserAction::BigDown => { self.scroll_current_window( curr_pod_id, - self.n_row / crate::config::BIG_SCROLL_AMOUNT, + Scroll::Down(self.n_row / crate::config::BIG_SCROLL_AMOUNT), ); } UserAction::GoTop => { - self.scroll_current_window(curr_pod_id, -i32::MAX); + self.scroll_current_window(curr_pod_id, Scroll::Up(u16::MAX)); } UserAction::GoBot => { - self.scroll_current_window(curr_pod_id, i32::MAX); + self.scroll_current_window(curr_pod_id, Scroll::Down(u16::MAX)); } // this shouldn't occur because we only trigger this @@ -519,7 +524,7 @@ impl<'a> Ui<'a> { /// the specified amount and refreshes /// the window. /// Positive Scroll is down. - pub fn scroll_current_window(&mut self, pod_id: Option, scroll: i32) { + pub fn scroll_current_window(&mut self, pod_id: Option, scroll: Scroll) { match self.active_menu { ActiveMenu::PodcastMenu => { if pod_id.is_some() { @@ -678,17 +683,17 @@ impl<'a> Ui<'a> { /// the screen is too small to display the details panel, this size /// will be 0 #[allow(clippy::useless_let_if_seq)] - pub fn calculate_sizes(n_col: i32) -> (i32, i32, i32) { + pub fn calculate_sizes(n_col: u16) -> (u16, u16, u16) { let pod_col; let ep_col; let det_col; if n_col > crate::config::DETAILS_PANEL_LENGTH { - pod_col = n_col / 3; - ep_col = n_col / 3 + 1; - det_col = n_col - pod_col - ep_col + 2; + pod_col = (n_col + 2) / 3; + ep_col = (n_col + 2) / 3; + det_col = n_col + 2 - pod_col - ep_col; } else { - pod_col = n_col / 2; - ep_col = n_col - pod_col + 1; + pod_col = (n_col + 1) / 2; + ep_col = n_col + 1 - pod_col; det_col = 0; } return (pod_col, ep_col, det_col); @@ -800,8 +805,8 @@ impl<'a> Ui<'a> { } /// Create a details panel. - pub fn make_details_panel(n_row: i32, n_col: i32, start_y: i32, start_x: i32) -> Panel { - return Panel::new("Details".to_string(), 2, n_row, n_col, start_y, start_x); + pub fn make_details_panel(n_row: u16, n_col: u16, start_x: u16) -> Panel { + return Panel::new("Details".to_string(), 2, n_row, n_col, start_x); } /// Updates the details panel with information about the current @@ -810,7 +815,6 @@ impl<'a> Ui<'a> { if self.details_panel.is_some() { let (curr_pod_id, curr_ep_id) = self.get_current_ids(); let det = self.details_panel.as_mut().unwrap(); - det.erase(); if let Some(pod_id) = curr_pod_id { if let Some(ep_id) = curr_ep_id { // get a couple details from the current podcast @@ -864,8 +868,6 @@ impl<'a> Ui<'a> { }; det.details_template(0, details); }; - - det.refresh(); } } } diff --git a/src/ui/notification.rs b/src/ui/notification.rs index bb99fa4..f47a995 100644 --- a/src/ui/notification.rs +++ b/src/ui/notification.rs @@ -1,7 +1,8 @@ +use std::io; use std::time::{Duration, Instant}; use super::ColorType; -use pancurses::{Input, Window}; +use crossterm::{cursor, queue, style}; /// Holds details of a notification message. #[derive(Debug, Clone, PartialEq)] @@ -36,9 +37,9 @@ impl Notification { /// not necessarily. #[derive(Debug)] pub struct NotifWin { - window: Window, - total_rows: i32, - total_cols: i32, + start_y: u16, + total_rows: u16, + total_cols: u16, msg_stack: Vec, persistent_msg: Option, current_msg: Option, @@ -46,10 +47,9 @@ pub struct NotifWin { impl NotifWin { /// Creates a new NotifWin. - pub fn new(total_rows: i32, total_cols: i32) -> Self { - let win = pancurses::newwin(1, total_cols, total_rows - 1, 0); + pub fn new(start_y: u16, total_rows: u16, total_cols: u16) -> Self { return Self { - window: win, + start_y: start_y, total_rows: total_rows, total_cols: total_cols, msg_stack: Vec::new(), @@ -60,10 +60,19 @@ impl NotifWin { /// Initiates the window -- primarily, sets the background on the /// window. - pub fn init(&mut self) { - self.window - .bkgd(pancurses::ColorPair(ColorType::Normal as u8)); - self.window.refresh(); + pub fn redraw(&mut self) { + // clear the panel + // TODO: Set the background color first + let empty = vec![" "; self.total_cols as usize]; + let empty_string = empty.join(""); + for r in 0..(self.total_rows - 1) { + queue!( + io::stdout(), + cursor::MoveTo(0, self.start_y + r), + style::Print(&empty_string), + ) + .unwrap(); + } } /// Checks if the current notification needs to be changed, and @@ -105,10 +114,7 @@ impl NotifWin { } else { // otherwise, there was a notification before but there // isn't now, so erase - self.window.erase(); - self.window - .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); - self.window.refresh(); + self.redraw(); self.current_msg = None; } } @@ -119,100 +125,101 @@ impl NotifWin { /// input line. This returns the user's input; if the user cancels /// their input, the String will be empty. pub fn input_notif(&self, prefix: &str) -> String { - self.window.mv(self.total_rows - 1, 0); - self.window.addstr(&prefix); - self.window.keypad(true); - self.window.refresh(); - pancurses::curs_set(2); + // self.window.mv(self.total_rows - 1, 0); + // self.window.addstr(&prefix); + // self.window.keypad(true); + // self.window.refresh(); + // pancurses::curs_set(2); - let mut inputs = String::new(); - let mut cancelled = false; + // let mut inputs = String::new(); + // let mut cancelled = false; - let min_x = prefix.len() as i32; - let mut current_x = prefix.len() as i32; - let mut cursor_x = prefix.len() as i32; - loop { - match self.window.getch() { - // Cancel input - Some(Input::KeyExit) | Some(Input::Character('\u{1b}')) => { - cancelled = true; - break; - } - // Complete input - Some(Input::KeyEnter) | Some(Input::Character('\n')) => { - break; - } - Some(Input::KeyBackspace) | Some(Input::Character('\u{7f}')) => { - if current_x > min_x { - current_x -= 1; - cursor_x -= 1; - let _ = inputs.remove((cursor_x as usize) - prefix.len()); - self.window.mv(0, cursor_x); - self.window.delch(); - } - } - Some(Input::KeyDC) => { - if cursor_x < current_x { - let _ = inputs.remove((cursor_x as usize) - prefix.len()); - self.window.delch(); - } - } - Some(Input::KeyLeft) => { - if cursor_x > min_x { - cursor_x -= 1; - self.window.mv(0, cursor_x); - } - } - Some(Input::KeyRight) => { - if cursor_x < current_x { - cursor_x += 1; - self.window.mv(0, cursor_x); - } - } - Some(Input::Character(c)) => { - current_x += 1; - cursor_x += 1; - self.window.insch(c); - self.window.mv(0, cursor_x); - inputs.push(c); - } - Some(_) => (), - None => (), - } - self.window.refresh(); - } + // let min_x = prefix.len() as i32; + // let mut current_x = prefix.len() as i32; + // let mut cursor_x = prefix.len() as i32; + // loop { + // match self.window.getch() { + // // Cancel input + // Some(Input::KeyExit) | Some(Input::Character('\u{1b}')) => { + // cancelled = true; + // break; + // } + // // Complete input + // Some(Input::KeyEnter) | Some(Input::Character('\n')) => { + // break; + // } + // Some(Input::KeyBackspace) | Some(Input::Character('\u{7f}')) => { + // if current_x > min_x { + // current_x -= 1; + // cursor_x -= 1; + // let _ = inputs.remove((cursor_x as usize) - prefix.len()); + // self.window.mv(0, cursor_x); + // self.window.delch(); + // } + // } + // Some(Input::KeyDC) => { + // if cursor_x < current_x { + // let _ = inputs.remove((cursor_x as usize) - prefix.len()); + // self.window.delch(); + // } + // } + // Some(Input::KeyLeft) => { + // if cursor_x > min_x { + // cursor_x -= 1; + // self.window.mv(0, cursor_x); + // } + // } + // Some(Input::KeyRight) => { + // if cursor_x < current_x { + // cursor_x += 1; + // self.window.mv(0, cursor_x); + // } + // } + // Some(Input::Character(c)) => { + // current_x += 1; + // cursor_x += 1; + // self.window.insch(c); + // self.window.mv(0, cursor_x); + // inputs.push(c); + // } + // Some(_) => (), + // None => (), + // } + // self.window.refresh(); + // } - pancurses::curs_set(0); - self.window.clear(); - self.window.refresh(); + // pancurses::curs_set(0); + // self.window.clear(); + // self.window.refresh(); - if cancelled { - return String::from(""); - } - return inputs; + // if cancelled { + // return String::from(""); + // } + // return inputs; + return "".to_string(); } /// Prints a notification to the window. fn display_notif(&self, notif: &Notification) { - self.window.erase(); - self.window.mv(self.total_rows - 1, 0); - self.window.attrset(pancurses::A_NORMAL); - self.window.addstr(¬if.message); + // self.window.erase(); + // self.window.mv(self.total_rows - 1, 0); + // self.window.attrset(pancurses::A_NORMAL); + // self.window.addstr(¬if.message); - if notif.error { - self.window - .mvchgat(0, 0, -1, pancurses::A_BOLD, ColorType::Error as i16); - } - self.window.refresh(); + // if notif.error { + // self.window + // .mvchgat(0, 0, -1, pancurses::A_BOLD, ColorType::Error as i16); + // } + // self.window.refresh(); } /// Adds a notification to the user. `duration` indicates how long /// (in milliseconds) this message will remain on screen. Useful for /// presenting error messages, among other things. pub fn timed_notif(&mut self, message: String, duration: u64, error: bool) { - let expiry = Instant::now() + Duration::from_millis(duration); - self.msg_stack - .push(Notification::new(message, error, Some(expiry))); + // let expiry = Instant::now() + Duration::from_millis(duration); + // self.msg_stack + // .push(Notification::new(message, error, Some(expiry))); } /// Adds a notification that will stay on screen indefinitely. Must @@ -220,45 +227,45 @@ impl NotifWin { /// notification is already being displayed, this method will /// overwrite that message. pub fn persistent_notif(&mut self, message: String, error: bool) { - let notif = Notification::new(message, error, None); - self.persistent_msg = Some(notif.clone()); - if self.msg_stack.is_empty() { - self.display_notif(¬if); - self.current_msg = Some(notif); - } + // let notif = Notification::new(message, error, None); + // self.persistent_msg = Some(notif.clone()); + // if self.msg_stack.is_empty() { + // self.display_notif(¬if); + // self.current_msg = Some(notif); + // } } /// Clears any persistent notification that is being displayed. Does /// not affect timed notifications, user input notifications, etc. pub fn clear_persistent_notif(&mut self) { - self.persistent_msg = None; - if self.msg_stack.is_empty() { - self.window.erase(); - self.window.refresh(); - self.current_msg = None; - } + // self.persistent_msg = None; + // if self.msg_stack.is_empty() { + // self.window.erase(); + // self.window.refresh(); + // self.current_msg = None; + // } } /// Updates window size/location - pub fn resize(&mut self, total_rows: i32, total_cols: i32) { - self.total_rows = total_rows; - self.total_cols = total_cols; + pub fn resize(&mut self, total_rows: u16, total_cols: u16) { + // self.total_rows = total_rows; + // self.total_cols = total_cols; - // apparently pancurses does not implement `wresize()` - // from ncurses, so instead we create an entirely new - // window every time the terminal is resized...not ideal, - // but c'est la vie - let oldwin = std::mem::replace( - &mut self.window, - pancurses::newwin(1, total_cols, total_rows - 1, 0), - ); - oldwin.delwin(); + // // apparently pancurses does not implement `wresize()` + // // from ncurses, so instead we create an entirely new + // // window every time the terminal is resized...not ideal, + // // but c'est la vie + // let oldwin = std::mem::replace( + // &mut self.window, + // pancurses::newwin(1, total_cols, total_rows - 1, 0), + // ); + // oldwin.delwin(); - self.window - .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); - if let Some(curr) = &self.current_msg { - self.display_notif(curr); - } - self.window.refresh(); + // self.window + // .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); + // if let Some(curr) = &self.current_msg { + // self.display_notif(curr); + // } + // self.window.refresh(); } } diff --git a/src/ui/panel.rs b/src/ui/panel.rs index 24e9186..7b4bb36 100644 --- a/src/ui/panel.rs +++ b/src/ui/panel.rs @@ -1,8 +1,21 @@ +use std::{convert::TryInto, io}; + use chrono::{DateTime, Utc}; -use pancurses::{Attribute, Window}; +use crossterm::{cursor, queue, style}; use super::ColorType; + +pub const VERTICAL: &str = "│"; +pub const HORIZONTAL: &str = "─"; +pub const TOP_RIGHT: &str = "┐"; +pub const TOP_LEFT: &str = "┌"; +pub const BOTTOM_RIGHT: &str = "┘"; +pub const BOTTOM_LEFT: &str = "└"; +pub const TOP_TEE: &str = "┬"; +pub const BOTTOM_TEE: &str = "┴"; + + /// Struct holding the raw data used for building the details panel. pub struct Details { pub pod_title: Option, @@ -21,40 +34,40 @@ pub struct Details { /// calculate rows and columns relative to the Panel. #[derive(Debug)] pub struct Panel { - window: Window, screen_pos: usize, title: String, - n_row: i32, - n_col: i32, + start_x: u16, + n_row: u16, + n_col: u16, } impl Panel { /// Creates a new panel. - pub fn new( - title: String, - screen_pos: usize, - n_row: i32, - n_col: i32, - start_y: i32, - start_x: i32, - ) -> Self { - let panel_win = pancurses::newwin(n_row, n_col, start_y, start_x); - + pub fn new(title: String, screen_pos: usize, n_row: u16, n_col: u16, start_x: u16) -> Self { return Panel { - window: panel_win, screen_pos: screen_pos, title: title, + start_x: start_x, n_row: n_row, n_col: n_col, }; } /// Redraws borders and refreshes the window to display on terminal. - pub fn refresh(&self) { - self.window - .bkgd(pancurses::ColorPair(ColorType::Normal as u8)); + pub fn redraw(&self) { + // clear the panel + // TODO: Set the background color first + let empty = vec![" "; self.n_col as usize]; + let empty_string = empty.join(""); + for r in 0..(self.n_row - 1) { + queue!( + io::stdout(), + cursor::MoveTo(self.start_x, r), + style::Print(&empty_string), + ) + .unwrap(); + } self.draw_border(); - self.window.refresh(); } /// Draws a border around the window. @@ -63,196 +76,205 @@ impl Panel { let bot_left; match self.screen_pos { 0 => { - top_left = pancurses::ACS_ULCORNER(); - bot_left = pancurses::ACS_LLCORNER(); + top_left = TOP_LEFT; + bot_left = BOTTOM_LEFT; } _ => { - top_left = pancurses::ACS_TTEE(); - bot_left = pancurses::ACS_BTEE(); + top_left = TOP_TEE; + bot_left = BOTTOM_TEE; } } - self.window.border( - pancurses::ACS_VLINE(), - pancurses::ACS_VLINE(), - pancurses::ACS_HLINE(), - pancurses::ACS_HLINE(), - top_left, - pancurses::ACS_URCORNER(), - bot_left, - pancurses::ACS_LRCORNER(), - ); - - self.window.mvaddstr(0, 2, &self.title); - } + let mut border_top = vec![top_left]; + let mut border_bottom = vec![bot_left]; + for _ in 0..(self.n_col - 2) { + border_top.push(HORIZONTAL); + border_bottom.push(HORIZONTAL); + } + border_top.push(TOP_RIGHT); + border_bottom.push(BOTTOM_RIGHT); - /// Erases all content on the window, and redraws the border. Does - /// not refresh the screen. - pub fn erase(&self) { - self.window.erase(); - self.window - .bkgdset(pancurses::ColorPair(ColorType::Normal as u8)); - self.draw_border(); + queue!( + io::stdout(), + cursor::MoveTo(self.start_x, 0), + style::Print(border_top.join("")), + cursor::MoveTo(self.start_x, self.n_row - 1), + style::Print(border_bottom.join("")), + ) + .unwrap(); + + for r in 1..(self.n_row - 1) { + queue!( + io::stdout(), + cursor::MoveTo(self.start_x, r), + style::Print(VERTICAL.to_string()), + cursor::MoveTo(self.start_x + self.n_col - 1, r), + style::Print(VERTICAL.to_string()), + ) + .unwrap(); + } + + queue!( + io::stdout(), + cursor::MoveTo(self.start_x + 2, 0), + style::Print(&self.title), + ) + .unwrap(); } /// Writes a line of text to the window. Note that this does not do /// checking for line length, so strings that are too long will end /// up wrapping and may mess up the format. use `write_wrap_line()` /// if you need line wrapping. - pub fn write_line(&self, y: i32, string: String) { - self.window.mvaddstr(self.abs_y(y), self.abs_x(0), string); - } - - /// Writes a line of text to the window, first moving all text on - /// line `y` and below down one row. - pub fn insert_line(&self, y: i32, string: String) { - self.window.mv(self.abs_y(y), 0); - self.window.insertln(); - self.window.mv(self.abs_y(y), self.abs_x(0)); - self.window.addstr(string); - } - - /// Deletes a line of text from the window. - pub fn delete_line(&self, y: i32) { - self.window.mv(self.abs_y(y), self.abs_x(-1)); - self.window.deleteln(); + pub fn write_line(&self, y: u16, string: String) { + queue!( + io::stdout(), + cursor::MoveTo(self.abs_x(0), self.abs_y(y as i16)), + style::Print(string) + ) + .unwrap(); } /// Writes one or more lines of text from a String, word wrapping /// when necessary. `start_y` refers to the row to start at (word /// wrapping makes it unknown where text will end). Returns the row /// on which the text ended. - pub fn write_wrap_line(&self, start_y: i32, string: &str) -> i32 { - let mut row = start_y; - let max_row = self.get_rows(); - let wrapper = textwrap::wrap(string, self.get_cols() as usize); - for line in wrapper { - self.window.mvaddstr(self.abs_y(row), self.abs_x(0), line); - row += 1; - - if row >= max_row { - break; - } - } - return row - 1; + pub fn write_wrap_line(&self, start_y: u16, string: &str) -> u16 { + // let mut row = start_y; + // let max_row = self.get_rows(); + // let wrapper = textwrap::wrap(string, self.get_cols() as usize); + // for line in wrapper { + // self.window.mvaddstr(self.abs_y(row), self.abs_x(0), line); + // row += 1; + + // if row >= max_row { + // break; + // } + // } + // return row - 1; + return 0; } /// Write the specific template used for the details panel. This is /// not the most elegant code, but it works. - pub fn details_template(&self, start_y: i32, details: Details) { - let mut row = start_y - 1; - - self.window.attron(Attribute::Bold); - // podcast title - match details.pod_title { - Some(t) => row = self.write_wrap_line(row + 1, &t), - None => row = self.write_wrap_line(row + 1, "No title"), - } + pub fn details_template(&self, start_y: u16, details: Details) { + // let mut row = start_y - 1; - // episode title - match details.ep_title { - Some(t) => row = self.write_wrap_line(row + 1, &t), - None => row = self.write_wrap_line(row + 1, "No title"), - } - self.window.attroff(Attribute::Bold); - - row += 1; // blank line - - // published date - if let Some(date) = details.pubdate { - let new_row = self.write_wrap_line( - row + 1, - &format!("Published: {}", date.format("%B %-d, %Y")), - ); - self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); - row = new_row; - } + // self.window.attron(Attribute::Bold); + // // podcast title + // match details.pod_title { + // Some(t) => row = self.write_wrap_line(row + 1, &t), + // None => row = self.write_wrap_line(row + 1, "No title"), + // } - // duration - if let Some(dur) = details.duration { - let new_row = self.write_wrap_line(row + 1, &format!("Duration: {}", dur)); - self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); - row = new_row; - } + // // episode title + // match details.ep_title { + // Some(t) => row = self.write_wrap_line(row + 1, &t), + // None => row = self.write_wrap_line(row + 1, "No title"), + // } + // self.window.attroff(Attribute::Bold); - // explicit - if let Some(exp) = details.explicit { - let new_row = if exp { - self.write_wrap_line(row + 1, "Explicit: Yes") - } else { - self.write_wrap_line(row + 1, "Explicit: No") - }; - self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); - row = new_row; - } + // row += 1; // blank line - row += 1; // blank line + // // published date + // if let Some(date) = details.pubdate { + // let new_row = self.write_wrap_line( + // row + 1, + // &format!("Published: {}", date.format("%B %-d, %Y")), + // ); + // self.change_attr(row + 1, 0, 10, pancurses::A_UNDERLINE, ColorType::Normal); + // row = new_row; + // } - // description - match details.description { - Some(desc) => { - self.window.attron(Attribute::Bold); - row = self.write_wrap_line(row + 1, "Description:"); - self.window.attroff(Attribute::Bold); - let _row = self.write_wrap_line(row + 1, &desc); - } - None => { - let _row = self.write_wrap_line(row + 1, "No description."); - } - } + // // duration + // if let Some(dur) = details.duration { + // let new_row = self.write_wrap_line(row + 1, &format!("Duration: {}", dur)); + // self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); + // row = new_row; + // } + + // // explicit + // if let Some(exp) = details.explicit { + // let new_row = if exp { + // self.write_wrap_line(row + 1, "Explicit: Yes") + // } else { + // self.write_wrap_line(row + 1, "Explicit: No") + // }; + // self.change_attr(row + 1, 0, 9, pancurses::A_UNDERLINE, ColorType::Normal); + // row = new_row; + // } + + // row += 1; // blank line + + // // description + // match details.description { + // Some(desc) => { + // self.window.attron(Attribute::Bold); + // row = self.write_wrap_line(row + 1, "Description:"); + // self.window.attroff(Attribute::Bold); + // let _row = self.write_wrap_line(row + 1, &desc); + // } + // None => { + // let _row = self.write_wrap_line(row + 1, "No description."); + // } + // } } /// Changes the attributes (text style and color) for a line of /// text. pub fn change_attr( &self, - y: i32, - x: i32, - nchars: i32, + y: i16, + x: i16, + nchars: u16, attr: pancurses::chtype, color: ColorType, ) { - self.window - .mvchgat(self.abs_y(y), self.abs_x(x), nchars, attr, color as i16); + // self.window + // .mvchgat(self.abs_y(y), self.abs_x(x), nchars, attr, color as i16); } /// Updates window size - pub fn resize(&mut self, n_row: i32, n_col: i32, start_y: i32, start_x: i32) { - self.n_row = n_row; - self.n_col = n_col; - - // apparently pancurses does not implement `wresize()` - // from ncurses, so instead we create an entirely new - // window every time the terminal is resized...not ideal, - // but c'est la vie - let oldwin = std::mem::replace( - &mut self.window, - pancurses::newwin(n_row, n_col, start_y, start_x), - ); - oldwin.delwin(); + pub fn resize(&mut self, n_row: u16, n_col: u16, start_y: u16, start_x: u16) { + // self.n_row = n_row; + // self.n_col = n_col; + + // // apparently pancurses does not implement `wresize()` + // // from ncurses, so instead we create an entirely new + // // window every time the terminal is resized...not ideal, + // // but c'est la vie + // let oldwin = std::mem::replace( + // &mut self.window, + // pancurses::newwin(n_row, n_col, start_y, start_x), + // ); + // oldwin.delwin(); } /// Returns the effective number of rows (accounting for borders /// and margins). - pub fn get_rows(&self) -> i32 { + pub fn get_rows(&self) -> u16 { return self.n_row - 2; // border on top and bottom } /// Returns the effective number of columns (accounting for /// borders and margins). - pub fn get_cols(&self) -> i32 { + pub fn get_cols(&self) -> u16 { return self.n_col - 5; // 2 for border, 2 for margins, and 1 // extra for some reason... } - /// Calculates the y-value relative to the window rather than to the - /// panel (i.e., taking into account borders and margins). - fn abs_y(&self, y: i32) -> i32 { - return y + 1; + /// Calculates the y-value relative to the terminal rather than to + /// the panel (i.e., taking into account borders and margins). + fn abs_y(&self, y: i16) -> u16 { + return (y + 1) + .try_into() + .expect("Can't convert signed integer to unsigned"); } - /// Calculates the x-value relative to the window rather than to the - /// panel (i.e., taking into account borders and margins). - fn abs_x(&self, x: i32) -> i32 { - return x + 2; + /// Calculates the x-value relative to the terminal rather than to + /// the panel (i.e., taking into account borders and margins). + fn abs_x(&self, x: i16) -> u16 { + return (x + self.start_x as i16 + 2) + .try_into() + .expect("Can't convert signed integer to unsigned"); } }