diff --git a/src/java/mmm/core/api/recipes/I_CraftingRecipeWrapper.java b/src/java/mmm/core/api/recipes/I_CraftingRecipeWrapper.java
index 5d44c02..57471ee 100644
--- a/src/java/mmm/core/api/recipes/I_CraftingRecipeWrapper.java
+++ b/src/java/mmm/core/api/recipes/I_CraftingRecipeWrapper.java
@@ -8,6 +8,8 @@ import net.minecraft.item.ItemStack;
 
 public interface I_CraftingRecipeWrapper
 {
+	public String getIdentifier( );
+
 
 	public String getName( );
 
diff --git a/src/java/mmm/recipes/RCraftingWrappers.java b/src/java/mmm/recipes/RCraftingWrappers.java
index 1d2ba12..c1c2110 100644
--- a/src/java/mmm/recipes/RCraftingWrappers.java
+++ b/src/java/mmm/recipes/RCraftingWrappers.java
@@ -3,6 +3,7 @@ package mmm.recipes;
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 
 import mmm.core.api.recipes.I_CraftingRecipeWrapper;
 import mmm.core.api.recipes.I_CraftingRecipeWrapperFactory;
@@ -14,7 +15,8 @@ import net.minecraft.item.crafting.IRecipe;
 public class RCraftingWrappers
 {
 	private static final HashMap< Class< ? > , I_CraftingRecipeWrapperFactory > FACTORIES = new HashMap<>( );
-	private static final ArrayList< I_CraftingRecipeWrapper > RECIPES = new ArrayList<>( );
+	public static final ArrayList< I_CraftingRecipeWrapper > RECIPES = new ArrayList<>( );
+	public static final HashMap< String , I_CraftingRecipeWrapper > IDENTIFIERS = new HashMap<>( );
 
 
 	public static void register( final I_CraftingRecipeWrapperFactory factory )
@@ -39,10 +41,18 @@ public class RCraftingWrappers
 				System.err.println( "unsupported recipe class " + recipe.getClass( ) );
 				continue;
 			}
-			RCraftingWrappers.RECIPES.addAll( factory.createWrappers( recipe ) );
+			final List< I_CraftingRecipeWrapper > wrappers = factory.createWrappers( recipe );
+			for ( final I_CraftingRecipeWrapper wrapper : wrappers ) {
+				final String identifier = wrapper.getIdentifier( );
+				if ( RCraftingWrappers.IDENTIFIERS.containsKey( identifier ) ) {
+					throw new IllegalStateException( "duplicate wrapper ID " + identifier );
+				}
+				RCraftingWrappers.IDENTIFIERS.put( identifier , wrapper );
+			}
+			RCraftingWrappers.RECIPES.addAll( wrappers );
 		}
 
-		System.err.println( "wrapped " + RCraftingWrappers.RECIPES.size( ) + " recipes" );
+		System.err.println( "generated " + RCraftingWrappers.RECIPES.size( ) + " recipe wrappers" );
 	}
 
 
diff --git a/src/java/mmm/recipes/RShapedOreRecipeWrapper.java b/src/java/mmm/recipes/RShapedOreRecipeWrapper.java
index d06bce3..ca1de7e 100644
--- a/src/java/mmm/recipes/RShapedOreRecipeWrapper.java
+++ b/src/java/mmm/recipes/RShapedOreRecipeWrapper.java
@@ -7,6 +7,7 @@ import mmm.core.api.recipes.I_CraftingRecipeWrapper;
 import net.minecraft.inventory.IInventory;
 import net.minecraft.item.ItemStack;
 import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
+import net.minecraftforge.oredict.OreDictionary;
 import net.minecraftforge.oredict.ShapedOreRecipe;
 
 
@@ -27,6 +28,31 @@ public class RShapedOreRecipeWrapper
 	}
 
 
+	@Override
+	public String getIdentifier( )
+	{
+		final StringBuilder sb = new StringBuilder( "SHAPED_ORE;" );
+		final ItemStack recipeOutput = this.recipe.getRecipeOutput( );
+		sb.append( recipeOutput.getItem( ).getRegistryName( ) ).append( ',' ).append( recipeOutput.getMetadata( ) );
+		final Object[] input = this.recipe.getInput( );
+		for ( final Object inObject : input ) {
+			ItemStack stack;
+			sb.append( ';' );
+			if ( inObject instanceof ItemStack ) {
+				stack = (ItemStack) inObject;
+			} else if ( inObject instanceof List && ! ( (List< ? >) inObject ).isEmpty( ) ) {
+				stack = (ItemStack) ( (List< ? >) inObject ).get( 0 );
+			} else {
+				stack = null;
+			}
+			if ( stack != null ) {
+				sb.append( stack.getItem( ).getRegistryName( ) ).append( ',' ).append( stack.getMetadata( ) );
+			}
+		}
+		return sb.toString( );
+	}
+
+
 	@Override
 	public String getName( )
 	{
@@ -50,7 +76,7 @@ public class RShapedOreRecipeWrapper
 				final Object inObj = input[ i + j * this.width ];
 
 				if ( inObj instanceof ItemStack ) {
-					displayInventory.setInventorySlotContents( i + j * 3 , ( (ItemStack) inObj ).copy( ) );
+					RShapedOreRecipeWrapper.setSlot( displayInventory , i + j * 3 , (ItemStack) inObj );
 
 				} else if ( inObj instanceof List ) {
 					@SuppressWarnings( "unchecked" )
@@ -58,13 +84,24 @@ public class RShapedOreRecipeWrapper
 					if ( oreList.isEmpty( ) ) {
 						continue;
 					}
-					displayInventory.setInventorySlotContents( i + j * 3 , oreList.get( 0 ).copy( ) );
+					RShapedOreRecipeWrapper.setSlot( displayInventory , i + j * 3 , oreList.get( 0 ) );
 				}
 			}
 		}
 	}
 
 
+	private static void setSlot( final IInventory inventory , final int index , ItemStack stack )
+	{
+		if ( stack.getMetadata( ) == OreDictionary.WILDCARD_VALUE ) {
+			stack = new ItemStack( stack.getItem( ) , 1 , 0 );
+		} else {
+			stack = stack.copy( );
+		}
+		inventory.setInventorySlotContents( index , stack );
+	}
+
+
 	@Override
 	public boolean canCraft( final IInventory inventory )
 	{
diff --git a/src/java/mmm/recipes/RShapedRecipeWrapper.java b/src/java/mmm/recipes/RShapedRecipeWrapper.java
index 097ca62..1cf6b3d 100644
--- a/src/java/mmm/recipes/RShapedRecipeWrapper.java
+++ b/src/java/mmm/recipes/RShapedRecipeWrapper.java
@@ -5,6 +5,7 @@ import mmm.core.api.recipes.I_CraftingRecipeWrapper;
 import net.minecraft.inventory.IInventory;
 import net.minecraft.item.ItemStack;
 import net.minecraft.item.crafting.ShapedRecipes;
+import net.minecraftforge.oredict.OreDictionary;
 
 
 
@@ -20,6 +21,22 @@ public class RShapedRecipeWrapper
 	}
 
 
+	@Override
+	public String getIdentifier( )
+	{
+		final StringBuilder sb = new StringBuilder( "SHAPED;" );
+		final ItemStack recipeOutput = this.recipe.getRecipeOutput( );
+		sb.append( recipeOutput.getItem( ).getRegistryName( ) ).append( ',' ).append( recipeOutput.getMetadata( ) );
+		for ( final ItemStack stack : this.recipe.recipeItems ) {
+			sb.append( ';' );
+			if ( stack != null ) {
+				sb.append( stack.getItem( ).getRegistryName( ) ).append( ',' ).append( stack.getMetadata( ) );
+			}
+		}
+		return sb.toString( );
+	}
+
+
 	@Override
 	public String getName( )
 	{
@@ -39,15 +56,26 @@ public class RShapedRecipeWrapper
 	{
 		for ( int i = 0 ; i < this.recipe.recipeWidth ; i++ ) {
 			for ( int j = 0 ; j < this.recipe.recipeHeight ; j++ ) {
-				ItemStack itemStack = this.recipe.recipeItems[ i + j * this.recipe.recipeWidth ];
+				final ItemStack itemStack = this.recipe.recipeItems[ i + j * this.recipe.recipeWidth ];
 				if ( itemStack != null ) {
-					displayInventory.setInventorySlotContents( i + j * 3 , itemStack.copy( ) );
+					RShapedRecipeWrapper.setSlot( displayInventory , i + 3 * j , itemStack );
 				}
 			}
 		}
 	}
 
 
+	private static void setSlot( final IInventory inventory , final int index , ItemStack stack )
+	{
+		if ( stack.getMetadata( ) == OreDictionary.WILDCARD_VALUE ) {
+			stack = new ItemStack( stack.getItem( ) , 1 , 0 );
+		} else {
+			stack = stack.copy( );
+		}
+		inventory.setInventorySlotContents( index , stack );
+	}
+
+
 	@Override
 	public boolean canCraft( final IInventory inventory )
 	{
diff --git a/src/java/mmm/recipes/RShapelessOreRecipeWrapper.java b/src/java/mmm/recipes/RShapelessOreRecipeWrapper.java
index 10091d0..790f54d 100644
--- a/src/java/mmm/recipes/RShapelessOreRecipeWrapper.java
+++ b/src/java/mmm/recipes/RShapelessOreRecipeWrapper.java
@@ -50,6 +50,31 @@ public class RShapelessOreRecipeWrapper
 	}
 
 
+	@Override
+	public String getIdentifier( )
+	{
+		final StringBuilder sb = new StringBuilder( "SHAPELESS_ORE;" );
+		final ItemStack recipeOutput = this.recipe.getRecipeOutput( );
+		sb.append( recipeOutput.getItem( ).getRegistryName( ) ).append( ',' ).append( recipeOutput.getMetadata( ) );
+		final ArrayList< Object > input = this.recipe.getInput( );
+		for ( final Object inObject : input ) {
+			ItemStack stack;
+			sb.append( ';' );
+			if ( inObject instanceof ItemStack ) {
+				stack = (ItemStack) inObject;
+			} else if ( inObject instanceof List && ! ( (List< ? >) inObject ).isEmpty( ) ) {
+				stack = (ItemStack) ( (List< ? >) inObject ).get( 0 );
+			} else {
+				stack = null;
+			}
+			if ( stack != null ) {
+				sb.append( stack.getItem( ).getRegistryName( ) ).append( ',' ).append( stack.getMetadata( ) );
+			}
+		}
+		return sb.toString( );
+	}
+
+
 	@Override
 	public String getName( )
 	{
@@ -69,19 +94,32 @@ public class RShapelessOreRecipeWrapper
 	{
 		int i = 0;
 		for ( final ItemStack stack : this.combinedInputStacks ) {
-			displayInventory.setInventorySlotContents( i++ , stack.copy( ) );
+			RShapelessOreRecipeWrapper.setSlot( displayInventory , i++ , stack , stack.stackSize );
 		}
 		for ( final List< ItemStack > oreList : this.combinedInputOreLists.keySet( ) ) {
 			if ( oreList.isEmpty( ) ) {
 				continue;
 			}
-			final ItemStack stack = oreList.get( 0 ).copy( );
-			stack.stackSize = this.combinedInputOreLists.get( oreList );
-			displayInventory.setInventorySlotContents( i++ , stack );
+
+			final ItemStack stack = oreList.get( 0 );
+			RShapelessOreRecipeWrapper.setSlot( displayInventory , i++ , stack ,
+					this.combinedInputOreLists.get( oreList ) );
 		}
 	}
 
 
+	private static void setSlot( final IInventory inventory , final int index , ItemStack stack , final int size )
+	{
+		if ( stack.getMetadata( ) == OreDictionary.WILDCARD_VALUE ) {
+			stack = new ItemStack( stack.getItem( ) , 1 , 0 );
+		} else {
+			stack = stack.copy( );
+		}
+		stack.stackSize = size;
+		inventory.setInventorySlotContents( index , stack );
+	}
+
+
 	@Override
 	public boolean canCraft( final IInventory inventory )
 	{
diff --git a/src/java/mmm/recipes/RShapelessRecipeWrapper.java b/src/java/mmm/recipes/RShapelessRecipeWrapper.java
index 6638bed..f2d7f1a 100644
--- a/src/java/mmm/recipes/RShapelessRecipeWrapper.java
+++ b/src/java/mmm/recipes/RShapelessRecipeWrapper.java
@@ -37,6 +37,22 @@ public class RShapelessRecipeWrapper
 	}
 
 
+	@Override
+	public String getIdentifier( )
+	{
+		final StringBuilder sb = new StringBuilder( "SHAPELESS;" );
+		final ItemStack recipeOutput = this.recipe.getRecipeOutput( );
+		sb.append( recipeOutput.getItem( ).getRegistryName( ) ).append( ',' ).append( recipeOutput.getMetadata( ) );
+		for ( final ItemStack stack : this.recipe.recipeItems ) {
+			sb.append( ';' );
+			if ( stack != null ) {
+				sb.append( stack.getItem( ).getRegistryName( ) ).append( ',' ).append( stack.getMetadata( ) );
+			}
+		}
+		return sb.toString( );
+	}
+
+
 	@Override
 	public String getName( )
 	{
@@ -55,12 +71,28 @@ public class RShapelessRecipeWrapper
 	public void addInputsToDisplay( final IInventory displayInventory )
 	{
 		int i = 0;
-		for ( final ItemStack stack : this.combinedInputs ) {
-			displayInventory.setInventorySlotContents( i++ , stack.copy( ) );
+		for ( ItemStack stack : this.combinedInputs ) {
+			if ( stack.getMetadata( ) == OreDictionary.WILDCARD_VALUE ) {
+				stack = new ItemStack( stack.getItem( ) , 1 , 0 );
+			} else {
+				stack = stack.copy( );
+			}
+			RShapelessRecipeWrapper.setSlot( displayInventory , i++ , stack );
 		}
 	}
 
 
+	private static void setSlot( final IInventory inventory , final int index , ItemStack stack )
+	{
+		if ( stack.getMetadata( ) == OreDictionary.WILDCARD_VALUE ) {
+			stack = new ItemStack( stack.getItem( ) , 1 , 0 );
+		} else {
+			stack = stack.copy( );
+		}
+		inventory.setInventorySlotContents( index , stack );
+	}
+
+
 	@Override
 	public boolean canCraft( final IInventory inventory )
 	{
diff --git a/src/java/mmm/tech/base/workbench/TBWBContainer.java b/src/java/mmm/tech/base/workbench/TBWBContainer.java
index fb4ff6a..7887522 100644
--- a/src/java/mmm/tech/base/workbench/TBWBContainer.java
+++ b/src/java/mmm/tech/base/workbench/TBWBContainer.java
@@ -2,7 +2,10 @@ package mmm.tech.base.workbench;
 
 
 import mmm.MmmTech;
+import mmm.core.api.recipes.I_CraftingRecipeWrapper;
+import mmm.utils.UInventoryDisplay;
 import mmm.utils.gui.UGContainer;
+import mmm.utils.gui.UGSlotDisplay;
 import net.minecraft.entity.player.EntityPlayer;
 import net.minecraft.entity.player.InventoryPlayer;
 import net.minecraft.inventory.Slot;
@@ -17,6 +20,7 @@ public class TBWBContainer
 	public final TBWBTileEntity tileEntity;
 	public final World world;
 	public final BlockPos position;
+	public final UInventoryDisplay recipe;
 
 
 	public TBWBContainer( final InventoryPlayer playerInv , final TBWBTileEntity tileEntity )
@@ -26,8 +30,16 @@ public class TBWBContainer
 		this.position = tileEntity.getPos( );
 
 		this.addPlayerInventory( Slot::new , playerInv , 28 , 119 );
+
 		this.slotGroups.nextGroup( );
 		this.addGrid( Slot::new , tileEntity.storage , 8 , 15 );
+
+		this.slotGroups.nextGroup( );
+		this.recipe = new UInventoryDisplay( "Recipe" , 10 );
+		this.addGrid( UGSlotDisplay::new , this.recipe , //
+				3 , 3 , 0 , 86 , 32 );
+		this.addSlotToContainer( new UGSlotDisplay( this.recipe , 9 , 174 , 50 ) );
+
 		this.slotGroups.endGroups( );
 	}
 
@@ -40,4 +52,21 @@ public class TBWBContainer
 						this.position.getZ( ) + .5 ) <= 64.;
 	}
 
+
+	public void setCurrentRecipe( final I_CraftingRecipeWrapper wrapper , final boolean setDefault )
+	{
+		this.recipe.clear( );
+		if ( wrapper == null ) {
+			// XXX log if confirm is set
+			return;
+		}
+
+		wrapper.addInputsToDisplay( this.recipe );
+		this.recipe.setInventorySlotContents( 9 , wrapper.getOutput( ) );
+
+		if ( setDefault ) {
+			// this.tileEntity.setDefaultRecipe( wrapper.getIdentifier( ) );
+		}
+	}
+
 }
diff --git a/src/java/mmm/tech/base/workbench/TBWBGui.java b/src/java/mmm/tech/base/workbench/TBWBGui.java
index 3f2856b..9d0f568 100644
--- a/src/java/mmm/tech/base/workbench/TBWBGui.java
+++ b/src/java/mmm/tech/base/workbench/TBWBGui.java
@@ -2,8 +2,12 @@ package mmm.tech.base.workbench;
 
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
 
 import mmm.Mmm;
+import mmm.core.api.recipes.I_CraftingRecipeWrapper;
+import mmm.recipes.RCraftingWrappers;
 import mmm.utils.gui.A_UGContainerScreen;
 import mmm.utils.gui.UGArrowButton;
 import net.minecraft.client.gui.GuiButton;
@@ -27,6 +31,10 @@ public class TBWBGui
 	private UGArrowButton bNext;
 	private GuiButton bSetDefault;
 
+	private String searchString;
+	private List< I_CraftingRecipeWrapper > recipes;
+	private int currentRecipe;
+
 
 	public TBWBGui( final InventoryPlayer inventoryPlayer , final TBWBTileEntity tileEntity )
 	{
@@ -34,6 +42,10 @@ public class TBWBGui
 		this.container.slotGroups.showAll( );
 		this.xSize = 216;
 		this.ySize = 200;
+
+		this.searchString = "";
+		this.recipes = RCraftingWrappers.RECIPES;
+		this.setRecipe( -1 );
 	}
 
 
@@ -60,6 +72,8 @@ public class TBWBGui
 		this.buttonList.add( this.bPrevious );
 		this.buttonList.add( this.bNext );
 		this.buttonList.add( this.bSetDefault );
+		
+		this.enableButtons( );
 	}
 
 
@@ -90,7 +104,9 @@ public class TBWBGui
 	protected void keyTyped( final char typedChar , final int keyCode )
 			throws IOException
 	{
-		if ( !this.tfSearch.textboxKeyTyped( typedChar , keyCode ) ) {
+		if ( this.tfSearch.textboxKeyTyped( typedChar , keyCode ) ) {
+			this.handleFiltering( this.tfSearch.getText( ) );
+		} else {
 			super.keyTyped( typedChar , keyCode );
 		}
 	}
@@ -100,7 +116,78 @@ public class TBWBGui
 	protected void actionPerformed( final GuiButton button )
 			throws IOException
 	{
-		// TODO
+		if ( button == this.bNext && this.currentRecipe < this.recipes.size( ) - 1 ) {
+			this.setRecipe( this.currentRecipe + 1 );
+		} else if ( button == this.bPrevious && this.currentRecipe > 0 ) {
+			this.setRecipe( this.currentRecipe - 1 );
+		}
+	}
+
+
+	private void setRecipe( int index )
+	{
+		if ( index == -1 && !this.recipes.isEmpty( ) ) {
+			index = 0;
+		}
+		this.currentRecipe = index;
+		this.container.setCurrentRecipe( index == -1 ? null : this.recipes.get( index ) , false );
+		// CNetwork.sendToServer( new TBAFMessage( rName , false ) );
+		this.enableButtons( );
+	}
+
+
+	private void enableButtons( )
+	{
+		if ( this.bNext != null ) {
+			this.bNext.enabled = !this.recipes.isEmpty( ) && this.currentRecipe < this.recipes.size( ) - 1;
+			this.bPrevious.enabled = !this.recipes.isEmpty( ) && this.currentRecipe > 0;
+		}
+	}
+
+
+	private void handleFiltering( final String input )
+	{
+		final String newText = input.trim( ).toLowerCase( );
+		if ( this.searchString.equals( newText ) ) {
+			return;
+		}
+
+		final I_CraftingRecipeWrapper selected = this.currentRecipe == -1
+				? null
+				: this.recipes.get( this.currentRecipe );
+		final ArrayList< I_CraftingRecipeWrapper > fullList = RCraftingWrappers.RECIPES;
+		this.searchString = newText;
+		if ( "".equals( newText ) ) {
+			this.recipes = fullList;
+		} else {
+			if ( this.recipes == fullList ) {
+				this.recipes = new ArrayList<>( );
+			} else {
+				this.recipes.clear( );
+			}
+
+			final int nRecipes = fullList.size( );
+			for ( int i = 0 ; i < nRecipes ; i++ ) {
+				final I_CraftingRecipeWrapper recipe = fullList.get( i );
+				if ( I18n.format( recipe.getName( ) ).toLowerCase( ).contains( newText ) ) {
+					this.recipes.add( recipe );
+				}
+			}
+		}
+
+		if ( selected == null && !this.recipes.isEmpty( ) ) {
+			this.setRecipe( 0 );
+		} else if ( selected != null ) {
+			this.setRecipe( this.getRecipeIndex( selected ) );
+		} else {
+			this.setRecipe( -1 );
+		}
+	}
+
+
+	private int getRecipeIndex( I_CraftingRecipeWrapper selected )
+	{
+		return this.recipes.indexOf( selected );
 	}
 
 }