packages feed

apelsin 1.1 → 1.2

raw patch · 28 files changed

+1654/−983 lines, 28 filesdep +deepseqdep −stmdep ~tremulous-query

Dependencies added: deepseq

Dependencies removed: stm

Dependency ranges changed: tremulous-query

Files

apelsin.cabal view
@@ -1,5 +1,5 @@ Name:                   apelsin-Version:                1.1+Version:                1.2 Author:                 Christoffer Öjeling Maintainer:             christoffer@ojeling.net License:                GPL-3@@ -33,32 +33,39 @@ Executable apelsin   Default-Language:     Haskell2010   Default-Extensions:   BangPatterns NamedFieldPuns RecordWildCards-                        ScopedTypeVariables OverloadedStrings-  Other-Extensions:     CPP-  Build-Depends:        base>=4.3&&<5, array, mtl, stm, containers, transformers,+                        ScopedTypeVariables OverloadedStrings TupleSections GADTs+  Other-Extensions:     CPP StandaloneDeriving+  Build-Depends:        base>=4.3&&<5, array, mtl, containers, transformers,                         bytestring, directory, filepath, HTTP, network,-                        tremulous-query>=1.0.2, gtk, glib, process+                        tremulous-query>=1.0.5, gtk, glib, process, deepseq   Hs-Source-Dirs:       src   Main-is:              Apelsin.hs   Other-Modules:        System.Environment.XDG.BaseDir                         GtkExts                         About+                        AutoRefresh                         ClanFetcher                         Clanlist                         Config                         Constants+                        ConcurrentUtil+                        Exception2                         FilterBar                         FindPlayers                         GtkUtils+                        IndividualServerSettings                         InfoBox                         List2+                        Monad2+                        NumberSerializer                         Preferences                         ServerBrowser                         ServerInfo-                        STM2+                        SettingsDialog                         Toolbar                         TremFormatting                         Types+                        OnlineClans                         Paths_apelsin    Ghc-Options:          -Wall -threaded -fno-warn-unused-do-bind -funbox-strict-fields
src/About.hs view
@@ -1,16 +1,19 @@ module About where import Graphics.UI.Gtk import Constants+import Data.Version+import Paths_apelsin (version)  newAbout :: WindowClass w => w -> IO () newAbout win = do 	about <- aboutDialogNew 	aboutDialogSetUrlHook openInBrowser-	set about [-		  windowWindowPosition := WinPosCenterOnParent++	set about+		[ windowWindowPosition	:= WinPosCenterOnParent 		, windowTransientFor	:= win 		, aboutDialogProgramName:= programName-		, aboutDialogVersion	:= "1.1"+		, aboutDialogVersion	:= showVersion version 		, aboutDialogCopyright	:= "Copyright © 2011\nChristoffer Öjeling <christoffer@ojeling.net>" 		, aboutDialogComments	:= "A tremulous server and community browser\nLicense: GPLv3" 		, aboutDialogWebsite	:= "http://ojeling.net"
src/Apelsin.hs view
@@ -1,51 +1,66 @@ import Graphics.UI.Gtk import System.Glib.GError -import Control.Applicative import Control.Monad.IO.Class-import Control.Exception import Control.Monad-import Data.Char (toLower)-import qualified Data.Set as S+import Control.Exception+import Data.Char (ord) import Network.Socket import Network.Tremulous.Protocol+import Control.Concurrent import System.FilePath (joinPath) +import Exception2+import Monad2+import List2 import Types import Constants-import GtkUtils import ServerInfo import ServerBrowser import FindPlayers import Clanlist+import OnlineClans import ClanFetcher import Preferences import Config import Toolbar+import IndividualServerSettings as ISS  main :: IO () main = withSocketsDo $ do 	unsafeInitGUIForThreadedRTS 	win		<- windowNew 	config		<- configFromFile-	cacheclans	<- clanListFromCache+	settings	<- ISS.fromFile+ 	bundle	<-	(do-			mpolled		<- atomically $ newTMVar (PollResult [] 0 0 S.empty)-			mconfig		<- atomically $ newTMVar config-			mclans		<- atomically $ newTMVar cacheclans+			mpolled		<- newMVar (PollResult [] 0 0)+			mconfig		<- newMVar config+			mclans		<- newMVar []+			mrefresh	<- newEmptyMVar 			browserStore	<- listStoreNew []+			playerStore	<- listStoreNew []+			msettings	<- newMVar settings+			mauto		<- newEmptyMVar 			return Bundle {parent = win, ..} 			)-	mupdate <- atomically newEmptyTMVar++	mupdate <- newEmptyMVar 	(currentInfo, currentUpdate, currentSet)<- newServerInfo bundle mupdate-	(browser, browserUpdate, ent0)		<- newServerBrowser bundle currentSet-	(findPlayers, findUpdate, ent1)		<- newFindPlayers bundle currentSet-	(clanlist, clanlistUpdate, ent3)	<- newClanList bundle cacheclans currentSet+	(browser, browserUpdate)		<- newServerBrowser bundle currentSet+	(findPlayers, findUpdate)		<- newFindPlayers bundle currentSet+	(clanlist, clanlistUpdate)		<- newClanList bundle currentSet 	(onlineclans, onlineclansUpdate)	<- newOnlineClans bundle currentSet  	preferences				<- newPreferences bundle-	atomically $ putTMVar mupdate (findUpdate, onlineclansUpdate)+	putMVar mupdate (findUpdate, onlineclansUpdate) +	forkIO $ do+		cache <- clanListFromCache+		swapMVar (mclans bundle) cache+		postGUISync $ clanlistUpdate cache =<< readMVar (mpolled bundle)++ 	toolbar <- newToolbar bundle 		[] 		[browserUpdate, findUpdate, currentUpdate]@@ -62,8 +77,8 @@ 	leftView <- vBoxNew False 0 	boxPackStart leftView toolbar PackNatural 0 	boxPackStart leftView book PackGrow 0-	 + 	pane <- hPanedNew 	panedPack1 pane leftView True False 	panedPack2 pane currentInfo  False True@@ -71,7 +86,7 @@  	-- save the current window size and pane positon on exit 	on win deleteEvent $ tryEvent $ liftIO $ do-		Config {autoGeometry} <- atomically $ readTMVar (mconfig bundle)+		Config {autoGeometry} <- readMVar (mconfig bundle) 		when autoGeometry $ do 			file		<- inCacheDir "windowsize" 			(winw, winh)	<- windowGetSize win@@ -80,24 +95,20 @@ 		mainQuit  	ddir <- getDataDir-	handleGError (const $ trace $ "Window icon not found in: " ++ ddir) $ do-		let list = map-			(\x -> joinPath [ddir, "icons", "hicolor", x ++ "x" ++ x, "apps", "apelsin.png"])-			["16", "32", "48", "64"]-		icons <- mapM pixbufNewFromFile list-		set win [ windowIconList := icons]-	+	handle (\(_::GError) -> trace $ "Window icon not found in: " ++ ddir) $ do+		let list = map (iconPath ddir) ["16", "32", "48", "64"]+		windowSetDefaultIconList =<< mapM pixbufNewFromFile list+ 	-- Without allowshrink the window may change size back and forth when selecting new servers 	set win [ containerChild	:= pane 		, windowTitle		:= fullProgramName-		, windowAllowShrink	:= True-		]+		, windowAllowShrink	:= True ]  	--Showing it to get size requests 	widgetShowAll leftView  	-- Restore the window size and pane position-	when (autoGeometry config) $ handle ignoreIOException $ do+	when (autoGeometry config) $ ignoreIOException $ do 		file	<- inCacheDir "windowsize" 		fx	<- readFile file 		whenJust (mread fx) $ \(winw::Int, winh, ppos::Int) -> do@@ -109,31 +120,14 @@ 				, widgetWidthRequest	:= max (minw+panebuf) winw ] 	widgetShowAll win -	on win keyPressEvent $  do+	on win keyPressEvent $ do 		kmod	<- eventModifier-		k	<- map toLower <$> eventKeyName-		case kmod of-			[Alt]	| Just page <- mread k-				, page >= 1-				, page <= 5-				-> do	liftIO (set book [ notebookPage := page - 1])-					return True-			[Control]-				| k == "l" || k == "f"-				-> do	page <- liftIO (get book notebookPage)-					case page of-						0 -> liftIO $ widgetGrabFocus ent0-						1 -> liftIO $ widgetGrabFocus ent1-						3 -> liftIO $ widgetGrabFocus ent3-						_ -> return ()-					return True-			_ -> return False-			-	widgetGrabFocus ent0+		k	<- eventKeyVal+		let page = fromIntegral k - ord '0' - 1+		if kmod == [Alt] && page >= 0 && page <= 5+			then liftIO (notebookSetCurrentPage book page) >> return True+			else return False 	mainGUI --ignoreIOException :: IOException -> IO ()-ignoreIOException _ = return ()--+iconPath :: FilePath -> FilePath -> FilePath+iconPath ddir x = joinPath [ddir, "icons", "hicolor", x ++ "x" ++ x, "apps", configName++".png"]
+ src/AutoRefresh.hs view
@@ -0,0 +1,63 @@+module AutoRefresh (AutoSignal(..), autoSignal, autoRunner) where+import Control.Applicative+import Control.Concurrent+import Control.Exception+import Control.Monad++import Network.Tremulous.MicroTime+import Monad2+import Config++data AutoSignal = AutoUpdate | AutoStop | AutoStart | AutoPause | AutoResume+	deriving Show++data State = Running !ThreadId | Paused | Stopped++autoSignal :: MVar AutoSignal -> AutoSignal -> IO ()+autoSignal m = putMVar m++autoRunner :: MVar AutoSignal -> MVar Config -> IO a -> IO ThreadId+autoRunner m mconfig refreshAction = forkIO $ go Stopped Nothing+	where+	go :: State -> Maybe MicroTime -> IO a+	go state timestamp = do+		st <- takeMVar m+		case st of+			AutoStart -> case state of+				Running _ -> go state timestamp+				_ -> do	tid <- startAuto mconfig timestamp refreshAction+					go (Running tid) timestamp++			AutoStop -> case state of+				Running tid -> killThread tid >> go Stopped timestamp+				_ -> go Stopped timestamp++			AutoUpdate -> do+				timestamp' <- Just <$> getMicroTime+				case state of+					Running tid -> do+						killThread tid+						tid' <- startAuto mconfig timestamp' refreshAction+						go (Running tid') timestamp'+					_ -> go state timestamp'++			AutoPause | Running tid <- state -> do+				killThread tid+				go Paused timestamp++			AutoResume | Paused <- state -> do+				tid <- startAuto mconfig timestamp refreshAction+				go (Running tid) timestamp++			_ -> go state timestamp+++startAuto :: MVar Config -> Maybe MicroTime -> IO a -> IO ThreadId+startAuto mconfig lastRefresh refreshAction = uninterruptibleMask $ \restore -> forkIO $ do+		delay <- autoDelay <$> readMVar mconfig+		whenJust lastRefresh $ \past -> do+			now <- getMicroTime+			when (past+delay > now) $+				restore $ threadDelay (fromIntegral (past+delay-now))+		refreshAction+		return ()
src/ClanFetcher.hs view
@@ -1,26 +1,29 @@-module ClanFetcher( -	Clan(..), TagExpr, matchTagExpr, prettyTagExpr, tagExprGet, getClanList, clanListFromCache+module ClanFetcher(+	Clan(..), TagExpr, ClanID, matchTagExpr, prettyTagExpr, tagExprGet, getClanList, clanListFromCache+	, matchClanByID ) where -import Prelude as P hiding (catch)-import Control.Exception+import Prelude as P import Control.Applicative+import Control.Exception import Control.Monad import Data.ByteString.Char8 as B import Data.ByteString.Lazy.Char8 as L-import Data.Char (intToDigit)+import Data.List as List import Data.Maybe import Data.Ord import Network.Socket import Network.HTTP+import Network.Stream import Network.URI import Network.Tremulous.NameInsensitive-import TremFormatting +import TremFormatting import Constants+import Monad2 -data TagExpr =-	  TagPrefix !TI+data TagExpr+	= TagPrefix !TI 	| TagSuffix !TI 	| TagInfix !TI 	| TagContained !TI !TI@@ -29,21 +32,29 @@ instance Ord TagExpr where 	compare = comparing tagExprGet -tagExprGet :: TagExpr -> B.ByteString+tagExprGet :: TagExpr -> TI tagExprGet x = case x of-	TagPrefix (TI _ v)	-> v-	TagSuffix (TI _ v)	-> v-	TagInfix (TI _ v)	-> v-	TagContained (TI _ v) _	-> v+	TagPrefix v		-> v+	TagSuffix v		-> v+	TagInfix v		-> v+	TagContained v _	-> v -data Clan = Clan {-	  name		:: !TI+type ClanID = Int+data Clan = Clan+	{ clanID	:: !ClanID+	, name		:: !TI 	, website 	, irc		:: !B.ByteString+	, websitealive	:: !Bool 	, tagexpr	:: !TagExpr 	, clanserver	:: !(Maybe SockAddr) 	} +matchClanByID :: [Clan] -> TI -> ClanID -> Bool+matchClanByID clans raw cid = case List.find (\x -> clanID x == cid) clans of+	Just a -> matchTagExpr (tagexpr a) raw+	Nothing -> False+ mkTagExpr :: B.ByteString -> Maybe TagExpr mkTagExpr str 	| Just (x, xs) <- B.uncons str = case x of@@ -63,23 +74,24 @@ 	TagContained (TI _ xs) (TI _ ys)-> xs `B.isPrefixOf` str && ys `B.isSuffixOf` str 	where str = cleanedCase raw -prettyTagExpr :: TagExpr -> String+prettyTagExpr :: TagExpr -> B.ByteString prettyTagExpr expr = case expr of-	TagPrefix bs	-> esc bs ++ wild-	TagSuffix bs	-> wild ++ esc bs-	TagInfix bs	-> wild ++ esc bs ++ wild-	TagContained a b-> esc a ++ wild ++ esc b+	TagPrefix bs	-> esc bs `B.append` wild+	TagSuffix bs	-> wild `B.append` esc bs+	TagInfix bs	-> B.concat [wild, esc bs, wild]+	TagContained a b-> B.concat [esc a, wild, esc b] 	where	wild = "<span color=\"#BBB\">*</span>"-		esc  = htmlEscape . B.unpack . original+		esc  = htmlEscapeBS . original   rawToClan :: L.ByteString -> IO (Maybe [Clan])-rawToClan = fmap sequence . mapM (f . B.split '\t' . lazyToStrict) . P.filter (not . L.null) . L.split '\n' where -	f [_, rname, _, website, irc, rexpr, rserver]+rawToClan = fmap sequence . mapM (f . B.split '\t' . lazyToStrict) . P.filter (not . L.null) . L.split '\n' where+	f (rclanID:rname:rexpr:website:rwebsitealive:irc:rserver:_) 		| Just tagexpr <- mkTagExpr rexpr-		= do+		, Just (clanID,_) <- B.readInt rclanID = do 			let	(ip, port1)	= B.break (==':') rserver 				port		= B.drop 1 port1+				websitealive	= rwebsitealive == "1" 			clanserver <- if B.null ip || B.null port 				then return Nothing 				else getDNS (B.unpack ip) (B.unpack port)@@ -90,46 +102,37 @@ getClanList url = do 	cont <- get url 	case cont of-		Right _ -> return Nothing-		Left raw -> do-			file	<- inCacheDir "clans"+		Nothing -> return Nothing+		Just raw -> do+			file	<- cacheFile 			clans	<- rawToClan raw 			when (isJust clans) $ 				L.writeFile file raw- 			return clans-			 + clanListFromCache :: IO [Clan] clanListFromCache = handle err $ do-	file	<- inCacheDir "clans"+	file <- cacheFile 	fromMaybe [] <$> (rawToClan =<< L.readFile file) 	where-	err (_ :: IOError) = return [] -	+	err (_ :: IOException) = return [] -lazyToStrict :: L.ByteString -> B.ByteString		++lazyToStrict :: L.ByteString -> B.ByteString lazyToStrict = B.concat . toChunks +cacheFile :: IO FilePath+cacheFile = inCacheDir "clans" --- I blame this on buggy Network.HTTP-get :: HStream ty => String -> IO (Either ty String)-get url = case parseURI url of-		Nothing -> return $ Right "Error in URL"-		Just uri -> do-			resp <- catch (getRight `fmap` simpleHTTP (mkRequest GET uri)) err-			return $ case resp of-				Left msg				-> Right $ show msg-				Right (Response (2,0,0) _  _ body)	-> Left body-				Right (Response code reason _ _)	-> Right $ showRspCode code  ++ " " ++ reason-	where-	showRspCode (a,b,c) = P.map intToDigit [a,b,c]-	getRight (Right a) = Right a-	getRight (Left _) = error ""-	err :: IOException -> IO (Either String a )-	err = return . Left . show -getDNS :: String -> String -> IO (Maybe SockAddr)-getDNS host port = handle (\(_ :: IOException) -> return Nothing) $ do-	AddrInfo _ _ _ _ addr _ <- P.head `liftM` getAddrInfo Nothing (Just host) (Just port)-	return $ Just addr+get :: HStream ty => String -> IO (Maybe ty)+get url = case parseURI url of+	Nothing -> return Nothing+	Just uri -> do+		resp <- ex $ simpleHTTP (mkRequest GET uri)+		return $ case resp of+			Right (Response (2,0,0) _  _ body)	-> Just body+			_					-> Nothing+	-- It doesn't catch everything apparently+	where ex = handle (\(_ :: IOException) -> return (Left ErrorReset))
src/Clanlist.hs view
@@ -1,42 +1,38 @@-module Clanlist (newClanList, newClanSync, newOnlineClans) where-+module Clanlist (newClanList) where import Graphics.UI.Gtk  import Control.Concurrent-import Control.Concurrent.STM import Control.Monad import qualified Data.ByteString.Char8 as B import Data.Ord-import Data.Tree import Data.Maybe-import Data.List (sortBy) import Data.IORef import Network.Tremulous.NameInsensitive import Network.Tremulous.ByteStringUtils import qualified Network.Tremulous.Protocol as P+import qualified Network.Tremulous.StrictMaybe as SM import Network.Tremulous.Util  import Types import ClanFetcher import Constants-import Config import FilterBar import InfoBox import GtkUtils-import TremFormatting+import GtkExts+import Monad2+import Config  -newClanList :: Bundle -> [Clan] -> SetCurrent -> IO (VBox, ClanPolledHook, Entry)-newClanList Bundle{..} cache setCurrent = do-	raw			<- listStoreNew []-	filtered		<- treeModelFilterNew raw []-	sorted			<- treeModelSortNewWithModel filtered-	view			<- treeViewNewWithModel sorted+newClanList :: Bundle -> SetCurrent -> IO (VBox, ClanPolledHook)+newClanList bundle@Bundle{..} setCurrent = do+	gen@(GenFilterSort raw filtered sorted view)+		<- newGenFilterSort =<< listStoreNew [] 	scrollview		<- scrollIt view PolicyAutomatic PolicyAlways  	(infobox, statNow, statTot) <- newInfobox "clans" -	(filterbar, current, ent)	<- newFilterBar filtered statNow ""+	(filterbar, current) <- newFilterBar parent filtered statNow ""  	let updateF newraw P.PollResult{..} = do 		let new = (`map` newraw) $ \c -> case clanserver c of@@ -45,146 +41,84 @@ 		listStoreClear raw 		treeViewColumnsAutosize view 		mapM_ (listStoreAppend raw) new-		treeModelFilterRefilter filtered-		set statTot [ labelText := show (length new) ]-		n <- treeModelIterNChildren filtered Nothing-		set statNow [ labelText := show n ]+		labelSetText statTot . show =<< treeModelIterNChildren raw Nothing+		labelSetText statNow . show =<< treeModelIterNChildren filtered Nothing -	addColumnsFilterSort raw filtered sorted view 1 SortAscending -		[ (""		, False	, RendPixbuf haveServer-			, Just (comparing (isJust . clanserver . fst)))-		, ("_Name"	, False	, RendText (simpleColumn (unpackorig . name))-			, Just (comparing (name . fst)))-		, ("_Tag"	, False	, RendText (markupColumn (prettyTagExpr . tagexpr))-			, Just (comparing (tagexpr . fst)))-		, ("Website"	, False	, RendText (simpleColumn (B.unpack . showURL . website))-			, Nothing)-		, ("IRC"	, False , RendText (simpleColumn (B.unpack . irc))-			, Nothing)-		]+	addColumnFS gen "" False (Just $ comparing $ isJust . clanserver . fst)+		(rememberColumn bundle 0)+		cellRendererPixbufNew [] haveServer +	addColumnFS gen "_Name" False (Just $ comparing $ name . fst)+		(rememberColumn bundle 1)+		cellRendererTextNew [] (simpleColumn (original . name))++	addColumnFS gen "_Tag" False (Just $ comparing $ tagexpr . fst)+		(rememberColumn bundle 2)+		cellRendererTextNew [] (markupColumn (prettyTagExpr . tagexpr))++	addColumnFS gen "Website" False Nothing+		(const (return ()))+		cellRendererTextNew [] $ \rend (Clan{..},_) -> do+			set rend $ if websitealive+				then [ cellTextStrikethrough := False, cellTextUnderline := UnderlineSingle, cellTextForegroundColor := Color 0 0 maxBound ]+				else [ cellTextStrikethrough := True, cellTextUnderline := UnderlineNone, cellTextForegroundColor := Color maxBound 0 0 ]+			cellSetMarkup rend (showURL website)++	addColumnFS gen "IRC" False Nothing+		(const (return ()))+		cellRendererTextNew [] (simpleColumn irc)++	Config{..} <- readMVar mconfig+	treeSortableSetSortColumnId sorted clanlistSort clanlistOrder++ 	treeModelFilterSetVisibleFunc filtered $ \iter -> do 		(Clan{..}, _)	<- treeModelGetRow raw iter 		s		<- readIORef current-		let cmplist	= [ cleanedCase name, tagExprGet tagexpr ]-		return $ B.null s || smartFilter s cmplist+		return $ smartFilter s	[ cleanedCase name+					, cleanedCase (tagExprGet tagexpr) ]  	on view cursorChanged $ do-		(path, _)		<- treeViewGetCursor view-		(Clan{..}, active)	<- getElementFS raw sorted filtered path-		+		(path, col)		<- treeViewGetCursor view+		(Clan{..}, active)	<- getElementPath gen path+		title			<- maybe (return Nothing) treeViewColumnGetTitle col++		when (title == Just "Website" && (not . B.null) website) $ openInBrowser $+			if websitealive then B.unpack website+					else "http://wayback.archive.org/web/*/" ++ (B.unpack website)+ 		when active $ whenJust clanserver $ \server -> do-			P.PollResult{..} <- atomically $ readTMVar mpolled+			P.PollResult{..} <- readMVar mpolled 			whenJust (serverByAddress server polled) (setCurrent False) -	on view rowActivated $ \path _ -> do-		(Clan{..}, _) <- getElementFS raw sorted filtered path-		unless (B.null website) $-			openInBrowser (B.unpack website)+	on view rowActivated $ \path col -> do+		(Clan{..}, active) <- getElementPath gen path+		title <- treeViewColumnGetTitle  col+		case title of+			Just "Website" -> return ()+			_ -> when active $ whenJust clanserver $ \server -> do+				P.PollResult{..} <- readMVar mpolled+				whenJust (serverByAddress server polled) (setCurrent True)  	box <- vBoxNew False 0 	boxPackStart box filterbar PackNatural spacing 	boxPackStart box scrollview PackGrow 0 	boxPackStart box infobox PackNatural 0 -	-	updateF cache =<< atomically (readTMVar mpolled)--	return (box, updateF, ent)+	return (box, updateF) 	where-	showURL x = fromMaybe x (stripPrefix "http://" x)-	markupColumn f (item, _) = [ cellTextMarkup := Just (f item) ]-	simpleColumn f (item, _) = [ cellText := f item ]-	haveServer (Clan{..}, active) = case clanserver of+	showURL x = SM.fromMaybe x (stripPrefix "http://" x)+	markupColumn f rend (item, _) = cellSetMarkup rend (f item)+	simpleColumn f rend (item, _) = cellSetText rend (f item)+	haveServer rend (Clan{..}, active) = set rend $ case clanserver of 		Just _	-> [cellPixbufStockId := stockNetwork, cellSensitive := active] 		Nothing	-> [cellPixbufStockId := ""]  -newOnlineClans :: Bundle-> SetCurrent -> IO (ScrolledWindow, ClanPolledHook)-newOnlineClans Bundle{..} setServer = do-	Config {colors} <- atomically $ readTMVar mconfig--	let showName c =  case c of-		Left Clan{..}		-> "<b>" ++ htmlEscape (unpackorig name) ++ "</b>"-		Right (name, _)		-> pangoPretty colors name--	let showServer c =  case c of-		Left _ -> ""-		Right (_, P.GameServer{hostname}) -> pangoPretty colors hostname--	raw	<- treeStoreNew []-	view	<- treeViewNewWithModel raw--	treeViewExpandAll view-	addColumns raw view [-		  ("Name"	, 0	, True	, True	, True	, showName )-		, ("Server"	, 0	, True	, True	, True	, showServer )-		]--	let updateF clans P.PollResult{..} = do-		let players = buildTree $ sortByPlayers $-			associatePlayerToClans (makePlayerNameList polled) clans-		treeStoreClear raw-		treeViewColumnsAutosize view-		mapM_ (treeStoreInsertTree raw [] 0) players-		treeViewExpandAll view--	on view cursorChanged $ do-		(path, _)	<- treeViewGetCursor view-		item		<- getElement raw path-		case item of-			Left _ -> return ()-			Right (_, a) -> setServer False a--	on view rowActivated $ \path _ -> do-		gs		<- getElement raw path-		case gs of-			Left _ -> return ()-			Right (_, gameserver) -> setServer True gameserver--	scroll <- scrollIt view PolicyAutomatic PolicyAutomatic--	return (scroll, updateF)----- /// Utility functions ///////////////////////////////////////////////////////////////////////////--type PlayerList = [(TI, P.GameServer)]--type OnlineView = Forest (Either Clan (TI, P.GameServer))--associatePlayerToClans :: PlayerList -> [Clan] -> [(Clan, PlayerList)]-associatePlayerToClans players clans = map f clans-	where-	f c@Clan{tagexpr} = (c, filter (cmp tagexpr) players)-	cmp e = matchTagExpr e . fst--buildTree :: [(Clan, PlayerList)] -> OnlineView-buildTree = filter notEmpty . foldr f [] where-	f (clan, pls) xs = Node (Left clan) (map rightNode pls) : xs-	rightNode x = Node (Right x) []-	notEmpty (Node _ [])	= False-	notEmpty _		= True--sortByPlayers :: [(Clan, [b])] -> [(Clan, [b])]-sortByPlayers = sortBy (flip (comparing (\(a, b) -> (-length b, name a))))--newClanSync :: Bundle -> Button -> [ClanHook] -> [ClanPolledHook] -> IO ()-newClanSync Bundle{..} button clanHook bothHook = do-	set button [ widgetSensitive := False ]-	Config {clanSyncURL} <- atomically $ readTMVar mconfig-	forkIO $ do-		new <- getClanList clanSyncURL-		case new of-			Nothing	-> postGUISync $ do-				gtkError "Unable to download clanlist"-				set button [ widgetSensitive := True ]-			Just a	-> do-				result <- atomically $ do-					swapTMVar mclans a-					readTMVar mpolled-				postGUISync $ do-					mapM_ ($ a) clanHook-					mapM_ (\f -> f a result) bothHook-					set button [ widgetSensitive := True ]-	return ()+rememberColumn :: Bundle -> Int -> TreeViewColumn -> IO ()+rememberColumn Bundle{..} n col = do+	order <- treeViewColumnGetSortOrder col+	current <- takeMVar mconfig+	let new = current {clanlistSort = n, clanlistOrder = order}+	putMVar mconfig new+	configToFile parent new
+ src/ConcurrentUtil.hs view
@@ -0,0 +1,11 @@+module ConcurrentUtil where++import Control.Concurrent+import Control.Exception++readWithMVar :: MVar m -> (m -> IO ()) -> IO ()+readWithMVar m f = mask $ \restore -> do+	ret <- tryTakeMVar m+	case ret of+		Just a  -> putMVar m a >> restore (f a)+		Nothing -> return ()
src/Config.hs view
@@ -1,74 +1,166 @@-module Config where-+module Config (Config(..), ColorTheme, configFromFile, configToFile+	, makeColorsFromList, RefreshMode(..)+) where+import Graphics.UI.Gtk (SortType(..), Window) import Data.Array+import Data.Char import Control.Exception+import Control.Applicative import Network.Tremulous.Protocol+import Control.Monad.State.Strict+import Network.Tremulous.TupleReader (lookupDelete)+import Network.Tremulous.StrictMaybe as SM+import Network.Tremulous.MicroTime  import Constants+import List2 import GtkUtils import TremFormatting - type ColorTheme = Array Int TremFmt -data Config = Config {-	  configVersion	:: !Int-	, masterServers :: ![(String, Int, Int)]+data SortingOrderPretty = Ascending | Descending+	deriving (Show, Read, Enum)++data RefreshMode = Startup | Auto | Manual+	deriving (Show, Read, Eq)++data Config = Config+	{ masterServers :: ![(String, Int, Int)] 	, clanSyncURL	:: !String 	, tremPath 	, tremGppPath	:: !String-	, autoMaster+	, refreshMode	:: !RefreshMode 	, autoClan 	, autoGeometry	:: !Bool+	, autoDelay	:: !MicroTime 	, filterBrowser 	, filterPlayers	:: !String 	, filterEmpty	:: !Bool+	, browserSort+	, playersSort+	, clanlistSort	:: !Int+	, browserOrder+	, playersOrder+	, clanlistOrder	:: !SortType 	, delays	:: !Delay 	, colors	:: !ColorTheme-	} deriving (Show, Read)+	} -defaultConfig :: Config-defaultConfig = Config {-	  configVersion	= 1-	, masterServers = [("master.tremulous.net", 30710, 69), ("master.tremulous.net", 30700, 70)]-	, clanSyncURL	= "http://ddos-tremulous.eu/cw/api/clanlist"-	, tremPath	= defaultTremulousPath-	, tremGppPath	= defaultTremulousGPPPath-	, autoMaster	= True-	, autoClan	= True-	, autoGeometry	= True-	, filterBrowser	= ""-	, filterPlayers	= ""-	, filterEmpty	= True-	, delays	= defaultDelay-	, colors	= makeColorsFromList $-		TFNone "#000000" : map TFColor ["#d60503", "#25c200", "#eab93d", "#0021fe", "#04c9c9", "#e700d7"] ++ [TFNone "#000000"]-	} ++newSave :: Config -> String+newSave Config{delays=Delay{..}, ..} = unlines $+	[ f masterServers	mastersText+	, f clanSyncURL		clanText+	, f tremPath		t11Text+	, f tremGppPath		t12Text+	, f refreshMode		refreshModeText+	, f autoClan		autoClanText+	, f autoGeometry	geometryText+	, f (autoDelay`quot`1000000) autoDelayText+	, f filterBrowser	browserfilterText+	, f filterPlayers	playerfilterText+	, f filterEmpty		showEmptyText+	, f browserSort		browserSortText+	, f playersSort		playerSortText+	, f clanlistSort	clanlistSortText+	, f (conv browserOrder)	browserOrderText+	, f (conv playersOrder)	playerOrderText+	, f (conv clanlistOrder) clanlistOrderText+	, f packetTimeout	packetTimeoutText+	, f packetDuplication	packetDuplicationText+	, f throughputDelay	throughputDelayText+	] +++	zipWith (\a b -> f b (colorText ++ [a])) ['0'..'7'] (elems colors)+	where+	conv :: SortType -> SortingOrderPretty+	conv = toEnum . fromEnum+	f :: Show v => v -> String -> String+	f v k = k ++ " " ++ show v++newParse :: [(String, String)] -> Config+newParse = evalState $ do+	masterServers	<- f mastersText		[("master.tremulous.net", 30710, 69), ("master.tremulous.net", 30700, 70)]+	clanSyncURL	<- f clanText			"http://ddos-tremulous.eu/cw/api/2/clanlist"+	tremPath	<- f t11Text			defaultTremulousPath+	tremGppPath	<- f t12Text			defaultTremulousGPPPath+	refreshMode	<- f refreshModeText		Startup+	autoClan	<- f autoClanText		True+	autoGeometry	<- f geometryText		True+	autoDelay	<- (*1000000) <$> f autoDelayText		120 -- 120s+	filterBrowser	<- f browserfilterText		""+	filterPlayers	<- f playerfilterText		""+	filterEmpty	<- f showEmptyText		True+	browserSort	<- f browserSortText		3+	playersSort	<- f playerSortText		0+	clanlistSort	<- f clanlistSortText		1+	browserOrder	<- toEnum . fromEnum <$> f browserOrderText	Ascending+	playersOrder	<- toEnum . fromEnum <$> f playerOrderText	Ascending+	clanlistOrder	<- toEnum . fromEnum <$> f clanlistOrderText	Ascending+	packetTimeout	<- f packetTimeoutText	(packetTimeout defaultDelay)+	packetDuplication<- f packetDuplicationText	(packetDuplication defaultDelay)+	throughputDelay	<- f throughputDelayText	(throughputDelay defaultDelay)+	colors		<- makeColorsFromList <$> zipWithM (\a b -> f (colorText ++ [a]) b) ['0'..'7'] defaultColors+	return Config{delays = Delay{..}, ..}+	where+	f :: Read b  => String -> b -> State [(String, String)] b+	f key d = do+		s <- get+		let (e, s') = lookupDelete key s+		put s'+		return $ SM.fromMaybe d $ smread =<< e+	defaultColors = TFNone "#000000" : map TFColor ["#d60503", "#25c200", "#eab93d", "#0021fe", "#04c9c9", "#e700d7"] ++ [TFNone "#000000"]++smread :: (Read a) => String -> SM.Maybe a+smread x = case reads x of+	[(a, _)]	-> SM.Just a+	_		-> SM.Nothing++parse :: String -> Config+parse = newParse . map (breakDrop isSpace) . lines++mastersText, clanText, t11Text, t12Text, refreshModeText, autoClanText, geometryText, autoDelayText+	, browserfilterText, playerfilterText, showEmptyText, browserSortText, playerSortText+	, browserOrderText, playerOrderText, packetTimeoutText, clanlistSortText, clanlistOrderText+	, packetDuplicationText, throughputDelayText, colorText :: String+mastersText		= "masters"+clanText		= "clanlistUrl"+t11Text			= "tremulous1.1"+t12Text			= "tremulousGPP"+refreshModeText		= "refreshMode"+autoClanText		= "autoClan"+geometryText		= "saveGeometry"+autoDelayText		= "autoDelay"+browserfilterText	= "browserFilter"+playerfilterText	= "playersFilter"+showEmptyText		= "showEmpty"+browserSortText		= "browserSortColumn"+playerSortText		= "playerSortColumn"+clanlistSortText	= "clanlistSortColumn"+browserOrderText	= "browserSortingOrder"+playerOrderText		= "playerSortingOrder"+clanlistOrderText	= "clanlistSortingOrder"+packetTimeoutText	= "packetTimeout"+packetDuplicationText	= "packetDuplication"+throughputDelayText	= "throughputDelay"+colorText		= "color"+ makeColorsFromList :: [e] -> Array Int e makeColorsFromList = listArray (0,7) -configToFile :: Config -> IO ()-configToFile config = handle err $ do-	file	<- inConfDir "config"-	writeFile file (show config)+configToFile :: Window -> Config -> IO ()+configToFile win config = do+	file <- inConfDir "config"+	handle err $ do+	writeFile file (newSave config) 	where-	err (_::IOError) = gtkWarn "Error saving config file"+	err (e::IOError) = gtkWarn win $ "Unable to save settings:\n" ++ show e   configFromFile :: IO Config configFromFile = handle err $ do 	file	<- inConfDir "config" 	cont	<- readFile file-	case mread cont of-		Nothing -> do-			gtkWarn $ "Errors in " ++ file ++ "!\nUsing default"-			return defaultConfig-		Just a -> return a+	return $ parse cont 	where-	err (_::IOError) = return defaultConfig--mread :: (Read a) => String -> Maybe a-mread x = case reads x of-	[(a, _)]	-> Just a-	_		-> Nothing-+	err (_::IOError) = return $ parse ""
src/Constants.hs view
@@ -6,10 +6,11 @@ import System.Directory import System.IO import System.Process+import Control.Concurrent import Control.Exception  #ifdef CABAL_PATH-import Paths_apelsin+import Paths_apelsin hiding (getDataDir) #endif  configName, programName, fullProgramName :: IsString s => s@@ -30,7 +31,7 @@ 	return (dir </> x)  -getDataDir :: IO FilePath +getDataDir :: IO FilePath #ifdef CABAL_PATH getDataDir = dropTrailingPathSeparator `fmap` getDataFileName "" #else@@ -63,13 +64,16 @@  openInBrowser :: String -> IO () openInBrowser x = handle (\(_ :: IOError) -> return ()) $ do-	p <-defaultBrowser x-	createProcess p-		{close_fds = True, std_in = Inherit, std_out = Inherit, std_err = Inherit}+	p <- defaultBrowser x+	(_,_,_,hdl) <- createProcess p {close_fds = True}+	-- The following hack is needed to avoid ghost processes because of:+	-- http://hackage.haskell.org/trac/ghc/ticket/2123+	forkIO $ waitForProcess hdl >> return () 	return () -spacing, spacingHalf, spacingBig :: Integral i => i-spacing		= 4+spacing, spacingHalf, spacingBig, spacingHuge :: Integral i => i spacingHalf	= 2+spacing		= 4 spacingBig	= 8+spacingHuge	= 12 
+ src/Exception2.hs view
@@ -0,0 +1,14 @@+module Exception2 where+import Control.Exception++maybeIO :: IO (Maybe a) -> IO (Maybe a)+maybeIO = handleIOWith Nothing++tryIO :: IO a -> IO Bool+tryIO f = handleIOWith False (f >> return True)++ignoreIOException :: IO () -> IO ()+ignoreIOException = handleIOWith ()++handleIOWith :: a -> IO a -> IO a+handleIOWith w = handle (\(_ :: IOException) -> return w)
src/FilterBar.hs view
@@ -4,7 +4,8 @@ import Data.IORef import qualified Data.ByteString.Char8 as B import Data.ByteString.Char8 (ByteString)-import Data.Char+import Data.Char hiding (Control)+import Control.Applicative import Control.Monad.IO.Class import Network.Tremulous.ByteStringUtils as B @@ -12,53 +13,65 @@ import Constants  -newFilterBar :: (TreeModelClass self, TreeModelFilterClass self) => self -> Label -> String-	-> IO (HBox, IORef ByteString, Entry)-newFilterBar filtered stat initial  = do-	current <- newIORef ""-	+newFilterBar :: (TreeModelClass self, TreeModelFilterClass self) => Window -> self -> Label -> String+	-> IO (HBox, IORef [Expr])+newFilterBar win filtered stat initial  = do+	current <- newIORef []+ 	-- Filterbar 	ent <- entryNew 	set ent [ entryText := initial ] 	entrySetIconFromStock ent EntryIconSecondary stockClear-		+ 	lbl <- labelNew (Just "Filter:") 	set lbl [ widgetTooltipText := Just "Ctrl+L or Ctrl+F" ]-	+ 	findbar <- hBoxNew False 0 	boxPackStart findbar lbl PackNatural spacingHalf 	boxPackStart findbar ent PackGrow spacingHalf-	+ 	let f = do-		rawstr <- entryGetText ent-		let str = B.pack $ map toLower rawstr-		writeIORef current str+		str <- entryGetText ent+		writeIORef current (stringToExpr str) 		treeModelFilterRefilter filtered-		n <- treeModelIterNChildren filtered Nothing-		set stat [ labelText := show n ]-	f +		labelSetText stat . show =<< treeModelIterNChildren filtered Nothing+	f 	on ent editableChanged f  	on ent entryIconPress $-		const $ liftIO $editableDeleteText ent 0 (-1)-	-	return (findbar, current, ent)+		const $ liftIO $ editableDeleteText ent 0 (-1) -	+	on win keyPressEvent $ do+		s	<- eventModifier+		k	<- fromIntegral . keyvalToLower <$> eventKeyVal+		b 	<- liftIO $ widgetGetMapped ent+		if b && s == [Control] && (k == ord 'f' || k == ord 'l')+			then liftIO (widgetGrabFocus ent) >> return True+			else return False++	-- Focus on start+	on ent showSignal $ widgetGrabFocus ent++	return (findbar, current)++ data Expr = Is !ByteString | Not !ByteString  mkExpr :: ByteString -> Expr-mkExpr ss = case (B.head ss, B.tail ss) of-	('-', xs) | B.length xs > 0	-> Not (B.tail ss)-                  | otherwise		-> Is xs                 -	_				-> Is ss+mkExpr ss = case B.uncons ss of+	Just ('-', xs)	| B.length xs > 0	-> Not (B.tail ss)+			| otherwise		-> Is xs+	_					-> Is ss  matchExpr :: Expr -> [ByteString] -> Bool matchExpr (Is xs)  = any (xs `B.isInfixOf`) matchExpr (Not xs) = all (not . (xs `B.isInfixOf`)) -smartFilter :: ByteString -> [ByteString] -> Bool-smartFilter raw xs = all (`matchExpr` xs) search -	where-	search	= map mkExpr (B.splitfilter ' ' raw)-	+stringToExpr :: String -> [Expr]+stringToExpr = map mkExpr . B.splitfilter ' ' . B.pack . map toLower++smartFilter :: [Expr]-> [ByteString] -> Bool+smartFilter [] 	_	= True+smartFilter expr xs	= all (`matchExpr` xs) expr++
src/FindPlayers.hs view
@@ -1,71 +1,101 @@-module FindPlayers where+module FindPlayers (newFindPlayers, playerUpdateOne) where import Graphics.UI.Gtk  import Data.IORef import Data.Ord-import qualified Data.ByteString.Char8 as B+import Control.Concurrent import Network.Tremulous.Protocol import Network.Tremulous.Util+import qualified Network.Tremulous.StrictMaybe as SM  import Types import GtkUtils+import GtkExts import TremFormatting import FilterBar import InfoBox import Constants import Config -newFindPlayers :: Bundle -> SetCurrent -> IO (VBox, PolledHook, Entry)-newFindPlayers Bundle{..} setServer = do-	Config {..}	<- atomically $ readTMVar mconfig-	raw		<- listStoreNew []-	filtered	<- treeModelFilterNew raw []-	sorted		<- treeModelSortNewWithModel filtered	-	view		<- treeViewNewWithModel sorted-	-	addColumnsFilterSort raw filtered sorted view 0 SortAscending-		[ ("Name"	, True	, RendText (simpleColumn colors fst)-			, Just (comparing fst))-		, ("Server"	, True	, RendText (simpleColumn colors (hostname . snd))-			, Just (comparing (address .snd))) -		] +newFindPlayers :: Bundle -> SetCurrent -> IO (VBox, PolledHook)+newFindPlayers bundle@Bundle{..} setServer = do+	Config {..} <- readMVar mconfig+	(GenFilterSort raw filtered _ view) <- playerLikeList bundle setServer+ 	(infobox, statNow, statTot)	<- newInfobox "players"-	(filterbar, current, ent)	<- newFilterBar filtered statNow filterPlayers-	+	(filterbar, current)		<- newFilterBar parent filtered statNow filterPlayers+ 	treeModelFilterSetVisibleFunc filtered $ \iter -> do 		(item, GameServer{..}) <- treeModelGetRow raw iter 		s <- readIORef current-		return $ B.null s || smartFilter s [-				  cleanedCase item+		return $ smartFilter s+				[ cleanedCase item 				, proto2string protocol-				, maybe "" cleanedCase gamemod+				, SM.maybe "" cleanedCase gamemod 				]-		+ 	let updateFilter PollResult{..} = do 		listStoreClear raw-		let plist = makePlayerNameList polled-		mapM_ (listStoreAppend raw) plist-		treeModelFilterRefilter filtered-		set statTot [ labelText := show (length plist) ]-		n <- treeModelIterNChildren filtered Nothing-		set statNow [ labelText := show n ]-						-	on view cursorChanged $ do-		(path, _) <- treeViewGetCursor view-		setServer False . snd =<< getElementFS raw sorted filtered path+		mapM_ (listStoreAppend raw) (makePlayerNameList polled)+		labelSetText statTot . show =<< treeModelIterNChildren raw Nothing+		labelSetText statNow . show =<< treeModelIterNChildren filtered Nothing -	on view rowActivated $ \path _ -> do-		setServer True . snd =<< getElementFS raw sorted filtered path-	 	scroll <- scrollIt view PolicyAutomatic PolicyAlways-	+ 	box <- vBoxNew False 0 	boxPackStart box filterbar PackNatural spacing 	boxPackStart box scroll PackGrow 0 	boxPackStart box infobox PackNatural 0-	-	return (box, updateFilter, ent)-	where simpleColumn colors f item =-		[ cellTextEllipsize := EllipsizeEnd-		, cellTextMarkup := Just (pangoPretty colors (f item)) ]++	return (box, updateFilter)++playerLikeList :: Bundle -> SetCurrent -> IO (GenFilterSort ListStore (TI, GameServer))+playerLikeList bundle@Bundle{..} setCurrent= do+	Config {..} <- readMVar mconfig+	gen@(GenFilterSort _ _ sorted view) <- newGenFilterSort playerStore++	addColumnFS gen "_Name" True (Just (comparing fst))+		(rememberColumn bundle 0)+		cellRendererTextNew+		[cellTextEllipsize := EllipsizeEnd]+		(\rend -> cellSetMarkup rend . pangoPrettyBS colors . fst)++	addColumnFS gen "_Server" True (Just (comparing (hostname . snd)))+		(rememberColumn bundle 1)+		cellRendererTextNew+		[cellTextEllipsize := EllipsizeEnd]+		(\rend -> cellSetMarkup rend . pangoPrettyBS colors . hostname . snd)++	treeSortableSetSortColumnId sorted playersSort playersOrder++	on view cursorChanged $ do+		(path, _) <- treeViewGetCursor view+		setCurrent False . snd =<< getElementPath gen path++	on view rowActivated $ \path _ -> do+		setCurrent True . snd =<< getElementPath gen path++	return gen+++playerUpdateOne :: PlayerStore -> GameServer -> IO ()+playerUpdateOne raw gs = do+	lst <- listStoreToList raw+	let oldplayers = [ i | (i, (_, gs')) <- zip [0::Int ..] lst, address gs == address gs']+	let newplayers = map (\a -> (name a, gs)) (players gs)+	foldPlayers newplayers oldplayers+	where+	foldPlayers (n:ns) (i:os) = listStoreSetValue raw i n >> foldPlayers ns os+	foldPlayers []     (i:os) = listStoreRemove raw i     >> foldPlayers [] os+	foldPlayers (n:ns) []     = listStoreAppend raw n     >> foldPlayers ns []+	foldPlayers []     []     = return ()+++rememberColumn :: Bundle -> Int -> TreeViewColumn -> IO ()+rememberColumn Bundle{..} n col = do+	order <- treeViewColumnGetSortOrder col+	current <- takeMVar mconfig+	let new = current {playersSort = n, playersOrder = order}+	putMVar mconfig new+	configToFile parent new
src/GtkExts.hs view
@@ -1,24 +1,47 @@ module GtkExts ( 	EntryIconPosition(..) 	, entrySetIconFromStock+	, cellSetText+	, cellSetMarkup ) where import System.Glib.FFI import System.Glib.UTFString import Graphics.UI.GtkInternals import Graphics.UI.Gtk.General.StockItems import Graphics.UI.Gtk.General.Enums+import Data.ByteString.Char8  entrySetIconFromStock	:: EntryClass self => self 			-> EntryIconPosition 			-> StockId 			-> IO () entrySetIconFromStock entry iconPos stockId =-	(\(Entry arg1) arg2 ->-		withForeignPtr arg1 $ \argPtr1 ->-		withUTFString stockId $ \stockIdPtr ->-		gtk_entry_set_icon_from_stock argPtr1 arg2 stockIdPtr)-			(toEntry entry)-			((fromIntegral . fromEnum) iconPos)+	withForeignPtr arg1 $ \argPtr1 ->+	withUTFString stockId $ \stockIdPtr ->+	gtk_entry_set_icon_from_stock argPtr1 arg2 stockIdPtr+	where+	Entry arg1	= toEntry entry+	arg2		= (fromIntegral . fromEnum) iconPos -foreign import ccall safe "gtk_entry_set_icon_from_stock"-	gtk_entry_set_icon_from_stock :: Ptr Entry -> CInt -> Ptr CChar -> IO ()+foreign import ccall unsafe "gtk_entry_set_icon_from_stock"+	gtk_entry_set_icon_from_stock :: Ptr Entry -> CInt -> CString -> IO ()+++cellSetText :: CellRendererText -> ByteString -> IO ()+cellSetText (CellRendererText obj) text =+	withForeignPtr obj $ \wPtr ->+	useAsCString text $ \textPtr ->+	g_object_set wPtr prop_text textPtr nullPtr++cellSetMarkup :: CellRendererText -> ByteString -> IO ()+cellSetMarkup (CellRendererText obj) text =+	withForeignPtr obj $ \wPtr ->+	useAsCString text $ \textPtr ->+	g_object_set wPtr prop_markup textPtr nullPtr++prop_text, prop_markup :: CString+prop_text = unsafePerformIO $ newCString "text"+prop_markup = unsafePerformIO $ newCString "markup"++foreign import ccall unsafe "g_object_set"+	g_object_set :: Ptr CellRendererText -> CString -> CString -> Ptr () -> IO ()
src/GtkUtils.hs view
@@ -1,178 +1,148 @@ module GtkUtils where import Graphics.UI.Gtk-import Control.Monad +import Control.Applicative+import Data.Maybe+import Monad2+import Data.ByteString (ByteString) -whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()-whenJust x f = case x of-	Nothing	-> return ()-	Just a	-> f a -	-			 scrollIt, scrollItV :: WidgetClass widget => widget -> PolicyType -> PolicyType -> IO ScrolledWindow scrollIt widget pol1 pol2 = do 	scroll <- scrolledWindowNew Nothing Nothing 	scrolledWindowSetPolicy scroll pol1 pol2 	containerAdd scroll widget 	return scroll-	+ scrollItV widget pol1 pol2 = do 	scroll <- scrolledWindowNew Nothing Nothing 	scrolledWindowSetPolicy scroll pol1 pol2 	scrolledWindowAddWithViewport scroll widget-	Just vp <- binGetChild scroll+	vp <- binGetChild scroll+	whenJust vp $ \vp2 ->+		set (castToViewport vp2) [ viewportShadowType := ShadowNone ] 	set scroll [ scrolledWindowShadowType := ShadowNone ]-	set (castToViewport vp) [ viewportShadowType := ShadowNone ] 	return scroll-	 -simpleListView :: [(String, Bool, a -> String)] -> IO (ListStore a, TreeView)-simpleListView headers = do-	model <- listStoreNew []-	view <- treeViewNewWithModel model-	select <- treeViewGetSelection view-	treeSelectionSetMode select SelectionNone-	addColumns model view $-		map (\(title, isName, f) -> (title, if isName then 0 else 1, isName, isName, isName, f)) headers-	return (model, view)-	-	 -newLabeledFrame :: String -> IO Frame-newLabeledFrame lbl = do-	frame <- frameNew-	frameSetLabel frame lbl-	return frame-	-addColumns :: (TreeViewClass view, TreeModelClass (model row), TypedTreeModelClass model) =>-	model row -> view -> [(String, Float, Bool, Bool, Bool, row -> String)] -> IO ()-addColumns model view xs = mapM_ g xs where-	g (title, align, format, expand, ellipsize, f) = do-		col <- treeViewColumnNew-		set col	[ treeViewColumnTitle := title-			, treeViewColumnExpand := expand ]-		rend <- cellRendererTextNew-		set rend [ cellXAlign := align]-		when ellipsize $-			set rend [ cellTextEllipsizeSet := True, cellTextEllipsize := EllipsizeEnd]-		cellLayoutPackStart col rend True-		cellLayoutSetAttributes col rend model $ \row -> if format-			then [ cellTextMarkup := Just (f row) ]-			else [ cellText := f row ]-		treeViewAppendColumn view col-		-{--addColumnsSort raw model view xs = sequence_ $ zipWith f (iterate (+1) 0) xs where-	f n (title, format, expand, showf,  sortf) = do-		col <- treeViewColumnNew-		set col	[ treeViewColumnTitle := title-			, treeViewColumnExpand := expand ]-		rend <- cellRendererTextNew-		cellLayoutPackStart col rend True-		cellLayoutSetAttributeFunc col rend model $ \iter -> do-			cIter	<- treeModelSortConvertIterToChildIter model iter-			item	<- treeModelGetRow raw cIter-			set rend $ if format -				then [ cellTextMarkup := Just (showf item) ]-				else [ cellText := showf item ]-		treeViewAppendColumn view col-		case sortf of-			Nothing	-> return ()-			Just g	-> do-				col `treeViewColumnSetSortColumnId` n-				treeSortableSetSortFunc model n (xort raw g)--}-addColumnsFilter :: (TreeViewClass self1, TreeModelFilterClass self, TreeModelClass self, TypedTreeModelClass model) =>-	model t -> self -> self1 -> [(String, Bool, t -> String)] -> IO ()-addColumnsFilter raw model view xs = mapM_ f xs where-	f (title, expand, showf) = do-		col <- treeViewColumnNew-		set col	[ treeViewColumnTitle := title-			, treeViewColumnExpand := expand ]-		rend <- cellRendererTextNew-		when expand $-			set rend [ cellTextEllipsize := EllipsizeEnd]-		cellLayoutPackStart col rend True-		cellLayoutSetAttributeFunc col rend model $ \iter -> do-			cIter	<- treeModelFilterConvertIterToChildIter model iter-			item	<- treeModelGetRow raw cIter-			set rend [ cellTextMarkup := Just (showf item) ]-		treeViewAppendColumn view col+class GeneralTreeView c where+	getElementPath	:: TreeModelClass (a b) => c a b -> [Int] -> IO b+	getElementIter	:: TreeModelClass (a b) => c a b -> TreeIter -> IO b+	getStore	:: TreeModelClass (a b) => c a b -> a b+	getView		:: c a b -> TreeView -data RendType i = RendText (i -> [AttrOp CellRendererText]) | RendPixbuf (i -> [AttrOp CellRendererPixbuf])-addColumnsFilterSort :: (TreeViewClass self, TreeSortableClass self1, TreeModelSortClass self1,-	TreeModelFilterClass self2, TreeModelClass self1, TypedTreeModelClass model) =>-	model t -> self2 -> self1 -> self -> Int -> SortType-	-> [(String, Bool, RendType t, Maybe (t -> t -> Ordering))]++instance GeneralTreeView GenSimple where+	getView (GenSimple _ view)		= view+	getElementIter (GenSimple store _)	= treeModelGetRow store+	getElementPath (GenSimple store _) path	= treeModelGetRow store =<< getIterUnsafe store path+	getStore (GenSimple store _)		= store+++instance GeneralTreeView GenFilterSort where+	getStore (GenFilterSort store _ _ _)	= store+	getView (GenFilterSort _ _ _ view)	= view+	getElementPath g@(GenFilterSort _ _ sorted _) path = getElementIter g =<< getIterUnsafe sorted path+	getElementIter (GenFilterSort store filtered sorted _) it =+		treeModelSortConvertIterToChildIter sorted it >>=+		treeModelFilterConvertIterToChildIter filtered >>=+		treeModelGetRow store++data GenCellRend i	= RendText2 ByteString (i -> [AttrOp CellRendererText])+			| RendMarkup ByteString (i -> [AttrOp CellRendererText])+			| RendPixbuf2 (i -> [AttrOp CellRendererPixbuf])++data GenSimple store a where+	GenSimple	:: (TypedTreeModelClass store, TreeModelClass (store a))+			=> !(store a)+			-> !TreeView+			-> GenSimple store a+data GenFilterSort store a where+	GenFilterSort	:: ( TreeModelClass (store a), TypedTreeModelClass store+			   , TreeModelClass filter, TreeModelFilterClass filter+			   , TreeModelClass sort, TreeModelSortClass sort, TreeSortableClass sort)+			=> !(store a)+			-> !filter+			-> !sort+			-> !TreeView+			-> GenFilterSort store a++newGenSimple :: (TypedTreeModelClass store, TreeModelClass (store a))+             => store a+             -> IO (GenSimple store a)+newGenSimple store = do+	view	<- treeViewNewWithModel store+	return (GenSimple store view)++newGenFilterSort :: (TypedTreeModelClass store, TreeModelClass (store a))+             => store a+             -> IO (GenFilterSort store a)+newGenFilterSort  store = do+	filtered	<- treeModelFilterNew store []+	sorted		<- treeModelSortNewWithModel filtered+	view		<- treeViewNewWithModel sorted+	treeSortableSetDefaultSortFunc sorted Nothing+	return (GenFilterSort store filtered sorted view)++addColumn :: GenSimple a e -> String -> Bool -> [AttrOp CellRendererText] -> (CellRendererText -> e -> IO ()) -> IO Int+addColumn gen@(GenSimple store view) title expand rendOpts f = do+	col <- treeViewColumnNew+	set col	[ treeViewColumnTitle := title+		, treeViewColumnExpand := expand ]+	rend <- cellRendererTextNew+	set rend rendOpts+	set rend [cellTextEllipsize := EllipsizeEnd]+	cellLayoutPackStart col rend True+	cellLayoutSetAttributeFunc col rend store $ \iter -> do+		item <- getElementIter gen iter+		f rend item+	treeViewAppendColumn view col++addColumnFS :: CellRendererClass rend+	=> GenFilterSort a e+	-> String+	-> Bool+	-> Maybe (e -> e -> Ordering)+	-> (TreeViewColumn -> IO ())+	-> IO rend+	-> [AttrOp rend]+	-> (rend -> e -> IO ()) 	-> IO ()-addColumnsFilterSort raw filtered sorted view defaultSort sortType xs = zipWithM_ f [0..] xs where-	f n (title, expand, rt, sortf) = do-		col <- treeViewColumnNew-		set col	[ treeViewColumnTitle := title-			, treeViewColumnExpand := expand ]-		case rt of-			RendText attr -> do-				rend <- cellRendererTextNew-				cellLayoutPackStart col rend True-				cellLayoutSetAttributeFunc col rend sorted $ \iter -> do-					cIter	<- treeModelSortConvertIterToChildIter sorted iter-					rcIter	<- treeModelFilterConvertIterToChildIter filtered cIter-					item	<- treeModelGetRow raw rcIter-					set rend $ attr item-			RendPixbuf attr -> do-				rend <- cellRendererPixbufNew-				cellLayoutPackStart col rend True-				cellLayoutSetAttributeFunc col rend sorted $ \iter -> do-					cIter	<- treeModelSortConvertIterToChildIter sorted iter-					rcIter	<- treeModelFilterConvertIterToChildIter filtered cIter-					item	<- treeModelGetRow raw rcIter-					set rend $ attr item-		treeViewAppendColumn view col-		case sortf of-			Nothing	-> return ()-			Just g	-> do-				col `treeViewColumnSetSortColumnId` n-				treeSortableSetSortFunc sorted n $ \it1 it2 -> do-					rit1	<- treeModelFilterConvertIterToChildIter filtered it1-					rit2	<- treeModelFilterConvertIterToChildIter filtered it2-					xort raw g rit1 rit2-		treeSortableSetDefaultSortFunc sorted Nothing-		treeSortableSetSortColumnId sorted defaultSort sortType-		-getElementFS :: (TreeModelClass self, TreeModelFilterClass self1, TreeModelSortClass self, TypedTreeModelClass model) =>-	model b -> self -> self1 -> TreePath -> IO b-getElementFS store sorted filtered x = do-	Just vIter	<- treeModelGetIter sorted x-	sIter		<- treeModelSortConvertIterToChildIter sorted vIter-	fIter		<- treeModelFilterConvertIterToChildIter filtered sIter-	treeModelGetRow store fIter+addColumnFS gen@(GenFilterSort store filtered sorted view) title expand sortf action mkRend rendOpts f = do+	col <- treeViewColumnNew+	set col	[ treeViewColumnTitle := title+		, treeViewColumnExpand := expand ]+	afterColClicked col (action col)+	rend <- mkRend+	set rend rendOpts+	cellLayoutPackStart col rend True+	cellLayoutSetAttributeFunc col rend sorted $ \iter -> do+		item <- getElementIter gen iter+		f rend item -getElementF :: (TreeModelClass self, TreeModelFilterClass self, TypedTreeModelClass model) =>-	model b -> self -> TreePath -> IO b-getElementF store filtered path = do-	Just vIter	<- treeModelGetIter filtered path-	iter		<- treeModelFilterConvertIterToChildIter filtered vIter-	treeModelGetRow store iter-	-getElement :: (TreeModelClass (model m), TypedTreeModelClass model) => model m -> TreePath -> IO m-getElement raw path = do-	Just iter	<- treeModelGetIter raw path-	treeModelGetRow raw iter-		-gtkPopup :: MessageType -> String -> IO ()-gtkPopup what str = do+	n <- pred <$> treeViewAppendColumn view col+	whenJust sortf $ \g -> do+		treeViewColumnSetSortColumnId col n+		treeSortableSetSortFunc sorted n $ \it1 it2 -> do+			rit1 <- treeModelFilterConvertIterToChildIter filtered it1+			rit2 <- treeModelFilterConvertIterToChildIter filtered it2+			g <$> treeModelGetRow store rit1 <*> treeModelGetRow store rit2++++getIterUnsafe :: TreeModelClass self => self -> TreePath -> IO TreeIter+getIterUnsafe model path =+	fromMaybe (error "getElement: Imposssible error") <$> treeModelGetIter model path+++gtkPopup :: MessageType -> Window -> String -> IO ()+gtkPopup what win str = do 	a <- messageDialogNew Nothing [DialogDestroyWithParent, DialogModal] 		what ButtonsOk str-	set a [ windowWindowPosition := WinPosCenter]+	windowSetPosition a WinPosCenterOnParent+	windowSetTransientFor a win 	dialogRun a 	widgetDestroy a -gtkWarn, gtkError :: String -> IO ()+gtkWarn, gtkError :: Window -> String  -> IO () gtkWarn = gtkPopup MessageWarning gtkError = gtkPopup MessageError--xort :: TypedTreeModelClass model => model t -> (t -> t -> b) -> TreeIter -> TreeIter -> IO b-xort model g it1 it2 = do-a <- treeModelGetRow model it1 -b <- treeModelGetRow model  it2-return $ g a b 
+ src/IndividualServerSettings.hs view
@@ -0,0 +1,64 @@+module IndividualServerSettings (+	ServerArg(..), ServerSettings+	, getSettings, putSettings, fromFile, toFile+) where+import Control.DeepSeq+import Network.Socket+import Data.List+import Data.Maybe+import Data.Map (Map)+import qualified Data.Map as M+import Network.Tremulous.SocketExtensions+import NumberSerializer++import Constants+import Exception2+import List2+++data ServerArg = ServerArg+	{ serverPass+	, serverRcon+	, serverName		:: !String+	, serverFavorite	:: !Bool+	}++instance NFData ServerArg where+	rnf (ServerArg a b c _) = rnf a `seq` rnf b `seq` rnf c++type ServerSettings = Map SockAddr ServerArg++configFile :: IO FilePath+configFile = inConfDir "serversettings"++getSettings :: SockAddr -> ServerSettings -> ServerArg+getSettings = M.findWithDefault (ServerArg "" "" "" False)++putSettings :: SockAddr -> ServerArg -> ServerSettings -> ServerSettings+putSettings = M.insert++fromFile :: IO ServerSettings+fromFile = handleIOWith M.empty $ do+	cont <- readFile =<< configFile+	return  $! strict (parse cont)+	where strict a = deepseq a a+++toFile :: ServerSettings -> IO Bool+toFile settings = tryIO $ do+	fx <- configFile+	writeFile fx $ unlines $ map f $ M.toList settings+	where f ((SockAddrInet (PortNum port) ip), ServerArg a b c d) = intercalate "\t"+		[ intToHex 8 (ntohl ip)+		, intToHex 4 (ntohs port)+		, a, b, c+		, if d then "f" else "" ]+	      f _ = ""++parse :: String -> ServerSettings+parse = M.fromList . mapMaybe (f . split '\t') . lines+	where+	f (ip:port:pass:rcon:name:opts:_) = Just (addr, ServerArg pass rcon name fav)+		where addr = SockAddrInet (PortNum (htons (hexToInt port))) (htonl (hexToInt ip))+		      fav  = 'f' `elem` opts+	f _  = Nothing
src/InfoBox.hs view
@@ -3,7 +3,7 @@   newInfobox :: String -> IO (VBox, Label, Label)-newInfobox what= do+newInfobox what = do 	lst <- mapM (labelNew . Just) 		["Showing", "0", " out of", "0", ' ' : what] 		
src/List2.hs view
@@ -1,4 +1,4 @@-module List2 (stripw, intmean, replace) where+module List2 (stripw, intmean, replace, mread, split, breakDrop) where import Prelude hiding (foldr, foldl, foldr1, foldl1) import Data.Char import Data.Foldable@@ -10,7 +10,7 @@ dropWhileRev p = foldr (\x xs -> if p x && null xs then [] else x:xs) []  intmean :: (Integral i, Foldable f) => f i -> i-intmean l = if len == 0 then 0 else lsum `div` len where+intmean l = if len == 0 then 0 else lsum `quot` len where 	(len, lsum) = foldl' (\(!n, !s) elm -> (n+1, s+elm)) (0, 0) l  replace :: (a -> Bool) -> a -> [a] -> [a]@@ -18,3 +18,21 @@ 	| f x		= y : xs 	| otherwise	= x : replace f y xs replace _ _ []		= []++mread :: (Read a) => String -> Maybe a+mread x = case reads x of+	[(a, _)]	-> Just a+	_		-> Nothing++split :: Eq a => a -> [a] -> [[a]]+split _ [] = []+split d s  = case break (== d) s of+	(w, s') -> w : case s' of+		[_]   -> [[]]+		_:s'' -> split d s''++		[]    -> []++breakDrop :: (a -> Bool) -> [a] -> ([a], [a])+breakDrop f xs = let (a, b) = break f xs+	in (a, dropWhile f b)
+ src/Monad2.hs view
@@ -0,0 +1,24 @@+module Monad2 where+import Control.Monad+import Network.Socket+import Control.Exception+import Control.Applicative+import Data.Maybe++whileTrue :: Monad m => m Bool -> m ()+whileTrue f = f >>= \t -> when t (whileTrue f)++whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()+whenJust x f = case x of+	Nothing	-> return ()+	Just a	-> f a++whenM :: Monad m => m Bool -> m () -> m ()+whenM c m = c >>= \t -> if t then m else return ()++unlessM :: Monad m => m Bool -> m () -> m ()+unlessM c m = c >>= \t -> if not t then m else return ()++getDNS :: String -> String -> IO (Maybe SockAddr)+getDNS host port = handle (\(_ :: IOException) -> return Nothing) $+	fmap addrAddress . listToMaybe <$> getAddrInfo Nothing (Just host) (Just port)
+ src/NumberSerializer.hs view
@@ -0,0 +1,25 @@+module NumberSerializer where+import Data.Bits+import Network.Tremulous.SocketExtensions+import Network.Socket++intToHex :: (Integral i, Bits i) => Int -> i -> String+intToHex = go [] where+	go b 0 _   = b+	go b n int = go (toDigit (int .&. 0xF) : b) (n-1) (int `shiftR` 4)+	toDigit i | i <= 9    = conv (i + 0x30)+	          | otherwise = conv (i + 0x61-0xa)+	conv = toEnum . fromIntegral++hexToInt :: (Integral i, Bits i) => String -> i+hexToInt = go 0 where+	go b []     = b+	go b (x:xs) = go ((b `shiftL` 4) .|. toInt x) xs+	toInt c | c' <= 0x39 = c' - 0x30+	        | otherwise  = c' - (0x61-0xa)+	        where c' = conv c+	conv = fromIntegral . fromEnum+++stringToSockAddr :: String -> String -> SockAddr+stringToSockAddr ip port = SockAddrInet (PortNum (htons (hexToInt port))) (htonl (hexToInt ip))
+ src/OnlineClans.hs view
@@ -0,0 +1,83 @@+module OnlineClans (newOnlineClans) where+import Graphics.UI.Gtk++import Data.Monoid+import Data.Ord+import Data.Tree+import Data.List (sortBy)+import Control.Concurrent+import Network.Tremulous.NameInsensitive+import qualified Network.Tremulous.Protocol as P+import Network.Tremulous.Util+import qualified Data.ByteString.Char8 as B++import ClanFetcher+import Types+import Config+import GtkUtils+import GtkExts+import TremFormatting++newOnlineClans :: Bundle-> SetCurrent -> IO (ScrolledWindow, ClanPolledHook)+newOnlineClans Bundle{..} setServer = do+	Config {colors} <- readMVar mconfig++	gen@(GenSimple raw view) <- newGenSimple =<< treeStoreNew []++	addColumn gen "Name" True [cellTextEllipsize := EllipsizeEnd] $ \rend ->+		cellSetMarkup rend . showName colors+	addColumn gen "Server" True [cellTextEllipsize := EllipsizeEnd] $ \rend ->+		cellSetMarkup rend . showServer colors++	let updateF clans P.PollResult{..} = do+		let players = buildTree $ sortByPlayers $+			associatePlayerToClans (makePlayerNameList polled) clans+		treeStoreClear raw+		treeViewColumnsAutosize view+		mapM_ (treeStoreInsertTree raw [] 0) players+		treeViewExpandAll view++	on view cursorChanged $ do+		(path, _)	<- treeViewGetCursor view+		item		<- getElementPath gen path+		case item of+			Left _ -> return ()+			Right (_, gs) -> setServer False gs++	on view rowActivated $ \path _ -> do+		item		<- getElementPath gen path+		case item of+			Left _ -> return ()+			Right (_, gs) -> setServer True gs++	scroll <- scrollIt view PolicyAutomatic PolicyAutomatic++	return (scroll, updateF)+	where+	showName colors c =  case c of+		Left Clan{..}		-> B.concat ["<b>", htmlEscapeBS $ original name, "</b>"]+		Right (name, _)		-> pangoPrettyBS colors name++	showServer colors c =  case c of+		Left _ -> ""+		Right (_, P.GameServer{hostname}) -> pangoPrettyBS colors hostname+++type PlayerList = [(TI, P.GameServer)]+type OnlineView = Forest (Either Clan (TI, P.GameServer))++associatePlayerToClans :: PlayerList -> [Clan] -> [(Clan, PlayerList)]+associatePlayerToClans players clans = map f clans+	where+	f c@Clan{tagexpr} = (c, filter (cmp tagexpr) players)+	cmp e = matchTagExpr e . fst++buildTree :: [(Clan, PlayerList)] -> OnlineView+buildTree = filter notEmpty . foldr f [] where+	f (clan, pls) xs = Node (Left clan) (map rightNode pls) : xs+	rightNode x = Node (Right x) []+	notEmpty (Node _ [])	= False+	notEmpty _		= True++sortByPlayers :: [(Clan, [b])] -> [(Clan, [b])]+sortByPlayers = sortBy $ comparing (length . snd) `mappend` flip (comparing (name . fst))
src/Preferences.hs view
@@ -1,8 +1,9 @@-module Preferences where+{-# LANGUAGE CPP #-}+module Preferences (newPreferences) where import Graphics.UI.Gtk+ import Control.Monad import Control.Applicative hiding (empty)-import Control.Concurrent.STM import Data.Char import Data.Array import Text.Printf@@ -10,117 +11,117 @@ import System.FilePath  import Types-import Config+import qualified Config as C import Constants import GtkUtils+import Monad2 import TremFormatting +#define _CONNECT(WID, SIG, GET, SET) on WID SIG (do {new <- get WID GET; update (\x -> x { SET = new})})+#define _CONNECT_WITH(WID, SIG, GET, SET, F) on WID SIG (do {new <- get WID GET; update (\x -> x { SET = F new})})  newPreferences :: Bundle -> IO ScrolledWindow newPreferences Bundle{..} = do+	let update f = do+		new <- modifyMVar mconfig $ \old -> do+			let new = f old+			return (new, new)+		C.configToFile parent new+ 	-- Default filters-	(tbl, [filterBrowser', filterPlayers'], filterEmpty') <--		configTable ["_Browser:", "Find _players:"]-	filters <- newLabeledFrame "Default filters"-	set filters [ containerChild := tbl]+	(  tbl+	 , [filterBrowser, filterPlayers]+	 , filterEmpty+	 ) <- configTable+		[ "_Browser:"+		, "_Find players:"+		]+	filters <- framed "Default filters" tbl  	-- Tremulous path-	(pathstbl, [tremPath', tremGppPath']) <- pathTable parent ["_Tremulous 1.1:", "Tremulous _GPP:"]-	paths <- newLabeledFrame "Tremulous path or command"-	set paths [ containerChild := pathstbl]+	(pathstbl, [tremPath, tremGppPath]) <- pathTable parent ["_Tremulous 1.1:", "Tremulous _GPP:"]+	paths <- framed "Tremulous path or command" pathstbl  	-- Startup-	startupMaster	<- checkButtonNewWithMnemonic "_Refresh all servers"-	startupClan	<- checkButtonNewWithMnemonic "_Sync clan list"-	startupGeometry	<- checkButtonNewWithMnemonic "Restore _window geometry from previous session"-	-	(startup, startupBox) <- framedVBox ("On startup")-	boxPackStartDefaults startupBox startupMaster	-	boxPackStartDefaults startupBox startupClan-	boxPackStartDefaults startupBox startupGeometry-			+	autoClan	<- checkButtonNewWithMnemonic "_Sync clan list"+	autoGeometry	<- checkButtonNewWithMnemonic "Restore _window geometry from previous session"+	startupBox <- vBoxNew False 0+	boxPackStartDefaults startupBox autoClan+	boxPackStartDefaults startupBox autoGeometry+	startup <- framed "On startup" startupBox -	-- Colors+	--Refresh mode+	rb1 <- radioButtonNewWithMnemonic "_Once at startup"+	rb2 <- radioButtonNewWithMnemonicFromWidget rb1 "_Periodically each"+	rb3 <- radioButtonNewWithMnemonicFromWidget rb1 "_Manually"+	autoDelay <- spinButtonNewWithRange 0 3600 1+	seconds <- labelNew (Just "s") -	(colorTbl, colorList)	<- numberedColors -	(colors', colorBox)	<- framedVBox "Color theme"-	colorWarning		<- labelNew (Just "Note: Requires a restart to take effect")-	+	delaybox <- hBoxNew False spacing+	boxPackStart delaybox autoDelay PackNatural 0+	boxPackStart delaybox seconds PackNatural 0++	perbox <- hBoxNew False spacingBig+	boxPackStart perbox rb2 PackNatural 0+	boxPackStart perbox delaybox PackNatural 0++	rbBox <- vBoxNew False 0+	boxPackStartDefaults rbBox rb1+	boxPackStartDefaults rbBox perbox+	boxPackStartDefaults rbBox rb3+	refreshmode <- framed "Refresh all" rbBox++	-- Colors+	(colorTbl, colorList) <- numberedColors+	colorWarning <- labelNew (Just "Note: Requires a restart to take effect") 	miscSetAlignment colorWarning 0 0+	colorBox <- vBoxNew False spacing 	boxPackStart colorBox colorTbl PackNatural 0 	boxPackStart colorBox colorWarning PackNatural 0+	colors' <- framed "Color theme" colorBox   	-- Internals-	(itbl, [itimeout, iresend, ibuf]) <- mkInternals -	(internals, ibox) <- framedVBox "Polling Internals"+	(itbl, [packetTimeout', packetDuplication', throughputDelay']) <- mkInternals 	ilbl <- labelNew $ Just "Tip: Hover the cursor over each option for a description" 	miscSetAlignment ilbl 0 0-	--labelSetLineWrap ilbl True+	ibox <- vBoxNew False spacing 	boxPackStart ibox itbl PackNatural 0 	boxPackStart ibox ilbl PackNatural 0 --	-- Apply-	apply	<- buttonNewFromStock stockApply-	bbox	<- hBoxNew False 0-	boxPackStartDefaults bbox apply-	balign	<- alignmentNew 0.5 1 0 0-	set balign [ containerChild := bbox ]+	internals <- framed "Polling Internals" ibox -	on apply buttonActivated $ do-		filterBrowser	<- get filterBrowser' entryText-		filterPlayers	<- get filterPlayers' entryText-		filterEmpty	<- get filterEmpty' toggleButtonActive-		tremPath	<- get tremPath' entryText-		tremGppPath	<- get tremGppPath' entryText-		autoMaster	<- get startupMaster toggleButtonActive-		autoClan	<- get startupClan toggleButtonActive-		autoGeometry	<- get startupGeometry toggleButtonActive-		packetTimeout	<- (*1000) <$> spinButtonGetValueAsInt itimeout-		packetDuplication	<- spinButtonGetValueAsInt iresend-		throughputDelay	<- (*1000) <$> spinButtonGetValueAsInt ibuf-		-		rawcolors	<- forM colorList $ \(colb, cb) -> do-					bool <- get cb toggleButtonActive-					(if bool then TFColor else TFNone)-						 . colorToHex <$> colorButtonGetColor colb-		old		<- atomically $ takeTMVar mconfig-		let new		= old {filterBrowser, filterPlayers, autoMaster-				, autoClan, autoGeometry, tremPath, tremGppPath-				, colors = makeColorsFromList rawcolors-				, delays = Delay{..}, filterEmpty}-		atomically $ putTMVar mconfig new-		configToFile new-		return ()-	 	-- Main box--	box <- vBoxNew False spacing-	set box [ containerBorderWidth := spacingBig ]+	box <- vBoxNew False spacingHuge+	containerSetBorderWidth box spacing 	boxPackStart box filters PackNatural 0 	boxPackStart box paths PackNatural 0+	boxPackStart box refreshmode PackNatural 0 	boxPackStart box startup PackNatural 0 	boxPackStart box colors' PackNatural 0 	boxPackStart box internals PackNatural 0-	boxPackStart box balign PackGrow 0   	-- Set values from Config 	let updateF = do-		Config {..} 		<- atomically $ readTMVar mconfig-		set filterBrowser'	[ entryText := filterBrowser ]-		set filterPlayers'	[ entryText := filterPlayers ]-		set filterEmpty'	[ toggleButtonActive := filterEmpty]-		set tremPath'		[ entryText := tremPath ]-		set tremGppPath'	[ entryText := tremGppPath ]-		set startupMaster	[ toggleButtonActive := autoMaster ]-		set startupClan		[ toggleButtonActive := autoClan ]-		set startupGeometry	[ toggleButtonActive := autoGeometry ]-		set itimeout		[ spinButtonValue := fromIntegral (packetTimeout delays `div` 1000) ]-		set iresend		[ spinButtonValue := fromIntegral (packetDuplication delays) ]-		set ibuf		[ spinButtonValue := fromIntegral (throughputDelay delays `div` 1000) ]-		sequence_ $ zipWith f colorList (elems colors)+		c <- readMVar mconfig+		let Delay{..} = C.delays c+		set filterBrowser	[ entryText := C.filterBrowser c ]+		set filterPlayers	[ entryText := C.filterPlayers c ]+		set filterEmpty		[ toggleButtonActive := C.filterEmpty c]+		set tremPath		[ entryText := C.tremPath c ]+		set tremGppPath		[ entryText := C.tremGppPath c]+		set autoClan		[ toggleButtonActive := C.autoClan c]+		set autoGeometry	[ toggleButtonActive := C.autoGeometry c]+		set autoDelay		[ spinButtonValue := fromIntegral (C.autoDelay c `quot` 1000000) ]+		set packetTimeout'	[ spinButtonValue := fromIntegral (packetTimeout `quot` 1000) ]+		set packetDuplication'	[ spinButtonValue := fromIntegral packetDuplication ]+		set throughputDelay'	[ spinButtonValue := fromIntegral (throughputDelay `quot` 1000) ]+		set rb1			[ toggleButtonActive := C.refreshMode c == C.Startup ]+		set rb2			[ toggleButtonActive := C.refreshMode c == C.Auto ]+		set rb3			[ toggleButtonActive := C.refreshMode c == C.Manual ]++		zipWithM_ f colorList (elems (C.colors c)) 		where	f (a, b) (TFColor c) = do 				colorButtonSetColor a (hexToColor c) 				toggleButtonSetActive b True@@ -129,49 +130,100 @@ 				toggleButtonSetActive b False 				-- Apparently this is needed too 				toggleButtonToggled b+ 	updateF-		++	_CONNECT(filterBrowser, editableChanged, entryText, C.filterBrowser)+	_CONNECT(filterEmpty, toggled, toggleButtonActive, C.filterEmpty)+	_CONNECT(filterPlayers, editableChanged, entryText, C.filterPlayers)++	_CONNECT(tremPath, editableChanged, entryText, C.tremPath)+	_CONNECT(tremGppPath, editableChanged, entryText, C.tremGppPath)++	--_CONNECT(autoMaster, toggled, toggleButtonActive, C.autoMaster)+	_CONNECT(autoClan, toggled, toggleButtonActive, C.autoClan)+	_CONNECT(autoGeometry, toggled, toggleButtonActive, C.autoGeometry)+++	let radiofunc value = update (\x -> x { C.refreshMode = value})++	on rb1 toggled $ do	b <- toggleButtonGetActive rb1+				when b (radiofunc C.Startup)+	on rb2 toggled $ do	b <- toggleButtonGetActive rb2+				when b (radiofunc C.Auto)+	on rb3 toggled $ do	b <- toggleButtonGetActive rb3+				when b (radiofunc C.Manual)++	onValueSpinned autoDelay $ do+		value <- (*1000000) <$> spinButtonGetValueAsInt autoDelay+		update (\x -> x { C.autoDelay = fromIntegral value})++	onValueSpinned packetTimeout' $ do+		packetTimeout <- (*1000) <$> spinButtonGetValueAsInt packetTimeout'+		update (\x -> let delays = C.delays x in x {C.delays = delays {packetTimeout}})++	onValueSpinned packetDuplication' $ do+		packetDuplication <- spinButtonGetValueAsInt packetDuplication'+		update (\x -> let delays = C.delays x in x {C.delays = delays {packetDuplication}})++	onValueSpinned throughputDelay' $ do+		throughputDelay <- (*1000) <$> spinButtonGetValueAsInt throughputDelay'+		update (\x -> let delays = C.delays x in x {C.delays = delays {throughputDelay}})++	let updateColors = do+		rawcolors <- forM colorList $ \(colb, cb) -> do+			bool <- get cb toggleButtonActive+			(if bool then TFColor else TFNone)+				 . colorToHex <$> colorButtonGetColor colb+		update $ \x -> x {C.colors = C.makeColorsFromList rawcolors}++	forM colorList $ \(colb, cb) -> do+		on cb toggled updateColors+		afterColorSet colb updateColors+ 	scrollItV box PolicyNever PolicyAutomatic  configTable :: [String] -> IO (Table, [Entry], CheckButton) configTable ys = do-	tbl <- tableNew 0 0 False+	tbl <- paddedTableNew 	empty <- checkButtonNewWithMnemonic "_empty" 	let easyAttach pos lbl  = do 		a <- labelNewWithMnemonic lbl-		b <- entryNew-		set a [ labelMnemonicWidget := b ]++		ent <- entryNew+		b <- hBoxNew False spacingHalf+		boxPackStart b ent PackGrow 0+		when (pos == 0) $+			boxPackStart b empty PackNatural 0++		set a [ labelMnemonicWidget := ent ] 		miscSetAlignment a 0 0.5-		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] spacing spacingHalf-		tableAttach tbl b 1 2 pos (pos+1) [Expand, Fill] [] spacing spacingHalf-		when (pos == 0) $ -			tableAttach tbl empty 2 3 pos (pos+1) [Fill] [] spacing spacingHalf-				-		return b-		-	let mkTable xs = mapM (uncurry easyAttach) (zip [0..] xs)-	rt	<- mkTable ys+		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] 0 0+		tableAttach tbl b 1 2 pos (pos+1) [Expand, Fill] [] 0 0++		return ent++	rt <- zipWithM easyAttach [0..] ys 	return (tbl, rt, empty)  pathTable :: Window -> [String] -> IO (Table, [Entry]) pathTable parent ys = do-	tbl <- tableNew 0 0 False+	tbl <- paddedTableNew 	let easyAttach pos lbl  = do 		a <- labelNewWithMnemonic lbl 		(box, ent) <- pathSelectionEntryNew parent 		set a [ labelMnemonicWidget := ent ] 		miscSetAlignment a 0 0.5-		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] spacing spacingHalf-		tableAttach tbl box 1 2 pos (pos+1) [Expand, Fill] [] spacing spacingHalf+		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] 0 0+		tableAttach tbl box 1 2 pos (pos+1) [Expand, Fill] [] 0 0 		return ent-		-	let mkTable xs = mapM (uncurry easyAttach) (zip [0..] xs)-	rt	<- mkTable ys++	rt	<- zipWithM easyAttach [0..] ys 	return (tbl, rt)  mkInternals :: IO (Table, [SpinButton]) mkInternals = do-	tbl <- tableNew 0 0 False+	tbl <- paddedTableNew 	let easyAttach pos (lbl, lblafter, tip)  = do 		a <- labelNewWithMnemonic lbl 		b <- spinButtonNewWithRange 0 10000 1@@ -180,44 +232,50 @@ 			, widgetTooltipText := Just tip 			, miscXalign := 0 ] 		set c	[ miscXalign := 0 ]-		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] spacing spacingHalf-		tableAttach tbl b 1 2 pos (pos+1) [Fill] [] spacing spacingHalf-		tableAttach tbl c 2 3 pos (pos+1) [Fill] [] spacing spacingHalf+		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] 0 0+		tableAttach tbl b 1 2 pos (pos+1) [Fill] [] 0 0+		tableAttach tbl c 2 3 pos (pos+1) [Fill] [] 0 0 		return b-		-	let mkTable xs = mapM (uncurry easyAttach) (zip [0..] xs)++	let mkTable = zipWithM easyAttach [0..] 	rt <- mkTable	[ ("Respo_nse Timeout:", "ms", "How long Apelsin should wait before sending a new request to a server possibly not responding") 			, ("Maximum packet _duplication:", "times", "Maximum number of extra requests to send beoynd the initial one if a server does not respond" ) 			, ("Throughput _limit:", "ms", "Should be set as low as possible as long as pings from \"Refresh all servers\" remains the same as \"Refresh current\"") ] 	return (tbl, rt)  -framedVBox :: String -> IO (Frame, VBox)-framedVBox title = do-	box	<- vBoxNew False 0-	frame	<- newLabeledFrame title-	set box [ containerBorderWidth := spacing ]-	set frame	[ containerChild := box ]-	return (frame, box)-	+framed :: ContainerClass w => String -> w -> IO VBox+framed title box = do+	l <- labelNew (Just title)+	miscSetAlignment l 0 0+	labelSetAttributes l [AttrWeight 0 (-1) WeightBold]++	align <- alignmentNew 0.5 0.5 1 1+	alignmentSetPadding align 0 0 spacingBig 0+	containerAdd align box++	vb <- vBoxNew False spacing+	boxPackStart vb l PackNatural 0+	boxPackStart vb align PackNatural 0++	return vb+ numberedColors :: IO (Table, [(ColorButton, CheckButton)]) numberedColors = do-	tbl <- tableNew 0 0 False+	tbl <- paddedTableNew 	let easyAttach pos lbl  = do 		a <- labelNew (Just lbl) 		b <- colorButtonNew 		c <- checkButtonNew-		on c toggled $ do-			bool <- get c toggleButtonActive-			set b [ widgetSensitive := if bool then True else False ]+		on c toggled $+			widgetSetSensitive b =<< toggleButtonGetActive c 		miscSetAlignment a 0.5 0-		tableAttach tbl a pos (pos+1) 0 1 [Fill] [] spacingHalf spacingHalf-		tableAttach tbl b pos (pos+1) 1 2 [Fill] [] spacingHalf spacingHalf-		tableAttach tbl c pos (pos+1) 2 3 [] [] spacingHalf spacingHalf+		tableAttach tbl a pos (pos+1) 0 1 [Fill] [] 0 0+		tableAttach tbl b pos (pos+1) 1 2 [Fill] [] 0 0+		tableAttach tbl c pos (pos+1) 2 3 [] [] 0 0 		return (b, c)-	let mkTable xs = mapM (uncurry easyAttach) (zip (iterate (+1) 0) xs) -	xs <- mkTable ["^0", "^1", "^2", "^3", "^4", "^5", "^6", "^7"]+	xs <- zipWithM easyAttach [0..]  ["^0", "^1", "^2", "^3", "^4", "^5", "^6", "^7"] 	return (tbl, xs)  @@ -225,15 +283,14 @@ pathSelectionEntryNew :: Window -> IO (HBox, Entry) pathSelectionEntryNew parent = do 	box	<- hBoxNew False 0-	img	<- imageNewFromStock stockOpen (IconSizeUser 1) 	button	<- buttonNew-	set button [ buttonImage := img ]+	set button [ buttonImage :=> imageNewFromStock stockOpen (IconSizeUser 1) ] 	ent	<- entryNew  	boxPackStart box ent PackGrow 0 	boxPackStart box button PackNatural 0-	-		++ 	on button buttonActivated $ do 		fc	<- fileChooserDialogNew (Just "Select path") (Just parent) FileChooserActionOpen 			[ (stockCancel, ResponseCancel)@@ -249,17 +306,20 @@ 						set ent [ entryText := path ] 			_-> return () 		widgetDestroy fc-		-	-	return (box, ent)  --- And more fail by gtk to not include something like this by default+	return (box, ent) +paddedTableNew :: IO Table+paddedTableNew = do+	tbl <- tableNew 0 0 False+	set tbl	[ tableRowSpacing	:= spacingHalf+		, tableColumnSpacing	:= spacing ]+	return tbl+ colorToHex :: Color -> String colorToHex (Color a b c) = printf "#%02x%02x%02x" (f a) (f b) (f c)-	where f = (`div` 0x100)-+	where f = (`quot` 0x100)  hexToColor :: String -> Color hexToColor ('#':a:b:c:d:e:g:_)	= Color (f a b) (f c d) (f e g)
− src/STM2.hs
@@ -1,37 +0,0 @@-module STM2(-	module Control.Concurrent, module Control.Concurrent.STM-	, tryReadTMVar, withTMVar, clearTMVar, replaceTMVar, modifyTMVar-) where--import Control.Concurrent-import Control.Concurrent.STM-import Control.Monad--tryReadTMVar :: TMVar a -> STM (Maybe a)-tryReadTMVar v = do -	x <- tryTakeTMVar v-	case x of-		Just a	-> putTMVar v a >> return (Just a)-		Nothing	-> return Nothing--withTMVar :: TMVar a -> (a -> IO ()) -> IO ()-withTMVar mvar f = do-	tst <- atomically $ tryReadTMVar mvar-	case tst of-		Nothing	-> return ()-		Just a	-> f a-		-clearTMVar :: TMVar a -> STM ()		-clearTMVar mvar = do-	t <- isEmptyTMVar mvar-	unless t $ do-		takeTMVar mvar-		return ()--replaceTMVar :: TMVar a -> a -> STM ()-replaceTMVar t x = clearTMVar t >> putTMVar t x--modifyTMVar :: TMVar a -> (a -> a) -> STM ()-modifyTMVar x f = do-	t <- takeTMVar x-	putTMVar x (f t)
src/ServerBrowser.hs view
@@ -1,95 +1,129 @@-module ServerBrowser where+module ServerBrowser (newServerBrowser, browserUpdateOne) where import Graphics.UI.Gtk-import Control.Concurrent.STM import Data.IORef-import qualified Data.ByteString.Char8 as B import Data.Ord+import Data.Monoid import Network.Tremulous.Protocol+import qualified Network.Tremulous.StrictMaybe as SM import Text.Printf+import qualified Data.ByteString.Char8 as B  import Types import GtkUtils+import GtkExts import FilterBar import InfoBox import TremFormatting import Constants import Config -newServerBrowser :: Bundle -> SetCurrent -> IO (VBox, PolledHook,Entry)-newServerBrowser Bundle{browserStore=raw, ..} setServer = do-	Config {..}	<- atomically $ readTMVar mconfig-	filtered	<- treeModelFilterNew raw []-	sorted		<- treeModelSortNewWithModel filtered	-	view		<- treeViewNewWithModel sorted-	-	addColumnsFilterSort raw filtered sorted view 3 SortAscending-		[ ("_Game"	, False	, RendText (simpleColumn showGame)-		   	, Just (comparing (\x -> (protocol x, gamemod x))))-		, ("_Name"	, True	, RendText (markupColumn colors hostname)-			, Just (comparing hostname))-		, ("_Map"	, False	, RendText (simpleColumn (take 16 . unpackorig . mapname))-			, Just (comparing mapname))-		, ("P_ing"	, False	, RendText (intColumn (show . gameping))-			, Just (comparing gameping))-		, ("_Players"	, False	, RendText (intColumn (showPlayers))-			, Just (comparing nplayers))-		]+newServerBrowser :: Bundle -> SetCurrent -> IO (VBox, PolledHook)+newServerBrowser bundle@Bundle{..} setServer = do+	Config {..} <- readMVar mconfig+	gen@(GenFilterSort raw filtered sorted view) <- newGenFilterSort browserStore++	addColumnFS gen "_Game" False+		(Just (comparing protocol `mappend` comparing gamemod `mappend` comparing gameping))+		(rememberColumn bundle 0)+		cellRendererTextNew+		[]+		(\rend -> cellSetText rend . showGame)++	addColumnFS gen "_Name" True+		(Just (comparing hostname `mappend` comparing gameping))+		(rememberColumn bundle 1)+		cellRendererTextNew+		[cellTextEllipsize := EllipsizeEnd]+		(\rend -> cellSetMarkup rend . pangoPrettyBS colors . hostname)++	addColumnFS gen "_Map" False+		(Just (comparing mapname `mappend` comparing gameping))+		(rememberColumn bundle 2)+		cellRendererTextNew []+		(\rend -> cellSetText rend . SM.maybe "" (B.take 16 . original) . mapname)++	addColumnFS gen "P_ing" False+		(Just (comparing gameping `mappend` comparing hostname))+		(rememberColumn bundle 3)+		cellRendererTextNew+		[cellXAlign := 1]+		(\rend x -> set rend [cellText := show $ gameping x])++	addColumnFS gen "_Players" False+		(Just (comparing nplayers `mappend` flip (comparing gameping)))+		(rememberColumn bundle 4)+		cellRendererTextNew+		[cellXAlign := 1]+		(\rend x -> set rend [cellText := showPlayers x])+++	treeSortableSetSortColumnId sorted browserSort browserOrder+ 	(infobox, statNow, statTot, statRequested) <- newInfoboxBrowser-	-	(filterbar, current, ent) <- newFilterBar filtered statNow filterBrowser++	(filterbar, current) <- newFilterBar parent filtered statNow filterBrowser 	empty <- checkButtonNewWithMnemonic "_empty" 	set empty [ toggleButtonActive := filterEmpty ] 	boxPackStart filterbar empty PackNatural spacingHalf 	on empty toggled $ do 		treeModelFilterRefilter filtered-		n <- treeModelIterNChildren filtered Nothing-		set statNow [ labelText := show n ]+		labelSetText statNow . show =<< treeModelIterNChildren filtered Nothing   	treeModelFilterSetVisibleFunc filtered $ \iter -> do 		GameServer{..}	<- treeModelGetRow raw iter 		s		<- readIORef current 		showEmpty	<- toggleButtonGetActive empty-		return $ (showEmpty || not (null players)) && (B.null s ||-			smartFilter s [-				  cleanedCase hostname-				, cleanedCase mapname+		return $ (showEmpty || not (null players)) &&+			(smartFilter s+				[ cleanedCase hostname+				, SM.maybe "" cleanedCase mapname+				, SM.maybe "" cleanedCase version 				, proto2string protocol-				, maybe "" cleanedCase gamemod+				, SM.maybe "" cleanedCase gamemod 				])-			+ 	on view cursorChanged $ do 		(path, _) <- treeViewGetCursor view-		setServer False =<< getElementFS raw sorted filtered path+		setServer False =<< getElementPath gen path  	on view rowActivated $ \path _ ->-		setServer True =<< getElementFS raw sorted filtered path-		+		setServer True =<< getElementPath gen path+ 	scrollview <- scrolledWindowNew Nothing Nothing 	scrolledWindowSetPolicy scrollview PolicyNever PolicyAlways 	containerAdd scrollview view-	+ 	let updateF PollResult{..} = do 		listStoreClear raw 		treeViewColumnsAutosize view 		mapM_ (listStoreAppend raw) polled-		treeModelFilterRefilter filtered-		set statTot		[ labelText := show serversResponded ]-		set statRequested	[ labelText := show (serversRequested-serversResponded) ]-		n <- treeModelIterNChildren filtered Nothing-		set statNow 		[ labelText := show n ]-		+		labelSetText statTot       $ show serversResponded+		labelSetText statRequested $ show (serversRequested - serversResponded)+		labelSetText statNow . show =<< treeModelIterNChildren filtered Nothing+ 	box <- vBoxNew False 0 	boxPackStart box filterbar PackNatural spacing 	boxPackStart box scrollview PackGrow 0 	boxPackStart box infobox PackNatural 0-	-	return (box, updateF, ent)++	return (box, updateF) 	where-	showGame GameServer{..} = proto2string protocol ++ maybe "" (("-"++) . htmlEscape . unpackorig) gamemod+	showGame GameServer{..} = B.concat (proto2string protocol : SM.maybe [] ((\x -> ["-",x]) . original) gamemod) 	showPlayers GameServer{..} = printf "%d / %2d" nplayers slots-	markupColumn colors f item =-		[ cellTextEllipsize := EllipsizeEnd-		, cellTextMarkup := Just (pangoPretty colors (f item)) ]-	intColumn f item = [ cellText := f item , cellXAlign := 1 ]-	simpleColumn f item = [ cellText := f item ]++browserUpdateOne :: BrowserStore -> GameServer -> IO ()+browserUpdateOne raw gs = treeModelForeach raw $ \iter -> do+	let i = listStoreIterToIndex iter+	e <- listStoreGetValue raw i+	if address e == address gs+		then listStoreSetValue raw i gs >> return True+		else return False++rememberColumn :: Bundle -> Int -> TreeViewColumn -> IO ()+rememberColumn Bundle{..} n col = do+	order <- treeViewColumnGetSortOrder col+	current <- takeMVar mconfig+	let new = current {browserSort = n, browserOrder = order}+	putMVar mconfig new+	configToFile parent new
src/ServerInfo.hs view
@@ -5,75 +5,87 @@ import Control.Applicative import Control.Monad hiding (join) import Control.Exception+import Control.Concurrent import Data.Ord-import Data.List (sortBy, findIndex)+import Data.ByteString.Char8 (unpack)+import Data.List (sortBy) import System.Process import System.FilePath  import Network.Tremulous.Protocol+import qualified Network.Tremulous.StrictMaybe as SM import Network.Tremulous.Polling import Network.Tremulous.Util +import ConcurrentUtil import Types-import STM2+import Exception2 import List2+import Monad2 import TremFormatting import GtkUtils+import GtkExts import Constants import Config+import IndividualServerSettings+import SettingsDialog+import FindPlayers+import ServerBrowser+import AutoRefresh -newServerInfo :: Bundle -> TMVar (PolledHook, ClanPolledHook) -> IO (VBox, PolledHook, SetCurrent)+newServerInfo :: Bundle -> MVar (PolledHook, ClanPolledHook) -> IO (VBox, PolledHook, SetCurrent) newServerInfo Bundle{..} mupdate = do-	Config {colors} <- atomically $ readTMVar mconfig-	current		<- atomically newEmptyTMVar-	running		<- atomically newEmptyTMVar-	+	Config {colors} <- readMVar mconfig+	current		<- newEmptyMVar+	running		<- newEmptyMVar+ 	-- Host name-	hostnamex <- labelNew Nothing+	hostnamex <- labelNew (Just "Server") 	set hostnamex [ 		  labelWrap		:= True 		, labelJustify		:= JustifyCenter 		, labelSelectable	:= True 		, labelUseMarkup	:= True-		, labelLabel		:= formatHostname "Server"-		-- failgtk exception..-		--, labelWrapMode := WrapPartialWords+		, labelAttributes 	:= [AttrWeight 0 (-1) WeightBold, AttrScale 0 (-1) 1.2] 		]-	+	labelSetLineWrapMode hostnamex WrapPartialWords+ 	-- Pretty CVar table-	tbl <- tableNew 5 2 True+	tbl <- tableNew 0 0 True 	set tbl [ tableRowSpacing := spacing-		, tableColumnSpacing := spacingBig ]+		, tableColumnSpacing := spacing ] 	let easyAttach pos lbl  = do 		a <- labelNew (Just lbl) 		b <- labelNew Nothing+		let h = 0x8888+		labelSetAttributes a [AttrForeground 0 (-1) (Color h h h)]+ 		set a [ miscXalign := 1 ]-		set b [ miscXalign := 0 ]+		set b [ miscXalign := 0, labelSelectable := True] 		tableAttachDefaults tbl a 0 1 pos (pos+1) 		tableAttachDefaults tbl b 1 2 pos (pos+1) 		return b-		+ 	let mkTable = zipWithM easyAttach [0..]-	info <- mkTable ["IP:Port", "Game (mod)", "Map", "Timelimit (SD)"-			, "Slots (+private)", "Ping (server average)"]-	set (head info) [ labelSelectable := True ]-	+	info <- mkTable ["Address", "Game", "Map", "Timelimit / SD"+			, "Slots (+private)", "Ping / Average"]+	versionLabel <- labelNew Nothing+	bools <- labelNew Nothing++	labelSetSelectable versionLabel True+	labelSetSelectable bools True+ 	-- Players 	allplayers	<- vBoxNew False spacing 	alienshumans	<- hBoxNew True spacing-	-	let playerView x = simpleListView-		[ (x		, True	, pangoPretty colors . name)-		, ("Score"	, False	, show . kills)-		, ("Ping"	, False	, show . ping) ]-	(amodel, aview) <- playerView "Aliens"-	(hmodel, hview) <- playerView "Humans"-	(smodel, sview) <- simpleListView [("Spectators", True, pangoPretty colors . name)-					, ("Ping", False, show . ping)]-	-- For servers not giving the P CVar-	(umodel, uview) <- playerView "Players"-	-	++	GenSimple amodel aview <- playerView colors "Aliens" True+	GenSimple hmodel hview <- playerView colors "Humans" True+	GenSimple smodel sview <- playerView colors "Spectators" False+	-- For servers not giving the P CVar:+	GenSimple umodel uview <- playerView colors "Players" True++ 	ascroll <- scrollIt aview PolicyNever PolicyAutomatic 	hscroll <- scrollIt hview PolicyNever PolicyAutomatic 	uscroll	<- scrollIt uview PolicyNever PolicyAutomatic@@ -86,165 +98,235 @@ 	boxPackStart allplayers alienshumans PackGrow 0 	boxPackStart allplayers sscroll PackNatural 0 +	Requisition _ natural	<- widgetSizeRequest sscroll+	on allplayers sizeAllocate $ \(Rectangle _ _ _ h') -> do+		let h = h' - spacing+		Requisition _ sReq	<- widgetSizeRequest sview+		Requisition _ aReq	<- widgetSizeRequest aview+		Requisition _ hReq	<- widgetSizeRequest hview+		let setS play spec = do+			widgetSetSizeRequest ascroll (-1) play+			widgetSetSizeRequest hscroll (-1) play+			widgetSetSizeRequest sscroll (-1) spec+		let pReq = max hReq aReq++		if natural * 2 > h then+			let half = max 0 (h`quot`2) in setS half half+		else if (max sReq natural) + pReq + spacing <= h then+			setS (-1) (max sReq natural)+		else do+			let ratio	= fromIntegral pReq / (fromIntegral sReq + fromIntegral pReq) :: Double+			    pNew	= max natural (round (ratio * fromIntegral h))+			    sNew	= max natural (h - pNew)+			setS pNew sNew+ 	-- Action buttons-	join	<- buttonNewWithMnemonic "_Join Server"-	refresh	<- buttonNewWithMnemonic "Refresh _current"-	set join 	[ buttonImage :=> imageNewFromStock stockConnect IconSizeButton-			, widgetSensitive := False ]-	set refresh	[ buttonImage :=> imageNewFromStock stockRefresh IconSizeButton+	serverbuttons <- hButtonBoxNew+	buttonBoxSetLayout serverbuttons ButtonboxSpread+	let action lbl icon = do+		b <- buttonNewWithMnemonic lbl+		set b	[ buttonImage :=> imageNewFromStock icon IconSizeButton 			, widgetSensitive := False ]-	-	serverbuttons <- hBoxNew False 0-	boxPackStart serverbuttons join PackRepel 0-	boxPackStart serverbuttons refresh PackRepel 0-	+		boxPackStartDefaults serverbuttons b+		return b++	st 	<- action "_Settings" stockProperties+	refresh	<- action "_Refresh" stockRefresh+	join	<- action "_Join" stockConnect+ 	-- Packing 	rightpane <- vBoxNew False spacing 	set rightpane  [ containerBorderWidth := spacing ] 	boxPackStart rightpane hostnamex PackNatural 1 	boxPackStart rightpane tbl PackNatural 0+	boxPackStart rightpane versionLabel PackNatural 0+	boxPackStart rightpane bools PackNatural 0 	boxPackStart rightpane allplayers PackGrow 0 	boxPackStart rightpane uscroll PackGrow 0-	boxPackStart rightpane serverbuttons PackNatural 2+	boxPackStart rightpane serverbuttons PackNatural spacingHalf  -	let launchTremulous = withTMVar current $ \gs -> do-		tst <- atomically $ tryTakeTMVar running-		whenJust tst (ignoreIOException . terminateProcess)-		config <- atomically $ readTMVar mconfig+	let launchTremulous gs = whenM (isEmptyMVar running) $ do+		putMVar running ()+		config	<- readMVar mconfig+		ss	<- readMVar msettings  		set join [ widgetSensitive := False ] -		pid <- maybeIO (runTremulous config gs)+		pid <- maybeIO $ runTremulous config gs (getSettings (address gs) ss) 		case pid of-			Nothing -> gtkError $ "Unable to run \"" ++ path ++ "\".\nHave you set your path correctly in Preferences?"+			Nothing -> do+				gtkError parent $ "Unable to run \"" ++ path ++ "\".\nHave you set your path correctly in Preferences?"+				set join [ widgetSensitive := True ]+				takeMVar running 				where path = case protocol gs of 						70 -> tremGppPath config 						_  -> tremPath config-			Just a -> (atomically . putTMVar running) a-		-		forkIO $ do-			threadDelay 1000000-			postGUISync $ set join [ widgetSensitive := True ]-			return ()-		return ()+			Just a -> do+				autoSignal mauto AutoPause+				forkIO $ do+					waitForProcess a+					postGUISync $ set join [ widgetSensitive := True ]+					takeMVar running+					autoSignal mauto AutoResume+				return () -	on join buttonActivated launchTremulous -	++	let updateSettings joining = do+		gs@GameServer{..}	<- readMVar current+		ss			<- readMVar msettings+		let cur = getSettings address ss+		if not joining || null (serverPass cur) then do+			new <- newSettingsDialog parent colors protected gs cur+			case new of+				Nothing -> return False+				Just n -> do+					let ss' = putSettings address n ss+					swapMVar msettings ss'+					unlessM (toFile ss') $+						gtkWarn parent "Unable to save server specific settings"+					return True+		else return False++	let launchWithSettings gs+		| protected gs	= whenM (updateSettings True) (launchTremulous gs)+		| otherwise	= launchTremulous gs+++	on join buttonActivated $ readWithMVar current launchWithSettings+	on st buttonActivated (updateSettings False >> return ())++ 	let setF boolJoin gs@GameServer{..} = do+		labelSetMarkup hostnamex $ showHostname colors hostname 		zipWithM_ labelSetMarkup info 			[ show address 			, (proto2string protocol ++ (case gamemod of-					Nothing	-> ""-					Just z	-> " (" ++ unpackorig z ++ ")"))-			, unpackorig mapname-			, maybeQ timelimit ++ " (" ++ maybeQ suddendeath ++ ")"-			, show slots ++ " (+" ++ show privslots ++ ")"-			, show gameping ++-				" (" ++ (show . intmean . filter validping . map ping) players ++ ")"+					SM.Nothing	-> ""+					SM.Just z	-> " (" ++ (unpack . original) z ++ ")"))+			, maybeS mapname+			, maybeQ timelimit ++ " / " ++ maybeQ suddendeath+			, show slots ++ " (+" ++ maybeQ privslots ++ ")"+			, show gameping ++ " / " ++ meanPing players+			, maybeS version 			]-		hostnamex `labelSetMarkup` showHostname colors hostname-		+		labelSetText versionLabel (maybeS version)+		labelSetText bools $	unwords	[ if unlagged then "unlagged" else ""+						, if protected then "password" else "" ]+ 		listStoreClear amodel 		listStoreClear hmodel 		listStoreClear smodel 		listStoreClear umodel-		+ 		let	sortedPlayers		= scoreSort players 			(s', a', h', u')	= partitionTeams sortedPlayers-		if null u' then do		+		if null u' then do 			mapM_ (listStoreAppend amodel) a' 			mapM_ (listStoreAppend hmodel) h' 			mapM_ (listStoreAppend smodel) s' 			treeViewColumnsAutosize aview 			treeViewColumnsAutosize hview 			treeViewColumnsAutosize sview-			Requisition _ sReq	<- widgetSizeRequest sview-			-			set sscroll [ widgetHeightRequest := min 300 sReq ]-			widgetShow allplayers				-			widgetHide uscroll	+			widgetShow allplayers+			widgetHide uscroll 		else do 			mapM_ (listStoreAppend umodel) sortedPlayers 			treeViewColumnsAutosize uview 			widgetShow uscroll 			widgetShow uview 			widgetHide allplayers-		-		atomically $ replaceTMVar current gs-		-		set join [ widgetSensitive := True ]++		mask_ $ do+			tryTakeMVar current+			putMVar current gs++		whenM (isEmptyMVar running) $+			set join [ widgetSensitive := True ] 		set refresh [ widgetSensitive := True ]+		set st [ widgetSensitive := True ] -		when boolJoin launchTremulous-		return ()+		when boolJoin (launchWithSettings gs) -	let updateF PollResult{..} = withTMVar current $ \GameServer{address} ->+	let updateF PollResult{..} = readWithMVar current $ \GameServer{address} -> 			whenJust (serverByAddress address polled) (setF False)-			-	-	on refresh buttonActivated $ withTMVar current $ \x -> do+++	on refresh buttonActivated $ readWithMVar current $ \x -> do 		set refresh [ widgetSensitive := False ]-		Config {delays} <- atomically $ readTMVar mconfig+		Config {delays} <- readMVar mconfig 		forkIO $ do 			result <- pollOne delays (address x)-			-			whenJust result $ \new -> do-				pr <- atomically $ do-					pr@PollResult{polled} <- takeTMVar mpolled-					let pr' = pr ++			SM.whenJust result $ \new -> do+				pr <- do+					pr@PollResult{polled} <- takeMVar mpolled+					let pr' = pr 						{ polled = replace 							(\old -> address old == address new) 							new polled 						}-					putTMVar mpolled pr'+					putMVar mpolled pr' 					return pr'-						-				mm <- findIndex (\old -> address old == address new) <$>-					listStoreToList browserStore-				(fa, fb)	<- atomically (readTMVar mupdate)-				clans		<- atomically (readTMVar mclans)+				(_, fb)	<- readMVar mupdate+				clans	<- readMVar mclans 				postGUISync $ do-					fa pr 					fb clans pr 					setF False new-					-- This generates a gtk assertion fail. Howerver it-					-- seems innocent-					whenJust mm $ \i -> -						listStoreSetValue browserStore i new	+					browserUpdateOne browserStore new+					playerUpdateOne playerStore new+ 			postGUISync $ 				set refresh [ widgetSensitive := True ]-				+ 		return ()-		+ 	return (rightpane, updateF, setF) 	where 	validping x		= x > 0 && x < 999 	scoreSort		= sortBy (flip (comparing kills))-	formatHostname x	= "<b><big>" ++ x ++ "</big></b>"-	showHostname colors x	= formatHostname $ case pangoPretty colors x of-					"" -> "<i>Invalid name</i>"-					a  -> a-	maybeQ			= maybe "?" show+	showHostname _ (TI _ "")	= "<i>Invalid name</i>"+	showHostname colors x		= pangoPretty colors x+	maybeQ			= SM.maybe "?" show+	maybeS			= SM.maybe "" (unpack . original)+	meanPing		= show . intmean . filter validping . map ping -runTremulous :: Config -> GameServer -> IO (Maybe ProcessHandle)-runTremulous Config{..} GameServer{..} = do-	(_,_,_,p) <- createProcess ((proc com args) {cwd = ldir})-		{close_fds = True, std_in = Inherit, std_out = Inherit, std_err = Inherit}+runTremulous :: Config -> GameServer -> ServerArg-> IO (Maybe ProcessHandle)+runTremulous Config{..} GameServer{..} ServerArg{..} = do+	(_,_,_,p) <- createProcess (proc com args) {cwd = ldir, close_fds = True} 	maybe (Just p) (const Nothing) <$> getProcessExitCode p 	where-	(com, args) = case protocol of-		70 -> (tremGppPath, ["+connect", show address])-		_  -> (tremPath, ["-connect", show address, "+connect", show address])-		+	com = case protocol of+		70 -> tremGppPath+		_  -> tremPath++	args = concatMap (uncurry arg)+			[ ("connect", show address)+			, ("password", serverPass)+			, ("rconPassword", serverRcon)+			, ("name", serverName)+			]+ 	ldir = case takeDirectory com of 		"" -> Nothing 		x  -> Just x --ignoreIOException :: IO () -> IO ()-ignoreIOException = handle (\(_ :: IOError) -> return ())+arg :: String -> String -> [String]+arg _ "" = []+arg a s  = ['+':a, s] -maybeIO :: IO (Maybe a) -> IO (Maybe a)-maybeIO = handle (\(_ :: IOError) -> return Nothing)+playerView :: ColorArray -> String -> Bool -> IO (GenSimple ListStore Player)+playerView colors teamName showScore = do+	gen@(GenSimple _ view) <- newGenSimple =<< listStoreNew []+	select <- treeViewGetSelection view+	treeSelectionSetMode select SelectionNone+	addColumn gen teamName True [cellTextEllipsize := EllipsizeEnd] $ \rend ->+		cellSetMarkup rend . pangoPrettyBS colors . name+	when showScore $ do+		addColumn gen "Score" False [cellXAlign := 1] $ \rend item -> do+			set rend  [cellText := show $ kills item]+		return ()+	addColumn gen "Ping" False [cellXAlign := 1] $ \rend item ->+			set rend [ cellText := show $ ping item]+	return gen
+ src/SettingsDialog.hs view
@@ -0,0 +1,89 @@+module SettingsDialog where+import Graphics.UI.Gtk+import Control.Applicative+import Network.Tremulous.Protocol+import Control.Monad++import IndividualServerSettings+import TremFormatting+import Constants++--Favorite server commented out. Perhaps in a later version++newSettingsDialog :: Window -> ColorArray -> Bool -> GameServer -> ServerArg -> IO (Maybe ServerArg)+newSettingsDialog win colors requirepw GameServer{..} ServerArg{..} = do+	dia <- dialogNew+	set dia	[ windowTitle			:= "Server settings"+		, windowWindowPosition		:= WinPosCenterOnParent+		, windowTransientFor		:= win+		]++	dialogAddButton dia stockCancel ResponseCancel+	dialogAddButton dia stockOk     ResponseOk+	dialogSetDefaultResponse dia ResponseOk++	srv <- labelNew $ Just $ pangoPretty colors hostname+	set srv [ labelWrap		:= True+		, labelJustify		:= JustifyCenter+		, labelUseMarkup	:= True+		, labelAttributes 	:= [AttrWeight 0 (-1) WeightBold, AttrScale 0 (-1) 1.2]+		]+	labelSetLineWrapMode srv WrapPartialWords++	ip <- labelNew $ Just $ show address++	tbl <- tableNew 0 0 False+	set tbl	[ tableRowSpacing	:= spacingHalf+		, tableColumnSpacing	:= spacing+		]++	let easyAttach pos lbl tip def = do+		a <- labelNewWithMnemonic lbl+		b <- entryNew+		set b	[ entryActivatesDefault := True+			, entryText		:= def+			, entryWidthChars	:= 30 ]+		when (pos == 1 && requirepw) $ do+			labelSetAttributes a [AttrWeight 0 (-1) WeightBold]++		set a	[ labelMnemonicWidget	:= b+			, widgetTooltipText	:= Just tip ]+		miscSetAlignment a 0 0.5+		tableAttach tbl a 0 1 pos (pos+1) [Fill] [] 0 0+		tableAttach tbl b 1 2 pos (pos+1) [Expand, Fill] [] 0 0+		return b++	name	<- easyAttach 0 "Override _name:"+		"Set a custom name that will only be used on this server" serverName+	pass	<- easyAttach 1 "_Password:" "Server password" serverPass+	rcon	<- easyAttach 2 "_Rcon:" "Rcon password" serverRcon++	-- fav	<- checkButtonNewWithMnemonic "_Favorite server"+	-- toggleButtonSetActive fav serverFavorite++	box <- vBoxNew False spacing+	set box [ containerBorderWidth := spacing ]+	boxPackStart box srv PackNatural 0+	boxPackStart box ip PackNatural 0++	when requirepw $ do+		l <- labelNew (Just "This server requires a password!")+		labelSetAttributes l [AttrWeight 0 (-1) WeightBold]+		boxPackStart box l PackNatural 0+	boxPackStart box tbl PackNatural 0+	-- boxPackStart box fav PackNatural 0+	dbox <- dialogGetUpper dia+	boxPackStart dbox box PackNatural 0+	widgetShowAll dbox+	when requirepw (widgetGrabFocus pass)++	answer <- dialogRun dia+	case answer of+		ResponseOk 	-> Just <$> (ServerArg+						<$> get pass entryText+						<*> get rcon entryText+						<*> get name entryText+						<*> pure False) -- (toggleButtonGetActive fav)+					<* widgetDestroy dia+		_		-> widgetDestroy dia >> return Nothing+
src/Toolbar.hs view
@@ -1,84 +1,157 @@-module Toolbar where-+module Toolbar(newToolbar, getDNS) where import Graphics.UI.Gtk  import Control.Applicative-import Control.Exception+import Control.Concurrent import Control.Monad import Control.Monad.IO.Class import Data.Maybe import Data.Char (toLower) -import Network.Socket  import Network.Tremulous.Protocol import Network.Tremulous.Polling import Network.Tremulous.MicroTime +import GtkUtils import Types import Constants-import STM2-+import Monad2 import Config import About-import Clanlist--getDNS :: String -> String -> IO (Maybe SockAddr)-getDNS host port = handle (\(_ :: IOException) -> return Nothing) $ do-	AddrInfo _ _ _ _ addr _ <- Prelude.head `liftM` getAddrInfo Nothing (Just host) (Just port)-	return $ Just addr--	-whileTrue :: Monad m => m Bool -> m ()-whileTrue f = f >>= \t -> when t (whileTrue f)+import ClanFetcher+import AutoRefresh  newToolbar :: Bundle -> [ClanHook] -> [PolledHook] -> [ClanPolledHook] -> IO HBox-newToolbar bundle@Bundle{..} clanHook polledHook bothHook = do		+newToolbar bundle@Bundle{..} clanHook polledHook bothHook = do+	c		<- readMVar mconfig 	pb		<- progressBarNew 	set pb		[ widgetNoShowAll := True ]-	-	refresh		<- mkToolButton "Refresh all servers" stockRefresh "Ctrl+R or F5"-	clansync	<- mkToolButton "Sync clan list" stockSave "Ctrl+S or F6"		++	refresh		<- mkToolButton "Refresh all" stockRefresh "Ctrl+R or F5"+	clansync	<- mkToolButton "Sync clans" stockSave "Ctrl+S or F6" 	about		<- mkToolButton "About" stockAbout "F7"-		-	let doSync = newClanSync bundle clansync clanHook bothHook-	-	on about buttonActivated $ newAbout parent-	on clansync buttonActivated $ doSync-			-	align		<- alignmentNew 0 0 0 0-	alignbox	<- hBoxNew False spacing-	set align [ containerChild := alignbox ]-	-	boxPackStartDefaults alignbox refresh-	boxPackStartDefaults alignbox clansync-	boxPackStartDefaults alignbox about-			-	pbrbox		<- hBoxNew False spacing+	auto		<- mkAuto (refreshMode c)++	doSync		<- mLock clansync (newClanSync bundle clanHook bothHook)+	doRefresh	<- mLock refresh (newRefresh bundle polledHook bothHook pb)+	autoRunner mauto mconfig doRefresh++	on about buttonActivated (newAbout parent)+	on clansync buttonActivated doSync+	on refresh buttonActivated doRefresh++	on auto toggled $ do+		act <- toggleButtonGetActive auto+		autoSignal mauto (if act then AutoStart else AutoStop)++	toolbar	<- hBoxNew False 0 -- should be spacing?+	boxPackStartDefaults toolbar refresh+	boxPackStartDefaults toolbar auto+	boxPackStartDefaults toolbar clansync+	boxPackStartDefaults toolbar about++	align <- alignmentNew 0 0 0 0+	set align [ containerChild := toolbar ]++	pbrbox <- hBoxNew False spacing 	set pbrbox [ containerBorderWidth := spacing ] 	boxPackStart pbrbox align PackNatural 0 	boxPackStart pbrbox pb PackGrow 0-	-	-	let serverRefresh = do-		progressBarSetFraction pb 0-		widgetShow pb-		refresh `set` [ widgetSensitive := False ]-		Config {masterServers, delays=delays@Delay{..}} <- atomically $ readTMVar mconfig-		-		start <- getMicroTime -		-- This is a stupid guess based on that about 110 servers will respond and the master-		-- will take about 200ms to respond-		PollResult { serversResponded } <- atomically $ readTMVar mpolled-		let serversGuess = if serversResponded == 0 then 110 else serversResponded-		let tremTime = (packetDuplication + 1) * packetTimeout-			+ serversGuess * throughputDelay + 200 * 1000-		-		forkIO $ do-			hosts <- catMaybes <$> mapM-				(\(host, port, proto) -> fmap (`MasterServer` proto) <$> getDNS host (show port))-				masterServers-			pbth <- forkIO $ whileTrue $ do+	on parent keyPressEvent $  do+		kmod <- eventModifier+		k <- map toLower <$> eventKeyName+		case kmod of+			[Control]+				| k == "r" -> liftIO doRefresh		>> return True+				| k == "s" -> liftIO doSync		>> return True+			[]	| k == "f5" -> liftIO doRefresh		>> return True+			[]	| k == "f6" -> liftIO doSync		>> return True+			[]	| k == "f7" -> liftIO (newAbout parent)	>> return True+			_ -> return False++	case (refreshMode c) of+		Startup	-> doRefresh+		Auto	-> autoSignal mauto AutoStart+		Manual	-> return ()+	when (autoClan c) doSync++	return pbrbox++-- The reason a hbox is used is so the icons always gets displayed regardless of the gtk setting+mkToolButton :: String -> StockId -> String -> IO Button+mkToolButton text icon tip = do+	button <- buttonNew+	img <- imageNewFromStock icon IconSizeButton+	lbl <- labelNew (Just text)+	box <- hBoxNew False 2+	boxPackStart box img PackNatural 0+	boxPackStart box lbl PackNatural 0+	containerAdd button box+	set button	[ buttonRelief		:= ReliefNone+			, buttonFocusOnClick	:= False+			, widgetTooltipText	:= Just tip ]+	return button++mkAuto :: RefreshMode -> IO ToggleButton+mkAuto refreshMode = do+	button <- toggleButtonNew+	set button [ toggleButtonActive := refreshMode == Auto ]+	img <- imageNewFromStock stockMediaPlay IconSizeButton+	containerAdd button img+	set button	[ buttonRelief		:= ReliefNone+			, buttonFocusOnClick	:= False+			, widgetTooltipText	:= Just "Refresh all servers periodically" ]+	return button++mLock :: WidgetClass w => w -> (IO () -> IO ()) -> IO (IO ())+mLock widget f = do+	m <- newEmptyMVar+	let lock = do+		putMVar m ()+		set widget [ widgetSensitive := False ]+	let unlock = do+		takeMVar m+		postGUISync (set widget [ widgetSensitive := True ])++	return $ whenM (isEmptyMVar m) (lock >> f unlock)+++newClanSync :: Bundle -> [ClanHook] -> [ClanPolledHook] -> IO () -> IO ()+newClanSync Bundle{..} clanHook bothHook unlock = do+	Config {clanSyncURL} <- readMVar mconfig+	forkIO $ do+		new <- getClanList clanSyncURL+		case new of+			Nothing	-> postGUISync $ gtkError parent "Unable to download clanlist"+			Just a	-> do+				swapMVar mclans a+				result <- readMVar mpolled+				postGUISync $ do+					mapM_ ($ a) clanHook+					mapM_ (\f -> f a result) bothHook+		unlock+	return ()++newRefresh :: Bundle -> [PolledHook] -> [ClanPolledHook] -> ProgressBar -> IO () -> IO ()+newRefresh Bundle{..} polledHook bothHook pb unlock = do+	progressBarSetFraction pb 0+	widgetShow pb+	Config {masterServers, delays=delays@Delay{..}} <- readMVar mconfig++	-- This is a stupid guess based on that about 110 servers will respond and the master+	-- will take about 200ms to respond+	PollResult { serversResponded } <- readMVar mpolled+	let serversGuess = if serversResponded == 0 then 110 else serversResponded+	let tremTime = (packetDuplication + 1) * packetTimeout+		+ serversGuess * throughputDelay + 200 * 1000++	forkIO $ do+		hosts <- catMaybes <$> mapM+			(\(host, port, proto) -> fmap (`MasterServer` proto) <$> getDNS host (show port))+			masterServers+		start <- getMicroTime+		pbth <- forkIO $ whileTrue $ do 				threadDelay 10000 --10 ms, 100 fps 				now <- getMicroTime 				let diff = now - start@@ -89,42 +162,15 @@ 					postGUISync $ progressBarSetFraction pb 						(fromIntegral diff / fromIntegral tremTime) 					return True-				-			result <- pollMasters delays hosts -			atomically $ replaceTMVar mpolled result-			killThread pbth-			clans <- atomically $ readTMVar mclans-			postGUISync $ do-				mapM_ ($ result) polledHook-				mapM_ (\f -> f clans result) bothHook-				refresh `set` [ widgetSensitive := True ]-				widgetHide pb-		return ()-	on refresh buttonActivated serverRefresh--	Config {..} <- atomically $ readTMVar mconfig-	when autoMaster serverRefresh-	when autoClan doSync--	on parent keyPressEvent $  do-		kmod	<- eventModifier-		k	<- map toLower <$> eventKeyName-		case kmod of-			[Control]-				| k == "r" -> liftIO serverRefresh	>> return True-				| k == "s" -> liftIO doSync		>> return True-			[]	| k == "f5" -> liftIO serverRefresh	>> return True-			[]	| k == "f6" -> liftIO doSync		>> return True-			[]	| k == "f7" -> liftIO (newAbout parent)	>> return True-			_ -> return False 				-	-	return pbrbox-	where mkToolButton lbl icon tip = do-		button <- buttonNewWithLabel lbl-		set button	[ buttonImage		:=> imageNewFromStock icon IconSizeButton-				, buttonRelief		:= ReliefNone -				, buttonFocusOnClick	:= False-				, widgetTooltipText	:= Just tip ]-		return button-		+		result <- pollMasters delays hosts+		swapMVar mpolled result+		killThread pbth+		clans <- readMVar mclans+		autoSignal mauto AutoUpdate+		postGUISync $ do+			mapM_ ($ result) polledHook+			mapM_ (\f -> f clans result) bothHook+			widgetHide pb+		unlock+	return ()
src/TremFormatting.hs view
@@ -2,32 +2,44 @@ import Data.Array import Data.Char import Network.Tremulous.NameInsensitive+import qualified Data.ByteString.Char8 as B+import Data.ByteString.Char8 (ByteString)  data TremFmt = TFColor !String | TFNone !String 	deriving (Show, Read)  type ColorArray = Array Int TremFmt -htmlEscape :: String -> String-htmlEscape = foldr f [] where-	f x xs = case x of -		'<' -> "&lt;" ++ xs-		'>' -> "&gt;" ++ xs-		'&' -> "&amp;" ++ xs-		_   -> x : xs -	+htmlEscapeBS :: ByteString -> ByteString+htmlEscapeBS = B.concat . B.foldr f [] where+	f x xs = case x of+		'<' -> "&lt;" : xs+		'>' -> "&gt;" : xs+		'&' -> "&amp;" : xs+		_   -> B.singleton x : xs++pangoPrettyBS :: ColorArray -> TI -> ByteString+pangoPrettyBS arr = B.pack . pangoColors arr . B.unpack . original++pangoPretty :: ColorArray-> TI -> String+pangoPretty arr = pangoColors arr . B.unpack . original+ pangoColors :: ColorArray -> String -> String pangoColors arr = f False where-	f n ('^':x:xs) | isAlphaNum x = case arr ! (a `mod` 8) of+	--Replace with colors+	f n ('^':x:xs) | isAlphaNum x = case arr ! (a `rem` 8) of 		TFColor color	-> close n ++ "<span color=\"" ++ color ++ "\">" ++ f True xs 		TFNone	_	-> close n ++ f False xs 		where a = ord x - ord '0'-			  -			-	f n (x:xs)	= x:f n xs+	--Escape pango+	f n (x:xs) = case x of+		'<' -> "&lt;" ++ f n xs+		'>' -> "&gt;" ++ f n xs+		'&' -> "&amp;" ++ f n xs+		_   -> x : f n xs+ 	f n []		= close n-	+ 	close n = if n then "</span>" else "" -pangoPretty :: ColorArray-> TI -> String-pangoPretty arr = pangoColors arr . htmlEscape . unpackorig+
src/Types.hs view
@@ -1,22 +1,32 @@ module Types (-	module Control.Concurrent.STM-	, Bundle(..), ClanHook, PolledHook, ClanPolledHook, SetCurrent+	module Control.Concurrent, module ConcurrentUtil+	, Bundle(..), ClanHook, PolledHook, ClanPolledHook, SetCurrent, BrowserStore, PlayerStore ) where import Graphics.UI.Gtk++import Control.Concurrent+import ConcurrentUtil+import Network.Tremulous.Protocol import Config import ClanFetcher-import Control.Concurrent.STM-import Network.Tremulous.Protocol+import IndividualServerSettings+import AutoRefresh  data Bundle = Bundle {-	  mpolled	:: !(TMVar PollResult)-	, mconfig	:: !(TMVar Config)-	, mclans	:: !(TMVar [Clan])+	  mpolled	:: !(MVar PollResult)+	, mconfig	:: !(MVar Config)+	, mclans	:: !(MVar [Clan])+	, mrefresh	:: !(MVar ()) 	, parent	:: !Window-	, browserStore	:: !(ListStore GameServer)+	, browserStore	:: !BrowserStore+	, playerStore	:: !PlayerStore+	, msettings	:: !(MVar ServerSettings)+	, mauto		:: !(MVar AutoSignal) 	}  type ClanHook		= [Clan] -> IO () type PolledHook		= PollResult -> IO () type ClanPolledHook	= [Clan] -> PollResult -> IO () type SetCurrent		= Bool -> GameServer -> IO ()+type PlayerStore	= ListStore (TI, GameServer)+type BrowserStore	= ListStore GameServer