use crossterm::{input, InputEvent, KeyEvent, RawScreen}; use std::cmp; use std::convert::TryFrom; use std::fs::File; use std::io::{stdout, Write}; use std::path::PathBuf; use glk; use glk::helpers::fileref_manager::FilerefManager; use glk::helpers::stream; use glk::helpers::stream_manager::StreamManager; use glk::helpers::window_stream; use glk::types::{ event_t, evtype, filemode, fileusage, frefid_t, gestalt, keycode, seekmode, stream_result_t, strid_t, style, stylehint, winid_t, winmethod, wintype, }; use glk::{garglk, giblorb, gidispatch}; use glk::{latin1, unicode}; use glulxe; mod util; mod window; mod window_manager; use crate::util::unhandled; use crate::window_manager::WindowManager; /** Map crossterm key event to Glk keycode. */ fn map_key(key: KeyEvent) -> keycode { match key { KeyEvent::Left => keycode::Left, KeyEvent::Right => keycode::Right, KeyEvent::Up => keycode::Up, KeyEvent::Down => keycode::Down, KeyEvent::Enter => keycode::Return, KeyEvent::Delete => keycode::Delete, KeyEvent::Esc => keycode::Escape, KeyEvent::Tab => keycode::Tab, KeyEvent::PageUp => keycode::PageUp, KeyEvent::PageDown => keycode::PageDown, KeyEvent::Home => keycode::Home, KeyEvent::End => keycode::End, KeyEvent::F(1) => keycode::Func1, KeyEvent::F(2) => keycode::Func2, KeyEvent::F(3) => keycode::Func3, KeyEvent::F(4) => keycode::Func4, KeyEvent::F(5) => keycode::Func5, KeyEvent::F(6) => keycode::Func6, KeyEvent::F(7) => keycode::Func7, KeyEvent::F(8) => keycode::Func8, KeyEvent::F(9) => keycode::Func9, KeyEvent::F(10) => keycode::Func10, KeyEvent::F(11) => keycode::Func11, KeyEvent::F(12) => keycode::Func12, KeyEvent::Char(ch) => keycode::from_char(ch), _ => keycode::Unknown, } } fn fileref_prompt(usage: fileusage, fmode: filemode) -> Option { print!( "Enter name for {} ({})>", match usage.get_type() { fileusage::Data => "data", fileusage::SavedGame => "save", fileusage::Transcript => "transcript", fileusage::InputRecord => "input record", _ => "unknown", }, match fmode { filemode::Write => "write", filemode::Read => "read", filemode::ReadWrite => "read+write", filemode::WriteAppend => "append", _ => "unknown", } ); stdout().flush().unwrap(); let s = input().read_line().unwrap(); if s.len() != 0 { Some(s) } else { None } } pub struct MyGlk { blorbmap: *mut giblorb::giblorb_map_t, /** Requested line event. */ linebuf: Option<(winid_t, gidispatch::RetainedBuffer, usize)>, /** Requested char event. */ charbuf: Option<(winid_t, bool)>, /** Retained registry callbacks. */ retained_registry: gidispatch::RetainedRegistry, /** Stream manager. */ pub streams: StreamManager, /** Window manager. */ pub windows: WindowManager, /** Fileref manager. */ pub filerefs: FilerefManager, } impl MyGlk { fn new(named_base: PathBuf) -> Self { Self { blorbmap: 0 as *mut giblorb::giblorb_map_t, linebuf: None, charbuf: None, retained_registry: gidispatch::RetainedRegistry::new(), streams: StreamManager::new(), windows: WindowManager::new(), filerefs: FilerefManager::new(named_base, Box::new(fileref_prompt)), } } } impl glk::traits::Base for MyGlk { fn set_interrupt_handler(&mut self, _func: Option) {} fn tick(&mut self) {} fn gestalt(&mut self, sel: gestalt, val: u32, arr: &mut [u32]) -> u32 { use glk::types::gestalt_CharOutput; match sel { gestalt::Version => 0x00000705, gestalt::CharInput | gestalt::LineInput => { if (val >= keycode::Func12.0 && val <= keycode::Left.0) || char::try_from(val).is_ok() { 1 // Might be possible to input this } else { 0 // Invalid unicode, can't ever receive this } } gestalt::CharOutput => { if arr.len() >= 1 { // Fill in number of actual glyphs printed. Our estimation is here 1, but this // is not correct for wide characters such as emoji on most terminals. arr[0] = 1; } if val == 10 || (val >= 32 && val < 127) || (val >= 160 && val < 256) { // Assume Latin-1 can be printed exactly. gestalt_CharOutput::ExactPrint.0 } else if char::try_from(val).is_ok() { gestalt_CharOutput::ApproxPrint.0 // OK, but can't guarantee exact printing } else { gestalt_CharOutput::CannotPrint.0 // Invalid unicode, don't ever pass this } } gestalt::Unicode => 1, gestalt::GarglkText => 1, _ => { println!("unhandled glk_gestalt sel={} val={}", sel.0, val); 0 } } } /****************************************************/ fn char_to_lower(&mut self, ch: u8) -> u8 { latin1::to_lower(ch) } fn char_to_upper(&mut self, ch: u8) -> u8 { latin1::to_upper(ch) } /****************************************************/ fn window_get_root(&mut self) -> winid_t { self.windows.get_root() } fn window_open( &mut self, split: winid_t, method: winmethod, size: u32, wtype: wintype, rock: u32, ) -> winid_t { self.windows .open(&mut self.streams, split, method, size, wtype, rock) } fn window_close(&mut self, win: winid_t) -> stream_result_t { self.windows.close(&mut self.streams, win) } fn window_get_size(&mut self, win: winid_t) -> (u32, u32) { self.windows.get_size(win) } fn window_set_arrangement( &mut self, win: winid_t, method: winmethod, size: u32, keywin: winid_t, ) { self.windows.set_arrangement(win, method, size, keywin) } fn window_get_arrangement(&mut self, win: winid_t) -> (u32, u32, winid_t) { self.windows.get_arrangement(win) } fn window_iterate(&mut self, win: winid_t) -> (winid_t, u32) { self.windows.iterate(win) } fn window_get_rock(&mut self, win: winid_t) -> u32 { self.windows.get_rock(win) } fn window_get_type(&mut self, win: winid_t) -> wintype { self.windows.get_type(win) } fn window_get_parent(&mut self, win: winid_t) -> winid_t { self.windows.get_parent(win) } fn window_get_sibling(&mut self, win: winid_t) -> winid_t { self.windows.get_sibling(win) } fn window_clear(&mut self, win: winid_t) { self.windows.clear(win) } fn window_move_cursor(&mut self, win: winid_t, xpos: u32, ypos: u32) { self.windows.move_cursor(win, xpos, ypos) } fn window_get_stream(&mut self, win: winid_t) -> strid_t { self.windows.get_stream(win) } fn window_set_echo_stream(&mut self, win: winid_t, str: strid_t) { self.windows.set_echo_stream(win, str) } fn window_get_echo_stream(&mut self, win: winid_t) -> strid_t { self.windows.get_echo_stream(win) } fn set_window(&mut self, win: winid_t) { if win == (0 as winid_t) { self.streams.set_current(0 as strid_t); } else { self.streams.set_current(self.windows.get_stream(win)); } } /****************************************************/ fn stream_open_file(&mut self, fileref: frefid_t, fmode: filemode, rock: u32) -> strid_t { let (usage, path) = self.filerefs.get(fileref); self.streams.open_file(usage, &path, fmode, rock, false) } fn stream_open_memory( &mut self, buf: &mut gidispatch::RetainableBuffer, fmode: filemode, rock: u32, ) -> strid_t { self.streams.open_memory(buf, fmode, rock) } fn stream_close(&mut self, str: strid_t) -> stream_result_t { self.streams.close(str) } fn stream_iterate(&mut self, str: strid_t) -> (strid_t, u32) { self.streams.iterate(str) } fn stream_get_rock(&mut self, str: strid_t) -> u32 { self.streams.get_rock(str) } fn stream_set_position(&mut self, str: strid_t, pos: i32, mode: seekmode) { self.streams.set_position(str, pos, mode) } fn stream_get_position(&mut self, str: strid_t) -> u32 { self.streams.get_position(str) } fn stream_set_current(&mut self, str: strid_t) { self.streams.set_current(str) } fn stream_get_current(&mut self) -> strid_t { self.streams.get_current() } fn put_char(&mut self, ch: u8) { self.streams.put_char(ch) } fn put_char_stream(&mut self, str: strid_t, ch: u8) { self.streams.put_char_stream(str, ch) } fn put_buffer(&mut self, buf: &[u8]) { self.streams.put_buffer(buf) } fn put_buffer_stream(&mut self, str: strid_t, buf: &[u8]) { self.streams.put_buffer_stream(str, buf) } fn set_style(&mut self, styl: style) { self.streams.set_style(stream::Style::Glk(styl)) } fn set_style_stream(&mut self, str: strid_t, styl: style) { self.streams.set_style_stream(str, stream::Style::Glk(styl)) } fn get_char_stream(&mut self, str: strid_t) -> i32 { self.streams.get_char_stream(str) } fn get_line_stream(&mut self, str: strid_t, buf: &mut [u8]) -> u32 { self.streams.get_line_stream(str, buf) } fn get_buffer_stream(&mut self, str: strid_t, buf: &mut [u8]) -> u32 { self.streams.get_buffer_stream(str, buf) } /****************************************************/ fn stylehint_set(&mut self, wtype: wintype, styl: style, hint: stylehint, val: i32) { self.windows.stylehint_set(wtype, styl, hint, val) } fn stylehint_clear(&mut self, wtype: wintype, styl: style, hint: stylehint) { self.windows.stylehint_clear(wtype, styl, hint) } fn style_distinguish(&mut self, win: winid_t, styl1: style, styl2: style) -> u32 { self.windows.style_distinguish(win, styl1, styl2) } fn style_measure(&mut self, win: winid_t, styl: style, hint: stylehint) -> Option { self.windows.style_measure(win, styl, hint) } /****************************************************/ fn fileref_create_temp(&mut self, usage: fileusage, rock: u32) -> frefid_t { self.filerefs.create_temp(usage, rock) } fn fileref_create_by_name(&mut self, usage: fileusage, name: &[u8], rock: u32) -> frefid_t { self.filerefs.create_by_name(usage, name, rock) } fn fileref_create_by_prompt( &mut self, usage: fileusage, fmode: filemode, rock: u32, ) -> frefid_t { self.filerefs.create_by_prompt(usage, fmode, rock) } fn fileref_create_from_fileref( &mut self, usage: fileusage, fref: frefid_t, rock: u32, ) -> frefid_t { self.filerefs.create_from_fileref(usage, fref, rock) } fn fileref_destroy(&mut self, fref: frefid_t) { self.filerefs.destroy(fref) } fn fileref_iterate(&mut self, fref: frefid_t) -> (frefid_t, u32) { self.filerefs.iterate(fref) } fn fileref_get_rock(&mut self, fref: frefid_t) -> u32 { self.filerefs.get_rock(fref) } fn fileref_delete_file(&mut self, fref: frefid_t) { self.filerefs.delete_file(fref) } fn fileref_does_file_exist(&mut self, fref: frefid_t) -> u32 { self.filerefs.does_file_exist(fref) } /****************************************************/ fn select(&mut self) -> event_t { // TODO: block for next event if let Some((win, mut buf, _initlen)) = self.linebuf.take() { stdout().flush().unwrap(); self.set_style(style::Input); let s = input().read_line().unwrap(); self.set_style(style::Normal); let sbuf = latin1::from_str(&s); let newlen = cmp::min(buf.len(), sbuf.len()); buf[0..newlen].copy_from_slice(&sbuf[0..newlen]); // If there is an echo stream, echo the input line. let echostr = self.windows.get_echo_stream(win); if echostr != 0 as strid_t { self.put_buffer_stream(echostr, &sbuf); self.put_char_stream(echostr, b'\n'); } return event_t { type_: evtype::LineInput.0, win, val1: newlen as u32, val2: 0, }; } else if let Some((win, _unicode)) = self.charbuf.take() { stdout().flush().unwrap(); let _raw = RawScreen::into_raw_mode().unwrap(); let input = input(); let mut sync_stdin = input.read_sync(); // Raw keyboard character event loop. loop { if let Some(event) = sync_stdin.next() { match event { InputEvent::Keyboard(KeyEvent::Ctrl('c')) => { // Allow Ctrl-C to work to exit instantly (for testing). RawScreen::disable_raw_mode().unwrap(); println!(""); std::process::exit(0); } InputEvent::Keyboard(key) => { return event_t { type_: evtype::CharInput.0, win, val1: map_key(key).0, val2: 0, }; } _ => {} } } } } else { println!("Unhandled events requested"); } event_t { type_: 0, win: 0 as winid_t, val1: 0, val2: 0, } } fn select_poll(&mut self) -> event_t { // "glk_select_poll() does not check for or return evtype_CharInput, evtype_LineInput, or evtype_MouseInput events" // At the moment, glk_select_poll() checks for evtype_Timer, and possibly evtype_Arrange and evtype_SoundNotify events. event_t { type_: 0, win: 0 as winid_t, val1: 0, val2: 0, } } fn request_timer_events(&mut self, _millisecs: u32) { unhandled("glk_request_timer_events"); } fn request_line_event( &mut self, win: winid_t, buf: &mut gidispatch::RetainableBuffer, initlen: u32, ) { self.linebuf = Some((win, self.retained_registry.register(buf), initlen as usize)); } fn request_char_event(&mut self, win: winid_t) { self.charbuf = Some((win, false)); } fn request_mouse_event(&mut self, _win: winid_t) { unhandled("glk_request_mouse_event"); } fn cancel_line_event(&mut self, _win: winid_t) -> event_t { self.linebuf = None; // TODO event_t { type_: 0, win: 0 as winid_t, val1: 0, val2: 0, } } fn cancel_char_event(&mut self, _win: winid_t) { self.charbuf = None; } fn cancel_mouse_event(&mut self, _win: winid_t) { unhandled("glk_cancel_mouse_event"); } } impl glk::traits::Unicode for MyGlk { fn buffer_to_lower_case_uni(&mut self, buf: &mut [u32], numchars: u32) -> u32 { let s = unicode::to_string(&buf[0..numchars as usize]); let s = s.to_lowercase(); unicode::from_string(buf, &s) as u32 } fn buffer_to_upper_case_uni(&mut self, buf: &mut [u32], numchars: u32) -> u32 { let s = unicode::to_string(&buf[0..numchars as usize]); let s = s.to_uppercase(); unicode::from_string(buf, &s) as u32 } fn buffer_to_title_case_uni(&mut self, buf: &mut [u32], numchars: u32, lowerrest: u32) -> u32 { // Uppercase the first letter, optionally lower-case the rest. // This is not a correct title-case according to the unicode standard // described in the Glk specification. if numchars == 0 { return 0; } let first_letter = unicode::to_string(&buf[0..1]); let rest = unicode::to_string(&buf[1..numchars as usize]); let s = first_letter.to_uppercase() + &if lowerrest != 0 { rest.to_lowercase() } else { rest }; unicode::from_string(buf, &s) as u32 } fn put_char_uni(&mut self, ch: u32) { self.streams.put_char_uni(ch) } fn put_buffer_uni(&mut self, buf: &[u32]) { self.streams.put_buffer_uni(buf) } fn put_char_stream_uni(&mut self, str: strid_t, ch: u32) { self.streams.put_char_stream_uni(str, ch) } fn put_buffer_stream_uni(&mut self, str: strid_t, buf: &[u32]) { self.streams.put_buffer_stream_uni(str, buf) } fn get_char_stream_uni(&mut self, str: strid_t) -> i32 { self.streams.get_char_stream_uni(str) } fn get_buffer_stream_uni(&mut self, str: strid_t, buf: &mut [u32]) -> u32 { self.streams.get_buffer_stream_uni(str, buf) } fn get_line_stream_uni(&mut self, str: strid_t, buf: &mut [u32]) -> u32 { self.streams.get_line_stream_uni(str, buf) } fn stream_open_file_uni(&mut self, fileref: frefid_t, fmode: filemode, rock: u32) -> strid_t { let (usage, path) = self.filerefs.get(fileref); self.streams.open_file(usage, &path, fmode, rock, true) } fn stream_open_memory_uni( &mut self, buf: &mut gidispatch::RetainableBuffer, fmode: filemode, rock: u32, ) -> strid_t { self.streams.open_memory_uni(buf, fmode, rock) } fn request_char_event_uni(&mut self, win: winid_t) { self.charbuf = Some((win, true)); } fn request_line_event_uni( &mut self, _win: winid_t, _buf: &mut gidispatch::RetainableBuffer, _initlen: u32, ) { unhandled("glk_request_line_event_uni"); } } impl glk::garglk::GarGlkText for MyGlk { fn garglk_set_zcolors(&mut self, fg: garglk::zcolor, bg: garglk::zcolor) { self.streams.set_style(stream::Style::Zcolor(fg, bg)) } fn garglk_set_zcolors_stream(&mut self, str: strid_t, fg: garglk::zcolor, bg: garglk::zcolor) { self.streams .set_style_stream(str, stream::Style::Zcolor(fg, bg)) } fn garglk_set_reversevideo(&mut self, reverse: u32) { self.streams.set_style(stream::Style::Reverse(reverse)) } fn garglk_set_reversevideo_stream(&mut self, str: strid_t, reverse: u32) { self.streams .set_style_stream(str, stream::Style::Reverse(reverse)) } } impl giblorb::Handlers for MyGlk { /** The absolute minimum implementation of the giblorb handlers. * (inspired by glkterm's) */ fn giblorb_set_resource_map(&mut self, file: strid_t) -> giblorb::giblorb_err { // This call internally calls to Glk stream functions, which indirectly result in // methods of the MyGlk struct being called. I think this is strictly undefined behavior as // there will be two mutable references to the structure (though not concurrently used). // It also wouldn't work if the Glk API would be behind a Rc>. // But it seems to work for now. match giblorb::create_map(file) { Ok(map) => { self.blorbmap = map; giblorb::giblorb_err::None } Err(err) => err, } } fn giblorb_get_resource_map(&mut self) -> *mut giblorb::giblorb_map_t { self.blorbmap } } impl gidispatch::Handlers for MyGlk { fn gidispatch_set_object_registry(&mut self, registry: gidispatch::ObjRegistry) { // Propagate new object registry to manager objects self.streams.set_object_registry(registry); self.windows.set_object_registry(registry); self.filerefs.set_object_registry(registry); } fn gidispatch_get_objrock( &mut self, obj: gidispatch::objref, objclass: u32, ) -> gidispatch::rock { match gidispatch::class(objclass) { gidispatch::class::Window => self.windows.get_di_rock(obj as winid_t), gidispatch::class::Stream => self.streams.get_di_rock(obj as strid_t), gidispatch::class::Fileref => self.filerefs.get_di_rock(obj as frefid_t), _ => gidispatch::DUMMY_ROCK, } } fn gidispatch_set_retained_registry(&mut self, registry: gidispatch::RetainedRegistry) { self.retained_registry = registry; // Propagate new retained registry to manager objects self.streams.set_retained_registry(registry); self.windows.set_retained_registry(registry); } } /** Implement dispatcher trait for Glk layer. */ impl glk::traits::Api for MyGlk { fn base(&mut self) -> &mut dyn glk::traits::Base { self } fn unicode(&mut self) -> Option<&mut dyn glk::traits::Unicode> { Some(self) } fn garglk_text(&mut self) -> Option<&mut dyn garglk::GarGlkText> { Some(self) } fn giblorb(&mut self) -> Option<&mut dyn giblorb::Handlers> { Some(self) } fn gidispatch(&mut self) -> Option<&mut dyn gidispatch::Handlers> { Some(self) } } fn main() { let args: Vec = std::env::args().collect(); if args.len() < 2 { println!("Provide a story file as first argument on the command line."); std::process::exit(1); } let filename = args.get(1).unwrap(); let basepath = std::env::current_dir().unwrap(); let mut myglk = MyGlk::new(basepath); let file = File::open(filename).unwrap(); let gamefile = myglk.streams.from_file(file); glk::init(Box::new(myglk)); if glulxe::init(gamefile).is_err() { println!("Story file is not a ulx or blorb file containing one."); std::process::exit(1); } glulxe::main(); glk::reset(); }