1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//! # Nlog: Quick and Dirty logging for Windows
//!
//! Nlog is an implementation of the [`log`][log] crate that sends text to an untitled Windows Notepad window.
//!
//! ```rust
//! #[macro_use]
//! extern crate log;
//!
//! fn main() {
//!     nlog::init(log::LevelFilter::Info).unwrap();
//!
//!     info!("Hello, world! λ");
//! }
//! ```
//!
//! # Features
//!
//! * Unicode Support
//! * Thread-safe
//! * Works with Wine Notepad
//! * Completely WYSIWYG
//!
//! # Why?
//!
//! * Simple: Just open Notepad, copy or save logs as needed.
//! * Silent: No log files piling up
//! * Robust: Works even when no console or file I/O is available
//!
//! [log]: https://docs.rs/log

#[cfg(not(windows))]
compile_error!("this crate is designed for Windows platforms only");

#[macro_use]
extern crate lazy_static;

struct NLog;
static NLOG: NLog = NLog;

impl log::Log for NLog {
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        metadata.level() <= log::max_level()
    }

    fn log(&self, record: &log::Record) {
        if !self.enabled(record.metadata()) {
            return
        }

        let message = format!("[{:5}] {}\n", record.level(), record.args());

        nlog(&message);
    }

    fn flush(&self) { }
}

pub fn init(level: log::LevelFilter) -> Result<(), log::SetLoggerError> {
    log::set_logger(&NLOG)
        .map(|()| log::set_max_level(level))
}

fn nlog(message: &str) {
    use std::sync::Mutex;
    use std::ptr::null_mut as NULL;
    use winapi::um::winuser::{FindWindowA, FindWindowExA, SendMessageW, GetWindowTextLengthW, EM_GETSEL, EM_SETSEL, EM_REPLACESEL};
    use winapi::shared::minwindef::DWORD;

    // We don't *really* need to make a global buffer to cut down on allocations, but it feels right to do anyway.
    // Besides, it shows that Rust makes it easy to do safely.
    // The only `unsafe` in this entire function are winapi calls.
    lazy_static! {
        static ref NLOG_BUFFER: Mutex<Vec<u16>> = Mutex::default();
    }

    // SAFETY: All constraints of FindWindowA are satisfied.
    let notepad = unsafe {
        FindWindowA(NULL(), b"Untitled - Notepad\0".as_ptr() as _)
    };

    if notepad.is_null() {
        return
    }

    // SAFETY: All constraints of FindWindowExA are satisfied.
    let edit = unsafe {
        FindWindowExA(notepad, NULL(), b"EDIT\0".as_ptr() as _, NULL())
    };

    if edit.is_null() {
        return
    }

    use std::ffi::OsStr;
    use std::os::windows::ffi::OsStrExt;

    // Since NLOG_BUFFER is exclusive to this function,
    // and nothing here is very likely to panic, we can unwrap freely.
    //
    // Of course we could panic if capacity were to exceed isize::MAX bytes,
    // which would require trying to encode_wide a ~1GB string.
    // Surely you wouldn't do that to me, would you?
    let mut buf = NLOG_BUFFER.lock().unwrap();

    buf.clear();
    buf.extend(OsStr::new(message).encode_wide().filter(|&c| c != 0));
    buf.push(0);

    // SAFETY: All constraints of SendMessageW are satisfied.
    unsafe {
        let mut sel_a: DWORD = 0;
        let mut sel_z: DWORD = 0;
        let len = GetWindowTextLengthW(edit);
        SendMessageW(edit, EM_GETSEL as _, &mut sel_a as *mut _ as _, &mut sel_z as *mut _ as _);
        SendMessageW(edit, EM_SETSEL as _, len as _, len as _);
        SendMessageW(edit, EM_REPLACESEL as _, 1 as _, buf.as_ptr() as _);
        SendMessageW(edit, EM_SETSEL as _, sel_a as _, sel_z as _);
    }
}