[{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"This is the place where I dump coding or tech-related things I did on a certain day.\n","date":"15 May 2026","externalUrl":null,"permalink":"/dev-diary/","section":"Dev Diary","summary":"","title":"Dev Diary","type":"dev-diary"},{"content":"Yep, I started a new project. I SWEAR I will continue with File Valet another day. Hear me out! \u0026gt;:)\nInspiration # Yesterday (well technically today) at 1am, Youtube recommended me a video by Christopher Okhravi (a friend recommended me in the past already, I already enjoyed the idea back then but never went through with trying it out). Its about building an object-oriented representation of Pokémon battles. While the battles themselves seem simple on the surface, the logic behind them can get incredibly convoluted.\nThe goal of that video was to show how one can build useful abstractions of the whole battle system and thus create a kind of Domain Specific Language (DSL) from it. In Java or other object-oriented languages (which the video mainly focuses on) you use Interfaces and nest Objects within each other to achieve a DSL. In Rust we can do it similarly by nesting enums inside of each other (we actually wont need that many traits here), and that realization was what ultimately lead me to trying all of this out myself (and I had to go to sleep, fully hyped to tackle this on, at 2am\u0026hellip;).\nIf youre interested you can watch it right here or skip ahead to the next section c:\nWhat exactly is a DSL? # Domain specific languages are small languages made to express concepts within a certain sphere of problems. Opposite to general-purpose languages like Rust or Java, which can literally express anything, DSLs often focus on smaller areas. A well-designed DSL lets you model all problems in your problem space through combinations of symbols of that language, and it might even make the described problems as readable as if it would have been expressed through natural language.\nWe are already using DSLs everywhere. SQL is a DSL for querying data, regex for pattern matching, HTML for document structure. None of them are general-purpose, but can be interpreted by programs to solve the described problem: finding specific data in database, finding the specified pattern in a string or rendering the described HTML page in a browser window.\nWhen it comes to Pokémon battles, the DSL will need to be able to express who is fighting against each other, what moves they can choose and what they do or what abilities they have and what changes they might inflict on the battle, and much more. So that we can eventually build moves and their effects, and everything else thats relevant to a battle, in our own little language without touching the game logic itself.\nCreating a Pokémon DSL in Rust # In the video Christopher modeled everything top-to-bottom, starting at the top with the most general thing we want to model. While this might make us introduce types that we did not define yet early on, I think it is a nice approach to slowly explore the scope of everything. (For some reason I usually go bottom-to-top\u0026hellip; \u0026ldquo;o.o). I simplified the following code examples (stripping away how exactly the data is fetched or modeled, the core concepts stay the same. If youre interested how it currently looks like, you can scroll all the way down where I linked the repository).\nThe battle # The most general thing we want to model is the battle itself:\npub struct Battle { fighters: Vec\u0026lt;Fighter\u0026gt;, .. } Its literally just a collection of fighters which are gonna fight each other. We do not care who is who or who can attack whom yet.\nThe fighters are of some species which contains a name, types, stats, etc. They have some set of moves they are allowed to use in this battle and also a team id (for identifying who is to attack whom). In reality, same with the battle state, this will grow alot once you model the whole battle system.\npub struct Fighter { species: Species, moves: Vec\u0026lt;Move\u0026gt;, team: usize, .. } The moves are where it gets actually interesting. A move has a certain condition at which it is able to be executed, and once executed, it tries to do something, which is called an attempt. (Depending on the final model, you might actually want to have more than one condition, Christopher explained it in his video but I simplified it here).\npub struct Move { name: String, types: Vec\u0026lt;PokemonType\u0026gt;, condition: BattleCondition, attempt: Attempt, .. } Conditions # Before we talk about the attempt I want to talk about conditions, which were a bit more complex to model than in the video. I have decided to go for a tree-like structure where the leafs are predicates that can always be evaluated in a certain context.\n#[cfg_attr(feature = \u0026#34;serde\u0026#34;, derive(serde::Serialize, serde::Deserialize))] pub enum Condition\u0026lt;L\u0026gt; { Always, And(Box\u0026lt;Condition\u0026lt;L\u0026gt;\u0026gt;, Box\u0026lt;Condition\u0026lt;L\u0026gt;\u0026gt;), Or(Box\u0026lt;Condition\u0026lt;L\u0026gt;\u0026gt;, Box\u0026lt;Condition\u0026lt;L\u0026gt;\u0026gt;), Not(Box\u0026lt;Condition\u0026lt;L\u0026gt;\u0026gt;), Predicate(L), } To know in which context a leaf is to be evaluated, an L of a condition will need to implement checkable. It just checks that some condition on the context is either true or false.\npub trait Checkable { type Context; fn check(\u0026amp;self, ctx: \u0026amp;mut Self::Context) -\u0026gt; bool; } We can then implement a function check on our condition thats available whenever it has a leaf-type that implements Checkable. If we pass the proper context the leaf-type of a certain condition expects, we can evaluate it.\nimpl\u0026lt;L: Checkable\u0026gt; Condition\u0026lt;L\u0026gt; { pub fn check(\u0026amp;self, ctx: \u0026amp;mut L::Context) -\u0026gt; bool { match self { Condition::Always =\u0026gt; true, Condition::And(a, b) =\u0026gt; a.check(ctx) \u0026amp;\u0026amp; b.check(ctx), Condition::Or(a, b) =\u0026gt; a.check(ctx) || b.check(ctx), Condition::Not(a) =\u0026gt; !a.check(ctx), Condition::Predicate(p) =\u0026gt; p.check(ctx), } } } To make the conditions a usable part of our Pokémon battle DSL we will need to be able to check certain conditions given the current battle as context. For this I created a new leaf-type (or predicate) specifically for checking battle conditions.\npub enum BattlePredicate { HasFieldEffect(FieldEffect), Prob(Probability), Target { target: Target, cond: FighterCondition, }, .. } impl Checkable for BattlePredicate { type Context = Battle; fn check(\u0026amp;self, battle: \u0026amp;mut Battle) -\u0026gt; bool { match self { BattlePredicate::HasFieldEffect(effect) =\u0026gt; battle.has_field_effect(effect), BattlePredicate::Prob(p) =\u0026gt; p.roll(battle.rng()), BattlePredicate::Target { target, cond } =\u0026gt; { let fighter = target.resolve(battle); cond.check(\u0026amp;fighter) }, } } } Thanks to the Target predicate we are able to go even deeper and create conditions specific to one fighter of the current battle.\npub enum FighterPredicate { HasStatusEffect(StatusEffect), IsMaxHp, .. } impl Checkable for FighterPredicate { type Context = Fighter; fn check(\u0026amp;self, fighter: \u0026amp;mut Self::Context) -\u0026gt; bool { match self { Self::HasStatusEffect(effect) =\u0026gt; fighter.has_status_effect(effect), Self::IsMaxHp =\u0026gt; fighter.current_hp() == fighter.max_hp(), } } } Example conditions # Dream eater can only be used if a target is asleep:\nPredicate(Target(Opponent, Predicate(HasStatusEffect(Sleep)))) When trying to use rest, the user must not be at max HP:\nBattleCondition::Not(BattleCondition::Predicate(BattlePredicate::Target( Target::User, FighterCondition::Predicate(FighterPredicate::IsMaxHp), ))) Thunder during rain has 100% accuracy, so an accuracy check might look like this:\nBattleCondition::Or( BattleCondition::Predicate(BattlePredicate::HasFieldEffect(FieldEffect::Rain)), BattleCondition::Predicate(BattlePredicate::Prob(0.7)), ) Facade has double the power if the user is burned, poisoned or paralyzed:\nBattleCondition::Or( BattleCondition::Predicate(BattlePredicate::Target( Target::User, FighterCondition::Predicate(FighterPredicate::HasStatusEffect(StatusEffect::Burn)), )), BattleCondition::Or( BattleCondition::Predicate(BattlePredicate::Target( Target::User, FighterCondition::Predicate(FighterPredicate::HasStatusEffect(StatusEffect::Poison)), )), BattleCondition::Predicate(BattlePredicate::Target( Target::User, FighterCondition::Predicate(FighterPredicate::HasStatusEffect(StatusEffect::Paralysis)), )), ), ) This might look convoluted now, but when we eventually serialize it as .ron (Rust Object Notation), it will become a bit neater. For example with facade:\nOr( Predicate(Target(User, Predicate(HasStatusEffect(Burn)))), Or( Predicate(Target(User, Predicate(HasStatusEffect(Poison)))), Predicate(Target(User, Predicate(HasStatusEffect(Paralysis)))), ), ) Attempts # An attempt is the intent of a Pokémons move to do something:\npub struct Move { name: String, types: Vec\u0026lt;PokemonType\u0026gt;, condition: BattleCondition, attempt: Attempt, .. } And we are able to roughly model it like this:\npub enum Attempt { Attempt { condition: BattleCondition, success: Effect, failure: Effect, after: Effect, }, Cascade { attempts: Vec\u0026lt;Attempt\u0026gt;, }, Combo { condition: BattleCondition, hits: Number, effect: Effect, }, .. } They are essentially just different ways of combining effects. Attempt (the enum value) itself is just a check on a BattleCondition which will then execute certain effects depending on the outcome of the condition. Cascade on the other hand wraps multiple consecutive attempts, where the failure of one will mark the end of the chain, the following attempts will be skipped. Combo can be used to just execute an effect x amount of times without any individual of them being able to fail mid-chain.\nNumber is yet another type that can be evaluated on the battle state and returns a number. It might return a different number depending on certain conditions, just a random number in a specific range, or an exact number thats always the same. This type might grow and look very different depending on how the modeling of the rest of the battle system will turn out in the end. For now, it gives us more flexibility in defining conditional numbers within our DSL.\npub enum Number { Exact(usize), .. } impl Number { pub fn evaluate(\u0026amp;self, battle: \u0026amp;Battle) -\u0026gt; usize { match self { Number::Exact(n) =\u0026gt; *n, } } } Effects # An effect is something that mutates the battle in a specific way. It might depend on a condition or could even be chained into a sequence of different effects. The concrete effects could just be about dealing a specific amount of damage to a target, or to apply a status condition. Though (and you know if youve played Pokémon), those effects can grow quite complex.\npub enum Effect { None, Condition { cond: BattleCondition, success: Box\u0026lt;Effect\u0026gt;, failure: Box\u0026lt;Effect\u0026gt;, }, Sequence { effects: Vec\u0026lt;Effect\u0026gt;, }, DirectDamage { target: Target, amount: Number, }, OHKO(Target), // One-hit K.O. .. } impl Effect { pub fn apply(\u0026amp;self, battle: \u0026amp;mut Battle) { .. } } I do not yet know which moves might require me to do which refactorings. But what I know for now is that it might be helpful to find common denominators of more complex effects, like atomic buildings blocks. That would make the DSL more powerful and also make complex effects easier to understand. The overall approach might have to be re-evaluated depending on the effects to be added though.\nAbilities \u0026amp; Held Items # The current approach only allows us to describe what move execution entails, what a move tries to do and which effects it might have on the battle. To model abilities or held items we need something that can also act passively, like the oran berry that heals its user but only when its HP reaches a certain threshold.\nTo achieve this, I am gonna reuse the effects that change something within the battle, but pair it with a trigger. Anywhere in our engine that ultimately solves our DSL can we call battle.trigger(Trigger) which will then evaluate abilities and held items for their TriggerEffects. If the trigger is the same as the one currently triggered, the effect will be executed.\npub enum Trigger { TurnStart, TurnEnd, DamageDealt(Target), } pub struct TriggerEffect { trigger: Trigger, effect: Effect, } An ability would look like this. Intimidation for example could have a trigger like Trigger::SwitchIn and an effect of lowering all opponents HP. While other abilities might trigger at the start of every turn and influence damage calculation with its effect. The expressiveness of this system will solely rely on how well we place our triggers and how expressive our effects system already is.\npub struct Ability { name: String, triggers: Vec\u0026lt;TriggerEffect\u0026gt;, } An item is not much different, only that it also has an additional effect if used actively. Some items might have no held effect, some might not have an active effect, some might have both. Our system will allow for any combination.\npub struct ItemData { name: String, held: Vec\u0026lt;TriggerEffect\u0026gt;, active: Effect, } More examples # I will give you some examples of how expressive we can make this DSL, I will add some enum values for illustration without specifying their exact implementation.\nThe move swords dance which is raising the users attack by 2 stages.\nMove( name: \u0026#34;Swords Dance\u0026#34;, types: [Normal], condition: Always, attempt: Attempt( condition: Always, success: StatChange(User, Attack, Exact(2)), failure: None, after: None, ), ) The move toxic which will badly poison the opponent, if it has no status effect already.\nMove( name: \u0026#34;Toxic\u0026#34;, types: [Poison], condition: Always, attempt: Attempt( condition: Predicate(Prob(0.9)), success: Condition( cond: Not(Predicate(Target(Opponent, Predicate(HasAnyStatusEffect)))), success: ApplyStatus(Opponent, BadlyPoisoned), failure: None, ), failure: Miss, after: None, ), ) The move triple axel which gets stronger with each attempt but will ultimately stop if it misses once.\nMove( name: \u0026#34;Triple Axel\u0026#34;, types: [Ice], condition: Always, attempt: Cascade(attempts: [ Attempt( condition: Predicate(Prob(0.9)), success: TypeDamage(target: Opponent, category: Physical, power: Exact(20)), failure: Miss, after: None, ), Attempt( condition: Predicate(Prob(0.9)), success: TypeDamage(target: Opponent, category: Physical, power: Exact(40)), failure: Miss, after: None, ), Attempt( condition: Predicate(Prob(0.9)), success: TypeDamage(target: Opponent, category: Physical, power: Exact(60)), failure: Miss, after: None, ), ]), ) The move solar beam which can hit instantly if the sun is out, else it has to charge for a turn. (Multi-turn moves might need some special care though).\nMove( name: \u0026#34;Solar Beam\u0026#34;, types: [Grass], condition: Always, attempt: Attempt( condition: Always, success: Condition( cond: Predicate(Target(User, Predicate(HasVolatile(Charging(\u0026#34;Solar Beam\u0026#34;))))), success: Sequence(effects: [ RemoveVolatile(User, Charging(\u0026#34;Solar Beam\u0026#34;)), TypeDamage(target: Opponent, category: Special, power: Exact(120)), ]), failure: Condition( cond: Predicate(HasFieldEffect(Sun)), success: TypeDamage(target: Opponent, category: Special, power: Exact(120)), failure: Sequence(effects: [ ApplyVolatile(User, Charging(\u0026#34;Solar Beam\u0026#34;)), Message(\u0026#34;is absorbing light!\u0026#34;), ]), ), ), failure: None, after: None, ), ) The ability drizzle which will make it rain when the user is switched in.\nAbility( name: \u0026#34;Drizzle\u0026#34;, triggers: [ TriggerEffect( trigger: SwitchIn(User), effect: SetFieldEffect(Rain, Exact(5)), ), ], ) The ability speed boost which will raise the speed of the user at the end of each turn.\nAbility( name: \u0026#34;Speed Boost\u0026#34;, triggers: [ TriggerEffect( trigger: TurnEnd, effect: StatChange(User, Speed, Exact(1)), ), ], ) The ability poison heal which is healing the user if they would have taken poison damage this turn. We might be able to express this in a different way though, I dont know how well that SupressDefault work in the long run. I expect it to stop whatever would have come after that BeforePoisonDamage trigger but it might get messy.\nAbility( name: \u0026#34;Poison Heal\u0026#34;, triggers: [ TriggerEffect( trigger: BeforePoisonDamage(User), effect: Condition( cond: Predicate(Target(User, Predicate(HasStatusEffect(Poison)))), success: Sequence(effects: [ SuppressDefault, Heal(target: User, percent_of_max_hp: Exact(12)), ]), failure: None, ), ), ], ) The leftovers item, which will heal the user at the end of each turn.\nItemData( name: \u0026#34;Leftovers\u0026#34;, held: [ TriggerEffect( trigger: TurnEnd, effect: Heal(target: User, percent_of_max_hp: Exact(6)), ), ], active: None, ) The item focus sash which will prevent a pokemon from being one-hit K.O.\u0026rsquo;d.\nItemData( name: \u0026#34;Focus Sash\u0026#34;, held: [ TriggerEffect( trigger: BeforeFaint(User), effect: Condition( cond: Predicate(Target(User, Predicate(WasMaxHpBeforeHit))), success: Sequence(effects: [ SuppressDefault, SetHp(target: User, Exact(1)), ConsumeItem(User), Message(\u0026#34;held on using its Focus Sash!\u0026#34;), ]), failure: None, ), ), ], active: None, ) And last but not least, the oran berry which can heal the user if below 50% HP when held OR when used directly as an item.\nItemData( name: \u0026#34;Oran Berry\u0026#34;, held: [ TriggerEffect( trigger: DamageDealt(User), effect: Condition( cond: Predicate(Target(User, Predicate(HpBelow(Percent(50))))), success: Sequence(effects: [ Heal(target: User, flat: Exact(10)), ConsumeItem(User), ]), failure: None, ), ), ], active: Sequence(effects: [ Heal(target: User, flat: Exact(10)), ConsumeItem(User), ]), ) Conclusion # While I dont think this is the ULTIMATE way of modeling Pokémon battles, I think it can be really fun and rewarding. I will definitely try to continue this project in the future and will report back if I made any significant breakthroughs, lol. As of right now its still a rough sketch and nothing truly functional has come of it yet.\nI also think writing the DSL could become a bit tedious, so I definitely have to work on that too. There might be a way to parse the data from the Poké-API directly into my DSL, but thats for another day\u0026hellip; \u0026gt;:)\nRepository # Zitronenjoghurt/poke-dsl An experiment about creating a Domain Specific Language through nested enums in Rust for modeling Pokémon battles! Rust 0 0 ","date":"15 May 2026","externalUrl":null,"permalink":"/dev-diary/entry-3/","section":"Dev Diary","summary":"A 1am YouTube recommendation lead me down a rabbit hole of creating my own DSL for modeling Pokémon battles in Rust.","title":"Dev Diary - Entry #3","type":"dev-diary"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/tags/dsl/","section":"Tags","summary":"","title":"Dsl","type":"tags"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/","section":"Lemon Industries","summary":"","title":"Lemon Industries","type":"page"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/categories/pok%C3%A9-dsl/","section":"Categories","summary":"","title":"Poké DSL","type":"categories"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/tags/pok%C3%A9mon/","section":"Tags","summary":"","title":"Pokémon","type":"tags"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/tags/rust/","section":"Tags","summary":"","title":"Rust","type":"tags"},{"content":"","date":"15 May 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/tags/avif/","section":"Tags","summary":"","title":"Avif","type":"tags"},{"content":"Today I properly started with File Valet (you can find more information about it in the last entry).\nMulti-threading # Since egui can only draw the UI if the main thread isnt busy, we have to off-load all network operations to a separete thread. I always like encapsulating those shenanigans into their own struct for ease-of-use.\nI am essentially creating a struct that wraps communicating with the secondary thread:\npub struct FileValet { command_tx: mpsc::Sender\u0026lt;FvCommand\u0026gt;, event_rx: mpsc::Receiver\u0026lt;FvEvent\u0026gt;, ctx_handle: Option\u0026lt;std::thread::JoinHandle\u0026lt;()\u0026gt;\u0026gt;, pub state: Arc\u0026lt;FvState\u0026gt;, } impl FileValet { pub fn new() -\u0026gt; FvResult\u0026lt;Self\u0026gt; { let (command_tx, command_rx) = mpsc::channel(); let (event_tx, event_rx) = mpsc::channel(); let state = Arc::new(FvState::default()); let ctx = FvContext { command_rx, event_tx, ssh_session: None, state: state.clone(), }; let ctx_handle = std::thread::spawn(move || ctx.run()); Ok(Self { command_tx, event_rx, ctx_handle: Some(ctx_handle), state, }) } pub fn poll_event(\u0026amp;self) -\u0026gt; Option\u0026lt;FvEvent\u0026gt; { self.event_rx.try_recv().ok() } } While the Context contains the state of whatever runs on the secondary thread:\npub struct FvContext { pub command_rx: mpsc::Receiver\u0026lt;FvCommand\u0026gt;, pub event_tx: mpsc::Sender\u0026lt;FvEvent\u0026gt;, pub ssh_session: Option\u0026lt;Session\u0026gt;, pub state: Arc\u0026lt;FvState\u0026gt;, } impl FvContext { pub fn run(mut self) { while let Ok(command) = self.command_rx.recv() { self.handle_command(command); } } fn handle_command(\u0026amp;mut self, command: FvCommand) { if let Err(err) = match command { FvCommand::Connect { host, port, user } =\u0026gt; self.handle_connect(host, port, user), FvCommand::Disconnect =\u0026gt; { self.set_disconnected(); Ok(()) } } { self.send_event(FvEvent::error(err.to_string())) } } fn handle_connect(\u0026amp;mut self, host: String, port: u16, user: String) -\u0026gt; FvResult\u0026lt;()\u0026gt; { if self.state.is_connecting() { return Ok(()); } self.set_disconnected(); self.state.set_connecting(true); match self.try_connect(host, port, user) { Ok(session) =\u0026gt; { self.set_connected(session); Ok(()) } Err(err) =\u0026gt; { self.state.set_connecting(false); Err(err) } } } ... } They got their two-way communication, commands are sent from the main thread and processed by the secondary, while events are sent by the secondary and processed by the main one. They also got some shared state they can both access for small status information.\n#[derive(Debug)] pub struct FvState { connected: AtomicBool, connecting: AtomicBool, } impl Default for FvState { fn default() -\u0026gt; Self { Self { connected: AtomicBool::new(false), connecting: AtomicBool::new(false), } } } Overall, I think thats a nice way of handling multi-threaded tasks when dealing with egui. At least thats what I have grown accustomed to.\nSSH connection # The connection establishment itself will be pretty primitive for now. You just create a TCP connection, hand it to an ssh2::Session and youre (almost) done.\nfn try_connect(\u0026amp;self, host: String, port: u16, user: String) -\u0026gt; FvResult\u0026lt;Session\u0026gt; { let tcp = TcpStream::connect((host.as_str(), port))?; let mut session = Session::new()?; session.set_tcp_stream(tcp); session.handshake()?; self.authenticate(\u0026amp;session, \u0026amp;user)?; Ok(session) } For authentication I will just support key-auth for now. File Valet will either get that key through the specified user agent (will work if you added a key via ssh-add in terminal) or it will naively check common ssh filenames (there is probably a better approach to this, but it works for now).\nfn authenticate(\u0026amp;self, session: \u0026amp;Session, user: \u0026amp;str) -\u0026gt; FvResult\u0026lt;()\u0026gt; { if session.userauth_agent(user).is_ok() { return Ok(()); } let ssh_dir = dirs::home_dir() .ok_or(FvError::NoHomeDirectory)? .join(\u0026#34;.ssh\u0026#34;); for name in [\u0026#34;id_ed25519\u0026#34;, \u0026#34;id_rsa\u0026#34;, \u0026#34;id_ecdsa\u0026#34;] { let path = ssh_dir.join(name); if path.exists() \u0026amp;\u0026amp; session .userauth_pubkey_file(user, None, \u0026amp;path, None) .is_ok() { return Ok(()); } } Err(FvError::NoWorkingAuthenticationMethod) } This is how it looks in the UI at the moment. It will also show little toasts in the upper right on success or errors (egui-notify). Now that we got this we will also be able to talk to my server via SFTP. My dream of an easy workflow of updating pre-processed files to a specific directory on my server with as little friction as possible is coming ever so much closer! \u0026gt;:)\nUpload process # For uploading the files to my remote directory I will just use SCP over the SSH connection we already established, with the ssh2 crate this is relatively trivial. I am sending the data over in 128KB chunks to get a nice upload progress bar.\nfn scp_write(\u0026amp;self, data: \u0026amp;[u8], remote_path: \u0026amp;Path) -\u0026gt; FvResult\u0026lt;()\u0026gt; { let session = self.ssh.as_ref().ok_or(FvError::NotConnected)?; let mut remote = session.scp_send(remote_path, 0o644, data.len() as u64, None)?; for chunk in data.chunks(128 * 1024) { remote.write_all(chunk)?; self.state.upload.add_bytes_sent(chunk.len() as u64); } remote.send_eof()?; remote.wait_eof()?; remote.close()?; remote.wait_close()?; Ok(()) } To upload multiple different files Im just gonna wrap that SCP-write function and add more state updates.\nfn upload_all(\u0026amp;mut self, files: \u0026amp;[(Vec\u0026lt;u8\u0026gt;, PathBuf)]) -\u0026gt; FvResult\u0026lt;()\u0026gt; { for (data, remote_path) in files { self.state.upload.set_bytes_total(data.len() as u64); self.state.upload.set_bytes_sent(0); self.scp_write(data, remote_path)?; self.state.upload.add_files_bytes_sent(data.len() as u64); self.state.upload.add_files_complete(1); } Ok(()) } You might be wondering why I am using raw vectors instead of file handles. Most of the files Im gonna upload will be pre-processed, which means they will be created completely new from existing files (e.g. to convert PNG to AVIF). I COULD theoretically store the new files in the file system before uploading them to save memory, but I didnt bother doing that yet. Its fine for my usecase for now c:\nJust gonna hook it up with my command handler and its all done:\nfn handle_upload(\u0026amp;mut self, files: Vec\u0026lt;(Vec\u0026lt;u8\u0026gt;, PathBuf)\u0026gt;) -\u0026gt; FvResult\u0026lt;()\u0026gt; { let upload = \u0026amp;self.state.upload; self.state.upload.set_files_total(files.len() as u64); self.state .upload .set_files_bytes_total(files.iter().map(|(d, _)| d.len() as u64).sum()); upload.set_uploading(true); let result = self.upload_all(\u0026amp;files); self.state.upload.reset(); if result.is_ok() { self.send_event(FvEvent::UploadComplete); } result } Next time I will be able to write a nice UI for this whole upload process and then I am already pretty close to being done.\nZitronenjoghurt/file-valet A remote file copy tool made for a very specific workflow (managing media files in a flat file server directory). Rust 0 0 ","date":"14 May 2026","externalUrl":null,"permalink":"/dev-diary/entry-2/","section":"Dev Diary","summary":"Today I have continued my work on File Valet, implementing multi-threading and the SSH connection.","title":"Dev Diary - Entry #2","type":"dev-diary"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/tags/egui/","section":"Tags","summary":"","title":"Egui","type":"tags"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/categories/file-valet/","section":"Categories","summary":"","title":"File Valet","type":"categories"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/tags/ssh/","section":"Tags","summary":"","title":"Ssh","type":"tags"},{"content":"","date":"14 May 2026","externalUrl":null,"permalink":"/tags/threading/","section":"Tags","summary":"","title":"Threading","type":"tags"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":" New ventures # Today I started two new projects, one is the website you\u0026rsquo;re currently reading this on, and the other is a kinda-related new tool.\nLemon Industries Website # Why? # Why lemons? I dont know, I just really love the faint essence of it, like when theyre in a cocktail or something, but even then limes are slightly tastier\u0026hellip; yellow just looks better than green. I just love lemons for some reason.\nWhy website? I am coding actively as a hobby for a couple of years now and I am always sharing my progress and all the cool things with my friends. All the images, screenshots and progress reports become scattered around everywhere and drown in other unrelated messages\u0026hellip; I really wanted to have one central place where I could share all the things I am working on and keep track of my progress. I think it could also be fun to look back on this post in like 2-3 years time and see how things have changed (if I look back at the stuff I did 2-3 years back from now a part of me dies).\nI hate frontend # In my humble opinion, frontend development is a pain and not something I wanna do for fun in my freetime. I am already forced to do it at work and I would rather avoid it whenever I can. That is also what lead the creation of this website into a certain direction. In an ideal world I would just write the text I wanna write, have some basic features like image embedding, have it all sorted, organized neatly, pleasant to look at, with view and like counts, and \u0026hellip; I would lose myself and never finish it if I were to do this fully myself and while also trying to avoid any frontend shenanigans (which would be impossible), thats why I took a look out for alternatives.\nTechnology # In university we talked about Content Management Systems (CMS). There are a couple of different categories, there are the fully-fledged-out ones like Wordpess, Typo3, Ghost, etc.. There are headless ones where I forgot the names. And then there are so called \u0026ldquo;static site generators\u0026rdquo;. I did not wanna use a UI to configure stuff, while it has it\u0026rsquo;s charme, it can also be pretty annoying (like in Wordpress, honestly theres just too much to configure, and the plugin stuff is a pain too). And for Ghost, a really nice-looking and sleek app\u0026hellip; it was on my list. UNTIL I found Hugo, a static site generator that generates your website from markdown and some config files.\nI always liked markdown for editing any kinds of things where I wanna put some effort in writing, Obsidian comes to mind. I used it for studying more complex computer science topics in university and it was kind of fun to create my own little Wiki through markdown, it grew on me. I hate dabbling with all the format options in Word or Pages when the text I write is fundamentally just text split in sections, sub-sections and sub-sub-sections. So creating my own website just through markdown sounded like a dream.\nThen I saw Hugo supporting custom themes and dove deeper into the rabbit hole, eventually finding Blowfish. What really surprised me is that its more than just a theme. It severely extends the featureset of Hugo and even provides a CLI tool through (npx blowfish-tools) for creating and editing sites, so in most cases you dont even have to touch any configuration files. Setting up and configuring the website was a matter of an hour or two, which included setting up firebase for live view and like counts and a docker container for eventually deploying my website. It was really simple, and now I can write markdown and it turns into a website, I dont have to do any frontend code, thats all I need. If you want to do the same you literally just have to follow the guide on https://blowfish.page.\nIf you are interested in how this page looks like for me during editing: https://github.com/Zitronenjoghurt/lemon-personal\nPlans # Realistically, I will probably not write an entry every day. And most future entries might also not be this long or detailed (or maybe even more detailed it will really depend). Even more so once I start working full-time in the near future\u0026hellip; I will just go with the flow and plaster on this page whatever goes through my mind and keyboard at whatever convenient time.\nFile Valet # Yet another remote file copying tool\u0026hellip; but is that really it?\nhttps://github.com/Zitronenjoghurt/file-valet\nMy usecase # If I continue on with this website long into the future, I will not be able to keep any kind of media in its git repository (it will be way too much)\u0026hellip; Thats why I set up a centralized media server to serve any kinds of files online from a directory on my server. I even set up an NFS to connect my PC and the server to more easily drop images into the remote media directory right from my desktop or wherever else locally. Only problem: Its still too cumbersome. Screenshots created on MacOs have a weird, non-URL friendly name out of the box. And I would also prefer it having a name like a timestamp: screenshot-20260513194937.png instead of Screenshot 2026-05-13 at 7.49.37 PM.png. For optimizing storage capacity I would also like to convert the images to WEBP or AVIF (most browsers should properly support these by know? I only know about the memes of how badly supported there are in other software, but even that might not be the case anymore nowadays). In short: I want to upload files to a directory on a remote server WHILE pre-processing them in a very specific way.\nTechnology # As I am used to Rust + egui for making desktop applications, I will stick to those. I will also need some ssh crate to connect to my server via SFTP, probably ssh2. And something to encode the images with, probably ravif. I read AVIF compressed better than WEBP with a better overall image quality, so thats a clear winner then. The images are only for being embedded on my webpage anyway and almost all browsers support avif (even Safari).\nPlans # I already laid the rough foundation today and will continue tomorrow.\n","date":"13 May 2026","externalUrl":null,"permalink":"/dev-diary/entry-1/","section":"Dev Diary","summary":"I am talking about this website and another related new tool I worked on today.","title":"Dev Diary - Entry #1","type":"dev-diary"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/categories/lemon-industries-website/","section":"Categories","summary":"","title":"Lemon Industries Website","type":"categories"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/webp/","section":"Tags","summary":"","title":"Webp","type":"tags"},{"content":"Some time ago I have started working on a Game Boy emulator in rust. My goal was to have nice debugging features and good performance while providing a good UX.\nYou can try it out in the browser: https://gb.lemon.industries\nIts not made for mobile! Features # Full Game Boy video and audio Support for Windows, MacOS and Web Controller support Plays Game Boy games with MBC1, MBC2 and MBC3 cartridges (no RTC support yet) (M-)Cycle-accurate instruction and memory timing Save states for games that included a battery Includes bundled open source homebrew games Basic debugging tools ","date":"13 May 2026","externalUrl":null,"permalink":"/demos/citrine-gb/","section":"Demos","summary":"A Game Boy emulator for your browser (or native), written in Rust.","title":"Citrine - A Game Boy Emulator for Web and Native","type":"demos"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/categories/citrine-game-boy-emulator/","section":"Categories","summary":"","title":"Citrine Game Boy Emulator","type":"categories"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/demo/","section":"Tags","summary":"","title":"Demo","type":"tags"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/demos/","section":"Demos","summary":"","title":"Demos","type":"demos"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/emulation/","section":"Tags","summary":"","title":"Emulation","type":"tags"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/game-boy/","section":"Tags","summary":"","title":"Game Boy","type":"tags"},{"content":"","date":"13 May 2026","externalUrl":null,"permalink":"/tags/nintendo/","section":"Tags","summary":"","title":"Nintendo","type":"tags"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]