diff --git a/.gitignore b/.gitignore index 9330d81..66b43b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ target/ Cargo.lock ming-wm password.txt +password.env bmps/ignore-*.bmp *.alpha diff --git a/Cargo.toml b/Cargo.toml index f07b05d..871e18e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ming-wm" version = "0.1.0" edition = "2024" -default-run = "main" +default-run = "ming" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -32,13 +32,32 @@ audio_player = [ "id3", "mp4ameta", "metaflac", "rand", "rodio" ] lto = true [[bin]] -name = "main" +name = "ming" +path = "src/bin/main.rs" required-features = [ "main" ] [[bin]] -name = "audio_player" +name = "mingFiles_Audio_Player" +path = "src/bin/audio_player.rs" required-features = [ "audio_player" ] [[bin]] -name = "terminal" +name = "mingGames_Minesweeper" +path = "src/bin/minesweeper.rs" + +[[bin]] +name = "mingUtils_Terminal" +path = "src/bin/terminal.rs" required-features = [ "terminal" ] + +[[bin]] +name = "mingFiles_File_Explorer" +path = "src/bin/file_explorer.rs" + +[[bin]] +name = "mingEditing_Malvim" +path = "src/bin/malvim.rs" + +[[bin]] +name = "mingGames_Reversi" +path = "src/bin/reversi.rs" diff --git a/README.md b/README.md index c2933ed..7dae94d 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,37 @@ Ming-wm is a keyboard-based, retro-themed window manager for Linux. It is single ![example 1](/docs/images/ws1.png) ![example 2](/docs/images/ws3.png) -## Running +## Building -Create a `password.txt` file in the same directory as `build.rs`, otherwise the default password will be "incorrect mule lightbulb niche". +Create a `password.env` file in the same directory as `build.rs`, otherwise the default password will be "incorrect mule lightbulb niche". For best performance: + ``` cargo build --release --all-features -# Either, -./target/release/main -# or -cargo run --release ``` +The user may need to be added to the `video` group. + Exclude `--all-features` if the audio player window is not needed. To compile and use the audio player window, ALSA dev packages need to be installed (`alsa-lib-dev` on Alpine, `libasound2-dev` on Debian, `alsa-lib-devl` on Fedora, already included with `alsa-lib` on Arch). -### Running on Mobile Linux +## Installing + +After building, to properly install ming-wm, run the following to put the necessary binaries, font data, and bmp files into `/usr/local/bin`: + +```bash +chmod +x ./install +sudo ./install +``` + +Alternatively, to move the binaries to `~/.local/bin` (which probably needs to be added to `PATH`, run the following: + +```bash +chmod +x local-install +sudo ./local-install +``` + +## Running on Mobile Linux Running with an onscreen keyboard. The framebuffer may not be redrawn to the screen without a (real) key press. The volume down button seems to work. @@ -26,22 +41,30 @@ Running with an onscreen keyboard. The framebuffer may not be redrawn to the scr ``` cargo build --release -./target/release/main touch +./target/release/ming touch ``` Optionally, in landscape mode (todo: osk may be broken in landscape mode): ``` cargo build --release -./target/release/main touch rotate +./target/release/ming touch rotate ``` +After testing, the install scripts in the previous section can be used. + ![mobile example](/docs/images/mobile.png) ## Philosophy See [/docs/philosophy.md](/docs/philosophy.md) for some hopefully interesting ramblings. +## Security + +Make sure the permissions of `password.env` are so other users cannot read or write to it. If there is no plan to recompile, just delete it. + +Obviously, don't run the executable with `sudo` or `doas`, or as the root user! + ## License Licensed under the GPLv3. The font data in the `bmps/shippori-mincho` folder are covered by the OFL. The font was created by FONTDASU. The font data in `bmps/nimbus-roman` are licensed under the AGPL. This is a very slightly modified version of the font was created by URW Studio. The font data in `bmps/nimbus-romono` is also licensed under the AGPL. This is a slightly modified version of the Nimbus Roman font by URW Studio. diff --git a/build.rs b/build.rs index 4db1875..d3bd018 100644 --- a/build.rs +++ b/build.rs @@ -2,6 +2,7 @@ use std::fs::{ read_dir, read_to_string, write, File }; use std::io::Write; use std::env; use std::path::Path; +use std::process::Command; use blake2::{ Blake2b512, Digest }; use bmp_rust::bmp::BMP; @@ -48,7 +49,7 @@ fn font_chars_to_alphas(dir: &str) { fn main() { //hash + "salt" password and add to build - let password = read_to_string("password.txt").unwrap_or("incorrect mule lightbulb niche".to_string()).replace("\n", "") + "salt?sorrycryptographers"; + let password = read_to_string("password.env").unwrap_or("incorrect mule lightbulb niche".to_string()).replace("\n", "") + "salt?sorrycryptographers"; let mut hasher = Blake2b512::new(); hasher.update(password.as_bytes()); let out_dir = env::var_os("OUT_DIR").unwrap(); @@ -61,4 +62,7 @@ fn main() { font_chars_to_alphas(path.to_str().unwrap()); } } + //copy bmp folders to target + let profile = env::var_os("PROFILE").unwrap().to_string_lossy().to_string(); + Command::new("cp").arg("-r").arg("./bmps").arg(format!("./target/{}/bmps", profile)).output().unwrap(); } diff --git a/docs/window-likes/desktop-background.md b/docs/window-likes/desktop-background.md index a03112c..a19989a 100644 --- a/docs/window-likes/desktop-background.md +++ b/docs/window-likes/desktop-background.md @@ -8,16 +8,16 @@ In `$XDG_CONFIG_DIR/ming-wm/desktop-background`, you can configure what the desk If a line starts with "#", and is followed by 6 lowercase hex characters, then it will interpreted as a RGB colour. -If a line starts with "r", then what follows with be interpreted as a path to a BMP image file in BGRA order, and if it starts with any other character, what follows will be interpreted as a path to a BMP image file in RGBA order. +If a line starts with "r", then what follows with be interpreted as a path to a BMP image file in BGRA order, and if it starts with any other character, what follows will be interpreted as a path to a BMP image file in RGBA order. The path should be absolute. Example: ``` #008080 #003153 -r./bmps/castle1440x842.bmp -r./bmps/ming1440x842.bmp -r./bmps/blur1440x842.bmp +r/home/username/Pictures/castle1440x842.bmp +r/home/username/Pictures/ming1440x842.bmp +r/home/username/Pictures/blur1440x842.bmp ``` ## Unrelated: Themes Config diff --git a/docs/window-likes/terminal.md b/docs/window-likes/terminal.md index ccd093e..e0c747c 100644 --- a/docs/window-likes/terminal.md +++ b/docs/window-likes/terminal.md @@ -15,6 +15,10 @@ Once a command is entered, hit 'enter' to execute it. The terminal will change i In STDIN mode, any keys typed followed by the 'enter' key will send those keys to the command's STDIN, if it is still running. To escape STDIN mode, use the `esc` key. +### Sudo + +To get sudo to read from stdin, the `-S` option will need to be used (eg, `sudo -S ls`). Also, the password prompt will not show since the terminal is line-buffered. Just switch to STDIN mode, type in the password and hit enter. + ## Copy / Paste This window-like supports the paste [shortcut](../system/shortcuts.md) (`Alt+P`) if in INPUT or STDIN mode. diff --git a/install b/install new file mode 100755 index 0000000..9750f60 --- /dev/null +++ b/install @@ -0,0 +1,9 @@ +#!/bin/bash +cp -r ./target/release/bmps /usr/local/bin/bmps +cp ./target/release/ming /usr/local/bin/ming +cp ./target/release/mingUtils_Terminal /usr/local/bin/mingUtils_Terminal +cp ./target/release/mingGames_Reversi /usr/local/bin/mingGames_Reversi +cp ./target/release/mingGames_Minesweeper /usr/local/bin/mingGames_Minesweeper +cp ./target/release/mingFiles_File_Explorer /usr/local/bin/mingFiles_File_Explorer +cp ./target/release/mingFiles_Audio_Player /usr/local/bin/mingFiles_Audio_Player +cp ./target/release/mingEditing_Malvim /usr/local/bin/mingEditing_Malvim diff --git a/local-install b/local-install new file mode 100755 index 0000000..3ef92eb --- /dev/null +++ b/local-install @@ -0,0 +1,9 @@ +#!/bin/bash +cp -r ./target/release/bmps ~/.local/bin/bmps +cp ./target/release/ming ~/.local/bin/ming +cp ./target/release/mingUtils_Terminal ~/.local/bin/mingUtils_Terminal +cp ./target/release/mingGames_Reversi ~/.local/bin/mingGames_Reversi +cp ./target/release/mingGames_Minesweeper ~/.local/bin/mingGames_Minesweeper +cp ./target/release/mingFiles_File_Explorer ~/.local/bin/mingFiles_File_Explorer +cp ./target/release/mingFiles_Audio_Player ~/.local/bin/mingFiles_Audio_Player +cp ./target/release/mingEditing_Malvim ~/.local/bin/mingEditing_Malvim diff --git a/src/bin/audio_player.rs b/src/bin/audio_player.rs index 8ce0834..7427ce9 100644 --- a/src/bin/audio_player.rs +++ b/src/bin/audio_player.rs @@ -242,6 +242,8 @@ impl AudioPlayer { if new_path.exists() { self.base_directory = new_path.to_str().unwrap().to_string(); return "Set new base directory".to_string(); + } else { + return "Failed to set new base directory".to_string(); } } } diff --git a/src/bin/terminal.rs b/src/bin/terminal.rs index 38c2ebb..894f150 100644 --- a/src/bin/terminal.rs +++ b/src/bin/terminal.rs @@ -11,7 +11,7 @@ use std::fmt; use pty_process::blocking; use ming_wm::window_manager::{ DrawInstructions, WindowLike, WindowLikeType }; -use ming_wm::messages::{ WindowMessage, WindowMessageResponse }; +use ming_wm::messages::{ WindowMessage, WindowMessageResponse, ShortcutType }; use ming_wm::framebuffer::Dimensions; use ming_wm::themes::ThemeInfo; use ming_wm::utils::{ concat_paths, Substring }; @@ -202,9 +202,9 @@ impl WindowLike for Terminal { ShortcutType::ClipboardPaste(copy_string) => { if self.mode == Mode::Input || self.mode == Mode::Stdin { if self.mode == Mode::Input { - self.current_input += copy_string; + self.current_input += ©_string; } else { - self.current_stdin_input += copy_string; + self.current_stdin_input += ©_string; } self.calc_actual_lines(); WindowMessageResponse::JustRedraw @@ -224,7 +224,7 @@ impl WindowLike for Terminal { 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() - 1; + 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() { @@ -261,7 +261,7 @@ impl Terminal { } fn get_max_lines(&self) -> usize { - (self.dimensions[1] - PADDING * 2) / LINE_HEIGHT + (self.dimensions[1] - PADDING * 2 - LINE_HEIGHT) / LINE_HEIGHT } fn process_command(&mut self) -> Mode { diff --git a/src/components/highlight_button.rs b/src/components/highlight_button.rs index 60399c9..57d9fcc 100644 --- a/src/components/highlight_button.rs +++ b/src/components/highlight_button.rs @@ -11,7 +11,7 @@ pub struct HighlightButton { name_: String, top_left: Point, size: Dimensions, - text: String, + pub text: String, pub highlighted: bool, click_return: T, toggle_highlight_return: T, //also unhighlight return diff --git a/src/dirs.rs b/src/dirs.rs index 39234eb..11efe4e 100644 --- a/src/dirs.rs +++ b/src/dirs.rs @@ -38,3 +38,12 @@ pub fn config_dir() -> Option { } } +pub fn exe_dir(add: Option<&str>) -> PathBuf { + let mut exe_dir = env::current_exe().unwrap(); + exe_dir.pop(); + if let Some(add) = add { + exe_dir.push(add); + } + exe_dir +} + diff --git a/src/essential/start_menu.rs b/src/essential/start_menu.rs index e9b7367..9292fa2 100644 --- a/src/essential/start_menu.rs +++ b/src/essential/start_menu.rs @@ -6,15 +6,17 @@ use crate::window_manager::{ DrawInstructions, WindowLike, WindowLikeType }; use crate::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest }; use crate::framebuffer::Dimensions; use crate::themes::ThemeInfo; +use crate::fs::{ ExeWindowInfos, get_all_executable_windows }; use crate::components::Component; use crate::components::highlight_button::HighlightButton; +use crate::dirs::exe_dir; static CATEGORIES: [&'static str; 9] = ["About", "Utils", "Games", "Editing", "Files", "Internet", "Misc", "Help", "Logout"]; #[derive(Clone)] enum StartMenuMessage { CategoryClick(&'static str), - WindowClick(&'static str), + WindowClick(String), Back, ChangeAcknowledge, } @@ -22,7 +24,8 @@ enum StartMenuMessage { #[derive(Default)] pub struct StartMenu { dimensions: Dimensions, - components: Vec + Send>>, + executable_windows: ExeWindowInfos, + components: Vec>>, current_focus: String, y_each: usize, } @@ -34,6 +37,7 @@ impl WindowLike for StartMenu { self.dimensions = dimensions; self.y_each = (self.dimensions[1] - 1) / CATEGORIES.len(); self.add_category_components(); + self.executable_windows = get_all_executable_windows(); WindowMessageResponse::JustRedraw }, WindowMessage::KeyPress(key_press) => { @@ -69,7 +73,7 @@ impl WindowLike for StartMenu { let current_focus_index = self.get_focus_index().unwrap(); if key_press.key.is_lowercase() { //look forwards to see category/window that starts with that char - 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) { + if let Some(n_index) = self.components[current_focus_index..].iter().position(|c| c.text.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.current_focus = self.components[current_focus_index + n_index].name().to_string(); @@ -80,7 +84,7 @@ impl WindowLike for StartMenu { } } else { //look backwards to see category/window that starts with that char - if let Some(n_index) = self.components[..current_focus_index].iter().rev().position(|c| c.name().chars().next().unwrap_or('𐘂').to_uppercase().next().unwrap() == key_press.key) { + if let Some(n_index) = self.components[..current_focus_index].iter().rev().position(|c| c.text.chars().next().unwrap_or('𐘂').to_uppercase().next().unwrap() == key_press.key) { //now old focus, not current focus self.components[current_focus_index].handle_message(WindowMessage::Unfocus); self.current_focus = self.components[current_focus_index - n_index - 1].name().to_string(); @@ -105,7 +109,7 @@ impl WindowLike for StartMenu { //background DrawInstructions::Rect([0, 1], [self.dimensions[0] - 1, self.dimensions[1] - 1], theme_info.background), //mingde logo - DrawInstructions::Bmp([2, 2], "./bmps/mingde.bmp".to_string(), false), + DrawInstructions::Bmp([2, 2], exe_dir(Some("bmps/mingde.bmp")).to_string_lossy().to_string(), false), //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), ]; @@ -147,22 +151,12 @@ impl StartMenu { )) ]; //add window buttons - let mut to_add: Vec<&str> = Vec::new(); - if name == "Games" { - to_add.extend(["Minesweeper", "Reversi"]); - } else if name == "Editing" { - to_add.push("Malvim"); - } else if name == "Utils" { - to_add.push("Terminal"); - } else if name == "Files" { - to_add.extend(["File Explorer", "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.to_string(), StartMenuMessage::WindowClick(w_name), StartMenuMessage::ChangeAcknowledge, false - ))); + if let Some(to_add) = self.executable_windows.get(&("ming".to_string() + name)) { + for a in 0..to_add.len() { + self.components.push(Box::new(HighlightButton::new( + to_add[a].1.to_string(), [42, (a + 1) * self.y_each], [self.dimensions[0] - 42 - 1, self.y_each], to_add[a].0.to_string(), StartMenuMessage::WindowClick(to_add[a].1.clone()), StartMenuMessage::ChangeAcknowledge, false + ))); + } } WindowMessageResponse::JustRedraw } diff --git a/src/fs.rs b/src/fs.rs index a4686eb..9259e8c 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1,6 +1,10 @@ use std::fs::{ read_dir, File }; use std::path::PathBuf; use std::io::Read; +use std::collections::HashMap; + +use crate::dirs; +use crate::utils::get_rest_of_split; fn get_font_char(dir: &str, c: char) -> Option<(char, Vec>, u8)> { let c = if c == '/' { '𐘋' } else if c == '\\' { '𐚆' } else if c == '.' { '𐘅' } else { c }; @@ -20,12 +24,14 @@ fn get_font_char(dir: &str, c: char) -> Option<(char, Vec>, u8)> { pub fn get_font_char_from_fonts(fonts: &[String], c: char) -> (char, Vec>, u8) { for font in fonts { - if let Some(font_char) = get_font_char(&("./bmps/".to_string() + font), c) { + let p = dirs::exe_dir(Some(&("bmps/".to_string() + &font))).to_string_lossy().to_string(); + if let Some(font_char) = get_font_char(&p, c) { return font_char; } } + let p = dirs::exe_dir(Some(&("bmps/".to_string() + &fonts[0]))).to_string_lossy().to_string(); //so a ? char must be in every font - get_font_char(&("./bmps/".to_string() + &fonts[0]), '?').unwrap() + get_font_char(&p, '?').unwrap() } pub fn get_all_files(dir: PathBuf) -> Vec { @@ -41,3 +47,26 @@ pub fn get_all_files(dir: PathBuf) -> Vec { files } +//Category, Vec +pub type ExeWindowInfos = HashMap>; + +//well, doesn't actually look to see if its executable. Just if it contains a _ and has no file extension, and is a file +pub fn get_all_executable_windows() -> ExeWindowInfos { + let mut exes = HashMap::new(); + for entry in read_dir(dirs::exe_dir(None)).unwrap() { + let pb = entry.unwrap().path(); + if pb.is_file() && pb.extension().is_none() { + let parts = pb.file_stem().unwrap().to_string_lossy().to_string(); + let mut parts = parts.split('_'); + let category = parts.next().unwrap(); + let display = get_rest_of_split(&mut parts, Some(" ")); + let file_name = pb.file_name().unwrap().to_string_lossy().to_string(); + if display != String::new() && category.starts_with("ming") { + let pair = (display, file_name); + exes.entry(category.to_string()).and_modify(|v: &mut Vec<(String, String)>| (*v).push(pair.clone())).or_insert(vec![pair]); + } + } + } + exes +} + diff --git a/src/proxy_window_like.rs b/src/proxy_window_like.rs index e94fb80..68f7d51 100644 --- a/src/proxy_window_like.rs +++ b/src/proxy_window_like.rs @@ -2,12 +2,12 @@ use std::vec::Vec; use std::process::{ Command, Child, Stdio }; use std::io::{ BufReader, BufRead, Write }; use std::cell::RefCell; -use std::path::Path; use crate::window_manager::{ DrawInstructions, WindowLike, WindowLikeType }; use crate::messages::{ WindowMessage, WindowMessageResponse }; use crate::framebuffer::Dimensions; use crate::themes::ThemeInfo; +use crate::dirs; use crate::serialize::{ Serializable, DrawInstructionsVec }; pub struct ProxyWindowLike { @@ -74,18 +74,10 @@ impl Drop for ProxyWindowLike { } impl ProxyWindowLike { - pub fn new(command: &mut Command) -> Self { + pub fn new(name: &str) -> Self { + let loc = dirs::exe_dir(Some(name)).to_string_lossy().to_string(); ProxyWindowLike { - process: RefCell::new(command.stdout(Stdio::piped()).stdin(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap()), - } - } - - pub fn new_rust(file: &str) -> Self { - let loc = format!("./target/release/{}", file); - if Path::new(&loc).exists() { - ProxyWindowLike::new(Command::new(loc).stdout(Stdio::piped()).stdin(Stdio::piped()).stderr(Stdio::null())) - } else { - ProxyWindowLike::new(Command::new("cargo").arg("run").arg("--quiet").arg("--release").arg("--bin").arg(file).stdout(Stdio::piped()).stdin(Stdio::piped()).stderr(Stdio::null())) + process: RefCell::new(Command::new(loc).stdout(Stdio::piped()).stdin(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap()), } } diff --git a/src/serialize.rs b/src/serialize.rs index d1f901b..bac1707 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -4,6 +4,7 @@ use crate::themes::ThemeInfo; use crate::messages::{ WindowMessageResponse, WindowManagerRequest, KeyPress, WindowMessage, Direction, ShortcutType, InfoType }; use crate::window_manager::{ KeyChar, DrawInstructions, WindowLikeType }; use crate::framebuffer::Dimensions; +use crate::utils::get_rest_of_split; //serde + ron but worse! yay //not same as ron - simplified @@ -33,21 +34,6 @@ fn option_to_string(option: &Option) -> String { } } -fn get_rest_of_split(split: &mut dyn Iterator, sep: Option<&str>) -> String { - let mut rest = String::new(); - let mut n = split.next(); - loop { - rest += &n.unwrap(); - n = split.next(); - if n.is_some() { - rest += sep.unwrap_or(""); - } else { - break; - } - } - rest -} - fn get_color(serialized: &str) -> Result<[u8; 3], ()> { let rgb = serialized.split("\x1F"); let mut color = [0; 3]; diff --git a/src/utils.rs b/src/utils.rs index 11504ad..d7a17e2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -157,3 +157,19 @@ pub fn point_inside(point: Point, top_left: Point, size: Dimensions) -> bool { x >= x2 && y >= y2 && x <= x3 && y <= y3 } +pub fn get_rest_of_split(split: &mut dyn Iterator, sep: Option<&str>) -> String { + let mut rest = String::new(); + let mut n = split.next(); + loop { + if n.is_none() { + break; + } + rest += &n.unwrap(); + n = split.next(); + if n.is_some() && sep.is_some() { + rest += sep.unwrap(); + } + } + rest +} + diff --git a/src/window_manager.rs b/src/window_manager.rs index dc60a0c..e816b83 100644 --- a/src/window_manager.rs +++ b/src/window_manager.rs @@ -633,16 +633,10 @@ impl WindowManager { return; } let w: Option = match w.as_str() { - "Minesweeper" => Some(Box::new(ProxyWindowLike::new_rust("minesweeper"))), - "Reversi" => Some(Box::new(ProxyWindowLike::new_rust("reversi"))), - "Malvim" => Some(Box::new(ProxyWindowLike::new_rust("malvim"))), - "Terminal" => Some(Box::new(ProxyWindowLike::new_rust("terminal"))), - "Audio Player" => Some(Box::new(ProxyWindowLike::new_rust("audio_player"))), - "File Explorer" => Some(Box::new(ProxyWindowLike::new_rust("file_explorer"))), "StartMenu" => Some(Box::new(StartMenu::new())), "About" => Some(Box::new(About::new())), "Help" => Some(Box::new(Help::new())), - _ => None, + _ => Some(Box::new(ProxyWindowLike::new(&w))), }; if w.is_none() { return;