I've been working on this project for about two months now (ish)
I'm a sucker for minimalism. Partly, because I keep a number of older pc's and i have to keep things minimal if i want them to work. And partly because that's just the way I am.
When it comes to minimalism in PC user experience, there's some options: Tiling window managers that force-snap windows into a specified geometry; Floating window managers that are so minimal you have to summon a panel separately; And, small-ish desktop environments that aren't really that small and have to load half of Qt or half of GTK.
Fluxbox is the one window manager I can think of that sort of does what it should. It's small enough and configurable enough to not be a memory hog.
But I wanted to go smaller. Fluxbox is written in C++, which is (kind of) a superset of C. I'm not saying Fluxbox is bloated. It's one of the smaller ones, at around 5MB. A 5MB executable is already an achievement, but I figured something in C might be even smaller, leaving some things out.
So, I started working on Cygnus
I called it Cygnus because it's written in C, and licensed under GNU GPL. Also, Cygnus is Hyoga, and... if you know, you know...
I wrote it in C to keep it as small as possible, and added only the absolute minimum necessary to the window manager code, so that it can function as a standalone executable that includes the code to have a fully functional, independent window manager. This would be enough, if the user decided not to install anything else, and it would have an X-session, a window manager, a panel, workspaces, a root menu, a config file for custom keyboard shortcuts, a run dialogue window, and a refresh and exit function. All of this in under 32KB. That is 156 times smaller than Fluxbox.
While it is possible to compile cygnus.c and run it standalone, it's not the only thing I wrote. I was already there, so I wrote a whole suite, including the window manager, a file manager, a open-file utility, a screenshot utility, a media player, a webcam application, a notepad, an image viewer, a paint program (this one's in C++), an automount utility, a clock applet, and a calculator. And, even if the user decided to install everything (all the binaries plus all the documentation), it's still only around 900KB, and 5 times smaller than Fluxbox.
I installed it on 3 pc's: 1 intel haswell, 1 intel braswell, and 1 amd radeon apu. The smallest was the braswell (an acer chromebook), with a minimal Debian installation + Cygnus occupying less than 4GB of disk space, and less than 400 MB of RAM used.
I won't be pasting the code here. It's over a dozen programs and utilities, some of them with several hundred lines of code, and some with over a thousand. I'll post a couple of screenshots, and the link to the GitHub project page.
Screenshot of working Cygnus WM session, including file manager, calculator, paint program, notepad, root menu, panel and panel applets
Project Page on GitHub
Where'd My Left Sock Go?
Ideas, little orcs, and other imaginary creatures
Hangman Game
There's a certain thing about a classic hangman game. Some kind of charm, perhaps. I find it pretty versatile, especially for remote meetings. Sometimes there's got to be some little tension-breaker among co-workers.
So I wrote this little python script to run a hangman game, getting random words from a local word_list.txt file.
There's really not much to it. It's a single python script, one text file with the words, and nothing else. All it needs is a python interpreter. Regularly, tkinter is included in python3. Some linux distributions, however, have it in a separate package. Lubuntu, for example, made me apt-get install python3-tk in order for it to work, but other than that, nothing else. I prefer tkinter over pyQt6 (although pyQt6 is prettier in my not-so-humble opinion) because I'd rather not have to deal with virtual environments.
Code:
hangman.py
So I wrote this little python script to run a hangman game, getting random words from a local word_list.txt file.
There's really not much to it. It's a single python script, one text file with the words, and nothing else. All it needs is a python interpreter. Regularly, tkinter is included in python3. Some linux distributions, however, have it in a separate package. Lubuntu, for example, made me apt-get install python3-tk in order for it to work, but other than that, nothing else. I prefer tkinter over pyQt6 (although pyQt6 is prettier in my not-so-humble opinion) because I'd rather not have to deal with virtual environments.
- Random word taken from a local `word_list.txt` (one word per line).
- Classic ASCII-art hang-man that updates after each wrong guess.
- Input field for single-letter guesses (case-insensitive).
- New-Game button, win/lose messages, and a "reset" button.
- No external image files - everything is drawn with text.
Code:
hangman.py
"""
Simple Hang-Man game with a Tkinter GUI.
by Jonathan Torres
This program is free software distributed under the GNU
General Public License, version 3, or any later version
published by the Free Software Foundation.
"""
import random
import tkinter as tk
from pathlib import Path
from tkinter import messagebox
from typing import List
# ASCII stages
HANGMAN_STAGES: List[str] = [
"""
+---+
|
|
|
===""",
"""
+---+
O |
|
|
===""",
"""
+---+
O |
| |
|
===""",
"""
+---+
O |
/| |
|
===""",
"""
+---+
O |
/|\\ |
|
===""",
"""
+---+
O |
/|\\ |
/ |
===""",
"""
+---+
O |
/|\\ |
/ \\ |
===""",
]
MAX_WRONG = len(HANGMAN_STAGES) - 1
# wordfile loader helper
def load_word_list(path: Path) -> List[str]:
if not path.is_file():
raise FileNotFoundError(f"Word list not found: {path}")
"""hang"""
words = []
with path.open(encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if not line or line.startswith("#"):
continue
words.append(line.lower())
if not words:
raise ValueError("The word list is empty.")
return words
# main
class HangmanWindow:
def __init__(self, word_list: List[str]):
self.word_list = word_list
self.root = tk.Tk()
self.root.title("Hang-Man")
self.root.geometry("300x400")
self._setup_ui()
self._new_game()
def _setup_ui(self):
self.drawing_lbl = tk.Label(
self.root,
font=("Courier", 12),
justify="center"
)
self.word_lbl = tk.Label(
self.root,
font=("Sans Serif", 20, "bold")
)
#input field. width 2 because otherwise it looks too small.
self.guess_input = tk.Entry(self.root, font=("Sans Serif", 14), width=2)
self.guess_input.bind("<Return>", lambda e: self._process_guess())
self.wrong_lbl = tk.Label(
self.root,
font=("Sans Serif", 12),
fg="red"
)
self.new_btn = tk.Button(self.root, text="New Game", command=self._new_game)
self.quit_btn = tk.Button(self.root, text="Quit", command=self.root.quit)
self.drawing_lbl.pack(pady=10)
self.word_lbl.pack(pady=10)
self.guess_input.pack(pady=5)
self.wrong_lbl.pack(pady=5)
"""the"""
btn_frame = tk.Frame(self.root)
btn_frame.pack(pady=20, side="top", fill="none", expand=False)
self.new_btn.pack(side="left", padx=10)
self.quit_btn.pack(side="left", padx=10)
# game flow helpers
def _new_game(self):
#new
self.secret_word = random.choice(self.word_list)
self.correct_set = set()
self.wrong_set = set()
self.wrong_cnt = 0
self._update_ui()
self.guess_input.config(state="normal")
self.guess_input.delete(0, tk.END)
self.guess_input.focus()
def _process_guess(self):
# check input & win/lose status
guess = self.guess_input.get().lower()
self.guess_input.delete(0, tk.END)
if not guess.isalpha():
messagebox.showinfo("Invalid", "Please type a letter (A-Z).")
return
if guess in self.correct_set or guess in self.wrong_set:
messagebox.showinfo("Repeated", f"You already guessed \"{guess}\".")
return
if guess in self.secret_word:
self.correct_set.add(guess)
else:
self.wrong_set.add(guess)
self.wrong_cnt += 1
self._update_ui()
self._check_game_over()
# refresh
def _update_ui(self):
self.drawing_lbl.config(text=HANGMAN_STAGES[self.wrong_cnt])
"""stickman"""
displayed = " ".join(
ch if ch in self.correct_set else "_" for ch in self.secret_word
)
self.word_lbl.config(text=displayed)
# change color when almost lose/lose
if self.wrong_cnt == MAX_WRONG:
self.drawing_lbl.config(fg="red")
elif self.wrong_cnt >= MAX_WRONG - 1:
self.drawing_lbl.config(fg="darkred")
else:
self.drawing_lbl.config(fg="black")
# display wrong letters
if self.wrong_set:
wrong_letters = " ".join(sorted(self.wrong_set))
self.wrong_lbl.config(text=f"Wrong: {wrong_letters}")
else:
self.wrong_lbl.config(text="")
# checks
def _check_game_over(self):
if self.wrong_cnt >= MAX_WRONG:
self._end_game(False)
return
if all(ch in self.correct_set for ch in set(self.secret_word)):
self._end_game(True)
def _end_game(self, won: bool):
self.guess_input.config(state="disabled")
if won:
msg = f"You guessed it! The word was \"{self.secret_word}\"."
messagebox.showinfo("You win!", msg)
else:
self.drawing_lbl.config(text=HANGMAN_STAGES[-1])
msg = f"You missed it. The word was \"{self.secret_word}\"."
messagebox.showinfo("Game Over", msg)
# start
def main() -> None:
script_dir = Path(__file__).parent
words = load_word_list(script_dir / "word_list.txt")
"""for sparta!"""
window = HangmanWindow(words)
window.root.mainloop()
if __name__ == "__main__":
main()
beepr
This has got to be one of the most obnoxious things I've done.
I've always had a thing for lo-fi, chip-style audio. I'd been wanting to do something of the sort for android, but I've been pretty up-to-the-neck with other stuff, so I've not had much time.
Then, just last night, as I was wrapping up another code project, I thought I might as well throw in some sort of music maker. And I tried. Boy, did I try. Working with C, though, means messing with external audio libraries or trying to work directly with pcspeaker via ioctl. Nothing worked, and I decided I hate C forever (I don't).
Good thing python simply sort of works with just about anything under the sun. So I ditched C and started over, this time using pygame, and tkinter for the gui frontend.
And it worked. I'm using the pc keyboard, but it's also drawing an on-screen keyboard so you can either clickity-click or typity-doo. The UI is nothing out of this world: just an on-screen qwerty-layout keyboard (minus controls), and a small 'screen' to display typed characters. I assigned a frequency value to each key, with 'z' being the lowest (C3), and each key after (in row order) being exactly one semitone higher, so that the entire pc keyboard covers about 3 octaves.
I tried and failed many times to generate the notes via code, so the dumbed down solution was to generate individual .wav files and have each key trigger an aplay command to play them, based on the key bindings assigned to each key. The result is an obnoxious little keyboard to annoy the crap out of anyone within hearing range.
The reason I call it obnoxious is that it's ridiculously loud, and lowering the volume does nothing to ameliorate it, but I guess that's kinda part of the charm. It's not meant to be pretty. It's supposed to be lo-fi and chip-tune-like, and forcing the laptop speakers distorts the sound a little bit, so it comes off as a sort of square wave synth sound (ish).
I won't add any pictures because the UI is really just an on-screen keyboard and is like zero-percent descriptive of what the thing does.
You'll just have to trust me.
Code:
Project Page on GitHub:
I've always had a thing for lo-fi, chip-style audio. I'd been wanting to do something of the sort for android, but I've been pretty up-to-the-neck with other stuff, so I've not had much time.
Then, just last night, as I was wrapping up another code project, I thought I might as well throw in some sort of music maker. And I tried. Boy, did I try. Working with C, though, means messing with external audio libraries or trying to work directly with pcspeaker via ioctl. Nothing worked, and I decided I hate C forever (I don't).
Good thing python simply sort of works with just about anything under the sun. So I ditched C and started over, this time using pygame, and tkinter for the gui frontend.
And it worked. I'm using the pc keyboard, but it's also drawing an on-screen keyboard so you can either clickity-click or typity-doo. The UI is nothing out of this world: just an on-screen qwerty-layout keyboard (minus controls), and a small 'screen' to display typed characters. I assigned a frequency value to each key, with 'z' being the lowest (C3), and each key after (in row order) being exactly one semitone higher, so that the entire pc keyboard covers about 3 octaves.
I tried and failed many times to generate the notes via code, so the dumbed down solution was to generate individual .wav files and have each key trigger an aplay command to play them, based on the key bindings assigned to each key. The result is an obnoxious little keyboard to annoy the crap out of anyone within hearing range.
The reason I call it obnoxious is that it's ridiculously loud, and lowering the volume does nothing to ameliorate it, but I guess that's kinda part of the charm. It's not meant to be pretty. It's supposed to be lo-fi and chip-tune-like, and forcing the laptop speakers distorts the sound a little bit, so it comes off as a sort of square wave synth sound (ish).
I won't add any pictures because the UI is really just an on-screen keyboard and is like zero-percent descriptive of what the thing does.
You'll just have to trust me.
Code:
Project Page on GitHub:
#!/usr/bin/env python3
"""
beepr v0.1 - a keyboard based synthesizer made just for fun.
by Jonathan Torres
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
"""
import sys
import math
import time
import threading
import os
import tkinter as tk
from tkinter import ttk
import numpy as np
import wave
import subprocess
import tempfile
import shutil
# Configuration
SAMPLE_RATE = 44100
BASE_FREQ = 130.81 # C3
KEYS_LAYOUT = [
"1234567890",
"qwertyuiop",
"asdfghjkl",
"zxcvbnm,./"
]
# z-row (index 3) is lowest, 1-row (index 0) is highest
ALL_KEYS = KEYS_LAYOUT[3] + KEYS_LAYOUT[2] + KEYS_LAYOUT[1] + KEYS_LAYOUT[0]
class BeeprApp:
def __init__(self, root):
self.root = root
self.root.title("Beepr")
# Set initial window size: Width x Height
self.root.geometry("410x230")
self.root.resizable(False, False)
# Temp directory for wav files
self.temp_dir = tempfile.mkdtemp(prefix="beepr_")
self.sounds = {}
self.generate_sounds()
self.setup_ui()
self.setup_bindings()
# Pipe support
if not sys.stdin.isatty():
self.pipe_thread = threading.Thread(target=self.read_pipe, daemon=True)
self.pipe_thread.start()
self.root.protocol("WM_DELETE_WINDOW", self.on_exit)
def generate_sounds(self):
#Pre-generate WAV files for each key.
duration = 0.2 #I set this to 0.2 to keep the notes short. at 0.5 it's pretty muddy
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), False)
fade_duration = int(SAMPLE_RATE * 0.05)
fade_env = np.ones_like(t)
fade_env[:fade_duration] = np.linspace(0, 1, fade_duration)
fade_env[-fade_duration:] = np.linspace(1, 0, fade_duration)
for i, char in enumerate(ALL_KEYS):
freq = BASE_FREQ * (2 ** (i / 12.0))
"""tried to reduce volume by multiplying amplitude by 0.4
(sounds just as obnoxiously loud to me anyway)"""
tone = np.sin(2 * np.pi * freq * t) * fade_env * 0.4
audio = (tone * 32767).astype(np.int16)
wav_path = os.path.join(self.temp_dir, f"note_{i}.wav")
with wave.open(wav_path, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(audio.tobytes())
self.sounds[char] = wav_path
def setup_ui(self):
self.display_var = tk.StringVar()
self.display = ttk.Entry(self.root, textvariable=self.display_var, font=("Monospace", 14), state='readonly')
self.display.pack(fill=tk.X, padx=10, pady=10)
self.kb_frame = ttk.Frame(self.root)
self.kb_frame.pack(padx=10, pady=10)
self.buttons = {}
for row_idx, row_chars in enumerate(KEYS_LAYOUT):
row_frame = ttk.Frame(self.kb_frame)
row_frame.pack(side=tk.TOP, anchor=tk.W)
if row_idx == 1:
ttk.Label(row_frame, text=" ").pack(side=tk.LEFT)
elif row_idx == 2:
ttk.Label(row_frame, text=" ").pack(side=tk.LEFT)
elif row_idx == 3:
ttk.Label(row_frame, text=" ").pack(side=tk.LEFT)
for char in row_chars:
btn = ttk.Button(row_frame, text=char, width=3,
command=lambda c=char: self.play_key(c))
btn.pack(side=tk.LEFT, padx=1)
self.buttons[char] = btn
def setup_bindings(self):
self.root.bind("<Key>", self.on_key_press)
def on_key_press(self, event):
char = event.char.lower()
if char in self.sounds:
self.play_key(char)
def play_key(self, char):
if char in self.sounds:
#Use aplay directly cuz nothing else worked (or maybe I just suck at this)
subprocess.Popen(["aplay", "-q", self.sounds[char]],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
current_text = self.display_var.get()
if len(current_text) > 40:
current_text = current_text[1:]
self.display_var.set(current_text + char)
if char in self.buttons:
btn = self.buttons[char]
btn.state(['pressed'])
self.root.after(100, lambda: btn.state(['!pressed']))
def read_pipe(self):
while True:
line = sys.stdin.readline()
if not line:
break
for char in line:
c = char.lower()
if c in self.sounds:
self.root.after(0, lambda x=c: self.play_key(x))
time.sleep(0.15)
elif c == ' ':
time.sleep(0.15)
time.sleep(0.1)
def on_exit(self):
shutil.rmtree(self.temp_dir)
self.root.destroy()
def main():
root = tk.Tk()
app = BeeprApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Suscribirse a:
Comentarios
(
Atom
)



