separate windows out, IPCcargo run --release!
This commit is contained in:
182
src/bin/audio_player.rs
Normal file
182
src/bin/audio_player.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::io::BufReader;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
|
||||
use rodio::{ Decoder, OutputStream, Sink, Source };
|
||||
use rand::prelude::*;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::utils::{ concat_paths, format_seconds };
|
||||
use ming_wm::fs::get_all_files;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
const LINE_HEIGHT: usize = 18;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AudioPlayer {
|
||||
dimensions: Dimensions,
|
||||
base_directory: String,
|
||||
queue: Vec<(PathBuf, u64)>,
|
||||
stream: Option<Box<OutputStream>>,
|
||||
sink: Option<Sink>,
|
||||
command: String,
|
||||
response: String,
|
||||
}
|
||||
|
||||
impl WindowLike for AudioPlayer {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::ChangeDimensions(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
if key_press.key == '𐘂' { //the enter key
|
||||
self.response = self.process_command();
|
||||
self.command = String::new();
|
||||
} else if key_press.key == '𐘁' { //backspace
|
||||
if self.command.len() > 0 {
|
||||
self.command = self.command[..self.command.len() - 1].to_string();
|
||||
}
|
||||
} else {
|
||||
self.command += &key_press.key.to_string();
|
||||
}
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
_ => {
|
||||
WindowMessageResponse::DoNothing
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = vec![DrawInstructions::Text([2, self.dimensions[1] - LINE_HEIGHT], "times-new-roman".to_string(), if self.command.len() > 0 { self.command.clone() } else { self.response.clone() }, theme_info.text, theme_info.background, None, None)];
|
||||
if let Some(sink) = &self.sink {
|
||||
let current = &self.queue[self.queue.len() - sink.len()];
|
||||
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned();
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - current_name.len() * MONO_WIDTH as usize / 2, 2], "times-new-romono".to_string(), current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
let time_string = format!("{}/{}", format_seconds(sink.get_pos().as_secs()), format_seconds(current.1));
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], "times-new-romono".to_string(), time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
|
||||
}
|
||||
//
|
||||
instructions
|
||||
}
|
||||
|
||||
//properties
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Audio Player".to_string()
|
||||
}
|
||||
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::Window
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[500, 200]
|
||||
}
|
||||
|
||||
fn resizable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioPlayer {
|
||||
pub fn new() -> Self {
|
||||
let mut ap: Self = Default::default();
|
||||
ap.base_directory = "/".to_string();
|
||||
ap
|
||||
}
|
||||
|
||||
//t: toggle pause/play
|
||||
//l: next/skip
|
||||
//j: volume down
|
||||
//k: volume up
|
||||
//b <dir>: set base directory
|
||||
//p <dir>/<playlist file>: play directory or playlist in random order
|
||||
//todo: h for help?
|
||||
//just hit enter or any key to refresh
|
||||
fn process_command(&mut self) -> String {
|
||||
if self.command.len() == 1 {
|
||||
if let Some(sink) = &mut self.sink {
|
||||
if self.command == "t" {
|
||||
if sink.is_paused() {
|
||||
sink.play();
|
||||
return "Resumed".to_string();
|
||||
} else {
|
||||
sink.pause();
|
||||
return "Paused".to_string();
|
||||
}
|
||||
} else if self.command == "h" {
|
||||
//
|
||||
} else if self.command == "l" {
|
||||
sink.skip_one();
|
||||
return "Skipped".to_string();
|
||||
} else if self.command == "j" {
|
||||
sink.set_volume(sink.volume() - 0.1);
|
||||
return "Volume decreased".to_string();
|
||||
} else if self.command == "k" {
|
||||
sink.set_volume(sink.volume() + 0.1);
|
||||
return "Volume increased".to_string();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let parts: Vec<&str> = self.command.split(" ").collect();
|
||||
if self.command.starts_with("p ") {
|
||||
if parts.len() == 2 {
|
||||
if let Ok(new_path) = concat_paths(&self.base_directory, parts[1]) {
|
||||
if new_path.exists() {
|
||||
if let Some(sink) = &mut self.sink {
|
||||
sink.clear();
|
||||
}
|
||||
let mut queue = if new_path.ends_with(".playlist") {
|
||||
Vec::new() //placeholder
|
||||
} else {
|
||||
get_all_files(PathBuf::from(new_path))
|
||||
};
|
||||
let mut rng = rand::thread_rng();
|
||||
queue.shuffle(&mut rng);
|
||||
let (stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
self.queue = Vec::new();
|
||||
for item in &queue {
|
||||
let file = BufReader::new(File::open(item).unwrap());
|
||||
let decoded = Decoder::new(file).unwrap();
|
||||
self.queue.push((item.clone(), decoded.total_duration().unwrap().as_secs()));
|
||||
sink.append(decoded);
|
||||
}
|
||||
self.stream = Some(Box::new(stream));
|
||||
self.sink = Some(sink);
|
||||
return "Playing".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.command.starts_with("b ") {
|
||||
if parts.len() == 2 {
|
||||
if let Ok(new_path) = concat_paths(&self.base_directory, parts[1]) {
|
||||
if new_path.exists() {
|
||||
self.base_directory = new_path.to_str().unwrap().to_string();
|
||||
return "Set new base directory".to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(AudioPlayer::new());
|
||||
}
|
||||
|
||||
19
src/bin/main.rs
Normal file
19
src/bin/main.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use linux_framebuffer::Framebuffer;
|
||||
|
||||
use ming_wm::framebuffer::FramebufferInfo;
|
||||
use ming_wm::window_manager::init;
|
||||
|
||||
fn main() {
|
||||
let mut fb = Framebuffer::new("/dev/fb0").unwrap();
|
||||
let bytes_per_pixel = (fb.var_screen_info.bits_per_pixel as usize) / 8;
|
||||
let fb_info = FramebufferInfo {
|
||||
byte_len: (fb.var_screen_info.yres_virtual * fb.fix_screen_info.line_length) as usize,
|
||||
width: fb.var_screen_info.xres_virtual as usize,
|
||||
height: fb.var_screen_info.yres_virtual as usize,
|
||||
bytes_per_pixel,
|
||||
stride: fb.fix_screen_info.line_length as usize / bytes_per_pixel,
|
||||
};
|
||||
|
||||
init(fb, fb_info);
|
||||
}
|
||||
|
||||
504
src/bin/malvim.rs
Normal file
504
src/bin/malvim.rs
Normal file
@@ -0,0 +1,504 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::fs::{ read_to_string, write };
|
||||
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::utils::{ calc_actual_lines, calc_new_cursor_pos, Substring };
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
const LINE_HEIGHT: usize = 18;
|
||||
const PADDING: usize = 2;
|
||||
const BAND_HEIGHT: usize = 18;
|
||||
|
||||
struct FileInfo {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub changed: bool,
|
||||
pub top_line_pos: usize,
|
||||
pub line_pos: usize,
|
||||
//max is length (yeah, not length + 1)
|
||||
pub cursor_pos: usize,
|
||||
pub content: Vec<String>,
|
||||
//
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum State {
|
||||
#[default]
|
||||
None,
|
||||
Replace,
|
||||
MaybeDelete,
|
||||
Maybeg,
|
||||
Find,
|
||||
BackFind,
|
||||
//
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Normal,
|
||||
Insert,
|
||||
Command,
|
||||
}
|
||||
|
||||
impl fmt::Display for Mode {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
let write_str = match self {
|
||||
Mode::Normal => "NORMAL",
|
||||
Mode::Insert => "INSERT",
|
||||
Mode::Command => "COMMAND",
|
||||
};
|
||||
fmt.write_str(write_str)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Current {
|
||||
pub actual_lines: Vec<(bool, usize, String)>, //first, line #, actual line
|
||||
pub line_num_width: usize, //file line digits * MONO_WIDTH
|
||||
pub max_lines: usize, //max actual lines on screen
|
||||
pub max_chars_per_line: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Malvim {
|
||||
dimensions: Dimensions,
|
||||
state: State,
|
||||
mode: Mode,
|
||||
command: Option<String>,
|
||||
bottom_message: Option<String>,
|
||||
maybe_num: Option<usize>,
|
||||
files: Vec<FileInfo>,
|
||||
current_file_index: usize,
|
||||
current: Current,
|
||||
}
|
||||
|
||||
impl WindowLike for Malvim {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
let mut changed = true;
|
||||
let mut new = false;
|
||||
if key_press.key == '𐘃' { //escape key
|
||||
self.mode = Mode::Normal;
|
||||
self.state = State::None;
|
||||
changed = false;
|
||||
} else if key_press.key == ':' && self.mode == Mode::Normal && self.state == State::None {
|
||||
self.mode = Mode::Command;
|
||||
self.command = Some(String::new());
|
||||
changed = false;
|
||||
} else if key_press.key == 'i' && self.mode == Mode::Normal && self.state == State::None && self.files.len() > 0 {
|
||||
self.mode = Mode::Insert;
|
||||
changed = false;
|
||||
} else if self.mode == Mode::Insert {
|
||||
let current_file = &mut self.files[self.current_file_index];
|
||||
let current_length = current_file.content[current_file.line_pos].len();
|
||||
let line = ¤t_file.content[current_file.line_pos];
|
||||
if key_press.key == '𐘂' { //the enter key
|
||||
let mut line = line.clone();
|
||||
let (left, right) = line.split_at_mut(current_file.cursor_pos);
|
||||
current_file.content[current_file.line_pos] = left.to_string();
|
||||
current_file.content.insert(current_file.line_pos + 1, right.to_string());
|
||||
current_file.line_pos += 1;
|
||||
current_file.cursor_pos = 0;
|
||||
} else if key_press.key == '𐘁' { //backspace
|
||||
if current_length > 0 && current_file.cursor_pos > 0 {
|
||||
current_file.content[current_file.line_pos] = line.remove(current_file.cursor_pos, 1);
|
||||
current_file.cursor_pos -= 1;
|
||||
} else {
|
||||
if current_file.line_pos > 0 {
|
||||
//merge the prev line and this line
|
||||
let old_previous_line_length = current_file.content[current_file.line_pos - 1].len();
|
||||
let removed = current_file.content.remove(current_file.line_pos);
|
||||
current_file.content[current_file.line_pos - 1] += &removed;
|
||||
current_file.line_pos -= 1;
|
||||
current_file.cursor_pos = old_previous_line_length;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
current_file.content[current_file.line_pos] = line.substring(0, current_file.cursor_pos).to_string() + &key_press.key.to_string() + line.substring(current_file.cursor_pos, line.len());
|
||||
current_file.cursor_pos += 1;
|
||||
}
|
||||
} else if self.mode == Mode::Normal && self.files.len() > 0 {
|
||||
let current_file = &mut self.files[self.current_file_index];
|
||||
let current_length = current_file.content[current_file.line_pos].len();
|
||||
//
|
||||
if self.state == State::Replace {
|
||||
if current_length > 0 && current_file.cursor_pos < current_length {
|
||||
let line = ¤t_file.content[current_file.line_pos];
|
||||
current_file.content[current_file.line_pos] = line.substring(0, current_file.cursor_pos).to_string() + &key_press.key.to_string() + line.substring(current_file.cursor_pos + 1, line.len());
|
||||
}
|
||||
self.state = State::None;
|
||||
} else if self.state == State::MaybeDelete {
|
||||
if key_press.key == 'd' {
|
||||
current_file.content.remove(current_file.line_pos);
|
||||
if current_file.content.len() == 0 {
|
||||
current_file.content = vec![String::new()];
|
||||
} else if current_file.line_pos == current_file.content.len() {
|
||||
current_file.line_pos = current_file.content.len() - 1;
|
||||
}
|
||||
let new_length = current_file.content[current_file.line_pos].len();
|
||||
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length);
|
||||
} else if key_press.key == 'w' {
|
||||
let line = ¤t_file.content[current_file.line_pos];
|
||||
if line.len() > 0 {
|
||||
//offset until space or eol
|
||||
let offset = line.chars().skip(current_file.cursor_pos).position(|c| c == ' ').unwrap_or(line.chars().count() - current_file.cursor_pos);
|
||||
current_file.content[current_file.line_pos] = line.remove(current_file.cursor_pos, offset);
|
||||
let new_length = current_file.content[current_file.line_pos].len();
|
||||
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length);
|
||||
}
|
||||
}
|
||||
self.state = State::None;
|
||||
} else if self.state == State::Maybeg {
|
||||
if key_press.key == 'g' {
|
||||
current_file.line_pos = 0;
|
||||
let new_length = current_file.content[current_file.line_pos].len();
|
||||
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length);
|
||||
}
|
||||
self.state = State::None;
|
||||
} else if self.state == State::Find || self.state == State::BackFind {
|
||||
let old_pos = current_file.cursor_pos;
|
||||
let find_pos = if self.state == State::Find {
|
||||
old_pos + current_file.content[current_file.line_pos].chars().skip(old_pos + 1).position(|c| c == key_press.key).unwrap_or(0) + 1
|
||||
} else {
|
||||
old_pos - current_file.content[current_file.line_pos].chars().rev().skip(current_length - old_pos + 1).position(|c| c == key_press.key).unwrap_or(0)- 2
|
||||
};
|
||||
current_file.cursor_pos = find_pos;
|
||||
self.state = State::None;
|
||||
} else if key_press.key == 'x' {
|
||||
if current_length > 0 && current_file.cursor_pos < current_length {
|
||||
let line = ¤t_file.content[current_file.line_pos];
|
||||
current_file.content[current_file.line_pos] = line.remove(current_file.cursor_pos, 1);
|
||||
if current_length == 1 {
|
||||
current_file.cursor_pos = 0;
|
||||
}
|
||||
}
|
||||
} else if key_press.key == 'h' {
|
||||
current_file.cursor_pos = current_file.cursor_pos.checked_sub(1).unwrap_or(0);
|
||||
changed = false;
|
||||
} else if key_press.key == 'j' || key_press.key == 'k' {
|
||||
if key_press.key == 'j' {
|
||||
current_file.line_pos += 1;
|
||||
if current_file.line_pos == current_file.content.len() {
|
||||
current_file.line_pos = current_file.content.len() - 1;
|
||||
}
|
||||
} else {
|
||||
current_file.line_pos = current_file.line_pos.checked_sub(1).unwrap_or(0);
|
||||
}
|
||||
let new_length = current_file.content[current_file.line_pos].len();
|
||||
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length);
|
||||
changed = false;
|
||||
} else if key_press.key == 'l' {
|
||||
if current_length > 0 {
|
||||
current_file.cursor_pos += 1;
|
||||
if current_file.cursor_pos > current_file.content[current_file.line_pos].len() {
|
||||
current_file.cursor_pos = current_file.content[current_file.line_pos].len();
|
||||
}
|
||||
}
|
||||
changed = false;
|
||||
} else if key_press.key == '0' {
|
||||
current_file.cursor_pos = 0;
|
||||
changed = false;
|
||||
} else if key_press.key == '$' {
|
||||
//yeah, no `- 1`, that's right
|
||||
current_file.cursor_pos = current_file.content[current_file.line_pos].len();
|
||||
changed = false;
|
||||
} else if key_press.key == '^' {
|
||||
current_file.line_pos = current_file.content[current_file.line_pos].chars().position(|c| c != ' ').unwrap_or(0);
|
||||
changed = false;
|
||||
} else if key_press.key == 'r' {
|
||||
self.state = State::Replace;
|
||||
changed = false;
|
||||
} else if key_press.key == 'd' {
|
||||
self.state = State::MaybeDelete;
|
||||
changed = false;
|
||||
} else if key_press.key == 'g' {
|
||||
self.state = State::Maybeg;
|
||||
changed = false;
|
||||
} else if key_press.key == 'G' {
|
||||
current_file.line_pos = current_file.content.len() - 1;
|
||||
let new_length = current_file.content[current_file.line_pos].len();
|
||||
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length);
|
||||
changed = false;
|
||||
} else if key_press.key == 'f' {
|
||||
self.state = State::Find;
|
||||
changed = false;
|
||||
} else if key_press.key == 'F' {
|
||||
self.state = State::BackFind;
|
||||
changed = false;
|
||||
}
|
||||
//
|
||||
} else if self.mode == Mode::Command {
|
||||
self.bottom_message = None;
|
||||
let command = self.command.clone().unwrap_or("".to_string());
|
||||
if key_press.key == '𐘂' { //the enter key
|
||||
new = self.process_command();
|
||||
self.command = None;
|
||||
self.mode = Mode::Normal;
|
||||
} else if key_press.key == '𐘁' { //backspace
|
||||
if command.len() > 0 {
|
||||
self.command = Some(command[..command.len() - 1].to_string());
|
||||
}
|
||||
} else {
|
||||
self.command = Some(command.to_string() + &key_press.key.to_string());
|
||||
}
|
||||
changed = false;
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
}
|
||||
if changed || new {
|
||||
self.calc_current(); //too over zealous but whatever
|
||||
if changed {
|
||||
self.files[self.current_file_index].changed = true;
|
||||
}
|
||||
}
|
||||
self.calc_top_line_pos();
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::ChangeDimensions(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = vec![
|
||||
//the top white bar
|
||||
DrawInstructions::Rect([0, 0], [self.dimensions[0], BAND_HEIGHT], theme_info.alt_text),
|
||||
//black background
|
||||
DrawInstructions::Rect([0, BAND_HEIGHT], [self.dimensions[0], self.dimensions[1] - BAND_HEIGHT * 3], theme_info.alt_background),
|
||||
//slight above bottom blue bar
|
||||
DrawInstructions::Rect([0, self.dimensions[1] - BAND_HEIGHT * 2], [self.dimensions[0], BAND_HEIGHT], theme_info.top),
|
||||
//black background
|
||||
DrawInstructions::Rect([0, self.dimensions[1] - BAND_HEIGHT], [self.dimensions[0], BAND_HEIGHT], theme_info.alt_background),
|
||||
];
|
||||
//write file tabs
|
||||
let mut used_width = 0;
|
||||
for file_index in 0..self.files.len() {
|
||||
let file_info = &self.files[file_index];
|
||||
let future_used_width = used_width + 4 + (file_info.name.len() + if file_info.changed { 2 } else { 0 }) * MONO_WIDTH as usize;
|
||||
//just cut off when too many file tabs open to fit
|
||||
if future_used_width > self.dimensions[0] {
|
||||
break;
|
||||
}
|
||||
let background = if file_index == self.current_file_index {
|
||||
theme_info.alt_background
|
||||
} else {
|
||||
theme_info.alt_secondary
|
||||
};
|
||||
instructions.extend(vec![
|
||||
DrawInstructions::Rect([used_width, 2], [future_used_width, BAND_HEIGHT - 2], background),
|
||||
DrawInstructions::Text([used_width + 2, 2], "times-new-romono".to_string(), if file_info.changed { "+ ".to_string() } else { String::new() } + &file_info.name, theme_info.alt_text, background, Some(0), Some(MONO_WIDTH)),
|
||||
]);
|
||||
used_width = future_used_width;
|
||||
}
|
||||
//write the actual current file
|
||||
let mut sub_line_num = 0; //a line in a file can be split into multiple lines for display
|
||||
if self.files.len() > 0 {
|
||||
let current_file = &self.files[self.current_file_index];
|
||||
let current = &self.current;
|
||||
for line_num in current_file.top_line_pos..(current_file.top_line_pos + current.max_lines) {
|
||||
if line_num == current.actual_lines.len() {
|
||||
break;
|
||||
}
|
||||
let line = ¤t.actual_lines[line_num];
|
||||
let rel_line_num = line_num - current_file.top_line_pos;
|
||||
//write line num text (if start of line)
|
||||
let y0 = BAND_HEIGHT + rel_line_num * LINE_HEIGHT + PADDING;
|
||||
if line.0 {
|
||||
instructions.push(DrawInstructions::Text([PADDING, y0], "times-new-romono".to_string(), line.1.to_string(), theme_info.alt_secondary, theme_info.alt_background, Some(0), Some(MONO_WIDTH)));
|
||||
sub_line_num = 0;
|
||||
}
|
||||
let x1 = current.line_num_width + PADDING * 2;
|
||||
//write actual line
|
||||
//line.2
|
||||
instructions.push(DrawInstructions::Text([x1, y0], "times-new-romono".to_string(), line.2.clone(), theme_info.alt_text, theme_info.alt_background, Some(0), Some(MONO_WIDTH)));
|
||||
sub_line_num += 1;
|
||||
let max = sub_line_num * current.max_chars_per_line;
|
||||
let min = max - current.max_chars_per_line;
|
||||
if line.1 == current_file.line_pos && current_file.cursor_pos >= min && current_file.cursor_pos < max {
|
||||
let top_left = [x1 + (current_file.cursor_pos - min) * MONO_WIDTH as usize, y0];
|
||||
//the cursor is on this line, draw it
|
||||
instructions.push(DrawInstructions::Rect(top_left, [MONO_WIDTH as usize, LINE_HEIGHT], theme_info.top));
|
||||
//draw the char over it
|
||||
if line.2.len() > 0 {
|
||||
instructions.push(DrawInstructions::Text(top_left, "times-new-romono".to_string(), line.2.chars().nth(current_file.cursor_pos - min).unwrap().to_string(), theme_info.top_text, theme_info.top, Some(0), Some(MONO_WIDTH)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//bottom blue band stuff
|
||||
//write mode
|
||||
instructions.push(DrawInstructions::Text([0, self.dimensions[1] - BAND_HEIGHT * 2 + 1], "times-new-romono".to_string(), self.mode.to_string(), theme_info.top_text, theme_info.top, Some(0), Some(MONO_WIDTH)));
|
||||
let file_status;
|
||||
if self.files.len() > 0 {
|
||||
file_status = self.files[self.current_file_index].name.clone();
|
||||
} else {
|
||||
file_status = "No file open".to_string();
|
||||
}
|
||||
instructions.push(DrawInstructions::Text([self.dimensions[0] - file_status.len() * (MONO_WIDTH as usize), self.dimensions[1] - BAND_HEIGHT * 2 + 1], "times-new-romono".to_string(), file_status, theme_info.top_text, theme_info.top, Some(0), Some(MONO_WIDTH)));
|
||||
//write command or bottom message
|
||||
if self.mode == Mode::Command {
|
||||
instructions.push(DrawInstructions::Text([0, self.dimensions[1] - BAND_HEIGHT], "times-new-romono".to_string(), ":".to_string() + &self.command.clone().unwrap_or("".to_string()), theme_info.top_text, theme_info.top, Some(0), Some(MONO_WIDTH)));
|
||||
} else if self.mode == Mode::Normal && self.bottom_message.is_some() {
|
||||
instructions.push(DrawInstructions::Text([0, self.dimensions[1] - BAND_HEIGHT], "times-new-romono".to_string(), self.bottom_message.clone().unwrap(), theme_info.top_text, theme_info.top, Some(0), Some(MONO_WIDTH)));
|
||||
}
|
||||
instructions
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Malvim".to_string()
|
||||
}
|
||||
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::Window
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[410, 410]
|
||||
}
|
||||
|
||||
fn resizable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Malvim {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn calc_top_line_pos(&mut self) {
|
||||
if self.files.len() == 0 {
|
||||
return;
|
||||
}
|
||||
//now, see if the line_pos is still visible from the top_line_pos,
|
||||
//if not, move top_line_pos down until it is
|
||||
let current_file = &self.files[self.current_file_index];
|
||||
let actual_line_pos = self.current.actual_lines.iter().position(|l| l.1 == current_file.line_pos).unwrap();
|
||||
if current_file.top_line_pos + self.current.max_lines < actual_line_pos {
|
||||
self.files[self.current_file_index].top_line_pos = actual_line_pos.checked_sub(self.current.max_lines - 1).unwrap_or(0);
|
||||
} else if actual_line_pos < current_file.top_line_pos {
|
||||
self.files[self.current_file_index].top_line_pos = actual_line_pos;
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_current(&mut self) {
|
||||
if self.files.len() == 0 {
|
||||
return;
|
||||
}
|
||||
let current_file = &self.files[self.current_file_index];
|
||||
let line_num_width = current_file.content.len().to_string().len() * MONO_WIDTH as usize;
|
||||
let max_chars_per_line = (self.dimensions[0] - line_num_width - PADDING * 2) / MONO_WIDTH as usize;
|
||||
let actual_lines = calc_actual_lines(current_file.content.iter(), max_chars_per_line, true);
|
||||
let max_lines = (self.dimensions[1] - BAND_HEIGHT * 3 - PADDING) / LINE_HEIGHT;
|
||||
self.current = Current {
|
||||
actual_lines,
|
||||
line_num_width,
|
||||
max_lines,
|
||||
max_chars_per_line,
|
||||
};
|
||||
}
|
||||
|
||||
fn process_command(&mut self) -> bool {
|
||||
let mut parts = self.command.as_ref().unwrap().split(" ");
|
||||
let first = parts.next().unwrap();
|
||||
let arg = parts.next().unwrap_or("");
|
||||
if first == "e" || first == "edit" || ((first == "t" || first == "tabe") && self.files.len() > 0) {
|
||||
//find the file and open it
|
||||
let mut failed = false;
|
||||
let mut new_path = if self.files.len() > 0 {
|
||||
PathBuf::from(self.files[self.current_file_index].path.clone()).parent().unwrap().to_path_buf()
|
||||
} else {
|
||||
PathBuf::from("/")
|
||||
};
|
||||
for part in arg.split("/") {
|
||||
if part == ".." {
|
||||
if let Some(parent) = new_path.parent() {
|
||||
new_path = parent.to_path_buf();
|
||||
} else {
|
||||
failed = true;
|
||||
}
|
||||
} else {
|
||||
new_path.push(part);
|
||||
}
|
||||
}
|
||||
if !failed && new_path.is_file() {
|
||||
let name = new_path.file_name().unwrap().to_string_lossy().into_owned();
|
||||
let path = new_path.to_string_lossy().into_owned();
|
||||
if let Ok(content) = read_to_string(new_path) {
|
||||
let file_info = FileInfo {
|
||||
name,
|
||||
path,
|
||||
changed: false,
|
||||
top_line_pos: 0,
|
||||
line_pos: 0,
|
||||
cursor_pos: 0,
|
||||
content: content.split("\n").map(|s| s.to_string()).collect(),
|
||||
};
|
||||
if first == "e" || first == "edit" {
|
||||
if self.files.len() > 0 {
|
||||
self.files[self.current_file_index] = file_info;
|
||||
} else {
|
||||
self.files.push(file_info);
|
||||
}
|
||||
} else {
|
||||
//t(abe)
|
||||
self.current_file_index += 1;
|
||||
if self.current_file_index == self.files.len() - 1 {
|
||||
self.files.push(file_info);
|
||||
} else {
|
||||
self.files.insert(self.current_file_index, file_info);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
self.bottom_message = Some("Failed to open that file".to_string());
|
||||
}
|
||||
} else {
|
||||
self.bottom_message = Some("That is not a file or does not exist".to_string());
|
||||
}
|
||||
} else if self.files.len() == 0 {
|
||||
self.bottom_message = Some("No files are open, so can only do :e(dit)".to_string());
|
||||
} else if first == "w" || first == "write" {
|
||||
let current_file = &self.files[self.current_file_index];
|
||||
write(¤t_file.path, ¤t_file.content.join("\n"));
|
||||
self.files[self.current_file_index].changed = false;
|
||||
} else if first == "q" || first == "quit" {
|
||||
self.files.remove(self.current_file_index);
|
||||
self.current_file_index = self.current_file_index.checked_sub(1).unwrap_or(0);
|
||||
} else if first == "p" || first == "tabp" {
|
||||
self.current_file_index = self.current_file_index.checked_sub(1).unwrap_or(self.files.len() - 1);
|
||||
return true;
|
||||
} else if first == "n" || first == "tabn" {
|
||||
self.current_file_index += 1;
|
||||
if self.current_file_index == self.files.len() {
|
||||
self.current_file_index = 0;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
self.bottom_message = Some("Not a command".to_string());
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(Malvim::new());
|
||||
}
|
||||
|
||||
356
src/bin/minesweeper.rs
Normal file
356
src/bin/minesweeper.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::collections::VecDeque;
|
||||
use core::convert::TryFrom;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
const HEX_CHARS: [char; 16] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'];
|
||||
|
||||
fn u8_to_hex(u: u8) -> String {
|
||||
let mut h = String::new();
|
||||
h.push(HEX_CHARS[(u / 16) as usize]);
|
||||
h.push(HEX_CHARS[(u % 16) as usize]);
|
||||
h
|
||||
}
|
||||
|
||||
fn hex_to_u8(c1: char, c2: char) -> u8 {
|
||||
(HEX_CHARS.iter().position(|c| c == &c1).unwrap() * 16 + HEX_CHARS.iter().position(|c| c == &c2).unwrap()) as u8
|
||||
}
|
||||
|
||||
//16x16 with 40 mines
|
||||
|
||||
#[derive(Default)]
|
||||
struct MineTile {
|
||||
mine: bool,
|
||||
revealed: bool,
|
||||
touching: u8,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq)]
|
||||
enum State {
|
||||
#[default]
|
||||
Seed,
|
||||
BeforePlaying,
|
||||
Playing,
|
||||
Won,
|
||||
Lost,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Minesweeper {
|
||||
dimensions: Dimensions,
|
||||
state: State,
|
||||
tiles: [[MineTile; 16]; 16],
|
||||
random_chars: String,
|
||||
random_seed: u32, //user types in random keyboard stuff at beginning
|
||||
first_char: char, //defaults to '\0'
|
||||
}
|
||||
|
||||
impl WindowLike for Minesweeper {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
if self.state == State::Seed {
|
||||
if self.random_chars.len() == 4 {
|
||||
let mut r_chars = self.random_chars.chars();
|
||||
self.random_seed = ((r_chars.next().unwrap() as u8 as u32) << 24) | ((r_chars.next().unwrap() as u8 as u32) << 16) | ((r_chars.next().unwrap() as u8 as u32) << 8) | (r_chars.next().unwrap() as u8 as u32);
|
||||
self.random_chars = String::new();
|
||||
self.state = State::BeforePlaying;
|
||||
} else {
|
||||
if u8::try_from(key_press.key).is_ok() {
|
||||
self.random_chars.push(key_press.key);
|
||||
}
|
||||
}
|
||||
WindowMessageResponse::JustRerender
|
||||
} else if self.state == State::BeforePlaying || self.state == State::Playing {
|
||||
if key_press.key == '𐘁' { //backspace
|
||||
self.first_char = '\0';
|
||||
WindowMessageResponse::DoNothing
|
||||
} else if self.first_char == '\0' {
|
||||
if HEX_CHARS.iter().find(|c| c == &&key_press.key).is_some() {
|
||||
self.first_char = key_press.key;
|
||||
}
|
||||
WindowMessageResponse::DoNothing
|
||||
} else if HEX_CHARS.iter().find(|c| c == &&key_press.key).is_some() {
|
||||
let u = hex_to_u8(self.first_char, key_press.key) as usize;
|
||||
let y = u / 16;
|
||||
let x = u % 16;
|
||||
if HEX_CHARS.iter().find(|c| c == &&key_press.key).is_some() {
|
||||
if self.state == State::BeforePlaying {
|
||||
loop {
|
||||
self.new_tiles();
|
||||
if self.tiles[y][x].touching == 0 && !self.tiles[y][x].mine {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state = State::Playing;
|
||||
//if that tile not reveal it, reveal it and all adjacent zero touching squares, etc
|
||||
if self.tiles[y][x].mine {
|
||||
self.tiles[y][x].revealed = true;
|
||||
self.state = State::Lost;
|
||||
} else if self.tiles[y][x].touching == 0 {
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back([x, y]);
|
||||
let mut to_reveal = Vec::new();
|
||||
while queue.len() > 0 {
|
||||
let current = queue.pop_front().unwrap();
|
||||
self.on_adjacent_tiles(current[0], current[1], |x2, y2| {
|
||||
if !queue.contains(&[x2, y2]) && !to_reveal.contains(&[x2, y2]) {
|
||||
if self.tiles[y2][x2].touching == 0 {
|
||||
queue.push_back([x2, y2]);
|
||||
} else {
|
||||
to_reveal.push([x2, y2]);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
to_reveal.push(current);
|
||||
}
|
||||
for r in to_reveal {
|
||||
self.tiles[r[1]][r[0]].revealed = true;
|
||||
}
|
||||
} else {
|
||||
self.tiles[y][x].revealed = true;
|
||||
}
|
||||
self.first_char = '\0';
|
||||
if self.state != State::Lost {
|
||||
//check for win
|
||||
let mut won = true;
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let tile = &self.tiles[y][x];
|
||||
if !tile.revealed && !tile.mine {
|
||||
won = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if won {
|
||||
self.state = State::Won;
|
||||
}
|
||||
}
|
||||
WindowMessageResponse::JustRerender
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
} else {
|
||||
self.tiles = Default::default();
|
||||
self.state = State::Seed;
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
if self.state == State::Seed {
|
||||
vec![
|
||||
DrawInstructions::Text([4, 4], "times-new-roman".to_string(), "Type in random characters to initalise the seed".to_string(), theme_info.text, theme_info.background, None, None),
|
||||
DrawInstructions::Text([4, 4 + 16], "times-new-roman".to_string(), self.random_chars.clone(), theme_info.text, theme_info.background, None, None),
|
||||
]
|
||||
} else {
|
||||
let mut instructions = vec![
|
||||
//top border
|
||||
DrawInstructions::Rect([1, 0], [self.dimensions[0] - 7, 5], [128, 128, 128]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 0], [4, 1], [128, 128, 128]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 1], [3, 1], [128, 128, 128]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 2], [2, 1], [128, 128, 128]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 3], [1, 1], [128, 128, 128]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 4], [1, 1], [128, 128, 128]),
|
||||
//left border
|
||||
DrawInstructions::Rect([1, 0], [5, self.dimensions[1] - 5], [128, 128, 128]),
|
||||
DrawInstructions::Rect([1, self.dimensions[1] - 5], [1, 4], [128, 128, 128]),
|
||||
DrawInstructions::Rect([2, self.dimensions[1] - 5], [1, 3], [128, 128, 128]),
|
||||
DrawInstructions::Rect([3, self.dimensions[1] - 5], [1, 2], [128, 128, 128]),
|
||||
DrawInstructions::Rect([4, self.dimensions[1] - 5], [1, 1], [128, 128, 128]),
|
||||
//bottom border
|
||||
DrawInstructions::Rect([6, self.dimensions[1] - 6], [self.dimensions[0] - 2, 5], [255, 255, 255]),
|
||||
DrawInstructions::Rect([5, self.dimensions[1] - 5], [1, 4], [255, 255, 255]),
|
||||
DrawInstructions::Rect([4, self.dimensions[1] - 4], [1, 3], [255, 255, 255]),
|
||||
DrawInstructions::Rect([3, self.dimensions[1] - 3], [1, 2], [255, 255, 255]),
|
||||
DrawInstructions::Rect([2, self.dimensions[1] - 2], [1, 1], [255, 255, 255]),
|
||||
//right border
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 5], [5, self.dimensions[1]], [255, 255, 255]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 2, 0], [1, 5], [255, 255, 255]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 3, 1], [1, 4], [255, 255, 255]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 4, 2], [1, 3], [255, 255, 255]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 5, 3], [1, 2], [255, 255, 255]),
|
||||
DrawInstructions::Rect([self.dimensions[0] - 6, 4], [1, 1], [255, 255, 255]),
|
||||
];
|
||||
let tile_size = (self.dimensions[0] - 10) / 16;
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let tile = &self.tiles[y][x];
|
||||
if tile.revealed {
|
||||
if tile.mine {
|
||||
instructions.push(DrawInstructions::Text([x * tile_size + tile_size / 2 + 2, y * tile_size + tile_size / 2], "times-new-roman".to_string(), "x".to_string(), [255, 0, 0], theme_info.background, None, None));
|
||||
} else {
|
||||
let color = match tile.touching {
|
||||
1 => [0, 0, 255],
|
||||
2 => [0, 255, 0],
|
||||
3 => [255, 0, 0],
|
||||
4 => [128, 0, 128],
|
||||
5 => [176, 48, 96],
|
||||
6 => [127, 255, 212],
|
||||
7 => [0, 0, 0],
|
||||
//8
|
||||
_ => [128, 128, 128],
|
||||
};
|
||||
instructions.push(DrawInstructions::Text([x * tile_size + tile_size / 2 + 5, y * tile_size + tile_size / 2 + 2], "times-new-roman".to_string(), tile.touching.to_string(), color, theme_info.background, None, None));
|
||||
}
|
||||
} else {
|
||||
let top_left = [x * tile_size + 6, y * tile_size + 5];
|
||||
//do not do the corners in respect of our poor poor heap (vector size too big would be bad)
|
||||
instructions.extend(vec![
|
||||
//top border
|
||||
DrawInstructions::Rect([top_left[0], top_left[1]], [tile_size - 3, 3], [255, 255, 255]),
|
||||
//
|
||||
//left border
|
||||
DrawInstructions::Rect([top_left[0], top_left[1]], [3, tile_size - 3], [255, 255, 255]),
|
||||
//
|
||||
//bottom border
|
||||
DrawInstructions::Rect([top_left[0] + 3, top_left[1] + tile_size - 4], [tile_size - 4, 3], [128, 128, 128]),
|
||||
//
|
||||
//right bottom
|
||||
DrawInstructions::Rect([top_left[0] + tile_size - 4, top_left[1] + 3], [3, tile_size - 4], [128, 128, 128]),
|
||||
//
|
||||
DrawInstructions::Text([x * tile_size + tile_size / 2 - 2, y * tile_size + tile_size / 2], "times-new-roman".to_string(), u8_to_hex((y * 16 + x) as u8), theme_info.text, theme_info.background, None, None),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.state == State::Lost {
|
||||
instructions.extend(vec![DrawInstructions::Text([4, 4], "times-new-roman".to_string(), "You LOST!!! Press a key to play again.".to_string(), theme_info.text, theme_info.background, None, None)]);
|
||||
} else if self.state == State::Won {
|
||||
instructions.extend(vec![DrawInstructions::Text([4, 4], "times-new-roman".to_string(), "You WON!!! Press a key to play again.".to_string(), theme_info.text, theme_info.background, None, None)]);
|
||||
}
|
||||
instructions
|
||||
}
|
||||
}
|
||||
|
||||
//properties
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Minesweeper".to_string()
|
||||
}
|
||||
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::Window
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[410, 410]
|
||||
}
|
||||
}
|
||||
|
||||
impl Minesweeper {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
//https://en.wikipedia.org/wiki/Xorshift
|
||||
//from 0 to 15
|
||||
pub fn random(&mut self) -> usize {
|
||||
let mut x = self.random_seed;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
self.random_seed = x;
|
||||
self.random_seed as usize % 16
|
||||
}
|
||||
|
||||
pub fn on_adjacent_tiles(&self, x: usize, y: usize, mut action: impl FnMut(usize, usize) -> (), if_mine: bool) {
|
||||
if y > 0 {
|
||||
//above
|
||||
if self.tiles[y - 1][x].mine == if_mine {
|
||||
action(x, y - 1);
|
||||
}
|
||||
if x > 0 {
|
||||
//above to the left
|
||||
if self.tiles[y - 1][x - 1].mine == if_mine {
|
||||
action(x - 1, y - 1);
|
||||
}
|
||||
}
|
||||
if x < 15 {
|
||||
//above to the right
|
||||
if self.tiles[y - 1][x + 1].mine == if_mine {
|
||||
action(x + 1, y - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if x > 0 {
|
||||
//to the left
|
||||
if self.tiles[y][x - 1].mine == if_mine {
|
||||
action(x - 1, y);
|
||||
}
|
||||
}
|
||||
if x < 15 {
|
||||
//to the right
|
||||
if self.tiles[y][x + 1].mine == if_mine {
|
||||
action(x + 1, y);
|
||||
}
|
||||
}
|
||||
if y < 15 {
|
||||
//below
|
||||
if self.tiles[y + 1][x].mine == if_mine {
|
||||
action(x, y + 1);
|
||||
}
|
||||
if x > 0 {
|
||||
//below to the left
|
||||
if self.tiles[y + 1][x - 1].mine == if_mine {
|
||||
action(x - 1, y + 1);
|
||||
}
|
||||
}
|
||||
if x < 15 {
|
||||
//below to the right
|
||||
if self.tiles[y + 1][x + 1].mine == if_mine {
|
||||
action(x + 1, y + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_tiles(&mut self) {
|
||||
self.tiles = Default::default();
|
||||
//40 mines
|
||||
for _ in 0..40 {
|
||||
loop {
|
||||
let x = self.random();
|
||||
let y = self.random();
|
||||
//
|
||||
if !self.tiles[y][x].mine {
|
||||
self.tiles[y][x].mine = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//calculate touching
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let mut touching = 0;
|
||||
self.on_adjacent_tiles(x, y, |_, _| {
|
||||
touching += 1;
|
||||
}, true);
|
||||
self.tiles[y][x].touching = touching;
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(Minesweeper::new());
|
||||
}
|
||||
|
||||
204
src/bin/start_menu.rs
Normal file
204
src/bin/start_menu.rs
Normal file
@@ -0,0 +1,204 @@
|
||||
#![allow(warnings)]
|
||||
|
||||
use std::vec;
|
||||
use std::vec::Vec;
|
||||
use std::boxed::Box;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest };
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::components::Component;
|
||||
use ming_wm::components::highlight_button::HighlightButton;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
//todo: move to essential
|
||||
|
||||
static CATEGORIES: [&'static str; 9] = ["About", "Utils", "Games", "Editing", "Files", "System", "Misc", "Help", "Logout"];
|
||||
|
||||
#[derive(Clone)]
|
||||
enum StartMenuMessage {
|
||||
CategoryClick(&'static str),
|
||||
WindowClick(&'static str),
|
||||
Back,
|
||||
ChangeAcknowledge,
|
||||
}
|
||||
|
||||
pub struct StartMenu {
|
||||
dimensions: Dimensions,
|
||||
components: Vec<Box<dyn Component<StartMenuMessage> + Send>>,
|
||||
current_focus: String,
|
||||
old_focus: String,
|
||||
y_each: usize,
|
||||
}
|
||||
|
||||
impl WindowLike for StartMenu {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
self.y_each = (self.dimensions[1] - 1) / CATEGORIES.len();
|
||||
self.add_category_components();
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
//up and down
|
||||
if key_press.key == 'k' || key_press.key == 'j' {
|
||||
let old_focus_index = self.get_focus_index().unwrap();
|
||||
self.components[old_focus_index].handle_message(WindowMessage::Unfocus);
|
||||
let current_focus_index = if key_press.key == 'j' {
|
||||
if old_focus_index + 1 == self.components.len() {
|
||||
0
|
||||
} else {
|
||||
old_focus_index + 1
|
||||
}
|
||||
} else {
|
||||
if old_focus_index == 0 {
|
||||
self.components.len() - 1
|
||||
} else {
|
||||
old_focus_index - 1
|
||||
}
|
||||
};
|
||||
self.old_focus = self.current_focus.to_string();
|
||||
self.current_focus = self.components[current_focus_index].name().to_string();
|
||||
self.components[current_focus_index].handle_message(WindowMessage::Focus);
|
||||
WindowMessageResponse::JustRerender
|
||||
} else if key_press.key == '𐘂' { //the enter key
|
||||
let focus_index = self.get_focus_index();
|
||||
if let Some(focus_index) = focus_index {
|
||||
let r = self.components[focus_index].handle_message(WindowMessage::FocusClick);
|
||||
self.handle_start_menu_message(r)
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
} else {
|
||||
let current_focus_index = self.get_focus_index().unwrap();
|
||||
if let Some(n_index) = self.components[current_focus_index..].iter().position(|c| c.name().chars().next().unwrap_or('𐘂').to_lowercase().next().unwrap() == key_press.key) {
|
||||
//now old focus, not current focus
|
||||
self.components[current_focus_index].handle_message(WindowMessage::Unfocus);
|
||||
self.old_focus = self.current_focus.clone();
|
||||
self.current_focus = self.components[current_focus_index + n_index].name().to_string();
|
||||
self.components[current_focus_index + n_index].handle_message(WindowMessage::Focus);
|
||||
WindowMessageResponse::JustRerender
|
||||
} else {
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = vec![
|
||||
//top thin border
|
||||
DrawInstructions::Rect([0, 0], [self.dimensions[0], 1], theme_info.border_left_top),
|
||||
//right thin border
|
||||
DrawInstructions::Rect([self.dimensions[0] - 1, 0], [1, self.dimensions[1]], theme_info.border_right_bottom),
|
||||
//background
|
||||
DrawInstructions::Rect([0, 1], [self.dimensions[0] - 1, self.dimensions[1] - 1], theme_info.background),
|
||||
//mingde logo
|
||||
DrawInstructions::Mingde([2, 2]),
|
||||
//I truly don't know why, it should be - 44 but - 30 seems to work better :shrug:
|
||||
DrawInstructions::Gradient([2, 42], [40, self.dimensions[1] - 30], [255, 201, 14], [225, 219, 77], 15),
|
||||
];
|
||||
for component in &self.components {
|
||||
instructions.extend(component.draw(theme_info));
|
||||
}
|
||||
instructions
|
||||
}
|
||||
|
||||
//properties
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::StartMenu
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[175, 250]
|
||||
}
|
||||
}
|
||||
|
||||
impl StartMenu {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
dimensions: [0, 0],
|
||||
components: Vec::new(),
|
||||
current_focus: String::new(), //placeholder, will be set in init
|
||||
old_focus: String::new(),
|
||||
y_each: 0, //will be set in add_category_components
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_start_menu_message(&mut self, message: Option<StartMenuMessage>) -> WindowMessageResponse {
|
||||
if let Some(message) = message {
|
||||
match message {
|
||||
StartMenuMessage::CategoryClick(name) => {
|
||||
if name == "Logout" {
|
||||
WindowMessageResponse::Request(WindowManagerRequest::Lock)
|
||||
} else {
|
||||
self.current_focus = "Back".to_string();
|
||||
self.components = vec![
|
||||
Box::new(HighlightButton::new(
|
||||
"Back".to_string(), [42, 1], [self.dimensions[0] - 42 - 1, self.y_each], "Back", StartMenuMessage::Back, StartMenuMessage::ChangeAcknowledge, true
|
||||
))
|
||||
];
|
||||
//add window buttons
|
||||
let mut to_add: Vec<&str> = Vec::new();
|
||||
if name == "Games" {
|
||||
to_add.push("Minesweeper");
|
||||
} else if name == "Editing" {
|
||||
to_add.push("Malvim");
|
||||
} else if name == "Utils" {
|
||||
to_add.push("Terminal");
|
||||
} else if name == "Files" {
|
||||
to_add.push("Audio Player");
|
||||
}
|
||||
//
|
||||
for a in 0..to_add.len() {
|
||||
let w_name = to_add[a];
|
||||
self.components.push(Box::new(HighlightButton::new(
|
||||
w_name.to_string(), [42, (a + 1) * self.y_each], [self.dimensions[0] - 42 - 1, self.y_each], w_name, StartMenuMessage::WindowClick(w_name), StartMenuMessage::ChangeAcknowledge, false
|
||||
)));
|
||||
}
|
||||
WindowMessageResponse::JustRerender
|
||||
}
|
||||
},
|
||||
StartMenuMessage::WindowClick(name) => {
|
||||
//open the selected window
|
||||
WindowMessageResponse::Request(WindowManagerRequest::OpenWindow(name.to_string()))
|
||||
},
|
||||
StartMenuMessage::Back => {
|
||||
self.add_category_components();
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
StartMenuMessage::ChangeAcknowledge => {
|
||||
//
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
}
|
||||
} else {
|
||||
//maybe should be JustRerender?
|
||||
WindowMessageResponse::DoNothing
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_category_components(&mut self) {
|
||||
self.current_focus = "About".to_string();
|
||||
self.components = Vec::new();
|
||||
for c in 0..CATEGORIES.len() {
|
||||
let name = CATEGORIES[c];
|
||||
self.components.push(Box::new(HighlightButton::new(
|
||||
name.to_string(), [42, self.y_each * c + 1], [self.dimensions[0] - 42 - 1, self.y_each], name, StartMenuMessage::CategoryClick(name), StartMenuMessage::ChangeAcknowledge, c == 0
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_focus_index(&self) -> Option<usize> {
|
||||
self.components.iter().filter(|c| c.focusable()).position(|c| c.name() == &self.current_focus)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(StartMenu::new());
|
||||
}
|
||||
|
||||
178
src/bin/terminal.rs
Normal file
178
src/bin/terminal.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::vec::Vec;
|
||||
use std::vec;
|
||||
use std::process::{ Command, Output };
|
||||
use std::str::from_utf8;
|
||||
use std::io;
|
||||
|
||||
use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType };
|
||||
use ming_wm::messages::{ WindowMessage, WindowMessageResponse };
|
||||
use ming_wm::framebuffer::Dimensions;
|
||||
use ming_wm::themes::ThemeInfo;
|
||||
use ming_wm::utils::concat_paths;
|
||||
use ming_wm::ipc::listen;
|
||||
|
||||
const MONO_WIDTH: u8 = 10;
|
||||
const LINE_HEIGHT: usize = 15;
|
||||
const PADDING: usize = 4;
|
||||
|
||||
enum CommandResponse {
|
||||
ActualCommand(io::Result<Output>),
|
||||
Custom,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Terminal {
|
||||
dimensions: Dimensions,
|
||||
lines: Vec<String>,
|
||||
actual_lines: Vec<String>, //wrapping
|
||||
actual_line_num: usize, //what line # is at the top, for scrolling
|
||||
current_input: String,
|
||||
current_path: String,
|
||||
}
|
||||
|
||||
//for some reason key presses, then moving the window leaves the old window still there, behind it. weird
|
||||
|
||||
impl WindowLike for Terminal {
|
||||
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
|
||||
match message {
|
||||
WindowMessage::Init(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
self.current_path = "/".to_string();
|
||||
self.lines = vec!["Mingde Terminal".to_string(), "".to_string()];
|
||||
self.calc_actual_lines();
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::ChangeDimensions(dimensions) => {
|
||||
self.dimensions = dimensions;
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
WindowMessage::KeyPress(key_press) => {
|
||||
if key_press.key == '𐘁' { //backspace
|
||||
if self.current_input.len() > 0 {
|
||||
self.current_input = self.current_input[..self.current_input.len() - 1].to_string();
|
||||
} else {
|
||||
return WindowMessageResponse::DoNothing;
|
||||
}
|
||||
} else if key_press.key == '𐘂' { //the enter key
|
||||
self.lines.push("$ ".to_string() + &self.current_input);
|
||||
if let CommandResponse::ActualCommand(maybe_output) = self.process_command() {
|
||||
if let Ok(output) = maybe_output {
|
||||
let write_output = if output.status.success() {
|
||||
output.stdout
|
||||
} else {
|
||||
output.stderr
|
||||
};
|
||||
for line in from_utf8(&write_output).unwrap_or("Failed to parse process output as utf-8").split("\n") {
|
||||
self.lines.push(line.to_string());
|
||||
}
|
||||
} else {
|
||||
self.lines.push("Failed to execute process".to_string());
|
||||
}
|
||||
}
|
||||
self.current_input = String::new();
|
||||
} else {
|
||||
self.current_input += &key_press.key.to_string();
|
||||
}
|
||||
self.calc_actual_lines();
|
||||
self.actual_line_num = self.actual_lines.len().checked_sub(self.get_max_lines()).unwrap_or(0);
|
||||
WindowMessageResponse::JustRerender
|
||||
},
|
||||
_ => WindowMessageResponse::DoNothing,
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
|
||||
let mut instructions = vec![
|
||||
DrawInstructions::Rect([0, 0], self.dimensions, theme_info.alt_background),
|
||||
];
|
||||
//add the visible lines of text
|
||||
let end_line = self.actual_line_num + self.get_max_lines();
|
||||
let mut text_y = PADDING;
|
||||
for line_num in self.actual_line_num..end_line {
|
||||
if line_num == self.actual_lines.len() {
|
||||
break;
|
||||
}
|
||||
let line = self.actual_lines[line_num].clone();
|
||||
instructions.push(DrawInstructions::Text([PADDING, text_y], "times-new-romono".to_string(), line, theme_info.alt_text, theme_info.alt_background, Some(0), Some(MONO_WIDTH)));
|
||||
text_y += LINE_HEIGHT;
|
||||
}
|
||||
instructions
|
||||
}
|
||||
|
||||
fn title(&self) -> String {
|
||||
"Terminal".to_string()
|
||||
}
|
||||
|
||||
fn subtype(&self) -> WindowLikeType {
|
||||
WindowLikeType::Window
|
||||
}
|
||||
|
||||
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
|
||||
[410, 410]
|
||||
}
|
||||
|
||||
fn resizable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn get_max_lines(&self) -> usize {
|
||||
(self.dimensions[1] - PADDING * 2) / LINE_HEIGHT
|
||||
}
|
||||
|
||||
fn process_command(&mut self) -> CommandResponse {
|
||||
if self.current_input.starts_with("clear ") || self.current_input == "clear" {
|
||||
self.lines = Vec::new();
|
||||
CommandResponse::Custom
|
||||
} else if self.current_input.starts_with("cd ") {
|
||||
let mut cd_split = self.current_input.split(" ");
|
||||
cd_split.next().unwrap();
|
||||
let arg = cd_split.next().unwrap();
|
||||
if let Ok(new_path) = concat_paths(&self.current_path, arg) {
|
||||
if new_path.exists() {
|
||||
self.current_path = new_path.to_str().unwrap().to_string();
|
||||
}
|
||||
}
|
||||
CommandResponse::Custom
|
||||
} else {
|
||||
CommandResponse::ActualCommand(Command::new("sh").arg("-c").arg(&self.current_input).current_dir(&self.current_path).output())
|
||||
}
|
||||
}
|
||||
|
||||
fn calc_actual_lines(&mut self) {
|
||||
self.actual_lines = Vec::new();
|
||||
let max_chars_per_line = (self.dimensions[0] - PADDING * 2) / MONO_WIDTH as usize;
|
||||
for line_num in 0..=self.lines.len() {
|
||||
let mut working_line = if line_num == self.lines.len() {
|
||||
"$ ".to_string() + &self.current_input + "█"
|
||||
} else {
|
||||
self.lines[line_num].clone()
|
||||
};
|
||||
//cannot index or do .len() because those count bytes not characters
|
||||
loop {
|
||||
if working_line.chars().count() <= max_chars_per_line {
|
||||
self.actual_lines.push(working_line);
|
||||
break;
|
||||
} else {
|
||||
let mut working_line_chars = working_line.chars();
|
||||
let mut push_string = String::new();
|
||||
for _i in 0..max_chars_per_line {
|
||||
push_string += &working_line_chars.next().unwrap().to_string();
|
||||
}
|
||||
self.actual_lines.push(push_string);
|
||||
working_line = working_line_chars.collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
listen(Terminal::new());
|
||||
}
|
||||
|
||||
11
src/bin/test.rs
Normal file
11
src/bin/test.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use std::process::{ Command, Stdio };
|
||||
use std::io::{ Read, Write };
|
||||
|
||||
fn main() {
|
||||
println!("a");
|
||||
let mut a = Command::new("cargo").arg("run").arg("-q").arg("--bin").arg("start_menu").stdout(Stdio::piped()).stdin(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap();
|
||||
a.stdin.unwrap().write_all("subtype\n".to_string().as_bytes());
|
||||
let mut output = String::new();
|
||||
a.stdout.as_mut().unwrap().read_to_string(&mut output);
|
||||
println!("{}", output);
|
||||
}
|
||||
Reference in New Issue
Block a user