6 Commits

Author SHA1 Message Date
c4876e5606 v1.2.3: add arg to fix framebuffer not redrawing
on some devices, the framebuffer will not redraw unless something is written to stdout, so the arg 'force-stdout' has been added to write spaces to stdout after every key press or touch event
2025-11-03 00:39:59 +00:00
Jon Dough
497beb7bb0 change video url 2025-09-28 04:41:06 +00:00
stjet
40f6795163 v1.2.2: text measuring improvements, minor code clean
fixed some clippy warnings, ignored many that I do not care about. also, added demo video and updated koxinga image
2025-09-28 04:39:41 +00:00
stjet
10daa9982b v1.2.1: text measuring support
several malvim features added, font data caching, taskbar title overflow fix, new background
2025-09-06 06:23:57 +00:00
stjet
08c2358bdc v1.2
random lockscreen message, remove rand dep for audio player, add version to about window, add o/O to malvim, add circles to draw, bug fixes, minor byte savings for font .alpha format
2025-08-18 17:27:16 +00:00
stjet
2c4455f623 lines, barebones drawing window
addition of lines means ipc slightly changed, though can be ignored. also, minor malvim fix
2025-08-12 06:54:21 +00:00
40 changed files with 847 additions and 233 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "ming-wm" name = "ming-wm"
version = "1.1.0" version = "1.2.3"
repository = "https://github.com/stjet/ming-wm" repository = "https://github.com/stjet/ming-wm"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
edition = "2021" edition = "2021"
@@ -11,6 +11,20 @@ default-run = "ming"
[workspace] [workspace]
members = [ "wm", "linux" ] members = [ "wm", "linux" ]
[workspace.lints.clippy]
len_zero = "allow"
comparison_to_empty = "allow"
manual_saturating_arithmetic = "allow"
result_unit_err = "allow"
needless_borrow = "allow"
needless_borrows_for_generic_args = "allow"
redundant_static_lifetimes = "allow"
collapsible_else_if = "allow"
too_many_arguments = "allow"
[lints]
workspace = true
[build-dependencies] [build-dependencies]
bmp-rust = "0.5.0" bmp-rust = "0.5.0"
bitcoin_hashes = { version = "0.16.0", default-features = false } bitcoin_hashes = { version = "0.16.0", default-features = false }
@@ -20,7 +34,6 @@ ming-wm-lib = { path = "ming-wm-lib" }
wm = { path = "wm", optional = true } wm = { path = "wm", optional = true }
linux = { path = "linux", optional = true } linux = { path = "linux", optional = true }
rodio = { version = "0.19.0", default-features = false, features = [ "flac", "mp3", "symphonia-vorbis", "wav" ], optional = true } rodio = { version = "0.19.0", default-features = false, features = [ "flac", "mp3", "symphonia-vorbis", "wav" ], optional = true }
rand = { version = "0.9.0", default-features = false, features = [ "small_rng" ], optional = true }
id3 = { version = "1.10.0", optional = true } id3 = { version = "1.10.0", optional = true }
mp4ameta = { version = "0.11.0", optional = true } mp4ameta = { version = "0.11.0", optional = true }
metaflac = { version = "0.2.5", optional = true } metaflac = { version = "0.2.5", optional = true }
@@ -28,7 +41,7 @@ metaflac = { version = "0.2.5", optional = true }
[features] [features]
default = [ "wm", "terminal" ] default = [ "wm", "terminal" ]
terminal = [ "linux" ] terminal = [ "linux" ]
audio_player = [ "id3", "mp4ameta", "metaflac", "rand", "rodio" ] audio_player = [ "id3", "mp4ameta", "metaflac", "rodio" ]
[profile.release] [profile.release]
lto = true lto = true
@@ -63,3 +76,7 @@ path = "src/bin/malvim.rs"
[[bin]] [[bin]]
name = "mingGames_Reversi" name = "mingGames_Reversi"
path = "src/bin/reversi.rs" path = "src/bin/reversi.rs"
[[bin]]
name = "mingUtils_Draw"
path = "src/bin/draw.rs"

View File

@@ -1,8 +1,10 @@
Ming-wm is a keyboard-based, retro-themed window manager for Linux. It is neither for Wayland or the X Window System - it writes directly to the framebuffer. Inspirations include i3, Haiku, SerenityOS, and Windows98, and it is a conceptual successor to my previous projects [ming-de](https://github.com/stjet/mingde) and [ming-os](https://github.com/stjet/ming-os). Ming-wm is a keyboard-based, retro-themed window manager for Linux. It is neither for Wayland or the X Window System - it writes directly to the framebuffer. Inspirations include i3, Haiku, SerenityOS, and Windows98.
![example 1](/docs/images/ws1.png) ![example 1](/docs/images/ws1.png)
![example 2](/docs/images/ws3.png) ![example 2](/docs/images/ws3.png)
https://github.com/user-attachments/assets/5a598287-27d5-40f2-9b5f-9c1e67cab2f5
The [Koxinga web browser](https://github.com/stjet/koxinga) can be separately installed. The [Koxinga web browser](https://github.com/stjet/koxinga) can be separately installed.
![koxinga browser example](/docs/images/koxinga.png) ![koxinga browser example](/docs/images/koxinga.png)
@@ -65,6 +67,20 @@ ming touch rotate
<image alt="mobile example" src="/docs/images/mobile.png" width="50%"> <image alt="mobile example" src="/docs/images/mobile.png" width="50%">
### Troubleshooting
If key presses do nothing, that is, if on the lockscreen, despite key presses (physical or of the OSK), nothing happens (no asterisk characters show up), try adding the `force-stdout` arg. For example:
```
ming force-stdout
```
Or:
```
ming touch force-stdout
```
## Philosophy ## Philosophy
See [/docs/philosophy.md](/docs/philosophy.md) for some hopefully interesting ramblings. See [/docs/philosophy.md](/docs/philosophy.md) for some hopefully interesting ramblings.

BIN
bmps/arhants1440x842.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 MiB

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

View File

@@ -35,7 +35,7 @@ fn font_chars_to_alphas(dir: &str) {
ch.push(row); ch.push(row);
} }
let ch: Vec<String> = ch.into_iter().map(|row| { let ch: Vec<String> = ch.into_iter().map(|row| {
row.join(",") row.join(",").replace(",,,,", ":").replace(",,,", ";").replace(",,", ".")
}).collect(); }).collect();
let chars: Vec<char> = file_name[0].chars().collect(); let chars: Vec<char> = file_name[0].chars().collect();
File::create(dir.to_string() + "/" + &chars[0].to_string() + ".alpha").unwrap().write_all( File::create(dir.to_string() + "/" + &chars[0].to_string() + ".alpha").unwrap().write_all(
@@ -61,7 +61,9 @@ fn main() {
} }
//copy bmp folders to target //copy bmp folders to target
let profile = env::var_os("PROFILE").unwrap().to_string_lossy().to_string(); let profile = env::var_os("PROFILE").unwrap().to_string_lossy().to_string();
Command::new("rm").arg("-rf").arg(format!("./target/{}/ming_bmps", profile)).output().unwrap(); //delete at target first so cp works
Command::new("cp").arg("-r").arg("./bmps").arg(format!("./target/{}/ming_bmps", profile)).output().unwrap(); Command::new("cp").arg("-r").arg("./bmps").arg(format!("./target/{}/ming_bmps", profile)).output().unwrap();
//also copy the docs folder //also copy the docs folder
Command::new("rm").arg("-rf").arg(format!("./target/{}/ming_docs", profile)).output().unwrap();
Command::new("cp").arg("-r").arg("./docs").arg(format!("./target/{}/ming_docs", profile)).output().unwrap(); Command::new("cp").arg("-r").arg("./docs").arg(format!("./target/{}/ming_docs", profile)).output().unwrap();
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 122 KiB

15
docs/window-likes/draw.md Normal file
View File

@@ -0,0 +1,15 @@
Very basic drawing board.
## Moving
Arrow keys or hjkl. `i` to enter input mode. Use the enter key to finish a line or rectangle. Escape to cancel line or rectangle in progress.
## Input Commands
- `line/l`: Start line with current point as start. Move and hit enter to determine line endpoint
- `rect/r`: Start rect with current point as a corner. Move and hit enter to determine the opposite corner
- `circle/c`: Start circle with current point as centre. Move and hit enter to determine radius
- `color/co/colour [lowercase 6 char hex string]`: Set current colour
- `linewidth/lw [int]`: Set current line width
- `undo`: Undo last draw
- `clear`: Clear all drawings

View File

@@ -14,22 +14,28 @@ It is probably best to read a Vim tutorial for the basics. All supportd keystrok
- `w[rite]` - `w[rite]`
- `/<query>` - `/<query>`
Tab completion is supported for the `<file>` argument. Tab completion is supported for the `<file>` argument. Down arrow will clear the current command, and up arrow will fill in the last ran command.
### Supported in Normal Mode ### Supported in Normal Mode
- `:` - `:`
- `i` - `i`
- `o`, `O`
- `A` - `A`
- `r` - `r`
- `dd` - `dd`
- `dw` - `<number>dd`
- `dw` (`dw` is not identical to vim's behaviour), `dW`
- `d$` - `d$`
- `G` - `G`
- `gg` - `gg`
- `<number>gg` - `<number>gg`
- `f<char>` - `f<char>`, `F<char>`
- `F<char>` - `<number>f<char>`, `<number>F<char>`
- `;` (same as `f<char>` but with the char the cursor is on, so not the same as vim)
- `<num>;`
- `,` (same as `F<char>` but with the char the cursor is on, so not the same as vim)
- `<num>,`
- `x` - `x`
- `h` (or left arrow), `j` (or down arrow), `k` (or up arrow), `l` (or right arrow) - `h` (or left arrow), `j` (or down arrow), `k` (or up arrow), `l` (or right arrow)
- `<num>h`, `<num>j` (or down arrow), `<num>k` (or up arrow), `<num>l` - `<num>h`, `<num>j` (or down arrow), `<num>k` (or up arrow), `<num>l`

View File

@@ -16,3 +16,4 @@ 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_File_Explorer /usr/local/bin/mingFiles_File_Explorer
cp ./target/release/mingFiles_Audio_Player /usr/local/bin/mingFiles_Audio_Player cp ./target/release/mingFiles_Audio_Player /usr/local/bin/mingFiles_Audio_Player
cp ./target/release/mingEditing_Malvim /usr/local/bin/mingEditing_Malvim cp ./target/release/mingEditing_Malvim /usr/local/bin/mingEditing_Malvim
cp ./target/release/mingUtils_Draw /usr/local/bin/mingUtils_Draw

View File

@@ -4,5 +4,18 @@ version = "0.1.0"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
edition = "2021" edition = "2021"
[lints.clippy]
len_zero = "allow"
comparison_to_empty = "allow"
manual_saturating_arithmetic = "allow"
result_unit_err = "allow"
needless_borrow = "allow"
needless_borrows_for_generic_args = "allow"
redundant_static_lifetimes = "allow"
collapsible_else_if = "allow"
too_many_arguments = "allow"
useless_conversion = "allow"
unnecessary_mut_passed = "allow"
[dependencies] [dependencies]
libc = "0.2" libc = "0.2"

View File

@@ -71,9 +71,9 @@ impl Iterator for RawStdin {
b'D' => Key::ArrowLeft, b'D' => Key::ArrowLeft,
_ => Key::Other(n), _ => Key::Other(n),
} }
} else if n.is_ok() { } else if let Ok(n) = n {
//Alt+<char> sends Esc+<char> //Alt+<char> sends Esc+<char>
Key::Alt(char::from(n.unwrap())) Key::Alt(char::from(n))
} else { } else {
Key::Esc Key::Esc
} }

View File

@@ -16,3 +16,4 @@ cp ./target/release/mingGames_Minesweeper ~/.local/bin/mingGames_Minesweeper
cp ./target/release/mingFiles_File_Explorer ~/.local/bin/mingFiles_File_Explorer 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/mingFiles_Audio_Player ~/.local/bin/mingFiles_Audio_Player
cp ./target/release/mingEditing_Malvim ~/.local/bin/mingEditing_Malvim cp ./target/release/mingEditing_Malvim ~/.local/bin/mingEditing_Malvim
cp ./target/release/mingUtils_Draw ~/.local/bin/mingUtils_Draw

View File

@@ -1,10 +1,21 @@
[package] [package]
name = "ming-wm-lib" name = "ming-wm-lib"
version = "0.2.0" version = "0.2.3"
repository = "https://github.com/stjet/ming-wm" repository = "https://github.com/stjet/ming-wm"
description = "library for building windows for ming-wm in rust" description = "library for building windows for ming-wm in rust"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
edition = "2021" edition = "2021"
[lints.clippy]
len_zero = "allow"
comparison_to_empty = "allow"
manual_saturating_arithmetic = "allow"
result_unit_err = "allow"
needless_borrow = "allow"
needless_borrows_for_generic_args = "allow"
redundant_static_lifetimes = "allow"
collapsible_else_if = "allow"
too_many_arguments = "allow"
[dependencies] [dependencies]

126
ming-wm-lib/src/fonts.rs Normal file
View File

@@ -0,0 +1,126 @@
use std::fs::File;
use std::io::Read;
use std::collections::HashMap;
use crate::dirs;
#[derive(Clone)]
pub struct FontCharInfo {
pub c: char,
pub data: Vec<Vec<u8>>,
pub top_offset: u8,
pub height: usize,
pub width: usize,
}
fn get_font_char(dir: &str, c: char) -> Option<FontCharInfo> {
let c = if c == '/' { '𐘋' } else if c == '\\' { '𐚆' } else if c == '.' { '𐘅' } else { c };
if let Ok(mut file) = File::open(dir.to_string() + "/" + &c.to_string() + ".alpha") {
let mut ch: Vec<Vec<u8>> = Vec::new();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let lines: Vec<&str> = contents.split("\n").collect();
for ln in lines.iter().skip(1) {
//.unwrap_or(0) is important because zeroes are just empty
ch.push(ln.replace(":", ",,,,").replace(";", ",,,").replace(".", ",,").split(",").map(|n| n.parse().unwrap_or(0)).collect());
}
return Some(FontCharInfo {
c,
top_offset: lines[0].parse().unwrap(),
height: lines.len() - 1,
width: ch[0].len(),
data: ch,
});
}
None
}
pub fn get_font_char_from_fonts(fonts: &[String], c: char) -> FontCharInfo {
for font in fonts {
let p = dirs::exe_dir(Some(&("ming_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(&("ming_bmps/".to_string() + &fonts[0]))).to_string_lossy().to_string();
//so a ? char should be in every font. otherwise will just return blank
get_font_char(&p, '?').unwrap_or(FontCharInfo {
c: '?',
data: vec![vec![0]],
top_offset: 0,
height: 1,
width: 1,
})
}
pub struct MeasureInfo {
pub height: usize,
pub width: usize,
}
pub fn measure_text(fonts: &[String], text: &str, horiz_spacing: Option<usize>) -> MeasureInfo {
let mut height = 0;
let mut width = 0;
for c in text.chars() {
let i = get_font_char_from_fonts(fonts, c);
let c_height = i.top_offset as usize + i.height;
if c_height > height {
height = c_height;
}
width += i.width + horiz_spacing.unwrap_or(1);
}
width -= horiz_spacing.unwrap_or(1);
MeasureInfo {
height,
width,
}
}
pub fn measure_text_with_cache(fc_getter: &mut CachedFontCharGetter, fonts: &[String], text: &str, horiz_spacing: Option<usize>) -> MeasureInfo {
let mut height = 0;
let mut width = 0;
for c in text.chars() {
let i = fc_getter.get(fonts, c);
let c_height = i.top_offset as usize + i.height;
if c_height > height {
height = c_height;
}
width += i.width + horiz_spacing.unwrap_or(1);
}
width -= horiz_spacing.unwrap_or(1);
MeasureInfo {
height,
width,
}
}
#[derive(Default)]
pub struct CachedFontCharGetter {
cache: HashMap<char, FontCharInfo>,
cache_size: usize, //# of items cached
pub max_cache_size: usize,
}
impl CachedFontCharGetter {
pub fn new(max_cache_size: usize) -> Self {
Self {
max_cache_size,
..Default::default()
}
}
pub fn get(&mut self, fonts: &[String], c: char) -> FontCharInfo {
if let Some(cached) = self.cache.get(&c) {
cached.clone()
} else {
let got = get_font_char_from_fonts(fonts, c);
if self.cache_size == self.max_cache_size {
self.cache_size = 0;
self.cache = HashMap::new();
}
self.cache.insert(c, got.clone());
self.cache_size += 1;
got
}
}
}

View File

@@ -5,6 +5,7 @@ pub mod serialize;
pub mod messages; pub mod messages;
pub mod ipc; pub mod ipc;
pub mod components; pub mod components;
pub mod fonts;
pub mod dirs; pub mod dirs;
pub mod utils; pub mod utils;
pub mod logging; pub mod logging;

View File

@@ -44,11 +44,7 @@ pub enum WindowMessageResponse {
impl WindowMessageResponse { impl WindowMessageResponse {
pub fn is_key_char_request(&self) -> bool { pub fn is_key_char_request(&self) -> bool {
if let WindowMessageResponse::Request(WindowManagerRequest::DoKeyChar(_)) = self { matches!(self, WindowMessageResponse::Request(WindowManagerRequest::DoKeyChar(_)))
true
} else {
false
}
} }
} }

View File

@@ -83,7 +83,7 @@ impl Serializable for ThemeInfo {
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
//strip newline at the end //strip newline at the end
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
let mut theme_info: ThemeInfo = Default::default(); let mut theme_info: ThemeInfo = Default::default();
let arrays = serialized.split(":"); let arrays = serialized.split(":");
//won't error or panic if less than 9... rest will just be black by default I guess //won't error or panic if less than 9... rest will just be black by default I guess
@@ -162,7 +162,7 @@ impl Serializable for WindowMessageResponse {
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
//strip newline at the end //strip newline at the end
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
let mut parts = serialized.split("/"); let mut parts = serialized.split("/");
match parts.next().unwrap_or("Invalid") { match parts.next().unwrap_or("Invalid") {
"JustRedraw" => Ok(WindowMessageResponse::JustRedraw), "JustRedraw" => Ok(WindowMessageResponse::JustRedraw),
@@ -221,6 +221,7 @@ impl Serializable for DrawInstructions {
DrawInstructions::Gradient(p, d, c1, c2, u) => format!("Gradient/{}\x1E{}\x1E{}\x1E{}\x1E{}", array_to_string(p), array_to_string(d), array_to_string(c1), array_to_string(c2), u), DrawInstructions::Gradient(p, d, c1, c2, u) => format!("Gradient/{}\x1E{}\x1E{}\x1E{}\x1E{}", array_to_string(p), array_to_string(d), array_to_string(c1), array_to_string(c2), u),
DrawInstructions::Bmp(p, s, b) => format!("Bmp/{}\x1E{}\x1E{}", array_to_string(p), s, b), DrawInstructions::Bmp(p, s, b) => format!("Bmp/{}\x1E{}\x1E{}", array_to_string(p), s, b),
DrawInstructions::Circle(p, u, c) => format!("Circle/{}\x1E{}\x1E{}", array_to_string(p), u, array_to_string(c)), DrawInstructions::Circle(p, u, c) => format!("Circle/{}\x1E{}\x1E{}", array_to_string(p), u, array_to_string(c)),
DrawInstructions::Line(s, e, w, c) => format!("Line/{}\x1E{}\x1E{}\x1E{}", array_to_string(s), array_to_string(e), w, array_to_string(c)),
} }
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
@@ -402,6 +403,35 @@ impl Serializable for DrawInstructions {
let c = get_color(arg.unwrap())?; let c = get_color(arg.unwrap())?;
Ok(DrawInstructions::Circle(p, u.unwrap(), c)) Ok(DrawInstructions::Circle(p, u.unwrap(), c))
}, },
"Line" => {
let rest = get_rest_of_split(&mut parts, Some("/"));
//(s, e, w, c)
let mut args = rest.split("\x1E");
let arg = args.next();
if arg.is_none() {
return Err(());
}
let s = get_two_array(arg.unwrap())?;
let arg = args.next();
if arg.is_none() {
return Err(());
}
let e = get_two_array(arg.unwrap())?;
let arg = args.next();
if arg.is_none() {
return Err(());
}
let w = arg.unwrap().parse();
if w.is_err() {
return Err(());
}
let arg = args.next();
if arg.is_none() {
return Err(());
}
let c = get_color(arg.unwrap())?;
Ok(DrawInstructions::Line(s, e, w.unwrap(), c))
},
_ => Err(()), _ => Err(()),
} }
} }
@@ -414,12 +444,12 @@ impl Serializable for DrawInstructionsVec {
if self.len() == 0 { if self.len() == 0 {
return "empty".to_string(); return "empty".to_string();
} }
let collected: Vec<_> = self.into_iter().map(|ins| ins.serialize()).collect(); let collected: Vec<_> = self.iter().map(|ins| ins.serialize()).collect();
collected.join("\x1D") collected.join("\x1D")
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
//strip newline at the end //strip newline at the end
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
if serialized == "empty" { if serialized == "empty" {
return Ok(Vec::new()); return Ok(Vec::new());
} }
@@ -474,7 +504,7 @@ impl Serializable for WindowLikeType {
} }
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
match serialized { match serialized {
"LockScreen" => Ok(WindowLikeType::LockScreen), "LockScreen" => Ok(WindowLikeType::LockScreen),
"Window" => Ok(WindowLikeType::Window), "Window" => Ok(WindowLikeType::Window),
@@ -501,7 +531,7 @@ impl Serializable for Dimensions {
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
//strip newline at the end //strip newline at the end
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
let d = get_two_array(serialized)?; let d = get_two_array(serialized)?;
Ok(d) Ok(d)
} }
@@ -562,7 +592,7 @@ impl Serializable for WindowMessage {
} }
} }
fn deserialize(serialized: &str) -> Result<Self, ()> { fn deserialize(serialized: &str) -> Result<Self, ()> {
let serialized = if serialized.ends_with("\n") { &serialized[..serialized.len() - 1] } else { serialized }; let serialized = serialized.strip_suffix("\n").unwrap_or(serialized);
let mut parts = serialized.split("/"); let mut parts = serialized.split("/");
match parts.next().unwrap_or("Invalid") { match parts.next().unwrap_or("Invalid") {
"Init" => { "Init" => {
@@ -686,9 +716,9 @@ impl Serializable for WindowMessage {
return Err(()); return Err(());
} }
if let Ok(n) = arg2.unwrap().parse() { if let Ok(n) = arg2.unwrap().parse() {
return Ok(WindowMessage::Info(InfoType::WindowsInWorkspace(w_vec, n))); Ok(WindowMessage::Info(InfoType::WindowsInWorkspace(w_vec, n)))
} else { } else {
return Err(()); Err(())
} }
}, },
"Focus" => Ok(WindowMessage::Focus), "Focus" => Ok(WindowMessage::Focus),

View File

@@ -11,15 +11,17 @@ pub enum Themes {
//Parchment, //Parchment,
} }
impl Themes { impl std::str::FromStr for Themes {
pub fn from_str(name: &str) -> Option<Self> { type Err = ();
fn from_str(name: &str) -> Result<Self, Self::Err> {
match name { match name {
"Standard" => Some(Themes::Standard), "Standard" => Ok(Themes::Standard),
"Night" => Some(Themes::Night), "Night" => Ok(Themes::Night),
"Industrial" => Some(Themes::Industrial), "Industrial" => Ok(Themes::Industrial),
"Forest" => Some(Themes::Forest), "Forest" => Ok(Themes::Forest),
"Royal" => Some(Themes::Royal), "Royal" => Ok(Themes::Royal),
_ => None, _ => Err(()),
} }
} }
} }

View File

@@ -1,12 +1,21 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::fs::read_dir; use std::fs::read_dir;
use crate::fonts::measure_text;
use crate::framebuffer_types::{ Dimensions, Point }; use crate::framebuffer_types::{ Dimensions, Point };
pub fn min(one: usize, two: usize) -> usize { pub fn min(one: usize, two: usize) -> usize {
if one > two { two } else { one } if one > two { two } else { one }
} }
pub fn random_u32(seed: u32) -> u32 {
let mut seed = seed;
seed ^= seed << 13;
seed ^= seed >> 17;
seed ^= seed >> 5;
seed
}
pub trait Substring { pub trait Substring {
fn substring(&self, start: usize, end: usize) -> &str; fn substring(&self, start: usize, end: usize) -> &str;
fn remove(&self, index: usize, len: usize) -> String; fn remove(&self, index: usize, len: usize) -> String;
@@ -88,15 +97,20 @@ pub fn calc_actual_lines<'a>(lines: impl Iterator<Item = &'a String>, max_chars_
actual_lines actual_lines
} }
pub fn calc_new_cursor_pos(cursor_pos: usize, new_length: usize) -> usize { /// truncate to ... if too long (uses `measure_text`)
if cursor_pos >= new_length { pub fn trunc_words(fonts: &[String], to_measure: String, horiz_spacing: Option<usize>, max_text_width: usize) -> String {
if new_length == 0 { if measure_text(fonts, &to_measure, horiz_spacing).width > max_text_width {
0 let mut current = String::new();
} else { for c in to_measure.chars() {
new_length - 1 let to_measure = current.clone() + &c.to_string() + "...";
if measure_text(fonts, &to_measure, horiz_spacing).width > max_text_width {
break;
} }
current += &c.to_string();
}
current + "..."
} else { } else {
cursor_pos to_measure.clone()
} }
} }
@@ -227,3 +241,4 @@ pub fn get_all_files(dir: PathBuf) -> Vec<PathBuf> {
} }
files files
} }

View File

@@ -14,11 +14,18 @@ pub enum KeyChar {
#[derive(Debug)] #[derive(Debug)]
pub enum DrawInstructions { pub enum DrawInstructions {
/// Top left point, dimensions, colour
Rect(Point, Dimensions, RGBColor), Rect(Point, Dimensions, RGBColor),
Text(Point, Vec<String>, String, RGBColor, RGBColor, Option<usize>, Option<u8>), //font and text /// Top left point, fonts, text, colour, background colour, horizontal spacing, monospace width
Text(Point, Vec<String>, String, RGBColor, RGBColor, Option<usize>, Option<u8>),
/// Top left point, dimensions, start colour, end colour, steps
Gradient(Point, Dimensions, RGBColor, RGBColor, usize), Gradient(Point, Dimensions, RGBColor, RGBColor, usize),
/// Top left point, path to file, reverse
Bmp(Point, String, bool), Bmp(Point, String, bool),
/// Centre point, radius, colour
Circle(Point, usize, RGBColor), Circle(Point, usize, RGBColor),
/// Start point, end point, line width, line colour
Line(Point, Point, usize, RGBColor),
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]

View File

@@ -2,13 +2,13 @@ use std::vec::Vec;
use std::vec; use std::vec;
use std::io::BufReader; use std::io::BufReader;
use std::path::PathBuf; use std::path::PathBuf;
use std::collections::HashMap;
use std::fs::{ read_to_string, File }; use std::fs::{ read_to_string, File };
use std::time::{ Duration, SystemTime, UNIX_EPOCH }; use std::time::{ Duration, SystemTime, UNIX_EPOCH };
use std::thread; use std::thread;
use std::sync::{ Arc, Mutex }; use std::sync::{ Arc, Mutex };
use rodio::{ Decoder, OutputStream, Sink, Source }; use rodio::{ Decoder, OutputStream, Sink, Source };
use rand::{ SeedableRng, prelude::SliceRandom, rngs::SmallRng };
use id3::TagLike; use id3::TagLike;
use mp4ameta; use mp4ameta;
use metaflac; use metaflac;
@@ -17,7 +17,8 @@ use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLik
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType };
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::utils::{ concat_paths, get_all_files, path_autocomplete, format_seconds, Substring }; use ming_wm_lib::fonts::measure_text;
use ming_wm_lib::utils::{ concat_paths, random_u32, get_all_files, path_autocomplete, format_seconds, Substring };
use ming_wm_lib::dirs::home; use ming_wm_lib::dirs::home;
use ming_wm_lib::ipc::listen; use ming_wm_lib::ipc::listen;
@@ -150,11 +151,15 @@ impl WindowLike for AudioPlayer {
let queue = &internal_locked.queue; let queue = &internal_locked.queue;
let current = &queue[queue.len() - sink_len]; let current = &queue[queue.len() - sink_len];
let current_name = current.0.file_name().unwrap().to_string_lossy().into_owned(); 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], vec!["nimbus-romono".to_string(), "shippori-mincho".to_string()], current_name.clone(), theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); let fonts = ["nimbus-roman".to_string(), "shippori-mincho".to_string()];
let cn_width = measure_text(&fonts, &current_name, None).width;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - cn_width / 2, 2], fonts.to_vec(), current_name.clone(), theme_info.text, theme_info.background, Some(0), None));
if let Some(artist) = &current.2 { if let Some(artist) = &current.2 {
let artist_string = "by ".to_string() + &artist; let artist_string = "by ".to_string() + &artist;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - artist_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT + 2], vec!["nimbus-romono".to_string()], artist_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); let as_width = measure_text(&fonts, &artist_string, None).width;
instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - as_width / 2, LINE_HEIGHT + 2], fonts.to_vec(), artist_string, theme_info.text, theme_info.background, Some(0), None));
} }
//in this case no chance of mincho so MONO_WIDTH method of calculating width is ok
let time_string = format!("{}/{}", format_seconds(internal_locked.sink.get_pos().as_secs()), format_seconds(current.1)); let time_string = format!("{}/{}", format_seconds(internal_locked.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 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH))); instructions.push(DrawInstructions::Text([self.dimensions[0] / 2 - time_string.len() * MONO_WIDTH as usize / 2, LINE_HEIGHT * 2 + 2], vec!["nimbus-romono".to_string()], time_string, theme_info.text, theme_info.background, Some(0), Some(MONO_WIDTH)));
} else { } else {
@@ -253,8 +258,13 @@ impl AudioPlayer {
} else { } else {
get_all_files(PathBuf::from(new_path)) get_all_files(PathBuf::from(new_path))
}; };
let mut rng = SmallRng::seed_from_u64(SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()); let mut seed = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().subsec_millis();
queue.shuffle(&mut rng); let mut q_weights: HashMap<PathBuf, u32> = HashMap::new();
for q in &queue {
seed = random_u32(seed);
q_weights.insert(q.to_path_buf(), seed);
}
queue.sort_by(|a, b| q_weights[a].cmp(&q_weights[b]));
if self.command.starts_with("p ") { if self.command.starts_with("p ") {
let mut locked_internal = self.internal.lock().unwrap(); let mut locked_internal = self.internal.lock().unwrap();
(*locked_internal).sink.clear(); (*locked_internal).sink.clear();

254
src/bin/draw.rs Normal file
View File

@@ -0,0 +1,254 @@
use std::vec::Vec;
use std::vec;
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
use ming_wm_lib::framebuffer_types::{ Dimensions, Point, RGBColor };
use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::utils::{ hex_to_u8, HEX_CHARS, Substring };
use ming_wm_lib::ipc::listen;
enum DrawAction {
Line(Point, Option<Point>, usize, RGBColor),
Rect(Point, Option<Dimensions>, RGBColor),
Circle(Point, Option<usize>, RGBColor),
}
impl DrawAction {
fn name(&self) -> String {
match self {
DrawAction::Line(_, _, _, _) => "Line",
DrawAction::Rect(_, _, _) => "Rect",
DrawAction::Circle(_, _, _) => "Circle",
}.to_string()
}
}
#[derive(Default, PartialEq)]
enum Mode {
#[default]
Move,
Input,
}
#[derive(Default)]
struct Draw {
mode: Mode,
dimensions: Dimensions,
draw_actions: Vec<DrawAction>,
current_location: Point,
current_input: String,
current_color: RGBColor,
current_linewidth: usize,
current_action: Option<DrawAction>,
}
impl WindowLike for Draw {
fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse {
match message {
WindowMessage::Init(dimensions) => {
self.dimensions = dimensions;
WindowMessageResponse::JustRedraw
},
WindowMessage::ChangeDimensions(dimensions) => {
self.dimensions = dimensions;
WindowMessageResponse::JustRedraw
},
WindowMessage::KeyPress(key_press) => {
if key_press.is_escape() && (self.current_action.is_some() || self.mode != Mode::Move) {
self.current_action = None;
self.mode = Mode::Move;
self.current_input = String::new();
WindowMessageResponse::JustRedraw
} else if self.mode == Mode::Input {
if key_press.is_backspace() && self.current_input.len() > 0 {
self.current_input = self.current_input.remove_last();
WindowMessageResponse::JustRedraw
} else if key_press.is_enter() {
//process current input
let mut parts = self.current_input.split(" ");
match parts.next().unwrap() {
"line" | "l" => {
self.current_action = Some(DrawAction::Line(self.current_location, None, self.current_linewidth, self.current_color));
},
"rect" | "r" => {
self.current_action = Some(DrawAction::Rect(self.current_location, None, self.current_color));
},
"circle" | "c" => {
self.current_action = Some(DrawAction::Circle(self.current_location, None, self.current_color));
},
"colour" | "color" | "co" => {
//hex to u8
if let Some(hex_color) = parts.next() {
if hex_color.len() == 6 && hex_color.chars().all(|c| HEX_CHARS.contains(&c)) {
let mut hex_chars = hex_color.chars();
self.current_color = [hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap()), hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap()), hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap())];
}
}
},
"linewidth" | "lw" => {
if let Ok(linewidth) = parts.next().unwrap_or("").parse::<usize>() {
self.current_linewidth = linewidth;
}
},
"undo" | "u" => {
self.draw_actions.pop();
},
"clear" | "cl" => {
self.draw_actions = Vec::new();
},
_ => {},
};
self.mode = Mode::Move;
self.current_input = String::new();
WindowMessageResponse::JustRedraw
} else if key_press.is_regular() {
self.current_input += &key_press.key.to_string();
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if key_press.key == 'i' && self.current_action.is_none() {
self.mode = Mode::Input;
WindowMessageResponse::JustRedraw
} else if key_press.is_enter() {
if let Some(current_action) = &self.current_action {
self.draw_actions.push(match current_action {
DrawAction::Line(p, _, u, r) => DrawAction::Line(*p, Some(self.current_location), *u, *r),
DrawAction::Rect(p, _, r) => {
let d = [
if self.current_location[0] > p[0] {
self.current_location[0] - p[0]
} else {
p[0] - self.current_location[0]
},
if self.current_location[1] > p[1] {
self.current_location[1] - p[1]
} else {
p[1] - self.current_location[1]
}
];
//find top left corner
let tl = [
if p[0] < self.current_location[0] {
p[0]
} else {
self.current_location[0]
},
if p[1] < self.current_location[1] {
p[1]
} else {
self.current_location[1]
}
];
DrawAction::Rect(tl, Some(d), *r)
},
DrawAction::Circle(p, _, c) => {
let r = ((self.current_location[1] as f64 - p[1] as f64).powi(2) + (self.current_location[0] as f64 - p[0] as f64).powi(2)).sqrt();
DrawAction::Circle(*p, Some(r.round() as usize), *c)
},
});
self.current_action = None;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if key_press.is_up_arrow() || key_press.key == 'k' {
if self.current_location[1] > 0 {
self.current_location[1] -= 1;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if key_press.is_down_arrow() || key_press.key == 'j' {
if self.current_location[1] + 1 < self.dimensions[1] {
self.current_location[1] += 1;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if key_press.is_left_arrow() || key_press.key == 'h' {
if self.current_location[0] > 0 {
self.current_location[0] -= 1;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else if key_press.is_right_arrow() || key_press.key == 'l' {
if self.current_location[0] + 1 < self.dimensions[0] {
self.current_location[0] += 1;
WindowMessageResponse::JustRedraw
} else {
WindowMessageResponse::DoNothing
}
} else {
WindowMessageResponse::DoNothing
}
},
_ => WindowMessageResponse::DoNothing,
}
}
fn draw(&self, theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
let mut instructions = Vec::new();
//draw previous actions
for action in &self.draw_actions {
instructions.push(match action {
DrawAction::Line(p1, p2, lw, c) => DrawInstructions::Line(*p1, p2.unwrap(), *lw, *c),
DrawAction::Rect(p, d, c) => DrawInstructions::Rect(*p, d.unwrap(), *c),
DrawAction::Circle(p, r, c) => DrawInstructions::Circle(*p, r.unwrap(), *c),
});
}
//draw cursor (crosshair)
let crosshair_min_x = self.current_location[0].checked_sub(6).unwrap_or(0);
let crosshair_min_y = self.current_location[1].checked_sub(6).unwrap_or(0);
//^going over should be handled by the drawer, probably?
instructions.push(DrawInstructions::Line([crosshair_min_x, self.current_location[1]], [self.current_location[0] + 6, self.current_location[1]], 1, self.current_color));
instructions.push(DrawInstructions::Line([self.current_location[0], crosshair_min_y], [self.current_location[0], self.current_location[1] + 6], 1, self.current_color));
//draw info or current input
instructions.push(DrawInstructions::Text([2, self.dimensions[1] - 19], vec!["nimbus-roman".to_string()], if self.current_input == String::new() {
if let Some(current_action) = &self.current_action {
current_action.name()
} else if self.mode == Mode::Move {
"'i' to enter input mode".to_string()
} else {
"Awaiting input".to_string()
}
} else {
self.current_input.clone()
}, theme_info.text, theme_info.background, None, None));
instructions
}
//properties
fn title(&self) -> String {
"Draw".to_string()
}
fn subtype(&self) -> WindowLikeType {
WindowLikeType::Window
}
fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions {
[410, 410]
}
fn resizable(&self) -> bool {
true
}
}
impl Draw {
pub fn new() -> Self {
//apparently this is legal. thanks clippy
Self {
current_linewidth: 1,
..Default::default() //no comma here allowed though??
}
}
}
pub fn main() {
listen(Draw::new());
}

View File

@@ -111,14 +111,13 @@ impl WindowLike for FileExplorer {
self.metadata = Some(metadata(&selected_entry.path).unwrap()); self.metadata = Some(metadata(&selected_entry.path).unwrap());
} }
WindowMessageResponse::JustRedraw WindowMessageResponse::JustRedraw
} else { } else if self.state == State::Info {
if self.state == State::Info { //pressing any key to exit info mode
self.state = State::List; self.state = State::List;
WindowMessageResponse::JustRedraw WindowMessageResponse::JustRedraw
} else { } else {
WindowMessageResponse::DoNothing WindowMessageResponse::DoNothing
} }
}
}, },
_ => WindowMessageResponse::DoNothing, _ => WindowMessageResponse::DoNothing,
} }
@@ -128,7 +127,7 @@ impl WindowLike for FileExplorer {
let mut instructions = Vec::new(); let mut instructions = Vec::new();
if self.state == State::List { if self.state == State::List {
//top bar with path name //top bar with path name
instructions.push(DrawInstructions::Text([5, 0], vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()], "Current: ".to_string() + &self.current_path.to_string_lossy().to_string(), theme_info.text, theme_info.background, None, None)); instructions.push(DrawInstructions::Text([5, 0], vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()], "Current: ".to_string() + self.current_path.to_string_lossy().as_ref(), theme_info.text, theme_info.background, None, None));
//the actual files and directories //the actual files and directories
let mut start_y = HEIGHT; let mut start_y = HEIGHT;
let mut i = self.top_position; let mut i = self.top_position;
@@ -142,10 +141,10 @@ impl WindowLike for FileExplorer {
} }
//unwrap_or not used because "Arguments passed to unwrap_or are eagerly evaluated", apparently //unwrap_or not used because "Arguments passed to unwrap_or are eagerly evaluated", apparently
let name = entry.override_name.clone(); let name = entry.override_name.clone();
let name = if name.is_none() { let name = if let Some(name) = name {
entry.path.file_name().unwrap().to_os_string().into_string().unwrap() name
} else { } else {
name.unwrap() entry.path.file_name().unwrap().to_os_string().into_string().unwrap()
}; };
instructions.push(DrawInstructions::Text([5, start_y + 4], vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()], name, if is_selected { theme_info.top_text } else { theme_info.text }, if is_selected { theme_info.top } else { theme_info.background }, None, None)); instructions.push(DrawInstructions::Text([5, start_y + 4], vec!["nimbus-roman".to_string(), "shippori-mincho".to_string()], name, if is_selected { theme_info.top_text } else { theme_info.text }, if is_selected { theme_info.top } else { theme_info.background }, None, None));
start_y += HEIGHT; start_y += HEIGHT;

View File

@@ -9,7 +9,7 @@ use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManager
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType }; use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
use ming_wm_lib::utils::{ calc_actual_lines, calc_new_cursor_pos, Substring }; use ming_wm_lib::utils::{ calc_actual_lines, Substring };
use ming_wm_lib::dirs::home; use ming_wm_lib::dirs::home;
use ming_wm_lib::utils::{ get_rest_of_split, path_autocomplete }; use ming_wm_lib::utils::{ get_rest_of_split, path_autocomplete };
use ming_wm_lib::ipc::listen; use ming_wm_lib::ipc::listen;
@@ -19,6 +19,8 @@ const LINE_HEIGHT: usize = 18;
const PADDING: usize = 2; const PADDING: usize = 2;
const BAND_HEIGHT: usize = 19; const BAND_HEIGHT: usize = 19;
const WORD_END: [char; 8] = ['.', ',', ':', '[', ']', '{', '}', ' '];
struct FileInfo { struct FileInfo {
pub name: String, pub name: String,
pub path: String, pub path: String,
@@ -43,6 +45,12 @@ enum State {
// //
} }
impl State {
fn is_numberable(&self) -> bool {
*self == State::Maybeg || *self == State::Find || *self == State::BackFind
}
}
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
enum Mode { enum Mode {
#[default] #[default]
@@ -77,6 +85,7 @@ struct Malvim {
state: State, state: State,
mode: Mode, mode: Mode,
command: Option<String>, command: Option<String>,
prev_command: Option<String>,
bottom_message: Option<String>, bottom_message: Option<String>,
maybe_num: Option<usize>, maybe_num: Option<usize>,
files: Vec<FileInfo>, files: Vec<FileInfo>,
@@ -103,10 +112,22 @@ impl WindowLike for Malvim {
self.mode = Mode::Command; self.mode = Mode::Command;
self.command = Some(String::new()); self.command = Some(String::new());
changed = false; changed = false;
} else if (key_press.key == 'i' || key_press.key == 'A') && self.mode == Mode::Normal && self.state == State::None && self.files.len() > 0 { } else if (key_press.key == 'i' || key_press.key == 'A' || key_press.key == 'o' || key_press.key == 'O') && self.mode == Mode::Normal && self.state == State::None && self.files.len() > 0 {
if key_press.key == 'A' {
let current_file = &mut self.files[self.current_file_index]; let current_file = &mut self.files[self.current_file_index];
if key_press.key == 'A' {
current_file.cursor_pos = current_file.content[current_file.line_pos].chars().count(); current_file.cursor_pos = current_file.content[current_file.line_pos].chars().count();
} else if key_press.key == 'o' || key_press.key == 'O' {
let current_line = &current_file.content[current_file.line_pos];
let spaces = Malvim::calc_spaces(self.autoindent, current_line);
let n = if key_press.key == 'o' {
current_file.line_pos + 1
} else {
current_file.line_pos
};
current_file.content.insert(n, " ".repeat(spaces));
current_file.line_pos = n;
current_file.cursor_pos = spaces;
new = true;
} }
self.mode = Mode::Insert; self.mode = Mode::Insert;
changed = false; changed = false;
@@ -117,23 +138,10 @@ impl WindowLike for Malvim {
if key_press.is_enter() { if key_press.is_enter() {
let mut line: Vec<char> = line.chars().collect(); let mut line: Vec<char> = line.chars().collect();
let (left, right) = line.split_at_mut(current_file.cursor_pos); let (left, right) = line.split_at_mut(current_file.cursor_pos);
let left = left.into_iter().map(|c| c.to_string()).collect::<Vec<String>>().join(""); let left = left.iter_mut().map(|c| c.to_string()).collect::<Vec<String>>().join("");
let right = right.into_iter().map(|c| c.to_string()).collect::<Vec<String>>().join(""); let right = right.iter_mut().map(|c| c.to_string()).collect::<Vec<String>>().join("");
current_file.content[current_file.line_pos] = left.to_string(); current_file.content[current_file.line_pos] = left.to_string();
let spaces = if self.autoindent { let spaces = Malvim::calc_spaces(self.autoindent, &left);
//find out how many spaces the line starts with, copy that to the new line
let mut spaces = 0;
for c in left.chars() {
if c == ' ' {
spaces += 1;
} else {
break;
}
}
spaces
} else {
0
};
current_file.content.insert(current_file.line_pos + 1, " ".repeat(spaces) + &right); current_file.content.insert(current_file.line_pos + 1, " ".repeat(spaces) + &right);
current_file.line_pos += 1; current_file.line_pos += 1;
current_file.cursor_pos = spaces; current_file.cursor_pos = spaces;
@@ -168,15 +176,18 @@ impl WindowLike for Malvim {
self.state = State::None; self.state = State::None;
} else if self.state == State::MaybeDelete { } else if self.state == State::MaybeDelete {
if key_press.key == 'd' { if key_press.key == 'd' {
for _ in 0..self.maybe_num.unwrap_or(1) {
current_file.content.remove(current_file.line_pos); current_file.content.remove(current_file.line_pos);
if current_file.content.len() == 0 { if current_file.content.len() == 0 {
current_file.content = vec![String::new()]; current_file.content = vec![String::new()];
} else if current_file.line_pos == current_file.content.len() { } else if current_file.line_pos == current_file.content.len() {
current_file.line_pos = current_file.content.len() - 1; current_file.line_pos = current_file.content.len() - 1;
break;
}
} }
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
} else if key_press.key == 'w' || key_press.key == '$' { } else if key_press.key == 'w' || key_press.key == 'W' || key_press.key == '$' {
let line = &current_file.content[current_file.line_pos]; let line = &current_file.content[current_file.line_pos];
let line_len = line.chars().count(); let line_len = line.chars().count();
if line_len > 0 && current_file.cursor_pos < line_len { if line_len > 0 && current_file.cursor_pos < line_len {
@@ -184,18 +195,26 @@ impl WindowLike for Malvim {
let mut line_chars = line.chars().skip(current_file.cursor_pos).peekable(); let mut line_chars = line.chars().skip(current_file.cursor_pos).peekable();
//deref is Copy //deref is Copy
let current_char = *line_chars.peek().unwrap(); let current_char = *line_chars.peek().unwrap();
let offset = if key_press.key == 'w' { let offset = if key_press.key == 'W' || key_press.key == 'w' {
line_chars.position(|c| if current_char == ' ' { line_chars.position(|c| if key_press.key == 'w' {
if WORD_END.contains(&current_char) {
c != current_char
} else {
WORD_END.contains(&c)
}
} else {
if current_char == ' ' {
c != ' ' c != ' '
} else { } else {
c == ' ' c == ' '
}
}).unwrap_or(line_len - current_file.cursor_pos) }).unwrap_or(line_len - current_file.cursor_pos)
} else { } else {
line_chars.count() line_chars.count()
}; };
current_file.content[current_file.line_pos] = line.remove(current_file.cursor_pos, offset); current_file.content[current_file.line_pos] = line.remove(current_file.cursor_pos, offset);
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
} }
} }
self.state = State::None; self.state = State::None;
@@ -206,15 +225,21 @@ impl WindowLike for Malvim {
current_file.line_pos = current_file.content.len() - 1; current_file.line_pos = current_file.content.len() - 1;
} }
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
} }
changed = false; changed = false;
self.state = State::None; self.state = State::None;
} else if self.state == State::Find || self.state == State::BackFind { } else if self.state == State::Find || self.state == State::BackFind || key_press.key == ';' || key_press.key == ',' {
let old_pos = current_file.cursor_pos; let mut old_pos = current_file.cursor_pos;
let find_pos = if self.state == State::Find { let find_char = if self.state == State::Find || self.state == State::BackFind {
key_press.key
} else {
current_file.content[current_file.line_pos].chars().nth(old_pos).unwrap()
};
for _ in 0..self.maybe_num.unwrap_or(1) {
let find_pos = if self.state == State::Find || key_press.key == ';' {
if old_pos < current_file.content[current_file.line_pos].chars().count() { if old_pos < current_file.content[current_file.line_pos].chars().count() {
let found_index = current_file.content[current_file.line_pos].chars().skip(old_pos + 1).position(|c| c == key_press.key); let found_index = current_file.content[current_file.line_pos].chars().skip(old_pos + 1).position(|c| c == find_char);
if let Some(found_index) = found_index { if let Some(found_index) = found_index {
old_pos + found_index + 1 old_pos + found_index + 1
} else { } else {
@@ -226,7 +251,7 @@ impl WindowLike for Malvim {
} else { } else {
//how does this work again? no idea //how does this work again? no idea
if old_pos != 0 { if old_pos != 0 {
let found_index = current_file.content[current_file.line_pos].chars().rev().skip(current_length - old_pos).position(|c| c == key_press.key); let found_index = current_file.content[current_file.line_pos].chars().rev().skip(current_length - old_pos).position(|c| c == find_char);
if let Some(found_index) = found_index { if let Some(found_index) = found_index {
old_pos - found_index - 1 old_pos - found_index - 1
} else { } else {
@@ -237,6 +262,8 @@ impl WindowLike for Malvim {
} }
}; };
current_file.cursor_pos = find_pos; current_file.cursor_pos = find_pos;
old_pos = current_file.cursor_pos;
}
changed = false; changed = false;
self.state = State::None; self.state = State::None;
} else if key_press.key == 'x' { } else if key_press.key == 'x' {
@@ -262,7 +289,7 @@ impl WindowLike for Malvim {
current_file.line_pos = current_file.line_pos.checked_sub(self.maybe_num.unwrap_or(1)).unwrap_or(0); current_file.line_pos = current_file.line_pos.checked_sub(self.maybe_num.unwrap_or(1)).unwrap_or(0);
} }
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
changed = false; changed = false;
} else if key_press.key == 'l' || key_press.is_right_arrow() { } else if key_press.key == 'l' || key_press.is_right_arrow() {
if current_length > 0 { if current_length > 0 {
@@ -295,7 +322,7 @@ impl WindowLike for Malvim {
} else if key_press.key == 'G' { } else if key_press.key == 'G' {
current_file.line_pos = current_file.content.len() - 1; current_file.line_pos = current_file.content.len() - 1;
let new_length = current_file.content[current_file.line_pos].chars().count(); let new_length = current_file.content[current_file.line_pos].chars().count();
current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); current_file.cursor_pos = Malvim::calc_new_cursor_pos(current_file.cursor_pos, new_length);
changed = false; changed = false;
} else if key_press.key == 'f' { } else if key_press.key == 'f' {
self.state = State::Find; self.state = State::Find;
@@ -392,7 +419,7 @@ impl WindowLike for Malvim {
changed = false; changed = false;
} }
//reset maybe_num if not num //reset maybe_num if not num
if !numbered && self.state != State::Maybeg { if !numbered && !self.state.is_numberable() {
self.maybe_num = None; self.maybe_num = None;
} }
} else if self.mode == Mode::Command { } else if self.mode == Mode::Command {
@@ -400,7 +427,8 @@ impl WindowLike for Malvim {
let command = self.command.clone().unwrap_or("".to_string()); let command = self.command.clone().unwrap_or("".to_string());
if key_press.is_enter() { if key_press.is_enter() {
new = self.process_command(); new = self.process_command();
self.command = None; self.prev_command = self.command.take();
//line above does same as `self.command = None`
self.mode = Mode::Normal; self.mode = Mode::Normal;
} else if key_press.key == '\t' { //tab } else if key_press.key == '\t' { //tab
let mut parts = command.split(" ").skip(1); let mut parts = command.split(" ").skip(1);
@@ -421,7 +449,13 @@ impl WindowLike for Malvim {
} }
} else if key_press.is_backspace() { } else if key_press.is_backspace() {
if command.len() > 0 { if command.len() > 0 {
self.command = Some(command[..command.len() - 1].to_string()); self.command = Some(command.remove_last());
}
} else if key_press.is_arrow() {
if key_press.is_up_arrow() {
self.command = self.prev_command.clone();
} else if key_press.is_down_arrow() {
self.command = Some(String::new());
} }
} else { } else {
self.command = Some(command.to_string() + &key_press.key.to_string()); self.command = Some(command.to_string() + &key_press.key.to_string());
@@ -461,7 +495,7 @@ impl WindowLike for Malvim {
if i == 0 { if i == 0 {
//modify current line //modify current line
let line = &current_file.content[current_file.line_pos]; let line = &current_file.content[current_file.line_pos];
current_file.content[current_file.line_pos] = line.substring(0, current_file.cursor_pos).to_string() + &cs + line.substring(current_file.cursor_pos, line.chars().count()); current_file.content[current_file.line_pos] = line.substring(0, current_file.cursor_pos).to_string() + cs + line.substring(current_file.cursor_pos, line.chars().count());
current_file.cursor_pos += copy_string.len(); current_file.cursor_pos += copy_string.len();
} else { } else {
//insert a new line //insert a new line
@@ -591,6 +625,34 @@ impl Malvim {
Default::default() Default::default()
} }
fn calc_spaces(autoindent: bool, left: &str) -> usize {
if autoindent {
let mut spaces = 0;
for c in left.chars() {
if c == ' ' {
spaces += 1;
} else {
break;
}
}
spaces
} else {
0
}
}
fn calc_new_cursor_pos(cursor_pos: usize, new_length: usize) -> usize {
if cursor_pos >= new_length {
if new_length == 0 {
0
} else {
new_length - 1
}
} else {
cursor_pos
}
}
fn calc_top_line_pos(&mut self) { fn calc_top_line_pos(&mut self) {
if self.files.len() == 0 { if self.files.len() == 0 {
return; return;
@@ -688,7 +750,7 @@ impl Malvim {
} }
} else if self.files.len() == 0 { } else if self.files.len() == 0 {
self.bottom_message = Some("No files are open, so can only do :e(dit)".to_string()); self.bottom_message = Some("No files are open, so can only do :e(dit)".to_string());
} else if first.starts_with("/") { } else if let Some(s_first) = first.strip_prefix("/") {
let current_file = &mut self.files[self.current_file_index]; let current_file = &mut self.files[self.current_file_index];
if current_file.content.len() > 0 { if current_file.content.len() > 0 {
let p1 = if arg == "" { let p1 = if arg == "" {
@@ -702,7 +764,7 @@ impl Malvim {
} else { } else {
" ".to_string() + &rest " ".to_string() + &rest
}; };
let query = first[1..].to_string() + &p1 + &rest; let query = s_first.to_string() + &p1 + &rest;
let mut lines = current_file.content.iter().skip(current_file.line_pos); let mut lines = current_file.content.iter().skip(current_file.line_pos);
for i in 0..(current_file.content.len() - current_file.line_pos) { for i in 0..(current_file.content.len() - current_file.line_pos) {
let line = if i == 0 { let line = if i == 0 {

View File

@@ -7,7 +7,7 @@ use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLik
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::utils::{ u8_to_hex, hex_to_u8, HEX_CHARS }; use ming_wm_lib::utils::{ random_u32, u8_to_hex, hex_to_u8, HEX_CHARS };
use ming_wm_lib::ipc::listen; use ming_wm_lib::ipc::listen;
//16x16 with 40 mines //16x16 with 40 mines
@@ -252,15 +252,11 @@ impl Minesweeper {
//https://en.wikipedia.org/wiki/Xorshift //https://en.wikipedia.org/wiki/Xorshift
//from 0 to 15 //from 0 to 15
pub fn random(&mut self) -> usize { pub fn random(&mut self) -> usize {
let mut x = self.random_seed; self.random_seed = random_u32(self.random_seed);
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
self.random_seed = x;
self.random_seed as usize % 16 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) { pub fn on_adjacent_tiles(&self, x: usize, y: usize, mut action: impl FnMut(usize, usize), if_mine: bool) {
if y > 0 { if y > 0 {
//above //above
if self.tiles[y - 1][x].mine == if_mine { if self.tiles[y - 1][x].mine == if_mine {

View File

@@ -1,5 +1,6 @@
use std::vec::Vec; use std::vec::Vec;
use std::vec; use std::vec;
use std::cmp::Ordering;
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType }; use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse };
@@ -101,13 +102,11 @@ impl WindowLike for Reversi {
} }
} }
} }
if white_tiles == black_tiles { self.state = match white_tiles.cmp(&black_tiles) {
self.state = State::Tie; Ordering::Equal => State::Tie,
} else if white_tiles > black_tiles { Ordering::Greater => State::WhiteWin,
self.state = State::WhiteWin; Ordering::Less => State::BlackWin,
} else { };
self.state = State::BlackWin;
}
} }
} }
self.current_number = None; self.current_number = None;

View File

@@ -149,8 +149,7 @@ impl WindowLike for Terminal {
Mode::Running => { Mode::Running => {
//update //update
let mut changed = false; let mut changed = false;
loop { while let Ok(ci) = self.pty_outerr_rx.as_mut().unwrap().recv_timeout(Duration::from_millis(5)) {
if let Ok(ci) = self.pty_outerr_rx.as_mut().unwrap().recv_timeout(Duration::from_millis(5)) {
if char::from(ci) == '\n' { if char::from(ci) == '\n' {
let append_line = strip_ansi_escape_codes(bytes_to_string(self.process_current_line.clone())); let append_line = strip_ansi_escape_codes(bytes_to_string(self.process_current_line.clone()));
self.output += &append_line; self.output += &append_line;
@@ -167,9 +166,6 @@ impl WindowLike for Terminal {
self.process_current_line.push(ci); self.process_current_line.push(ci);
} }
changed = true; changed = true;
} else {
break;
}
} }
let running_process = self.running_process.as_mut().unwrap(); let running_process = self.running_process.as_mut().unwrap();
if let Some(_status) = running_process.try_wait().unwrap() { if let Some(_status) = running_process.try_wait().unwrap() {
@@ -377,13 +373,9 @@ impl Terminal {
let mut stdin = self.running_process.as_mut().unwrap().stdin.take().unwrap(); let mut stdin = self.running_process.as_mut().unwrap().stdin.take().unwrap();
let (tx2, rx2) = channel(); let (tx2, rx2) = channel();
thread::spawn(move || { thread::spawn(move || {
loop { while let Ok(write_line) = rx2.recv() {
if let Ok(write_line) = rx2.recv() {
let write_line: String = write_line + "\n"; let write_line: String = write_line + "\n";
stdin.write(write_line.as_bytes()).unwrap(); stdin.write_all(write_line.as_bytes()).unwrap();
} else {
break;
}
} }
}); });
self.pty_outerr_rx = Some(rx1); self.pty_outerr_rx = Some(rx1);

View File

@@ -72,7 +72,7 @@ fn init(framebuffer: Framebuffer, framebuffer_info: FramebufferInfo) {
writer.init(framebuffer_info.clone()); writer.init(framebuffer_info.clone());
let mut wm: WindowManager = WindowManager::new(writer, framebuffer, dimensions, rotate, grayscale, PASSWORD_HASH); let mut wm: WindowManager = WindowManager::new(writer, framebuffer, dimensions, rotate, grayscale, env!("CARGO_PKG_VERSION").to_string(), PASSWORD_HASH);
let mut stdout = RawStdout::new(stdout()); let mut stdout = RawStdout::new(stdout());
stdout.enter_raw_mode().unwrap(); stdout.enter_raw_mode().unwrap();
@@ -107,10 +107,10 @@ fn init(framebuffer: Framebuffer, framebuffer_info: FramebufferInfo) {
}); });
let touch = args.contains(&"touch".to_string()); let touch = args.contains(&"touch".to_string());
let force_stdout = args.contains(&"force-stdout".to_string()); //write to stdout to force framebuffer redraw
//read touchscreen presses (hopefully) //read touchscreen presses (hopefully)
thread::spawn(move || { thread::spawn(move || {
//spawn evtest, parse it for touch coords
if touch { if touch {
let mut events = Input::new("/dev/input/by-path/first-touchscreen").unwrap(); //panics in threads don't matter in this case let mut events = Input::new("/dev/input/by-path/first-touchscreen").unwrap(); //panics in threads don't matter in this case
let mut x: Option<usize> = None; let mut x: Option<usize> = None;
@@ -153,10 +153,17 @@ fn init(framebuffer: Framebuffer, framebuffer_info: FramebufferInfo) {
for message in rx { for message in rx {
match message { match message {
ThreadMessage::KeyChar(kc) => wm.handle_message(WindowManagerMessage::KeyChar(kc.clone())), ThreadMessage::KeyChar(kc) => {
wm.handle_message(WindowManagerMessage::KeyChar(kc.clone()));
if force_stdout {
println!(" "); //without any stdout, on some user's devices, for some reason the framebuffer doesn't get redrawn to the screen
}
},
ThreadMessage::Touch(x, y) => { ThreadMessage::Touch(x, y) => {
wm.handle_message(WindowManagerMessage::Touch(x, y)); wm.handle_message(WindowManagerMessage::Touch(x, y));
if force_stdout {
println!(" "); //without any stdout, on my phone, for some reason the framebuffer doesn't get redrawn to the screen println!(" "); //without any stdout, on my phone, for some reason the framebuffer doesn't get redrawn to the screen
}
}, },
ThreadMessage::Clear => { ThreadMessage::Clear => {
write!(stdout.stdout, "{}", CLEAR_ALL).unwrap(); write!(stdout.stdout, "{}", CLEAR_ALL).unwrap();

View File

@@ -1,10 +1,13 @@
[package] [package]
name = "wm" name = "wm"
version = "1.0.3" version = "1.2.0"
repository = "https://github.com/stjet/ming-wm" repository = "https://github.com/stjet/ming-wm"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
edition = "2021" edition = "2021"
[lints]
workspace = true
[dependencies] [dependencies]
ming-wm-lib = { path = "../ming-wm-lib" } ming-wm-lib = { path = "../ming-wm-lib" }
linux = { path = "../linux" } linux = { path = "../linux" }

View File

@@ -13,6 +13,7 @@ use ming_wm_lib::components::paragraph::Paragraph;
pub struct About { pub struct About {
dimensions: Dimensions, dimensions: Dimensions,
components: Vec<Box<dyn Component<()> + Send>>, components: Vec<Box<dyn Component<()> + Send>>,
version: String,
} }
impl WindowLike for About { impl WindowLike for About {
@@ -40,7 +41,7 @@ impl WindowLike for About {
//properties //properties
fn title(&self) -> String { fn title(&self) -> String {
"About".to_string() "About".to_string() + " - v" + &self.version
} }
fn subtype(&self) -> WindowLikeType { fn subtype(&self) -> WindowLikeType {
@@ -53,10 +54,11 @@ impl WindowLike for About {
} }
impl About { impl About {
pub fn new() -> Self { pub fn new(version: String) -> Self {
Self { Self {
dimensions: [0, 0], dimensions: [0, 0],
components: Vec::new(), components: Vec::new(),
version,
} }
} }
} }

View File

@@ -53,7 +53,7 @@ impl WindowLike for DesktopBackground {
} }
} else if line.len() > 1 { } else if line.len() > 1 {
//first character of line is either r or any other character, but is not part of the path //first character of line is either r or any other character, but is not part of the path
return vec![DrawInstructions::Bmp([0, 0], line[1..].to_string(), line.chars().next().unwrap() == 'r')]; return vec![DrawInstructions::Bmp([0, 0], line[1..].to_string(), line.starts_with('r'))];
} }
} }
} }

View File

@@ -1,5 +1,6 @@
use std::vec; use std::vec;
use std::vec::Vec; use std::vec::Vec;
use std::time::{ SystemTime, UNIX_EPOCH }; //for psuedo-randomness
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
@@ -7,12 +8,11 @@ use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManager
use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType }; use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType };
use bitcoin_hashes::Sha512; use bitcoin_hashes::Sha512;
//const PASSWORD_HASH: [u8; 64] = [220, 88, 183, 188, 240, 27, 107, 181, 58, 191, 198, 170, 114, 38, 7, 148, 6, 179, 75, 128, 231, 171, 172, 220, 85, 38, 36, 113, 116, 146, 70, 197, 163, 179, 158, 192, 130, 53, 247, 48, 47, 209, 95, 96, 179, 211, 4, 122, 254, 127, 21, 165, 139, 199, 151, 226, 216, 176, 123, 41, 194, 221, 58, 69];
pub struct LockScreen { pub struct LockScreen {
dimensions: Dimensions, dimensions: Dimensions,
input_password: String, input_password: String,
password_hash: [u8; 64], password_hash: [u8; 64],
lines: [String; 3],
} }
impl WindowLike for LockScreen { impl WindowLike for LockScreen {
@@ -51,9 +51,11 @@ impl WindowLike for LockScreen {
fn draw(&self, _theme_info: &ThemeInfo) -> Vec<DrawInstructions> { fn draw(&self, _theme_info: &ThemeInfo) -> Vec<DrawInstructions> {
vec![ vec![
DrawInstructions::Rect([0, 0], self.dimensions, [0, 0, 0]), DrawInstructions::Rect([0, 0], self.dimensions, [0, 0, 0]),
DrawInstructions::Text([4, 4], vec!["nimbus-roman".to_string()], "The bulldozer outside the kitchen window was quite a big one.".to_string(), [255, 255, 255], [0, 0, 0], None, None), DrawInstructions::Text([4, 4], vec!["nimbus-roman".to_string()], self.lines[0].clone(), [255, 255, 255], [0, 0, 0], None, None),
DrawInstructions::Text([4, 4 + 16], vec!["nimbus-roman".to_string()], "\"Yellow,\" he thought, and stomped off back to his bedroom to get dressed.".to_string(), [255, 255, 255], [0, 0, 0], None, None), //He is my brother.
DrawInstructions::Text([4, 4 + 16 * 2], vec!["nimbus-roman".to_string()], "He stared at it.".to_string(), [255, 255, 255], [0, 0, 0], None, None), DrawInstructions::Text([4, 4 + 16], vec!["nimbus-roman".to_string()], self.lines[1].clone(), [255, 255, 255], [0, 0, 0], None, None),
//But I must kill him and keep strong to do it.
DrawInstructions::Text([4, 4 + 16 * 2], vec!["nimbus-roman".to_string()], self.lines[2].clone(), [255, 255, 255], [0, 0, 0], None, None),
DrawInstructions::Text([4, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "Password: ".to_string(), [255, 255, 255], [0, 0, 0], None, None), DrawInstructions::Text([4, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "Password: ".to_string(), [255, 255, 255], [0, 0, 0], None, None),
DrawInstructions::Text([80, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "*".repeat(self.input_password.len()), [255, 255, 255], [0, 0, 0], None, None), DrawInstructions::Text([80, 4 + 16 * 3], vec!["nimbus-roman".to_string()], "*".repeat(self.input_password.len()), [255, 255, 255], [0, 0, 0], None, None),
] ]
@@ -71,10 +73,24 @@ impl WindowLike for LockScreen {
impl LockScreen { impl LockScreen {
pub fn new(password_hash: [u8; 64]) -> Self { pub fn new(password_hash: [u8; 64]) -> Self {
let possible_lines = [
[
"\"He took about forty pounds,\" the old man said aloud.".to_string(),
"He took my harpoon too and all the rope, he thought, and now my fish bleeds again and there will be others.".to_string(),
"He did not like to look at the fish anymore since it had been mutilated.".to_string()
],
[
"The bulldozer outside the kitchen window was quite a big one.".to_string(),
"\"Yellow,\" he thought, and stomped off back to his bedroom to get dressed.".to_string(),
"He stared at it.".to_string()
],
];
let rand_index = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as usize % possible_lines.len();
Self { Self {
dimensions: [0, 0], dimensions: [0, 0],
input_password: String::new(), input_password: String::new(),
password_hash, password_hash,
lines: possible_lines[rand_index].clone(),
} }
} }
} }

View File

@@ -6,6 +6,7 @@ use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLik
use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType, InfoType, WindowsVec }; use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse, WindowManagerRequest, ShortcutType, InfoType, WindowsVec };
use ming_wm_lib::framebuffer_types::Dimensions; use ming_wm_lib::framebuffer_types::Dimensions;
use ming_wm_lib::themes::ThemeInfo; use ming_wm_lib::themes::ThemeInfo;
use ming_wm_lib::utils::trunc_words;
use ming_wm_lib::components::Component; use ming_wm_lib::components::Component;
use ming_wm_lib::components::toggle_button::ToggleButton; use ming_wm_lib::components::toggle_button::ToggleButton;
@@ -78,7 +79,8 @@ impl WindowLike for Taskbar {
break; break;
} }
let info = &self.windows_in_workspace[wi]; let info = &self.windows_in_workspace[wi];
let name = &info.1; let max_text_width = META_WIDTH - PADDING * 2;
let name = trunc_words(&["nimbus-roman".to_string()], info.1.clone(), None, max_text_width);
let mut b = ToggleButton::new(name.to_string() + "-window", [PADDING * 2 + 44 + (META_WIDTH + PADDING) * wi, PADDING], [META_WIDTH, self.dimensions[1] - (PADDING * 2)], name.to_string(), TaskbarMessage::Nothing, TaskbarMessage::Nothing); let mut b = ToggleButton::new(name.to_string() + "-window", [PADDING * 2 + 44 + (META_WIDTH + PADDING) * wi, PADDING], [META_WIDTH, self.dimensions[1] - (PADDING * 2)], name.to_string(), TaskbarMessage::Nothing, TaskbarMessage::Nothing);
b.inverted = info.0 == self.focused_id; b.inverted = info.0 == self.focused_id;
instructions.extend(b.draw(theme_info)); instructions.extend(b.draw(theme_info));
@@ -123,5 +125,3 @@ impl Taskbar {
} }
} }
} }

View File

@@ -4,9 +4,7 @@ use std::vec::Vec;
use bmp_rust::bmp::BMP; use bmp_rust::bmp::BMP;
use ming_wm_lib::framebuffer_types::*; use ming_wm_lib::framebuffer_types::*;
use crate::fs::get_font_char_from_fonts; use ming_wm_lib::fonts::{ CachedFontCharGetter, FontCharInfo };
type FontChar = (char, Vec<Vec<u8>>, u8);
fn color_with_alpha(color: RGBColor, bg_color: RGBColor, alpha: u8) -> RGBColor { fn color_with_alpha(color: RGBColor, bg_color: RGBColor, alpha: u8) -> RGBColor {
/*let factor: f32 = alpha as f32 / 255.0; /*let factor: f32 = alpha as f32 / 255.0;
@@ -47,6 +45,7 @@ pub struct FramebufferInfo {
//currently doesn't check if writing onto next line accidentally //currently doesn't check if writing onto next line accidentally
pub struct FramebufferWriter { pub struct FramebufferWriter {
info: FramebufferInfo, info: FramebufferInfo,
fc_getter: CachedFontCharGetter,
buffer: Vec<u8>, buffer: Vec<u8>,
saved_buffer: Option<Vec<u8>>, saved_buffer: Option<Vec<u8>>,
rotate_buffer: Option<Vec<u8>>, rotate_buffer: Option<Vec<u8>>,
@@ -57,6 +56,7 @@ impl FramebufferWriter {
pub fn new(grayscale: bool) -> Self { pub fn new(grayscale: bool) -> Self {
Self { Self {
info: Default::default(), info: Default::default(),
fc_getter: CachedFontCharGetter::new(128), //an arbitrary high-ish number for max cache size
buffer: Vec::new(), buffer: Vec::new(),
saved_buffer: None, saved_buffer: None,
rotate_buffer: None, rotate_buffer: None,
@@ -108,6 +108,7 @@ impl FramebufferWriter {
.copy_from_slice(&color[..]); .copy_from_slice(&color[..]);
} }
//straight horizontal line
fn _draw_line(&mut self, start_pos: usize, bytes: &[u8]) { fn _draw_line(&mut self, start_pos: usize, bytes: &[u8]) {
self.buffer[start_pos..(start_pos + bytes.len())] self.buffer[start_pos..(start_pos + bytes.len())]
.copy_from_slice(bytes); .copy_from_slice(bytes);
@@ -127,17 +128,14 @@ impl FramebufferWriter {
} }
} }
pub fn draw_char(&mut self, top_left: Point, char_info: &FontChar, color: RGBColor, bg_color: RGBColor) { pub fn draw_char(&mut self, top_left: Point, char_info: &FontCharInfo, color: RGBColor, bg_color: RGBColor) {
let mut start_pos; let mut start_pos;
for row in 0..char_info.1.len() { for row in 0..char_info.height {
//char_info.2 is vertical offset start_pos = ((top_left[1] + row + char_info.top_offset as usize) * self.info.stride + top_left[0]) * self.info.bytes_per_pixel;
start_pos = ((top_left[1] + row + char_info.2 as usize) * self.info.stride + top_left[0]) * self.info.bytes_per_pixel; for col in &char_info.data[row] {
for col in &char_info.1[row] { if col > &0 && start_pos + 3 < self.info.byte_len {
if col > &0 {
if start_pos + 3 < self.info.byte_len {
self._draw_pixel(start_pos, color_with_alpha(color, bg_color, *col)); self._draw_pixel(start_pos, color_with_alpha(color, bg_color, *col));
} }
}
start_pos += self.info.bytes_per_pixel; start_pos += self.info.bytes_per_pixel;
} }
} }
@@ -147,8 +145,10 @@ impl FramebufferWriter {
pub fn draw_pixel(&mut self, point: Point, color: RGBColor) { pub fn draw_pixel(&mut self, point: Point, color: RGBColor) {
let start_pos = (point[1] * self.info.stride + point[0]) * self.info.bytes_per_pixel; let start_pos = (point[1] * self.info.stride + point[0]) * self.info.bytes_per_pixel;
if self.info.byte_len > start_pos {
self._draw_pixel(start_pos, color); self._draw_pixel(start_pos, color);
} }
}
//shapes //shapes
@@ -223,8 +223,8 @@ impl FramebufferWriter {
if c == ' ' { if c == ' ' {
top_left[0] += mono_width.unwrap_or(5) as usize; top_left[0] += mono_width.unwrap_or(5) as usize;
} else { } else {
let char_info = get_font_char_from_fonts(&fonts, c); let char_info = self.fc_getter.get(&fonts, c);
let char_width = char_info.1[0].len(); let char_width = char_info.width;
let add_after: usize; let add_after: usize;
if let Some(mono_width) = mono_width { if let Some(mono_width) = mono_width {
let mono_width = mono_width as usize; let mono_width = mono_width as usize;
@@ -244,6 +244,34 @@ impl FramebufferWriter {
} }
} }
//line
pub fn draw_line(&mut self, start: Point, end: Point, width: usize, color: RGBColor) {
//leftmost point
let lm;
let rm;
if start[0] < end[0] || (start[0] == end[0] && end[1] > start[1]) {
lm = start;
rm = end;
} else {
lm = end;
rm = start;
};
let dx = (rm[0] - lm[0]) as f64; //will not be negative
let dy = rm[1] as f64 - lm[1] as f64; //surely no point will be big enough to lose data here?
let use_x = dy.abs() < dx;
//slope
let m = if use_x { dy / dx } else { dx / dy };
for ci in 0..if use_x { dx as usize } else { dy.abs() as usize } {
let i = if !use_x && dy < 0.0 { -(ci as f64) } else { ci as f64 };
let ix = if use_x { i } else { (m * i).round() };
let iy = if use_x { (m * i).round() } else { i };
for j in 0..width {
self.draw_pixel([(lm[0] as f64 + ix) as usize + j, (lm[1] as f64 + iy) as usize], color);
}
}
}
//bmps //bmps
//reverse is workaround for when my bmp lib returns rgba instead of bgra //reverse is workaround for when my bmp lib returns rgba instead of bgra

View File

@@ -1,38 +1,9 @@
use std::fs::{ read_dir, File }; use std::fs::read_dir;
use std::io::Read;
use std::collections::HashMap; use std::collections::HashMap;
use ming_wm_lib::dirs; use ming_wm_lib::dirs;
use ming_wm_lib::utils::get_rest_of_split; use ming_wm_lib::utils::get_rest_of_split;
fn get_font_char(dir: &str, c: char) -> Option<(char, Vec<Vec<u8>>, u8)> {
let c = if c == '/' { '𐘋' } else if c == '\\' { '𐚆' } else if c == '.' { '𐘅' } else { c };
if let Ok(mut file) = File::open(dir.to_string() + "/" + &c.to_string() + ".alpha") {
let mut ch: Vec<Vec<u8>> = Vec::new();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let lines: Vec<&str> = contents.split("\n").collect();
for l in 1..lines.len() {
//.unwrap_or(0) is important because zeroes are just empty
ch.push(lines[l].split(",").map(|n| n.parse().unwrap_or(0)).collect());
}
return Some((c, ch, lines[0].parse().unwrap()));
}
None
}
pub fn get_font_char_from_fonts(fonts: &[String], c: char) -> (char, Vec<Vec<u8>>, u8) {
for font in fonts {
let p = dirs::exe_dir(Some(&("ming_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(&("ming_bmps/".to_string() + &fonts[0]))).to_string_lossy().to_string();
//so a ? char should be in every font. otherwise will just return blank
get_font_char(&p, '?').unwrap_or(('?', vec![vec![0]], 0))
}
//Category, Vec<Display name, file name> //Category, Vec<Display name, file name>
pub type ExeWindowInfos = HashMap<String, Vec<(String, String)>>; pub type ExeWindowInfos = HashMap<String, Vec<(String, String)>>;

View File

@@ -88,7 +88,7 @@ impl ProxyWindowLike {
if let Some(buffer) = buffer.stdout.as_mut() { if let Some(buffer) = buffer.stdout.as_mut() {
let mut output = String::new(); let mut output = String::new();
let mut reader = BufReader::new(buffer); let mut reader = BufReader::new(buffer);
if let Ok(_) = reader.read_line(&mut output) { if reader.read_line(&mut output).is_ok() {
output output
} else { } else {
String::new() String::new()

View File

@@ -6,11 +6,12 @@ use std::boxed::Box;
use std::cell::RefCell; use std::cell::RefCell;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::str::FromStr;
use linux::fb::Framebuffer; use linux::fb::Framebuffer;
use ming_wm_lib::framebuffer_types::{ Point, Dimensions }; use ming_wm_lib::framebuffer_types::{ Point, Dimensions };
use ming_wm_lib::themes::{ Themes, get_theme_info }; use ming_wm_lib::themes::{ Themes, get_theme_info };
use ming_wm_lib::utils::{ min, point_inside }; use ming_wm_lib::utils::{ min, point_inside, trunc_words };
use ming_wm_lib::messages::*; use ming_wm_lib::messages::*;
use ming_wm_lib::dirs::config_dir; use ming_wm_lib::dirs::config_dir;
use ming_wm_lib::window_manager_types::*; use ming_wm_lib::window_manager_types::*;
@@ -51,6 +52,7 @@ impl fmt::Debug for WindowLikeInfo {
} }
} }
pub struct WindowManager { pub struct WindowManager {
writer: RefCell<FramebufferWriter>, writer: RefCell<FramebufferWriter>,
rotate: bool, rotate: bool,
@@ -65,13 +67,14 @@ pub struct WindowManager {
current_workspace: u8, current_workspace: u8,
framebuffer: Framebuffer, framebuffer: Framebuffer,
clipboard: Option<String>, clipboard: Option<String>,
version: String,
password_hash: [u8; 64], password_hash: [u8; 64],
} }
//1 is up, 2 is down //1 is up, 2 is down
impl WindowManager { impl WindowManager {
pub fn new(writer: FramebufferWriter, framebuffer: Framebuffer, dimensions: Dimensions, rotate: bool, grayscale: bool, password_hash: [u8; 64]) -> Self { pub fn new(writer: FramebufferWriter, framebuffer: Framebuffer, dimensions: Dimensions, rotate: bool, grayscale: bool, version: String, password_hash: [u8; 64]) -> Self {
//println!("bg: {}x{}", dimensions[0], dimensions[1] - TASKBAR_HEIGHT - INDICATOR_HEIGHT); //println!("bg: {}x{}", dimensions[0], dimensions[1] - TASKBAR_HEIGHT - INDICATOR_HEIGHT);
let mut wm = WindowManager { let mut wm = WindowManager {
writer: RefCell::new(writer), writer: RefCell::new(writer),
@@ -87,6 +90,7 @@ impl WindowManager {
current_workspace: 0, current_workspace: 0,
framebuffer, framebuffer,
clipboard: None, clipboard: None,
version,
password_hash, password_hash,
}; };
wm.lock(); wm.lock();
@@ -165,8 +169,9 @@ impl WindowManager {
//if off_only is true, also handle request //if off_only is true, also handle request
//written confusingly but it works I promise //written confusingly but it works I promise
fn toggle_start_menu(&mut self, off_only: bool) -> WindowMessageResponse { fn toggle_start_menu(&mut self, off_only: bool) -> WindowMessageResponse {
let start_menu_exists = self.window_infos.iter().find(|w| w.window_like.subtype() == WindowLikeType::StartMenu).is_some(); let start_menu_exists = self.window_infos.iter().any(|w| w.window_like.subtype() == WindowLikeType::StartMenu);
if (start_menu_exists && off_only) || !off_only { //if (start_menu_exists && off_only) || !off_only {
if start_menu_exists || !off_only {
let taskbar_index = self.window_infos.iter().position(|w| w.window_like.subtype() == WindowLikeType::Taskbar).unwrap(); let taskbar_index = self.window_infos.iter().position(|w| w.window_like.subtype() == WindowLikeType::Taskbar).unwrap();
self.focused_id = self.window_infos[taskbar_index].id; self.focused_id = self.window_infos[taskbar_index].id;
if off_only { if off_only {
@@ -594,7 +599,7 @@ impl WindowManager {
} }
let w: Option<WindowBox> = match w.as_str() { let w: Option<WindowBox> = match w.as_str() {
"StartMenu" => Some(Box::new(StartMenu::new())), "StartMenu" => Some(Box::new(StartMenu::new())),
"About" => Some(Box::new(About::new())), "About" => Some(Box::new(About::new(self.version.clone()))),
"Help" => Some(Box::new(Help::new())), "Help" => Some(Box::new(Help::new())),
_ => Some(Box::new(ProxyWindowLike::new(&w))), _ => Some(Box::new(ProxyWindowLike::new(&w))),
}; };
@@ -648,6 +653,13 @@ impl WindowManager {
[top_left[0], top_left[1] + if is_window { WINDOW_TOP_HEIGHT } else { 0 }] [top_left[0], top_left[1] + if is_window { WINDOW_TOP_HEIGHT } else { 0 }]
} }
fn prevent_overflow_dimensions(dimensions: Dimensions, window_dimensions: Dimensions, top_left: Point) -> Dimensions {
[
min(dimensions[0], window_dimensions[0] - top_left[0]),
min(dimensions[1], window_dimensions[1] - top_left[1]),
]
}
//another issue with a huge vector of draw instructions; it takes up heap memory //another issue with a huge vector of draw instructions; it takes up heap memory
pub fn draw(&mut self, maybe_redraw_ids: Option<Vec<usize>>, use_saved_buffer: bool) { pub fn draw(&mut self, maybe_redraw_ids: Option<Vec<usize>>, use_saved_buffer: bool) {
let theme_info = get_theme_info(&self.theme).unwrap(); let theme_info = get_theme_info(&self.theme).unwrap();
@@ -694,18 +706,21 @@ impl WindowManager {
DrawInstructions::Text(top_left, fonts, text, color, bg_color, horiz_spacing, mono_width) => DrawInstructions::Text(WindowManager::get_true_top_left(top_left, is_window), fonts.clone(), text.clone(), *color, *bg_color, *horiz_spacing, *mono_width), DrawInstructions::Text(top_left, fonts, text, color, bg_color, horiz_spacing, mono_width) => DrawInstructions::Text(WindowManager::get_true_top_left(top_left, is_window), fonts.clone(), text.clone(), *color, *bg_color, *horiz_spacing, *mono_width),
DrawInstructions::Bmp(top_left, path, reverse) => DrawInstructions::Bmp(WindowManager::get_true_top_left(top_left, is_window), path.to_string(), *reverse), DrawInstructions::Bmp(top_left, path, reverse) => DrawInstructions::Bmp(WindowManager::get_true_top_left(top_left, is_window), path.to_string(), *reverse),
DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => DrawInstructions::Gradient(WindowManager::get_true_top_left(top_left, is_window), *dimensions, *start_color, *end_color, *steps), DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => DrawInstructions::Gradient(WindowManager::get_true_top_left(top_left, is_window), *dimensions, *start_color, *end_color, *steps),
DrawInstructions::Line(start, end, width, color) => DrawInstructions::Line(WindowManager::get_true_top_left(start, is_window), WindowManager::get_true_top_left(end, is_window), *width, *color),
} }
}).collect(); }).collect();
//draw window background //draw window background
instructions.push_front(DrawInstructions::Rect([0, 0], window_dimensions, theme_info.background)); instructions.push_front(DrawInstructions::Rect([0, 0], window_dimensions, theme_info.background));
//draw window top decorations and what not //draw window top decorations and what not
let title = trunc_words(&["nimbus-roman".to_string()], window_info.window_like.title(), None, window_dimensions[0]);
instructions.extend(vec![ instructions.extend(vec![
//left top border //left top border
DrawInstructions::Rect([0, 0], [window_dimensions[0], 1], theme_info.border_left_top), DrawInstructions::Rect([0, 0], [window_dimensions[0], 1], theme_info.border_left_top),
DrawInstructions::Rect([0, 0], [1, window_dimensions[1]], theme_info.border_left_top), DrawInstructions::Rect([0, 0], [1, window_dimensions[1]], theme_info.border_left_top),
//top //top
DrawInstructions::Rect([1, 1], [window_dimensions[0] - 2, WINDOW_TOP_HEIGHT - 3], theme_info.top), DrawInstructions::Rect([1, 1], [window_dimensions[0] - 2, WINDOW_TOP_HEIGHT - 3], theme_info.top),
DrawInstructions::Text([4, 4], vec!["nimbus-roman".to_string()], window_info.window_like.title().to_string(), theme_info.top_text, theme_info.top, None, None), //window title
DrawInstructions::Text([4, 4], vec!["nimbus-roman".to_string()], title, theme_info.top_text, theme_info.top, None, None),
//top bottom border //top bottom border
DrawInstructions::Rect([1, WINDOW_TOP_HEIGHT - 2], [window_dimensions[0] - 2, 2], theme_info.border_left_top), DrawInstructions::Rect([1, WINDOW_TOP_HEIGHT - 2], [window_dimensions[0] - 2, 2], theme_info.border_left_top),
//right bottom border //right bottom border
@@ -729,10 +744,7 @@ impl WindowManager {
match instruction { match instruction {
DrawInstructions::Rect(top_left, dimensions, color) => { DrawInstructions::Rect(top_left, dimensions, color) => {
//try and prevent overflows out of the window //try and prevent overflows out of the window
let true_dimensions = [ let true_dimensions = WindowManager::prevent_overflow_dimensions(dimensions, window_dimensions, top_left);
min(dimensions[0], window_dimensions[0] - top_left[0]),
min(dimensions[1], window_dimensions[1] - top_left[1]),
];
window_writer.draw_rect(top_left, true_dimensions, color); window_writer.draw_rect(top_left, true_dimensions, color);
}, },
DrawInstructions::Circle(centre, radius, color) => { DrawInstructions::Circle(centre, radius, color) => {
@@ -746,7 +758,11 @@ impl WindowManager {
window_writer.draw_bmp(top_left, path, reverse); window_writer.draw_bmp(top_left, path, reverse);
}, },
DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => { DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => {
window_writer.draw_gradient(top_left, dimensions, start_color, end_color, steps); let true_dimensions = WindowManager::prevent_overflow_dimensions(dimensions, window_dimensions, top_left);
window_writer.draw_gradient(top_left, true_dimensions, start_color, end_color, steps);
},
DrawInstructions::Line(start, end, width, color) => {
window_writer.draw_line(start, end, width, color);
}, },
} }
} }