<commit_before>// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::io::buffered::BufferedWriter;

struct DummyWriter;
impl Writer for DummyWriter {
    fn write(&mut self, _: &[u8]) {}
}

static ITER: int = 50;
static LIMIT: f64 = 2.0;

fn main() {
    let args = std::os::args();
    let (w, mut out) = if args.len() < 2 {
        println("Test mode: do not dump the image because it's not utf8,"
                + " which interferes with the test runner.");
        (1000, ~DummyWriter as ~Writer)
    } else {
        (from_str(args[1]).unwrap(),
         ~BufferedWriter::new(std::io::stdout()) as ~Writer)
    };
    let h = w;
    let mut byte_acc = 0u8;
    let mut bit_num = 0;

    writeln!(out, "P4\n{} {}", w, h);

    for y in range(0, h) {
        let y = y as f64;
        for x in range(0, w) {
            let mut z_r = 0f64;
            let mut z_i = 0f64;
            let mut t_r = 0f64;
            let mut t_i = 0f64;
            let c_r = 2.0 * (x as f64) / (w as f64) - 1.5;
            let c_i = 2.0 * (y as f64) / (h as f64) - 1.0;

            for _ in range(0, ITER) {
                if t_r + t_i > LIMIT * LIMIT {
                    break;
                }

                z_i = 2.0 * z_r * z_i + c_i;
                z_r = t_r - t_i + c_r;
                t_r = z_r * z_r;
                t_i = z_i * z_i;
            }

            byte_acc <<= 1;
            if t_r + t_i <= LIMIT * LIMIT {
                byte_acc |= 1;
            }

            bit_num += 1;

            if bit_num == 8 {
                out.write_u8(byte_acc);
                byte_acc = 0;
                bit_num = 0;
            } else if x == w - 1 {
                byte_acc <<= 8 - w % 8;
                out.write_u8(byte_acc);
                byte_acc = 0;
                bit_num = 0;
            }
        }
    }

    out.flush();
}
<commit_msg>Prettier long string breaking.<commit_after>// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::io::buffered::BufferedWriter;

struct DummyWriter;
impl Writer for DummyWriter {
    fn write(&mut self, _: &[u8]) {}
}

static ITER: int = 50;
static LIMIT: f64 = 2.0;

fn main() {
    let args = std::os::args();
    let (w, mut out) = if args.len() < 2 {
        println("Test mode: do not dump the image because it's not utf8, \
                which interferes with the test runner.");
        (1000, ~DummyWriter as ~Writer)
    } else {
        (from_str(args[1]).unwrap(),
         ~BufferedWriter::new(std::io::stdout()) as ~Writer)
    };
    let h = w;
    let mut byte_acc = 0u8;
    let mut bit_num = 0;

    writeln!(out, "P4\n{} {}", w, h);

    for y in range(0, h) {
        let y = y as f64;
        for x in range(0, w) {
            let mut z_r = 0f64;
            let mut z_i = 0f64;
            let mut t_r = 0f64;
            let mut t_i = 0f64;
            let c_r = 2.0 * (x as f64) / (w as f64) - 1.5;
            let c_i = 2.0 * (y as f64) / (h as f64) - 1.0;

            for _ in range(0, ITER) {
                if t_r + t_i > LIMIT * LIMIT {
                    break;
                }

                z_i = 2.0 * z_r * z_i + c_i;
                z_r = t_r - t_i + c_r;
                t_r = z_r * z_r;
                t_i = z_i * z_i;
            }

            byte_acc <<= 1;
            if t_r + t_i <= LIMIT * LIMIT {
                byte_acc |= 1;
            }

            bit_num += 1;

            if bit_num == 8 {
                out.write_u8(byte_acc);
                byte_acc = 0;
                bit_num = 0;
            } else if x == w - 1 {
                byte_acc <<= 8 - w % 8;
                out.write_u8(byte_acc);
                byte_acc = 0;
                bit_num = 0;
            }
        }
    }

    out.flush();
}
<|endoftext|>


<commit_before>use std::collections::HashSet;

pub fn anagrams_for<'a>(word: &str, possible_anagrams: &[&str]) -> HashSet<&'a str> {
    unimplemented!(
        "For the '{}' word find anagrams among the following words: {:?}",
        word,
        possible_anagrams
    );
}
<commit_msg>Pass first test<commit_after>use std::collections::HashSet;

pub fn anagrams_for<'a>(word: &str, possible_anagrams: &'a [&str]) -> HashSet<&'a str> {
    let mut anagrams: HashSet<&'a str> = HashSet::new();

    for possible in possible_anagrams {
        if is_anagram(word, possible) {
            anagrams.insert(possible);
        }
    }
    anagrams
}

fn is_anagram(word: &str, possible: &str) -> bool {
    if word == possible {
        return false;
    }

    return sorted_lowercase_chars(word) == sorted_lowercase_chars(possible);
}

fn sorted_lowercase_chars(word: &str) -> Vec<char> {
    let mut word_chars = word.to_lowercase().chars().collect::<Vec<char>>();
    word_chars.sort_unstable();

    return word_chars;
}
<|endoftext|>


<commit_before>use term_input::Key;
use termbox_simple::Termbox;

use std::convert::From;

use time::{self, Tm};

use crate::config::Colors;
use crate::exit_dialogue::ExitDialogue;
use crate::input_area::InputArea;
use crate::msg_area::line::SegStyle;
use crate::msg_area::{Layout, MsgArea};
use crate::trie::Trie;
use crate::widget::WidgetRet;

/// A messaging screen is just a text field to type messages and msg area to
/// show incoming/sent messages.
pub(crate) struct MessagingUI {
    /// Incoming and sent messages appear
    msg_area: MsgArea,

    // exit_dialogue handles input when available.
    // two fields (instead of an enum etc.) to avoid borrowchk problems
    input_field: InputArea,
    exit_dialogue: Option<ExitDialogue>,

    width: i32,
    height: i32,

    // Option to disable status messages ( join/part )
    show_status: bool,

    // All nicks in the channel. Need to keep this up-to-date to be able to
    // properly highlight mentions.
    nicks: Trie,

    last_activity_line: Option<ActivityLine>,
    last_activity_ts: Option<Timestamp>,
}

/// Like `time::Tm`, but we only care about hour and minute parts.
#[derive(PartialEq, Eq, Clone, Copy)]
pub(crate) struct Timestamp {
    hour: i32,
    min: i32,
}

impl Timestamp {
    /// The width of the timestamp plus a space
    pub(crate) const WIDTH: usize = 6;
    fn stamp(self, msg_area: &mut MsgArea) {
        msg_area.add_text(
            &format!("{:02}:{:02} ", self.hour, self.min),
            SegStyle::Timestamp,
        );
    }

    /// Inserts a blank space that is the size of a timestamp
    fn blank(msg_area: &mut MsgArea) {
        msg_area.add_text(
            &format!("{:^width$}", "", width = Timestamp::WIDTH),
            SegStyle::Timestamp,
        );
    }
}

impl From<Tm> for Timestamp {
    fn from(tm: Tm) -> Timestamp {
        Timestamp {
            hour: tm.tm_hour,
            min: tm.tm_min,
        }
    }
}

/// An activity line is just a line that we update on joins / leaves /
/// disconnects. We group activities that happen in the same minute to avoid
/// redundantly showing lines.
struct ActivityLine {
    ts: Timestamp,
    line_idx: usize,
}

impl MessagingUI {
    pub(crate) fn new(
        width: i32,
        height: i32,
        status: bool,
        scrollback: usize,
        msg_layout: Layout,
    ) -> MessagingUI {
        MessagingUI {
            msg_area: MsgArea::new(width, height - 1, scrollback, msg_layout),
            input_field: InputArea::new(width, get_input_field_max_height(height)),
            exit_dialogue: None,
            width,
            height,
            show_status: status,
            nicks: Trie::new(),
            last_activity_line: None,
            last_activity_ts: None,
        }
    }

    pub(crate) fn set_nick(&mut self, nick: String) {
        let nick_color = self.get_nick_color(&nick);
        self.input_field.set_nick(nick, nick_color);
        // update text field size
        let w = self.width;
        let h = self.height;
        self.resize(w, h);
    }

    pub(crate) fn get_nick(&self) -> Option<String> {
        self.input_field.get_nick()
    }

    pub(crate) fn draw(&mut self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
        match &self.exit_dialogue {
            Some(exit_dialogue) => {
                exit_dialogue.draw(tb, colors, pos_x, self.height - 1);
            }
            None => {
                // Draw InputArea first because it can trigger a resize of MsgArea
                self.input_field
                    .draw(tb, colors, pos_x, pos_y, self.height, &mut self.msg_area);
            }
        }
        self.msg_area.draw(tb, colors, pos_x, pos_y);
    }

    pub(crate) fn keypressed(&mut self, key: Key) -> WidgetRet {
        match key {
            Key::Ctrl('c') => {
                self.toggle_exit_dialogue();
                WidgetRet::KeyHandled
            }

            Key::Ctrl('u') | Key::PageUp => {
                self.msg_area.page_up();
                WidgetRet::KeyHandled
            }

            Key::Ctrl('d') | Key::PageDown => {
                self.msg_area.page_down();
                WidgetRet::KeyHandled
            }

            Key::ShiftUp => {
                self.msg_area.scroll_up();
                WidgetRet::KeyHandled
            }

            Key::ShiftDown => {
                self.msg_area.scroll_down();
                WidgetRet::KeyHandled
            }

            Key::Tab => {
                if self.exit_dialogue.is_none() {
                    self.input_field.autocomplete(&self.nicks);
                }
                WidgetRet::KeyHandled
            }

            Key::Home => {
                self.msg_area.scroll_top();
                WidgetRet::KeyHandled
            }

            Key::End => {
                self.msg_area.scroll_bottom();
                WidgetRet::KeyHandled
            }

            key => {
                let ret = {
                    if let Some(exit_dialogue) = self.exit_dialogue.as_ref() {
                        exit_dialogue.keypressed(key)
                    } else {
                        self.input_field.keypressed(key)
                    }
                };

                if let WidgetRet::Remove = ret {
                    self.exit_dialogue = None;
                    WidgetRet::KeyHandled
                } else {
                    ret
                }
            }
        }
    }

    pub(crate) fn resize(&mut self, width: i32, height: i32) {
        self.width = width;
        self.height = height;

        self.input_field
            .resize(width, get_input_field_max_height(height));
        // msg_area should resize based on input_field's rendered height
        let msg_area_height = height - self.input_field.get_height(width);
        self.msg_area.resize(width, msg_area_height);

        // We don't show the nick in exit dialogue, so it has the full width
        for exit_dialogue in &mut self.exit_dialogue {
            exit_dialogue.resize(width);
        }
    }

    /// Get contents of the input field and cursor location and clear it.
    pub(crate) fn flush_input_field(&mut self) -> (String, i32) {
        self.input_field.flush()
    }

    /// Add a line to input field history.
    pub(crate) fn add_input_field_history(&mut self, str: &str) {
        self.input_field.add_history(str)
    }

    /// Set input field contents.
    pub(crate) fn set_input_field(&mut self, str: &str) {
        self.input_field.set(str)
    }

    /// Set cursor location in the input field.
    pub(crate) fn set_cursor(&mut self, cursor: i32) {
        self.input_field.set_cursor(cursor);
    }

    fn toggle_exit_dialogue(&mut self) {
        let exit_dialogue = ::std::mem::replace(&mut self.exit_dialogue, None);
        if exit_dialogue.is_none() {
            // We don't show the nick in exit dialogue, so it has the full width
            self.exit_dialogue = Some(ExitDialogue::new(self.width));
        }
    }
}

/// Calculation for input field's maximum height
fn get_input_field_max_height(window_height: i32) -> i32 {
    window_height / 2
}

////////////////////////////////////////////////////////////////////////////////
// Adding new messages

impl MessagingUI {
    fn add_timestamp(&mut self, ts: Timestamp) {
        if let Some(ts_) = self.last_activity_ts {
            if ts_ != ts {
                ts.stamp(&mut self.msg_area);
            } else if matches!(self.msg_area.layout(), Layout::Aligned { .. }) {
                Timestamp::blank(&mut self.msg_area)
            }
        } else {
            ts.stamp(&mut self.msg_area);
        }
        self.last_activity_ts = Some(ts);
    }

    pub(crate) fn show_topic(&mut self, topic: &str, ts: Timestamp) {
        self.add_timestamp(ts);

        self.msg_area.add_text(topic, SegStyle::Topic);

        self.msg_area.flush_line();
    }

    pub(crate) fn add_client_err_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::ErrMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn add_client_notify_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::Faded);
        self.msg_area.flush_line();
        self.reset_activity_line();
    }

    pub(crate) fn add_client_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::UserMsg);
        self.msg_area.flush_line();
        self.reset_activity_line();
    }

    pub(crate) fn add_privmsg(
        &mut self,
        sender: &str,
        msg: &str,
        ts: Timestamp,
        highlight: bool,
        is_action: bool,
    ) {
        // HACK: Some servers (bridges) don't send RPL_NAMREPLY and JOIN/PART messages but we still
        // want to support tab completion on those servers, so when we see a message from someone
        // we add the user to the nick list so that tab completion will complete their nick. See
        // #253 for details.
        self.nicks.insert(sender);

        self.reset_activity_line();
        self.add_timestamp(ts);

        let nick_color = self.get_nick_color(sender);
        let nick_col_style = SegStyle::NickColor(nick_color);

        // actions are /me msgs so they don't show the nick in the nick column, but in the msg
        let layout = self.msg_area.layout();
        let format_nick = |s: &str| -> String {
            if let Layout::Aligned { max_nick_len, .. } = layout {
                let mut aligned = format!("{:>padding$.padding$}", s, padding = max_nick_len);
                if s.len() > max_nick_len {
                    aligned.pop();
                    aligned.push('…');
                }
                aligned
            } else {
                s.to_string()
            }
        };
        if is_action {
            self.msg_area
                .add_text(&format_nick("**"), SegStyle::UserMsg);
            // separator between nick and msg
            self.msg_area.add_text("  ", SegStyle::Faded);
            self.msg_area.add_text(sender, nick_col_style);
            // a space replacing the usual ':'
            self.msg_area.add_text(" ", SegStyle::UserMsg);
        } else {
            self.msg_area.add_text(&format_nick(sender), nick_col_style);
            // separator between nick and msg
            self.msg_area.add_text(": ", SegStyle::Faded);
        }

        let msg_style = if highlight {
            SegStyle::Highlight
        } else {
            SegStyle::UserMsg
        };

        self.msg_area.add_text(msg, msg_style);
        self.msg_area.set_current_line_alignment();
        self.msg_area.flush_line();
    }

    pub(crate) fn add_msg(&mut self, msg: &str, ts: Timestamp) {
        self.reset_activity_line();

        self.add_timestamp(ts);
        self.msg_area.add_text(msg, SegStyle::UserMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn add_err_msg(&mut self, msg: &str, ts: Timestamp) {
        self.reset_activity_line();

        self.add_timestamp(ts);
        self.msg_area.add_text(msg, SegStyle::ErrMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn clear(&mut self) {
        self.msg_area.clear();
    }

    fn get_nick_color(&self, sender: &str) -> usize {
        // Anything works as long as it's fast
        let mut hash: usize = 5381;
        for c in sender.chars() {
            hash = hash.wrapping_mul(33).wrapping_add(c as usize);
        }
        hash
    }
}

////////////////////////////////////////////////////////////////////////////////
// Keeping nick list up-to-date

impl MessagingUI {
    pub(crate) fn clear_nicks(&mut self) {
        self.nicks.clear();
    }

    pub(crate) fn join(&mut self, nick: &str, ts: Option<Timestamp>) {
        if self.show_status {
            if let Some(ts) = ts {
                let line_idx = self.get_activity_line_idx(ts);
                self.msg_area.modify_line(line_idx, |line| {
                    line.add_char('+', SegStyle::Join);
                    line.add_text(nick, SegStyle::Faded);
                });
            }
        }

        self.nicks.insert(nick);
    }

    pub(crate) fn part(&mut self, nick: &str, ts: Option<Timestamp>) {
        self.nicks.remove(nick);

        if self.show_status {
            if let Some(ts) = ts {
                let line_idx = self.get_activity_line_idx(ts);
                self.msg_area.modify_line(line_idx, |line| {
                    line.add_char('-', SegStyle::Part);
                    line.add_text(nick, SegStyle::Faded);
                });
            }
        }
    }

    /// `state` == `None` means toggle
    /// `state` == `Some(state)` means set it to `state`
    pub(crate) fn set_or_toggle_ignore(&mut self, state: Option<bool>) {
        self.show_status = state.unwrap_or(!self.show_status);
        if self.show_status {
            self.add_client_notify_msg("Ignore disabled");
        } else {
            self.add_client_notify_msg("Ignore enabled");
        }
    }

    pub(crate) fn is_showing_status(&self) -> bool {
        self.show_status
    }

    pub(crate) fn nick(&mut self, old_nick: &str, new_nick: &str, ts: Timestamp) {
        self.nicks.remove(old_nick);
        self.nicks.insert(new_nick);

        let line_idx = self.get_activity_line_idx(ts);
        self.msg_area.modify_line(line_idx, |line| {
            line.add_text(old_nick, SegStyle::Faded);
            line.add_char('>', SegStyle::Nick);
            line.add_text(new_nick, SegStyle::Faded);
        });
    }

    fn reset_activity_line(&mut self) {
        self.last_activity_line = None;
    }

    fn get_activity_line_idx(&mut self, ts: Timestamp) -> usize {
        match self.last_activity_line {
            Some(ref l) if l.ts == ts => {
                let line_idx = l.line_idx;
                // FIXME: It's a bit hacky to add a space in this function which from the name
                // looks like a getter.
                // The idea is that we want to add a space *before* adding new stuff, not *after*,
                // to avoid adding redundant spaces. The test `small_screen_1` breaks if we don't
                // get this right.
                self.msg_area
                    .modify_line(line_idx, |line| line.add_char(' ', SegStyle::UserMsg));
                line_idx
            }
            _ => {
                self.add_timestamp(ts);
                if let Layout::Aligned {
                    max_nick_len,
                    msg_nick_sep_len,
                    ..
                } = self.msg_area.layout()
                {
                    self.msg_area.add_text(
                        &format!("{:^max$}", "", max = max_nick_len + msg_nick_sep_len),
                        SegStyle::UserMsg,
                    )
                }
                self.msg_area.set_current_line_alignment();
                let line_idx = self.msg_area.flush_line();
                self.last_activity_line = Some(ActivityLine { ts, line_idx });
                line_idx
            }
        }
    }
}
<commit_msg>Aligned layout: don't allocate strings for whitespace<commit_after>use term_input::Key;
use termbox_simple::Termbox;

use std::convert::From;

use time::{self, Tm};

use crate::config::Colors;
use crate::exit_dialogue::ExitDialogue;
use crate::input_area::InputArea;
use crate::msg_area::line::SegStyle;
use crate::msg_area::{Layout, MsgArea};
use crate::trie::Trie;
use crate::widget::WidgetRet;

/// A messaging screen is just a text field to type messages and msg area to
/// show incoming/sent messages.
pub(crate) struct MessagingUI {
    /// Incoming and sent messages appear
    msg_area: MsgArea,

    // exit_dialogue handles input when available.
    // two fields (instead of an enum etc.) to avoid borrowchk problems
    input_field: InputArea,
    exit_dialogue: Option<ExitDialogue>,

    width: i32,
    height: i32,

    // Option to disable status messages ( join/part )
    show_status: bool,

    // All nicks in the channel. Need to keep this up-to-date to be able to
    // properly highlight mentions.
    nicks: Trie,

    last_activity_line: Option<ActivityLine>,
    last_activity_ts: Option<Timestamp>,
}

/// Like `time::Tm`, but we only care about hour and minute parts.
#[derive(PartialEq, Eq, Clone, Copy)]
pub(crate) struct Timestamp {
    hour: i32,
    min: i32,
}

// 80 characters. TODO: We need to make sure we don't need more whitespace than that. We should
// probably add an upper bound to max_nick_length config field?
static WHITESPACE: &str =
    "                                                                                ";

impl Timestamp {
    /// The width of the timestamp plus a space
    pub(crate) const WIDTH: usize = 6;
    fn stamp(self, msg_area: &mut MsgArea) {
        msg_area.add_text(
            &format!("{:02}:{:02} ", self.hour, self.min),
            SegStyle::Timestamp,
        );
    }

    /// Inserts spaces for a timestamp slot. Used in aligned layout.
    fn blank(msg_area: &mut MsgArea) {
        msg_area.add_text(&WHITESPACE[..Timestamp::WIDTH], SegStyle::Timestamp);
    }
}

impl From<Tm> for Timestamp {
    fn from(tm: Tm) -> Timestamp {
        Timestamp {
            hour: tm.tm_hour,
            min: tm.tm_min,
        }
    }
}

/// An activity line is just a line that we update on joins / leaves /
/// disconnects. We group activities that happen in the same minute to avoid
/// redundantly showing lines.
struct ActivityLine {
    ts: Timestamp,
    line_idx: usize,
}

impl MessagingUI {
    pub(crate) fn new(
        width: i32,
        height: i32,
        status: bool,
        scrollback: usize,
        msg_layout: Layout,
    ) -> MessagingUI {
        MessagingUI {
            msg_area: MsgArea::new(width, height - 1, scrollback, msg_layout),
            input_field: InputArea::new(width, get_input_field_max_height(height)),
            exit_dialogue: None,
            width,
            height,
            show_status: status,
            nicks: Trie::new(),
            last_activity_line: None,
            last_activity_ts: None,
        }
    }

    pub(crate) fn set_nick(&mut self, nick: String) {
        let nick_color = self.get_nick_color(&nick);
        self.input_field.set_nick(nick, nick_color);
        // update text field size
        let w = self.width;
        let h = self.height;
        self.resize(w, h);
    }

    pub(crate) fn get_nick(&self) -> Option<String> {
        self.input_field.get_nick()
    }

    pub(crate) fn draw(&mut self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
        match &self.exit_dialogue {
            Some(exit_dialogue) => {
                exit_dialogue.draw(tb, colors, pos_x, self.height - 1);
            }
            None => {
                // Draw InputArea first because it can trigger a resize of MsgArea
                self.input_field
                    .draw(tb, colors, pos_x, pos_y, self.height, &mut self.msg_area);
            }
        }
        self.msg_area.draw(tb, colors, pos_x, pos_y);
    }

    pub(crate) fn keypressed(&mut self, key: Key) -> WidgetRet {
        match key {
            Key::Ctrl('c') => {
                self.toggle_exit_dialogue();
                WidgetRet::KeyHandled
            }

            Key::Ctrl('u') | Key::PageUp => {
                self.msg_area.page_up();
                WidgetRet::KeyHandled
            }

            Key::Ctrl('d') | Key::PageDown => {
                self.msg_area.page_down();
                WidgetRet::KeyHandled
            }

            Key::ShiftUp => {
                self.msg_area.scroll_up();
                WidgetRet::KeyHandled
            }

            Key::ShiftDown => {
                self.msg_area.scroll_down();
                WidgetRet::KeyHandled
            }

            Key::Tab => {
                if self.exit_dialogue.is_none() {
                    self.input_field.autocomplete(&self.nicks);
                }
                WidgetRet::KeyHandled
            }

            Key::Home => {
                self.msg_area.scroll_top();
                WidgetRet::KeyHandled
            }

            Key::End => {
                self.msg_area.scroll_bottom();
                WidgetRet::KeyHandled
            }

            key => {
                let ret = {
                    if let Some(exit_dialogue) = self.exit_dialogue.as_ref() {
                        exit_dialogue.keypressed(key)
                    } else {
                        self.input_field.keypressed(key)
                    }
                };

                if let WidgetRet::Remove = ret {
                    self.exit_dialogue = None;
                    WidgetRet::KeyHandled
                } else {
                    ret
                }
            }
        }
    }

    pub(crate) fn resize(&mut self, width: i32, height: i32) {
        self.width = width;
        self.height = height;

        self.input_field
            .resize(width, get_input_field_max_height(height));
        // msg_area should resize based on input_field's rendered height
        let msg_area_height = height - self.input_field.get_height(width);
        self.msg_area.resize(width, msg_area_height);

        // We don't show the nick in exit dialogue, so it has the full width
        for exit_dialogue in &mut self.exit_dialogue {
            exit_dialogue.resize(width);
        }
    }

    /// Get contents of the input field and cursor location and clear it.
    pub(crate) fn flush_input_field(&mut self) -> (String, i32) {
        self.input_field.flush()
    }

    /// Add a line to input field history.
    pub(crate) fn add_input_field_history(&mut self, str: &str) {
        self.input_field.add_history(str)
    }

    /// Set input field contents.
    pub(crate) fn set_input_field(&mut self, str: &str) {
        self.input_field.set(str)
    }

    /// Set cursor location in the input field.
    pub(crate) fn set_cursor(&mut self, cursor: i32) {
        self.input_field.set_cursor(cursor);
    }

    fn toggle_exit_dialogue(&mut self) {
        let exit_dialogue = ::std::mem::replace(&mut self.exit_dialogue, None);
        if exit_dialogue.is_none() {
            // We don't show the nick in exit dialogue, so it has the full width
            self.exit_dialogue = Some(ExitDialogue::new(self.width));
        }
    }
}

/// Calculation for input field's maximum height
fn get_input_field_max_height(window_height: i32) -> i32 {
    window_height / 2
}

////////////////////////////////////////////////////////////////////////////////
// Adding new messages

impl MessagingUI {
    fn add_timestamp(&mut self, ts: Timestamp) {
        if let Some(ts_) = self.last_activity_ts {
            if ts_ != ts {
                ts.stamp(&mut self.msg_area);
            } else if matches!(self.msg_area.layout(), Layout::Aligned { .. }) {
                Timestamp::blank(&mut self.msg_area)
            }
        } else {
            ts.stamp(&mut self.msg_area);
        }
        self.last_activity_ts = Some(ts);
    }

    pub(crate) fn show_topic(&mut self, topic: &str, ts: Timestamp) {
        self.add_timestamp(ts);

        self.msg_area.add_text(topic, SegStyle::Topic);

        self.msg_area.flush_line();
    }

    pub(crate) fn add_client_err_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::ErrMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn add_client_notify_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::Faded);
        self.msg_area.flush_line();
        self.reset_activity_line();
    }

    pub(crate) fn add_client_msg(&mut self, msg: &str) {
        self.reset_activity_line();

        self.msg_area.add_text(msg, SegStyle::UserMsg);
        self.msg_area.flush_line();
        self.reset_activity_line();
    }

    pub(crate) fn add_privmsg(
        &mut self,
        sender: &str,
        msg: &str,
        ts: Timestamp,
        highlight: bool,
        is_action: bool,
    ) {
        // HACK: Some servers (bridges) don't send RPL_NAMREPLY and JOIN/PART messages but we still
        // want to support tab completion on those servers, so when we see a message from someone
        // we add the user to the nick list so that tab completion will complete their nick. See
        // #253 for details.
        self.nicks.insert(sender);

        self.reset_activity_line();
        self.add_timestamp(ts);

        let nick_color = self.get_nick_color(sender);
        let nick_col_style = SegStyle::NickColor(nick_color);

        // actions are /me msgs so they don't show the nick in the nick column, but in the msg
        let layout = self.msg_area.layout();
        let format_nick = |s: &str| -> String {
            if let Layout::Aligned { max_nick_len, .. } = layout {
                let mut aligned = format!("{:>padding$.padding$}", s, padding = max_nick_len);
                if s.len() > max_nick_len {
                    aligned.pop();
                    aligned.push('…');
                }
                aligned
            } else {
                s.to_string()
            }
        };
        if is_action {
            self.msg_area
                .add_text(&format_nick("**"), SegStyle::UserMsg);
            // separator between nick and msg
            self.msg_area.add_text("  ", SegStyle::Faded);
            self.msg_area.add_text(sender, nick_col_style);
            // a space replacing the usual ':'
            self.msg_area.add_text(" ", SegStyle::UserMsg);
        } else {
            self.msg_area.add_text(&format_nick(sender), nick_col_style);
            // separator between nick and msg
            self.msg_area.add_text(": ", SegStyle::Faded);
        }

        let msg_style = if highlight {
            SegStyle::Highlight
        } else {
            SegStyle::UserMsg
        };

        self.msg_area.add_text(msg, msg_style);
        self.msg_area.set_current_line_alignment();
        self.msg_area.flush_line();
    }

    pub(crate) fn add_msg(&mut self, msg: &str, ts: Timestamp) {
        self.reset_activity_line();

        self.add_timestamp(ts);
        self.msg_area.add_text(msg, SegStyle::UserMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn add_err_msg(&mut self, msg: &str, ts: Timestamp) {
        self.reset_activity_line();

        self.add_timestamp(ts);
        self.msg_area.add_text(msg, SegStyle::ErrMsg);
        self.msg_area.flush_line();
    }

    pub(crate) fn clear(&mut self) {
        self.msg_area.clear();
    }

    fn get_nick_color(&self, sender: &str) -> usize {
        // Anything works as long as it's fast
        let mut hash: usize = 5381;
        for c in sender.chars() {
            hash = hash.wrapping_mul(33).wrapping_add(c as usize);
        }
        hash
    }
}

////////////////////////////////////////////////////////////////////////////////
// Keeping nick list up-to-date

impl MessagingUI {
    pub(crate) fn clear_nicks(&mut self) {
        self.nicks.clear();
    }

    pub(crate) fn join(&mut self, nick: &str, ts: Option<Timestamp>) {
        if self.show_status {
            if let Some(ts) = ts {
                let line_idx = self.get_activity_line_idx(ts);
                self.msg_area.modify_line(line_idx, |line| {
                    line.add_char('+', SegStyle::Join);
                    line.add_text(nick, SegStyle::Faded);
                });
            }
        }

        self.nicks.insert(nick);
    }

    pub(crate) fn part(&mut self, nick: &str, ts: Option<Timestamp>) {
        self.nicks.remove(nick);

        if self.show_status {
            if let Some(ts) = ts {
                let line_idx = self.get_activity_line_idx(ts);
                self.msg_area.modify_line(line_idx, |line| {
                    line.add_char('-', SegStyle::Part);
                    line.add_text(nick, SegStyle::Faded);
                });
            }
        }
    }

    /// `state` == `None` means toggle
    /// `state` == `Some(state)` means set it to `state`
    pub(crate) fn set_or_toggle_ignore(&mut self, state: Option<bool>) {
        self.show_status = state.unwrap_or(!self.show_status);
        if self.show_status {
            self.add_client_notify_msg("Ignore disabled");
        } else {
            self.add_client_notify_msg("Ignore enabled");
        }
    }

    pub(crate) fn is_showing_status(&self) -> bool {
        self.show_status
    }

    pub(crate) fn nick(&mut self, old_nick: &str, new_nick: &str, ts: Timestamp) {
        self.nicks.remove(old_nick);
        self.nicks.insert(new_nick);

        let line_idx = self.get_activity_line_idx(ts);
        self.msg_area.modify_line(line_idx, |line| {
            line.add_text(old_nick, SegStyle::Faded);
            line.add_char('>', SegStyle::Nick);
            line.add_text(new_nick, SegStyle::Faded);
        });
    }

    fn reset_activity_line(&mut self) {
        self.last_activity_line = None;
    }

    fn get_activity_line_idx(&mut self, ts: Timestamp) -> usize {
        match self.last_activity_line {
            Some(ref l) if l.ts == ts => {
                let line_idx = l.line_idx;
                // FIXME: It's a bit hacky to add a space in this function which from the name
                // looks like a getter.
                // The idea is that we want to add a space *before* adding new stuff, not *after*,
                // to avoid adding redundant spaces. The test `small_screen_1` breaks if we don't
                // get this right.
                self.msg_area
                    .modify_line(line_idx, |line| line.add_char(' ', SegStyle::UserMsg));
                line_idx
            }
            _ => {
                self.add_timestamp(ts);
                if let Layout::Aligned {
                    max_nick_len,
                    msg_nick_sep_len,
                    ..
                } = self.msg_area.layout()
                {
                    self.msg_area.add_text(
                        &WHITESPACE[..max_nick_len + msg_nick_sep_len],
                        SegStyle::UserMsg,
                    )
                }
                self.msg_area.set_current_line_alignment();
                let line_idx = self.msg_area.flush_line();
                self.last_activity_line = Some(ActivityLine { ts, line_idx });
                line_idx
            }
        }
    }
}
<|endoftext|>


<commit_before>pub mod database;

use crate::config::Config;
use crate::err::{self, Result};
use crate::file::SaveDir;
use anime::local::{EpisodeMap, EpisodeMatcher};
use anime::remote::{RemoteService, SeriesInfo, Status};
use chrono::{Local, NaiveDate};
use database::{Database, Deletable, Insertable, Selectable};
use snafu::{ensure, OptionExt, ResultExt};
use std::borrow::Cow;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};

#[derive(Debug)]
pub struct Series {
    pub info: SeriesInfo,
    pub entry: SeriesEntry,
    pub config: SeriesConfig,
    pub episodes: EpisodeMap,
}

impl Series {
    pub fn from_args_and_remote<S, R>(
        args: &clap::ArgMatches,
        nickname: S,
        config: &Config,
        remote: &R,
    ) -> Result<Series>
    where
        S: Into<String>,
        R: RemoteService + ?Sized,
    {
        let nickname = nickname.into();

        // We should process as much local information as possible before sending requests to
        // the remote service to avoid potentially putting unnecessary load on the service should
        // any errors crop up.
        let path = match args.value_of("path") {
            Some(path) => {
                let path = PathBuf::from(path);
                ensure!(path.is_dir(), err::NotADirectory);
                path
            }
            None => detect::best_matching_folder(&nickname, &config.series_dir)?,
        };

        let title = {
            let path_str = path.file_name().context(err::NoDirName)?.to_string_lossy();
            detect::parse_folder_title(path_str).ok_or(err::Error::FolderTitleParse)?
        };

        let matcher = match args.value_of("matcher") {
            Some(pattern) => episode_matcher_with_pattern(pattern)?,
            None => EpisodeMatcher::new(),
        };

        let episodes = EpisodeMap::parse(&path, &matcher)?;

        // Now we can request all of that juicy data from the remote service.
        let info = best_matching_series_info(remote, title)?;
        let entry = SeriesEntry::from_remote(remote, &info)?;

        let config = SeriesConfig {
            id: info.id,
            nickname,
            path,
            episode_matcher: matcher,
            player_args: Vec::new(),
        };

        let series = Self {
            info,
            entry,
            config,
            episodes,
        };

        Ok(series)
    }

    pub fn save(&self, db: &Database) -> Result<()> {
        db.conn()
            .prepare_cached("BEGIN")
            .and_then(|mut query| query.execute(rusqlite::NO_PARAMS))?;

        self.config.insert_into_db(db, self.info.id)?;
        self.info.insert_into_db(db, ())?;
        self.entry.insert_into_db(db, ())?;

        db.conn()
            .prepare_cached("END")
            .and_then(|mut query| query.execute(rusqlite::NO_PARAMS))?;

        Ok(())
    }

    pub fn load<S>(db: &Database, nickname: S) -> Result<Series>
    where
        S: AsRef<str>,
    {
        let config = SeriesConfig::select_from_db(db, nickname.as_ref())?;
        let info = SeriesInfo::select_from_db(db, config.id)?;
        let entry = SeriesEntry::select_from_db(db, config.id)?;

        let episodes = EpisodeMap::parse(&config.path, &config.episode_matcher)?;

        Ok(Self {
            info,
            entry,
            config,
            episodes,
        })
    }

    pub fn delete<S>(db: &Database, nickname: S) -> Result<()>
    where
        S: AsRef<str>,
    {
        // The database is set up to remove all associated series data when we remove its configuration
        SeriesConfig::delete_from_db(db, nickname.as_ref())
    }

    pub fn episode_path(&self, episode: u32) -> Option<PathBuf> {
        let episode_filename = self.episodes.get(&episode)?;
        let mut path = self.config.path.clone();
        path.push(episode_filename);
        path.canonicalize().ok()
    }

    pub fn play_episode_cmd(&self, episode: u32, config: &Config) -> Result<Command> {
        let episode_path = self
            .episode_path(episode)
            .context(err::EpisodeNotFound { episode })?;

        let mut cmd = Command::new(&config.episode.player);
        cmd.arg(episode_path);
        cmd.args(&config.episode.player_args);
        cmd.args(&self.config.player_args);
        cmd.stdout(Stdio::null());
        cmd.stderr(Stdio::null());
        cmd.stdin(Stdio::null());

        Ok(cmd)
    }

    pub fn begin_watching<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        self.entry.sync_from_remote(remote)?;

        let entry = &mut self.entry;
        let last_status = entry.status();

        match last_status {
            Status::Watching | Status::Rewatching => {
                // There is an edge case where all episodes have been watched, but the status
                // is still set to watching / rewatching. Here we just start a rewatch
                if entry.watched_eps() >= self.info.episodes {
                    entry.set_status(Status::Rewatching, config);
                    entry.set_watched_eps(0);

                    if last_status == Status::Rewatching {
                        entry.set_times_rewatched(entry.times_rewatched() + 1);
                    }
                }
            }
            Status::Completed => {
                entry.set_status(Status::Rewatching, config);
                entry.set_watched_eps(0);
            }
            Status::PlanToWatch | Status::OnHold => entry.set_status(Status::Watching, config),
            Status::Dropped => {
                entry.set_status(Status::Watching, config);
                entry.set_watched_eps(0);
            }
        }

        self.entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn episode_completed<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let new_progress = self.entry.watched_eps() + 1;

        if new_progress >= self.info.episodes {
            // The watched episode range is inclusive, so it's fine to bump the watched count
            // if we're at exactly at the last episode
            if new_progress == self.info.episodes {
                self.entry.set_watched_eps(new_progress);
            }

            return self.series_complete(remote, config, db);
        }

        self.entry.set_watched_eps(new_progress);
        self.entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn episode_regressed<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let entry = &mut self.entry;
        entry.set_watched_eps(entry.watched_eps().saturating_sub(1));

        let new_status = match entry.status() {
            Status::Completed if entry.times_rewatched() > 0 => Status::Rewatching,
            Status::Rewatching => Status::Rewatching,
            _ => Status::Watching,
        };

        entry.set_status(new_status, config);
        entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn series_complete<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let entry = &mut self.entry;

        // A rewatch is typically only counted once the series is completed again
        if entry.status() == Status::Rewatching {
            entry.set_times_rewatched(entry.times_rewatched() + 1);
        }

        entry.set_status(Status::Completed, config);
        entry.sync_to_remote(remote)?;
        self.save(db)
    }
}

#[derive(Debug)]
pub struct SeriesEntry {
    entry: anime::remote::SeriesEntry,
    needs_sync: bool,
}

impl SeriesEntry {
    pub fn from_remote<R>(remote: &R, info: &SeriesInfo) -> Result<SeriesEntry>
    where
        R: RemoteService + ?Sized,
    {
        match remote.get_list_entry(info.id)? {
            Some(entry) => Ok(SeriesEntry::from(entry)),
            None => Ok(SeriesEntry::from(info.id)),
        }
    }

    pub fn force_sync_to_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if remote.is_offline() {
            return Ok(());
        }

        remote.update_list_entry(self.inner())?;
        self.needs_sync = false;
        Ok(())
    }

    pub fn sync_to_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if !self.needs_sync {
            return Ok(());
        }

        self.force_sync_to_remote(remote)
    }

    pub fn force_sync_from_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if remote.is_offline() {
            return Ok(());
        }

        *self = match remote.get_list_entry(self.id())? {
            Some(entry) => SeriesEntry::from(entry),
            None => SeriesEntry::from(self.id()),
        };

        Ok(())
    }

    pub fn sync_from_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if self.needs_sync {
            return Ok(());
        }

        self.force_sync_from_remote(remote)
    }

    #[inline(always)]
    pub fn inner(&self) -> &anime::remote::SeriesEntry {
        &self.entry
    }

    #[inline(always)]
    pub fn needs_sync(&self) -> bool {
        self.needs_sync
    }

    #[inline(always)]
    pub fn id(&self) -> u32 {
        self.entry.id
    }

    #[inline(always)]
    pub fn watched_eps(&self) -> u32 {
        self.entry.watched_eps
    }

    #[inline(always)]
    pub fn set_watched_eps(&mut self, watched_eps: u32) {
        self.entry.watched_eps = watched_eps;
        self.needs_sync = true;
    }

    #[inline(always)]
    pub fn score(&self) -> Option<u8> {
        self.entry.score
    }

    #[inline(always)]
    pub fn set_score(&mut self, score: Option<u8>) {
        self.entry.score = score;
        self.needs_sync = true;
    }

    #[inline(always)]
    pub fn status(&self) -> Status {
        self.entry.status
    }

    #[inline(always)]
    pub fn set_status(&mut self, status: Status, config: &Config) {
        match status {
            Status::Watching if self.start_date().is_none() => {
                self.entry.start_date = Some(Local::today().naive_local());
            }
            Status::Rewatching
                if self.start_date().is_none()
                    || (self.status() == Status::Completed && config.reset_dates_on_rewatch) =>
            {
                self.entry.start_date = Some(Local::today().naive_local());
            }
            Status::Completed
                if self.end_date().is_none()
                    || (self.status() == Status::Rewatching && config.reset_dates_on_rewatch) =>
            {
                self.entry.end_date = Some(Local::today().naive_local());
            }
            Status::Dropped if self.end_date().is_none() => {
                self.entry.end_date = Some(Local::today().naive_local());
            }
            _ => (),
        }

        self.entry.status = status;
        self.needs_sync = true;
    }

    #[inline(always)]
    pub fn times_rewatched(&self) -> u32 {
        self.entry.times_rewatched
    }

    #[inline(always)]
    pub fn set_times_rewatched(&mut self, times_rewatched: u32) {
        self.entry.times_rewatched = times_rewatched;
        self.needs_sync = true;
    }

    #[inline(always)]
    pub fn start_date(&self) -> Option<NaiveDate> {
        self.entry.start_date
    }

    #[inline(always)]
    pub fn end_date(&self) -> Option<NaiveDate> {
        self.entry.end_date
    }
}

impl From<anime::remote::SeriesEntry> for SeriesEntry {
    fn from(entry: anime::remote::SeriesEntry) -> SeriesEntry {
        SeriesEntry {
            entry,
            needs_sync: false,
        }
    }
}

impl From<u32> for SeriesEntry {
    fn from(id: u32) -> SeriesEntry {
        let remote_entry = anime::remote::SeriesEntry::new(id);
        SeriesEntry::from(remote_entry)
    }
}

#[derive(Debug)]
pub struct SeriesConfig {
    pub id: u32,
    pub nickname: String,
    pub path: PathBuf,
    pub episode_matcher: EpisodeMatcher,
    pub player_args: Vec<String>,
}

pub struct LastWatched(Option<String>);

impl LastWatched {
    pub fn load() -> Result<Self> {
        let path = Self::validated_path()?;

        if !path.exists() {
            return Ok(Self(None));
        }

        let last_watched = fs::read_to_string(&path).context(err::FileIO { path })?;
        Ok(Self(Some(last_watched)))
    }

    #[inline(always)]
    pub fn get(&self) -> &Option<String> {
        &self.0
    }

    pub fn set<'a, S>(&mut self, nickname: S) -> bool
    where
        S: Into<Cow<'a, str>>,
    {
        let nickname = nickname.into();

        let is_different = self
            .0
            .as_ref()
            .map(|existing| existing != nickname.as_ref())
            .unwrap_or(true);

        if is_different {
            self.0 = Some(nickname.into_owned());
        }

        is_different
    }

    pub fn save(&self) -> Result<()> {
        let contents = match &self.0 {
            Some(contents) => contents,
            None => return Ok(()),
        };

        let path = Self::validated_path()?;
        fs::write(&path, contents).context(err::FileIO { path })
    }

    pub fn validated_path() -> Result<PathBuf> {
        let mut path = SaveDir::LocalData.validated_path()?.to_path_buf();
        path.push("last_watched");
        Ok(path)
    }
}

pub fn best_matching_series_info<R, S>(remote: &R, name: S) -> Result<SeriesInfo>
where
    R: RemoteService + ?Sized,
    S: AsRef<str>,
{
    let name = name.as_ref();

    let mut results = remote.search_info_by_name(name)?;
    let index = detect::best_matching_info(name, results.as_slice())
        .context(err::NoMatchingSeries { name })?;

    let info = results.swap_remove(index);
    Ok(info)
}

pub fn episode_matcher_with_pattern<S>(pattern: S) -> Result<EpisodeMatcher>
where
    S: AsRef<str>,
{
    let pattern = pattern
        .as_ref()
        .replace("{title}", "(?P<title>.+)")
        .replace("{episode}", r"(?P<episode>\d+)");

    match EpisodeMatcher::from_pattern(pattern) {
        Ok(matcher) => Ok(matcher),
        // We want to use a more specific error message than the one the anime library
        // provides
        Err(anime::Error::MissingCustomMatcherGroup { group }) => {
            Err(err::Error::MissingEpisodeMatcherGroup { group })
        }
        Err(err) => Err(err.into()),
    }
}
<commit_msg>anup: use macro to generate getters & setters for SeriesEntry struct<commit_after>pub mod database;

use crate::config::Config;
use crate::err::{self, Result};
use crate::file::SaveDir;
use anime::local::{EpisodeMap, EpisodeMatcher};
use anime::remote::{RemoteService, SeriesInfo, Status};
use chrono::{Local, NaiveDate};
use database::{Database, Deletable, Insertable, Selectable};
use snafu::{ensure, OptionExt, ResultExt};
use std::borrow::Cow;
use std::fs;
use std::path::PathBuf;
use std::process::{Command, Stdio};

#[derive(Debug)]
pub struct Series {
    pub info: SeriesInfo,
    pub entry: SeriesEntry,
    pub config: SeriesConfig,
    pub episodes: EpisodeMap,
}

impl Series {
    pub fn from_args_and_remote<S, R>(
        args: &clap::ArgMatches,
        nickname: S,
        config: &Config,
        remote: &R,
    ) -> Result<Series>
    where
        S: Into<String>,
        R: RemoteService + ?Sized,
    {
        let nickname = nickname.into();

        // We should process as much local information as possible before sending requests to
        // the remote service to avoid potentially putting unnecessary load on the service should
        // any errors crop up.
        let path = match args.value_of("path") {
            Some(path) => {
                let path = PathBuf::from(path);
                ensure!(path.is_dir(), err::NotADirectory);
                path
            }
            None => detect::best_matching_folder(&nickname, &config.series_dir)?,
        };

        let title = {
            let path_str = path.file_name().context(err::NoDirName)?.to_string_lossy();
            detect::parse_folder_title(path_str).ok_or(err::Error::FolderTitleParse)?
        };

        let matcher = match args.value_of("matcher") {
            Some(pattern) => episode_matcher_with_pattern(pattern)?,
            None => EpisodeMatcher::new(),
        };

        let episodes = EpisodeMap::parse(&path, &matcher)?;

        // Now we can request all of that juicy data from the remote service.
        let info = best_matching_series_info(remote, title)?;
        let entry = SeriesEntry::from_remote(remote, &info)?;

        let config = SeriesConfig {
            id: info.id,
            nickname,
            path,
            episode_matcher: matcher,
            player_args: Vec::new(),
        };

        let series = Self {
            info,
            entry,
            config,
            episodes,
        };

        Ok(series)
    }

    pub fn save(&self, db: &Database) -> Result<()> {
        db.conn()
            .prepare_cached("BEGIN")
            .and_then(|mut query| query.execute(rusqlite::NO_PARAMS))?;

        self.config.insert_into_db(db, self.info.id)?;
        self.info.insert_into_db(db, ())?;
        self.entry.insert_into_db(db, ())?;

        db.conn()
            .prepare_cached("END")
            .and_then(|mut query| query.execute(rusqlite::NO_PARAMS))?;

        Ok(())
    }

    pub fn load<S>(db: &Database, nickname: S) -> Result<Series>
    where
        S: AsRef<str>,
    {
        let config = SeriesConfig::select_from_db(db, nickname.as_ref())?;
        let info = SeriesInfo::select_from_db(db, config.id)?;
        let entry = SeriesEntry::select_from_db(db, config.id)?;

        let episodes = EpisodeMap::parse(&config.path, &config.episode_matcher)?;

        Ok(Self {
            info,
            entry,
            config,
            episodes,
        })
    }

    pub fn delete<S>(db: &Database, nickname: S) -> Result<()>
    where
        S: AsRef<str>,
    {
        // The database is set up to remove all associated series data when we remove its configuration
        SeriesConfig::delete_from_db(db, nickname.as_ref())
    }

    pub fn episode_path(&self, episode: u32) -> Option<PathBuf> {
        let episode_filename = self.episodes.get(&episode)?;
        let mut path = self.config.path.clone();
        path.push(episode_filename);
        path.canonicalize().ok()
    }

    pub fn play_episode_cmd(&self, episode: u32, config: &Config) -> Result<Command> {
        let episode_path = self
            .episode_path(episode)
            .context(err::EpisodeNotFound { episode })?;

        let mut cmd = Command::new(&config.episode.player);
        cmd.arg(episode_path);
        cmd.args(&config.episode.player_args);
        cmd.args(&self.config.player_args);
        cmd.stdout(Stdio::null());
        cmd.stderr(Stdio::null());
        cmd.stdin(Stdio::null());

        Ok(cmd)
    }

    pub fn begin_watching<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        self.entry.sync_from_remote(remote)?;

        let entry = &mut self.entry;
        let last_status = entry.status();

        match last_status {
            Status::Watching | Status::Rewatching => {
                // There is an edge case where all episodes have been watched, but the status
                // is still set to watching / rewatching. Here we just start a rewatch
                if entry.watched_eps() >= self.info.episodes {
                    entry.set_status(Status::Rewatching, config);
                    entry.set_watched_eps(0);

                    if last_status == Status::Rewatching {
                        entry.set_times_rewatched(entry.times_rewatched() + 1);
                    }
                }
            }
            Status::Completed => {
                entry.set_status(Status::Rewatching, config);
                entry.set_watched_eps(0);
            }
            Status::PlanToWatch | Status::OnHold => entry.set_status(Status::Watching, config),
            Status::Dropped => {
                entry.set_status(Status::Watching, config);
                entry.set_watched_eps(0);
            }
        }

        self.entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn episode_completed<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let new_progress = self.entry.watched_eps() + 1;

        if new_progress >= self.info.episodes {
            // The watched episode range is inclusive, so it's fine to bump the watched count
            // if we're at exactly at the last episode
            if new_progress == self.info.episodes {
                self.entry.set_watched_eps(new_progress);
            }

            return self.series_complete(remote, config, db);
        }

        self.entry.set_watched_eps(new_progress);
        self.entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn episode_regressed<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let entry = &mut self.entry;
        entry.set_watched_eps(entry.watched_eps().saturating_sub(1));

        let new_status = match entry.status() {
            Status::Completed if entry.times_rewatched() > 0 => Status::Rewatching,
            Status::Rewatching => Status::Rewatching,
            _ => Status::Watching,
        };

        entry.set_status(new_status, config);
        entry.sync_to_remote(remote)?;
        self.save(db)
    }

    pub fn series_complete<R>(&mut self, remote: &R, config: &Config, db: &Database) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        let entry = &mut self.entry;

        // A rewatch is typically only counted once the series is completed again
        if entry.status() == Status::Rewatching {
            entry.set_times_rewatched(entry.times_rewatched() + 1);
        }

        entry.set_status(Status::Completed, config);
        entry.sync_to_remote(remote)?;
        self.save(db)
    }
}

#[derive(Debug)]
pub struct SeriesEntry {
    entry: anime::remote::SeriesEntry,
    needs_sync: bool,
}

impl SeriesEntry {
    pub fn from_remote<R>(remote: &R, info: &SeriesInfo) -> Result<SeriesEntry>
    where
        R: RemoteService + ?Sized,
    {
        match remote.get_list_entry(info.id)? {
            Some(entry) => Ok(SeriesEntry::from(entry)),
            None => Ok(SeriesEntry::from(info.id)),
        }
    }

    pub fn force_sync_to_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if remote.is_offline() {
            return Ok(());
        }

        remote.update_list_entry(self.inner())?;
        self.needs_sync = false;
        Ok(())
    }

    pub fn sync_to_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if !self.needs_sync {
            return Ok(());
        }

        self.force_sync_to_remote(remote)
    }

    pub fn force_sync_from_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if remote.is_offline() {
            return Ok(());
        }

        *self = match remote.get_list_entry(self.id())? {
            Some(entry) => SeriesEntry::from(entry),
            None => SeriesEntry::from(self.id()),
        };

        Ok(())
    }

    pub fn sync_from_remote<R>(&mut self, remote: &R) -> Result<()>
    where
        R: RemoteService + ?Sized,
    {
        if self.needs_sync {
            return Ok(());
        }

        self.force_sync_from_remote(remote)
    }

    #[inline(always)]
    pub fn inner(&self) -> &anime::remote::SeriesEntry {
        &self.entry
    }

    #[inline(always)]
    pub fn needs_sync(&self) -> bool {
        self.needs_sync
    }

    pub fn set_status(&mut self, status: Status, config: &Config) {
        match status {
            Status::Watching if self.start_date().is_none() => {
                self.entry.start_date = Some(Local::today().naive_local());
            }
            Status::Rewatching
                if self.start_date().is_none()
                    || (self.status() == Status::Completed && config.reset_dates_on_rewatch) =>
            {
                self.entry.start_date = Some(Local::today().naive_local());
            }
            Status::Completed
                if self.end_date().is_none()
                    || (self.status() == Status::Rewatching && config.reset_dates_on_rewatch) =>
            {
                self.entry.end_date = Some(Local::today().naive_local());
            }
            Status::Dropped if self.entry.end_date.is_none() => {
                self.entry.end_date = Some(Local::today().naive_local());
            }
            _ => (),
        }

        self.entry.status = status;
        self.needs_sync = true;
    }
}

macro_rules! impl_series_entry_getters_setters {
    ($($field:ident: $field_ty:ty => $setter:tt,)+) => {
        impl SeriesEntry {
            $(
            #[inline(always)]
            pub fn $field(&self) -> $field_ty {
                self.entry.$field
            }

            impl_series_entry_getters_setters!(setter $field, $field_ty, $setter);
            )+
        }
    };

    (setter $field:ident, $field_ty:ty, !) => {};

    (setter $field:ident, $field_ty:ty, $setter:ident) => {
        #[inline(always)]
        pub fn $setter(&mut self, value: $field_ty) {
            self.entry.$field = value;
            self.needs_sync = true;
        }
    }
}

impl_series_entry_getters_setters!(
    id: u32 => !,
    status: Status => !,
    watched_eps: u32 => set_watched_eps,
    score: Option<u8> => set_score,
    times_rewatched: u32 => set_times_rewatched,
    start_date: Option<NaiveDate> => !,
    end_date: Option<NaiveDate> => !,
);

impl From<anime::remote::SeriesEntry> for SeriesEntry {
    fn from(entry: anime::remote::SeriesEntry) -> SeriesEntry {
        SeriesEntry {
            entry,
            needs_sync: false,
        }
    }
}

impl From<u32> for SeriesEntry {
    fn from(id: u32) -> SeriesEntry {
        let remote_entry = anime::remote::SeriesEntry::new(id);
        SeriesEntry::from(remote_entry)
    }
}

#[derive(Debug)]
pub struct SeriesConfig {
    pub id: u32,
    pub nickname: String,
    pub path: PathBuf,
    pub episode_matcher: EpisodeMatcher,
    pub player_args: Vec<String>,
}

pub struct LastWatched(Option<String>);

impl LastWatched {
    pub fn load() -> Result<Self> {
        let path = Self::validated_path()?;

        if !path.exists() {
            return Ok(Self(None));
        }

        let last_watched = fs::read_to_string(&path).context(err::FileIO { path })?;
        Ok(Self(Some(last_watched)))
    }

    #[inline(always)]
    pub fn get(&self) -> &Option<String> {
        &self.0
    }

    pub fn set<'a, S>(&mut self, nickname: S) -> bool
    where
        S: Into<Cow<'a, str>>,
    {
        let nickname = nickname.into();

        let is_different = self
            .0
            .as_ref()
            .map(|existing| existing != nickname.as_ref())
            .unwrap_or(true);

        if is_different {
            self.0 = Some(nickname.into_owned());
        }

        is_different
    }

    pub fn save(&self) -> Result<()> {
        let contents = match &self.0 {
            Some(contents) => contents,
            None => return Ok(()),
        };

        let path = Self::validated_path()?;
        fs::write(&path, contents).context(err::FileIO { path })
    }

    pub fn validated_path() -> Result<PathBuf> {
        let mut path = SaveDir::LocalData.validated_path()?.to_path_buf();
        path.push("last_watched");
        Ok(path)
    }
}

pub fn best_matching_series_info<R, S>(remote: &R, name: S) -> Result<SeriesInfo>
where
    R: RemoteService + ?Sized,
    S: AsRef<str>,
{
    let name = name.as_ref();

    let mut results = remote.search_info_by_name(name)?;
    let index = detect::best_matching_info(name, results.as_slice())
        .context(err::NoMatchingSeries { name })?;

    let info = results.swap_remove(index);
    Ok(info)
}

pub fn episode_matcher_with_pattern<S>(pattern: S) -> Result<EpisodeMatcher>
where
    S: AsRef<str>,
{
    let pattern = pattern
        .as_ref()
        .replace("{title}", "(?P<title>.+)")
        .replace("{episode}", r"(?P<episode>\d+)");

    match EpisodeMatcher::from_pattern(pattern) {
        Ok(matcher) => Ok(matcher),
        // We want to use a more specific error message than the one the anime library
        // provides
        Err(anime::Error::MissingCustomMatcherGroup { group }) => {
            Err(err::Error::MissingEpisodeMatcherGroup { group })
        }
        Err(err) => Err(err.into()),
    }
}
<|endoftext|>


<commit_before>import float::num;
import std::cmp::fuzzy_eq;
import Num = num::num;

class Matrix4<T:copy fuzzy_eq Num> {
    let m11: T; let m12: T; let m13: T; let m14: T;
    let m21: T; let m22: T; let m23: T; let m24: T;
    let m31: T; let m32: T; let m33: T; let m34: T;
    let m41: T; let m42: T; let m43: T; let m44: T;

    new(m11: T, m12: T, m13: T, m14: T,
        m21: T, m22: T, m23: T, m24: T,
        m31: T, m32: T, m33: T, m34: T,
        m41: T, m42: T, m43: T, m44: T) {

        self.m11 = m11; self.m12 = m12; self.m13 = m13; self.m14 = m14;
        self.m21 = m21; self.m22 = m22; self.m23 = m23; self.m24 = m24;
        self.m31 = m31; self.m32 = m32; self.m33 = m33; self.m34 = m34;
        self.m41 = m41; self.m42 = m42; self.m43 = m43; self.m44 = m44;
    }

    fn fuzzy_eq(&&other: Matrix4<T>) -> bool {
        self.m11.fuzzy_eq(other.m11) && self.m12.fuzzy_eq(other.m12) &&
        self.m13.fuzzy_eq(other.m13) && self.m14.fuzzy_eq(other.m14) &&
        self.m21.fuzzy_eq(other.m21) && self.m22.fuzzy_eq(other.m22) &&
        self.m23.fuzzy_eq(other.m23) && self.m24.fuzzy_eq(other.m24) &&
        self.m31.fuzzy_eq(other.m31) && self.m32.fuzzy_eq(other.m32) &&
        self.m33.fuzzy_eq(other.m33) && self.m34.fuzzy_eq(other.m34) &&
        self.m41.fuzzy_eq(other.m41) && self.m42.fuzzy_eq(other.m42) &&
        self.m43.fuzzy_eq(other.m43) && self.m44.fuzzy_eq(other.m44)
    }

    fn mul_s(&&x: T) -> Matrix4<T> {
        ret Matrix4(self.m11.mul(x), self.m12.mul(x), self.m13.mul(x), self.m14.mul(x),
                    self.m21.mul(x), self.m22.mul(x), self.m23.mul(x), self.m24.mul(x),
                    self.m31.mul(x), self.m32.mul(x), self.m33.mul(x), self.m34.mul(x),
                    self.m41.mul(x), self.m42.mul(x), self.m43.mul(x), self.m44.mul(x));
    }

    pure fn to_array() -> [T]/~ {
        ret [
            self.m11, self.m12, self.m13, self.m14,
            self.m21, self.m22, self.m23, self.m24,
            self.m31, self.m32, self.m33, self.m34,
            self.m41, self.m42, self.m43, self.m44
        ]/~;
    }
}

fn ortho<T:copy fuzzy_eq Num>(left: T, right: T, bottom: T, top: T, near: T, far: T)
                           -> Matrix4<T> {

    let two = left.from_int(2);
    let one = left.from_int(1);
    let zero = left.from_int(0);
    let minus_two = left.from_int(-2);

    let tx = right.add(left).div(right.sub(left)).neg();
    let ty = top.add(bottom).div(top.sub(bottom)).neg();
    let tz = far.add(near).div(far.sub(near)).neg();

    ret Matrix4(two.div(right.sub(left)), zero, zero, zero,
                zero, two.div(top.sub(bottom)), zero, zero,
                zero, zero, minus_two.div(far.sub(near)), zero,
                tx, ty, tz, one);
}

fn identity<T:copy fuzzy_eq Num>(_0: T) -> Matrix4<T> {
    let _1 = _0.from_int(1);
    ret Matrix4(_1, _0, _0, _0,
                _0, _1, _0, _0,
                _0, _0, _1, _0,
                _0, _0, _0, _1);
}

#[test]
fn test_ortho() {
    let (left, right, bottom, top) = (0.0, 1.0, 0.1, 1.0);
    let (near, far) = (-1.0, 1.0);
    let result = ortho(left, right, bottom, top, near, far);
    let expected = Matrix4(2.0,  0.0,         0.0,  0.0,
                           0.0,  2.22222222,  0.0,  0.0,
                           0.0,  0.0,         -1.0, 0.0,
                           -1.0, -1.22222222, -0.0, 1.0);
    #debug("result=%? expected=%?", result, expected);
    assert result.fuzzy_eq(expected);
}

<commit_msg>Fix import order, important for resolve3<commit_after>import std::cmp::fuzzy_eq;
import Num = num::num;
import float::num;

class Matrix4<T:copy fuzzy_eq Num> {
    let m11: T; let m12: T; let m13: T; let m14: T;
    let m21: T; let m22: T; let m23: T; let m24: T;
    let m31: T; let m32: T; let m33: T; let m34: T;
    let m41: T; let m42: T; let m43: T; let m44: T;

    new(m11: T, m12: T, m13: T, m14: T,
        m21: T, m22: T, m23: T, m24: T,
        m31: T, m32: T, m33: T, m34: T,
        m41: T, m42: T, m43: T, m44: T) {

        self.m11 = m11; self.m12 = m12; self.m13 = m13; self.m14 = m14;
        self.m21 = m21; self.m22 = m22; self.m23 = m23; self.m24 = m24;
        self.m31 = m31; self.m32 = m32; self.m33 = m33; self.m34 = m34;
        self.m41 = m41; self.m42 = m42; self.m43 = m43; self.m44 = m44;
    }

    fn fuzzy_eq(&&other: Matrix4<T>) -> bool {
        self.m11.fuzzy_eq(other.m11) && self.m12.fuzzy_eq(other.m12) &&
        self.m13.fuzzy_eq(other.m13) && self.m14.fuzzy_eq(other.m14) &&
        self.m21.fuzzy_eq(other.m21) && self.m22.fuzzy_eq(other.m22) &&
        self.m23.fuzzy_eq(other.m23) && self.m24.fuzzy_eq(other.m24) &&
        self.m31.fuzzy_eq(other.m31) && self.m32.fuzzy_eq(other.m32) &&
        self.m33.fuzzy_eq(other.m33) && self.m34.fuzzy_eq(other.m34) &&
        self.m41.fuzzy_eq(other.m41) && self.m42.fuzzy_eq(other.m42) &&
        self.m43.fuzzy_eq(other.m43) && self.m44.fuzzy_eq(other.m44)
    }

    fn mul_s(&&x: T) -> Matrix4<T> {
        ret Matrix4(self.m11.mul(x), self.m12.mul(x), self.m13.mul(x), self.m14.mul(x),
                    self.m21.mul(x), self.m22.mul(x), self.m23.mul(x), self.m24.mul(x),
                    self.m31.mul(x), self.m32.mul(x), self.m33.mul(x), self.m34.mul(x),
                    self.m41.mul(x), self.m42.mul(x), self.m43.mul(x), self.m44.mul(x));
    }

    pure fn to_array() -> [T]/~ {
        ret [
            self.m11, self.m12, self.m13, self.m14,
            self.m21, self.m22, self.m23, self.m24,
            self.m31, self.m32, self.m33, self.m34,
            self.m41, self.m42, self.m43, self.m44
        ]/~;
    }
}

fn ortho<T:copy fuzzy_eq Num>(left: T, right: T, bottom: T, top: T, near: T, far: T)
                           -> Matrix4<T> {

    let two = left.from_int(2);
    let one = left.from_int(1);
    let zero = left.from_int(0);
    let minus_two = left.from_int(-2);

    let tx = right.add(left).div(right.sub(left)).neg();
    let ty = top.add(bottom).div(top.sub(bottom)).neg();
    let tz = far.add(near).div(far.sub(near)).neg();

    ret Matrix4(two.div(right.sub(left)), zero, zero, zero,
                zero, two.div(top.sub(bottom)), zero, zero,
                zero, zero, minus_two.div(far.sub(near)), zero,
                tx, ty, tz, one);
}

fn identity<T:copy fuzzy_eq Num>(_0: T) -> Matrix4<T> {
    let _1 = _0.from_int(1);
    ret Matrix4(_1, _0, _0, _0,
                _0, _1, _0, _0,
                _0, _0, _1, _0,
                _0, _0, _0, _1);
}

#[test]
fn test_ortho() {
    let (left, right, bottom, top) = (0.0, 1.0, 0.1, 1.0);
    let (near, far) = (-1.0, 1.0);
    let result = ortho(left, right, bottom, top, near, far);
    let expected = Matrix4(2.0,  0.0,         0.0,  0.0,
                           0.0,  2.22222222,  0.0,  0.0,
                           0.0,  0.0,         -1.0, 0.0,
                           -1.0, -1.22222222, -0.0, 1.0);
    #debug("result=%? expected=%?", result, expected);
    assert result.fuzzy_eq(expected);
}

<|endoftext|>


<commit_before><commit_msg>Refactor sim gen code<commit_after><|endoftext|>


<commit_before><commit_msg>Add boilerplate for create hook<commit_after>use toml::Value;

use libimagstore::storeid::StoreId;
use libimagstore::hook::Hook;
use libimagstore::hook::result::HookResult;
use libimagstore::hook::position::HookPosition;
use libimagstore::hook::accessor::{HookDataAccessor, HookDataAccessorProvider};
use libimagstore::hook::accessor::StoreIdAccessor;

#[derive(Debug)]
pub struct CreateHook {
    position: HookPosition,
    config: Option<Value>,
}

impl CreateHook {

    pub fn new(p: HookPosition) -> CreateHook {
        CreateHook {
            position: p,
            config: None,
        }
    }

}

impl Hook for CreateHook {

    fn name(&self) -> &'static str {
        "stdhook_git_create"
    }

    fn set_config(&mut self, config: &Value) {
        self.config = Some(config.clone());
    }

}

impl HookDataAccessorProvider for CreateHook {

    fn accessor(&self) -> HookDataAccessor {
        HookDataAccessor::StoreIdAccess(self)
    }
}

impl StoreIdAccessor for CreateHook {

    fn access(&self, id: &StoreId) -> HookResult<()> {
        debug!("[GIT CREATE HOOK]: {:?}", id);
        Ok(())
    }

}

<|endoftext|>


<commit_before>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::min;
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::vec::Vec;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const HORIZ_SEPARATOR: &'static str = "─";
const LIST_WIDTH: u16 = 32;

enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let mut stdout = MouseTerminal::from(stdout().into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    for entry in receiver.iter() {
        let mut redraw = false;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => {
                    redraw = true;
                    file_list.scroll_down();
                }

                Event::Key(Key::Char('k')) => {
                    redraw = true;
                    file_list.scroll_up();
                }

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        write!(stdout, "{}x", termion::cursor::Goto(x, y))?;
                        stdout.flush()?;
                    }

                    _ => (),
                },

                _ => {}
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            let (width, height) = termion::terminal_size()?;
            let offset = file_list.current_index.unwrap_or(0);

            for row in 0..height {
                let index = (offset + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    write!(
                        stdout,
                        "{}{}{}{}/{}:{}{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        color::Fg(fg),
                        color::Bg(bg),
                        entry.directory,
                        entry.file_name,
                        entry.line_number,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(
                        stdout,
                        "{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        "                     "
                    )?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            stdout.flush()?;
        }
    }

    Ok(())
}
<commit_msg>hide the cursor<commit_after>extern crate clap;
extern crate termion;

use std::collections::HashMap;
use std::cmp::min;
use std::fs::File;
use std::io::{stdin, stdout, BufRead, BufReader, Write};
use std::option::Option;
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::vec::Vec;

use clap::{App, Arg};
use termion::color;
use termion::event::{Event, Key, MouseEvent};
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;

const VERT_SEPARATOR: &'static str = "│";
const LIST_WIDTH: u16 = 32;

enum ChanMessage {
    SearchResult(ListEntry),
    SearchComplete,
    TerminalEvent(Event),
}

#[derive(Debug, Clone)]
struct ListEntry {
    directory: String,
    file_name: String,
    line_number: u32,
    text: String,
}

impl ListEntry {
    // format is: path/to/file.rs:123:line of text
    fn from_line(line: &str) -> Option<ListEntry> {
        let parts: Vec<&str> = line.splitn(3, ':').collect();

        if parts.len() != 3 {
            return None;
        }

        let path = Path::new(parts[0]);

        // FIXME: This cannot possibly be right. wtf.
        Some(ListEntry {
            directory: String::from(path.parent().unwrap().to_str().unwrap()),
            file_name: String::from(path.file_name().unwrap().to_str().unwrap()),
            line_number: parts[1].parse().unwrap(),
            text: String::from(parts[2]),
        })
    }
}

#[derive(Debug, Clone)]
struct FileList {
    entries: Vec<ListEntry>,
    current_index: Option<usize>,
}

impl FileList {
    fn new() -> FileList {
        FileList {
            entries: vec![],
            current_index: None,
        }
    }

    fn add_entry(&mut self, entry: ListEntry) {
        // Insert the new element into it's sorted position
        let insert_pos = {
            let new_key = (&entry.directory, &entry.file_name, &entry.line_number);
            let result = self.entries.binary_search_by(|other| {
                let k = (&other.directory, &other.file_name, &other.line_number);
                return k.cmp(&new_key);
            });

            match result {
                Ok(pos) | Err(pos) => pos,
            }
        };

        self.entries.insert(insert_pos, entry);

        if self.current_index.is_none() {
            self.current_index = Some(0);
        }
    }

    fn scroll_down(&mut self) {
        self.current_index = self.current_index
            .map(|prev| min(self.entries.len() - 1, prev + 1))
    }

    fn scroll_up(&mut self) {
        self.current_index = self.current_index
            .map(|prev| if prev == 0 { prev } else { prev - 1 })
    }
}

#[derive(Debug)]
struct UI {
    list: FileList,
    file_cache: HashMap<(String, String), File>,
}

fn main() {
    let matches = App::new("dredge")
        .about("Search interactively")
        .arg(
            Arg::with_name("program")
                .long("program")
                .short("p")
                .value_name("PROGRAM")
                .default_value("ag")
                .help("grep or ag or w/e"),
        )
        .arg(
            Arg::with_name("query")
                .help("what to search for")
                .required(true),
        )
        // .arg(
        //     Arg::with_name("directories")
        //         .help("where to search for it")
        //         .multiple(true)
        //         .default_value("."),
        // )
        // .arg(
        //     Arg::with_name("options")
        //         .help("options to pass directly to search program.")
        //         .multiple(true)
        //         .default_value("")
        //         .last(true),
        // )
        .get_matches();

    let (tx, rx) = channel();

    let cmd_tx = tx.clone();
    let term_tx = tx.clone();

    thread::spawn(move || {
        run_command(matches, cmd_tx).expect("failed to run command");
    });

    thread::spawn(move || {
        let stdin = stdin();

        for c in stdin.events() {
            term_tx
                .send(ChanMessage::TerminalEvent(c.unwrap()))
                .unwrap();
        }
    });

    ui_loop(rx).unwrap();
}

fn run_command(
    config: clap::ArgMatches,
    sender: Sender<ChanMessage>,
) -> Result<(), Box<std::error::Error>> {
    let cmd = Command::new("ag")
        .arg(config.value_of("query").unwrap())
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .spawn()?;

    let output = cmd.stdout.unwrap();
    let reader = BufReader::new(output);

    for line in reader.lines() {
        if let Some(entry) = ListEntry::from_line(&line.unwrap()) {
            sender.send(ChanMessage::SearchResult(entry))?;
        }
    }

    sender.send(ChanMessage::SearchComplete)?;

    Ok(())
}

fn ui_loop(receiver: Receiver<ChanMessage>) -> Result<(), Box<std::error::Error>> {
    let mut file_list = FileList::new();

    let mut stdout = MouseTerminal::from(stdout().into_raw_mode()?);
    write!(
        stdout,
        "{}{}",
        termion::clear::All,
        termion::cursor::Goto(1, 1)
    )?;

    for entry in receiver.iter() {
        let mut redraw = false;

        match entry {
            ChanMessage::SearchComplete => {
                if file_list.entries.is_empty() {
                    println!("No matches");
                    break;
                }
            }

            ChanMessage::TerminalEvent(evt) => match evt {
                Event::Key(Key::Ctrl('c')) | Event::Key(Key::Char('q')) => break,

                Event::Key(Key::Char('j')) => {
                    redraw = true;
                    file_list.scroll_down();
                }

                Event::Key(Key::Char('k')) => {
                    redraw = true;
                    file_list.scroll_up();
                }

                Event::Mouse(me) => match me {
                    MouseEvent::Press(_, x, y) => {
                        write!(stdout, "{}x", termion::cursor::Goto(x, y))?;
                        stdout.flush()?;
                    }

                    _ => (),
                },

                _ => {}
            },

            ChanMessage::SearchResult(entry) => {
                redraw = true;
                file_list.add_entry(entry);

                if file_list.current_index.is_none() {
                    file_list.current_index = Some(0)
                }
            }
        }

        if redraw {
            let (width, height) = termion::terminal_size()?;
            let offset = file_list.current_index.unwrap_or(0);

            for row in 0..height {
                let index = (offset + row as usize) as usize;

                if index < file_list.entries.len() {
                    let ref entry = &file_list.entries[index];
                    let (fg, bg): (&color::Color, &color::Color) =
                        if Some(index) == file_list.current_index {
                            (&color::Black, &color::White)
                        } else {
                            (&color::Reset, &color::Reset)
                        };

                    write!(
                        stdout,
                        "{}{}{}{}/{}:{}{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        color::Fg(fg),
                        color::Bg(bg),
                        entry.directory,
                        entry.file_name,
                        entry.line_number,
                        color::Fg(color::Reset),
                        color::Bg(color::Reset),
                    )?;
                } else {
                    write!(
                        stdout,
                        "{}{}",
                        termion::cursor::Goto(1, 1 + row as u16),
                        "                     "
                    )?;
                }

                write!(
                    stdout,
                    "{}{}",
                    termion::cursor::Goto(LIST_WIDTH, 1 + row),
                    VERT_SEPARATOR
                )?;
            }

            write!(stdout, "{}", termion::cursor::Hide)?;
            stdout.flush()?;
        }
    }

    Ok(())
}
<|endoftext|>