├── ChangeLog ├── FILES ├── License.txt ├── README ├── README.md ├── neap ├── neap.1 ├── neap.desktop └── screenshot.png /ChangeLog: -------------------------------------------------------------------------------- 1 | 2011-11-27 2 | - added config option "geometry" 3 | - added CLI option --geometry 4 | - added file neap.desktop (to be placed in /usr/share/applications/ 5 | - updated list of authors 6 | 7 | 2010-11-01 8 | - performance improvements 9 | - moved all the configuration to a NeapConfig object 10 | - added CLI option -v, --version 11 | - updated list of authors 12 | 13 | 2010-10-31 14 | - separated icon's desktop layout from "real" (window manager's) desktop layout 15 | - viewport pager uses configured 'rows' and 'cols' when present 16 | - user can now force a specific pager via config option 'pager', and command line argument '-g' or '--pager' 17 | - fixed left-click bug introduced with viewport support 18 | - removed debug information output from viewport pager 19 | 20 | 2010-10-30 21 | - preliminary viewport support 22 | - replaced calls to intern_atom() by get_atom() to improve performance 23 | - some Python3 improvements 24 | - TODO: add virtual/viewport switch to config 25 | - TODO: update documentation 26 | 27 | 2010-06-22 28 | - Added __setitem()__ and __getitem()__, replacing set() and get() 29 | - read_configfile() uses RawConfigParser now; removed hand-written parser 30 | - WARNING: config file now needs a section header, "[neap]" 31 | 32 | 2010-06-12 33 | - PEP 8 source code formatting 34 | 35 | 2010-06-04 36 | - many useful stylistic changes. Thanks Stephan! 37 | 38 | 2010-06-02 39 | - desktop layout handling completely rewritten 40 | - now dynamically adjusting to changes in the number or layout of workspaces 41 | 42 | 2010-06-01 43 | - added scroll wheel support to switch between desktops. Thanks Stephan! 44 | 45 | 2010-05-07 46 | - added support for non-neap-induced workspace switches, see issue #2 47 | 48 | 2010-04-10 49 | - moved inline license to separate file License.txt 50 | 51 | 2010-03-22 52 | - added config file support 53 | - TODO: support for ~/.config/ dir 54 | - minor source documentation improvements 55 | 56 | 2010-03-14 57 | - fixed call to gtk.gdk.Pixbuf.add_alpha to pass chars 58 | 59 | 2010-03-12 60 | - added command line option parser 61 | - minor bug fixes (off-by-one in grid layout) 62 | 63 | 2010-03-11 64 | - added support for transparent background 65 | 66 | 2010-03-10 67 | - added context menu with desktop switcher 68 | - added about dialog 69 | - added background color support 70 | 71 | 2010-03-08 72 | - incomplete desktop switching 73 | - basic class layout 74 | 75 | -------------------------------------------------------------------------------- /FILES: -------------------------------------------------------------------------------- 1 | Hello package maintainer. Here you'll find a list of files in the 2 | neap package, along with a short description and their respective 3 | destination directories. 4 | 5 | FILE DESCRIPTION LOCATION 6 | ---- ----------- -------- 7 | neap The neap executable [/bin/] 8 | neap.1 manpage [/usr/share/man/man1/] 9 | neap.desktop application launcher [/usr/share/applications/] 10 | 11 | Thanks for taking care :-) 12 | 13 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Philip Busch 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 16 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 17 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 18 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 20 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 21 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | To start, execute "neap" or "neap -h" 2 | 3 | Check https://github.com/vzxwco/neap 4 | for the latest release, documentation and sample config file. 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Hello, this is neap! 2 | ==================== 3 | neap is a neat X11 workspace pager unintrusive and light that runs in the notification area (i.e. systray) of your panel. neap follows freedesktop specifications. 4 | 5 | neap is already part of many major Linux distros such as Debian, OpenSuSE and ArchLinux. See the [neap wiki](https://github.com/vzxwco/neap/wiki) for more info about installation, configuration and collaboration. 6 | 7 | Previously, neap has been hosted at [Google Code](https://code.google.com/p/neap/). 8 | 9 | Download 10 | ======== 11 | You can get the latest neap release in the [download section](https://github.com/vzxwco/neap/releases/tag/v0.7.2). 12 | 13 | Features 14 | ======== 15 | * user-friendly: left-click switches to corresponding desktop 16 | * advanced functionality: right-click opens context menu with advanced desktop selector 17 | * full customization: normal / highlight / background color, padding, spacing and grid layout freely configurable 18 | * easy to customize: config file support and command line switches for all configuration options 19 | * well-documented: user guide for new users, fully documented source code for programmers 20 | * portable: adheres to common standards, i.e. the freedesktop specification. No weird dependencies, just Xlib and GTK 21 | * unobtrusive 22 | * fast 23 | 24 | Screenshot 25 | ========== 26 | In the bottom right corner of the screen, you see (from right to left) a clock and a battery status indicator, followed by two systray applications: my network manager and, finally, neap, currently showing the 2nd of my four virtual desktops: 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /neap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copyright (c) 2010-2011, Philip Busch 6 | See License.txt for details. 7 | """ 8 | 9 | import sys 10 | import os 11 | import re 12 | import math 13 | import ConfigParser 14 | from datetime import datetime 15 | from optparse import OptionParser 16 | 17 | import gtk 18 | import gobject 19 | from Xlib import X, display, error, Xatom, Xutil 20 | import Xlib.protocol.event 21 | 22 | __appname__ = 'neap' 23 | __author__ = ['Philip Busch ', 'Clément Lavoillotte', 'Samuel Bauer '] 24 | __version__ = '0.7.1' 25 | __license__ = 'BSD' 26 | __website__ = 'http://code.google.com/p/neap/' 27 | 28 | DEFAULT_CONFIG = \ 29 | """;{0} v{1} config file generated {2} 30 | [neap] 31 | 32 | ;number of pixels between outer border and the actual grid 33 | padding = 2 34 | 35 | ;number of pixels between grid boxes 36 | spacing = 1 37 | 38 | ;foreground color (inactive desktops) 39 | color_inactive = #333333 40 | 41 | ;highlight color (the active desktop) 42 | color_active = #999999 43 | 44 | ;background color ("transparent" for transparent background) 45 | color_background = transparent 46 | 47 | ;number of rows in desktop grid 48 | rows = 0 49 | 50 | ;number of columns in desktop grid 51 | columns = 0 52 | 53 | ;manual pager selection: virtual|viewport|auto 54 | ;pager = auto 55 | 56 | ;icon geometry: 20x20 57 | ;geometry: 20x20 58 | 59 | """ 60 | 61 | class Pager: 62 | '''Dummy pager, not intended to be used directly''' 63 | 64 | def __init__(self, display, screen, root): 65 | '''Initialization.''' 66 | 67 | self.display = display 68 | self.screen = screen 69 | self.root = root 70 | 71 | def get_desktop_tasks(self, num): 72 | '''Returns a list of tasks for desktop num.''' 73 | 74 | return self.root.get_full_property(self.display.get_atom('_NET_CLIENT_LIST'), Xatom.WINDOW).value 75 | 76 | def get_current_desktop(self): 77 | '''Returns the index of the currently active desktop.''' 78 | 79 | return 0 80 | 81 | def get_desktop_layout(self): 82 | '''Returns a tupel containing the number of rows and cols, from the window manager.''' 83 | 84 | return (1,1) 85 | 86 | def get_desktop_count(self): 87 | '''Returns the current number of desktops.''' 88 | 89 | return 1 90 | 91 | def get_desktop_names(self): 92 | '''Returns a list containing desktop names.''' 93 | 94 | return ['Desktop'] 95 | 96 | def switch_desktop(self, num): 97 | '''Sets the active desktop to num.''' 98 | 99 | pass 100 | 101 | def send_event(self, win, ctype, data, mask=None): 102 | '''Sends a ClientMessage event to the root window.''' 103 | 104 | data = (data + [0] * (5 - len(data)))[:5] 105 | ev = Xlib.protocol.event.ClientMessage(window=win, 106 | client_type=ctype, data=(32, data)) 107 | 108 | if not mask: 109 | mask = X.SubstructureRedirectMask | X.SubstructureNotifyMask 110 | self.root.send_event(ev, event_mask=mask) 111 | 112 | 113 | class VirtualDesktopPager(Pager): 114 | '''Virtual desktop / workspace -based pager. Should be used with most freedesktop-compliant window managers.''' 115 | 116 | def get_current_desktop(self): 117 | '''Returns the index of the currently active desktop.''' 118 | 119 | return self.root.get_full_property(self.display.get_atom('_NET_CURRENT_DESKTOP'), 0).value[0] 120 | 121 | def get_desktop_layout(self): 122 | '''Returns a tupel containing the number of rows and cols, from the window manager.''' 123 | 124 | grid = self.root.get_full_property(self.display.get_atom('_NET_DESKTOP_LAYOUT'), 0) 125 | 126 | rows = 0 127 | cols = 0 128 | 129 | if grid != None and grid.value[2] > 1 and grid.value[1] > 1: 130 | # if _NET_DESKTOP_LAYOUT has sane values, use them: 131 | rows = grid.value[2] 132 | cols = grid.value[1] 133 | else: 134 | # else compute nice defaults: 135 | count = self.get_desktop_count() 136 | rows = round(math.sqrt(count)) 137 | cols = math.ceil(math.sqrt(count)) 138 | 139 | return (int(rows), int(cols)) 140 | 141 | def get_desktop_count(self): 142 | '''Returns the current number of desktops.''' 143 | 144 | return self.root.get_full_property(self.display.get_atom('_NET_NUMBER_OF_DESKTOPS'), 0).value[0] 145 | 146 | def get_desktop_names(self): 147 | '''Returns a list containing desktop names.''' 148 | 149 | count = self.get_desktop_count() 150 | names = self.root.get_full_property(self.display.get_atom('_NET_DESKTOP_NAMES'), 0) 151 | 152 | if hasattr(names, 'value'): 153 | count = self.get_desktop_count() 154 | names = names.value.strip('\x00').split('\x00')[:count] 155 | else: 156 | names = [] 157 | for i in range(count): 158 | names.append(str(i)) 159 | 160 | if len(names) < count: 161 | for i in range(len(names), count): 162 | names.append(str(i)) 163 | 164 | return names 165 | 166 | def switch_desktop(self, num): 167 | '''Sets the active desktop to num.''' 168 | 169 | win = self.root 170 | ctype = self.display.get_atom('_NET_CURRENT_DESKTOP') 171 | data = [num] 172 | 173 | self.send_event(win, ctype, data) 174 | self.display.flush() 175 | 176 | 177 | class ViewportPager(Pager): 178 | '''Viewport-based pager. To be used with compiz and other viewport-based window managers.''' 179 | 180 | def get_sreen_resolution(self): 181 | '''Returns a tupel containing screen resolution in pixels as (width, height).''' 182 | 183 | return (self.screen.width_in_pixels, self.screen.height_in_pixels) 184 | 185 | def get_current_desktop(self): 186 | '''Returns the index of the currently active desktop.''' 187 | 188 | w,h = self.get_sreen_resolution() 189 | rows,cols = self.get_desktop_layout() 190 | vp = self.root.get_full_property(self.display.get_atom("_NET_DESKTOP_VIEWPORT"), 0).value 191 | return round(vp[1]/h)*cols + round(vp[0]/w); 192 | 193 | #TODO: optimize (cache ?) 194 | def get_desktop_layout(self): 195 | '''Returns a tupel containing the number of rows and cols, from the window manager.''' 196 | 197 | w,h = self.get_sreen_resolution() 198 | size = self.root.get_full_property(self.display.get_atom("_NET_DESKTOP_GEOMETRY"), 0) 199 | 200 | #default values 201 | rows = 1 202 | cols = 1 203 | 204 | if size != None: 205 | rows = round(size.value[1] / h) 206 | cols = round(size.value[0] / w) 207 | 208 | return (int(rows), int(cols)) 209 | 210 | def get_desktop_count(self): 211 | '''Returns the current number of desktops.''' 212 | 213 | rows,cols = self.get_desktop_layout() 214 | return rows * cols 215 | 216 | def get_desktop_names(self): 217 | '''Returns a list containing desktop names.''' 218 | 219 | count = self.get_desktop_count() 220 | names = [] 221 | for i in range(count): 222 | names.append('Workspace {0}'.format(i+1)) 223 | 224 | return names 225 | 226 | def switch_desktop(self, num): 227 | '''Sets the active desktop to num.''' 228 | 229 | w,h = self.get_sreen_resolution() 230 | rows,cols = self.get_desktop_layout() 231 | x = int(num % cols) 232 | y = int(round((num-x)/cols)) 233 | data = [x*w, y*h] 234 | 235 | win = self.root 236 | ctype = self.display.get_atom("_NET_DESKTOP_VIEWPORT") 237 | 238 | self.send_event(win, ctype, data) 239 | self.display.flush() 240 | 241 | 242 | def pager_auto_detect(display, screen, root): 243 | '''Auto-detects pager to use.''' 244 | pager = None 245 | 246 | grid = root.get_full_property(display.get_atom('_NET_DESKTOP_LAYOUT'), 0) 247 | size = root.get_full_property(display.get_atom("_NET_DESKTOP_GEOMETRY"), 0) 248 | 249 | if (grid != None and grid.value[2] > 1 and grid.value[1] > 1): 250 | return VirtualDesktopPager(display, screen, root) 251 | elif (hasattr(size, 'value') and (size.value[1]>screen.height_in_pixels or size.value[0]>screen.width_in_pixels)): 252 | return ViewportPager(display, screen, root) 253 | else: 254 | # defaults to VirtualDesktop 255 | return VirtualDesktopPager(display, screen, root) 256 | 257 | 258 | PAGERS = {'virtual': VirtualDesktopPager, 'viewport':ViewportPager, 'auto':pager_auto_detect} 259 | 260 | 261 | class NeapConfig(object): 262 | 263 | def __init__(self): 264 | '''Initialization.''' 265 | 266 | # program info 267 | self.name = __appname__ 268 | 269 | # default config 270 | self.conf = {} 271 | self.conf['padding'] = 3 272 | self.conf['spacing'] = 1 273 | self.conf['color_active'] = 'red' 274 | self.conf['color_inactive'] = 'green' 275 | self.conf['color_background'] = 'yellow' 276 | self.conf['rows'] = 0 277 | self.conf['columns'] = 0 278 | self.conf['pager'] = 'auto' 279 | self.conf['geometry'] = (20,20) 280 | 281 | def __setitem__(self, key, val): 282 | '''Sets config variable key to val.''' 283 | 284 | self.set(key, val) 285 | 286 | def __getitem__(self, key): 287 | '''Returns the value of config variable key.''' 288 | 289 | return self.conf[key] 290 | 291 | def set(self, key, val): 292 | '''Sets config variable key to val, including sanity-check.''' 293 | 294 | # discard empty values 295 | if val == None: 296 | return False 297 | 298 | if key == 'geometry': 299 | try: 300 | icon_width, icon_height = val.split("x") 301 | icon_width = int(icon_width) 302 | icon_height = int(icon_height) 303 | except Exception, e: 304 | return False 305 | self.conf['geometry'] = (icon_width, icon_height) 306 | return True 307 | 308 | if key == 'pager': 309 | if val in PAGERS: 310 | self.conf['pager'] = val 311 | return True 312 | return False 313 | 314 | # is val a number, a hex color or the string "transparent"? 315 | if not (val == "transparent" or 316 | re.match("#[a-fA-F0-9]{6}", val) != None or 317 | val.isdigit()): 318 | return False 319 | 320 | try: 321 | val = int(val) 322 | except ValueError: 323 | pass 324 | 325 | if val == 'transparent': 326 | val = '#2357bd' 327 | 328 | if key not in self.conf.keys(): 329 | return False 330 | 331 | self.conf[key] = val 332 | 333 | return True 334 | 335 | def write_configfile(self): 336 | '''Creates config file.''' 337 | 338 | home = os.path.expanduser('~') 339 | tmp = os.path.join(home, '.config/') 340 | if os.path.exists(tmp): 341 | basedir = os.path.join(tmp, 'neap') 342 | if not os.path.exists(basedir): 343 | # if os.mkdir(basedir, 0755) < 0: # (python 2) 344 | if os.mkdir(basedir, 0o755) < 0: 345 | print ('{0}: cannot create directory {1}'.format(self.name, 346 | basedir)) 347 | return None 348 | else: 349 | basedir = home 350 | 351 | print ('now: {0}'.format(basedir)) 352 | 353 | return 0 354 | 355 | def read_configfiles(self, paths): 356 | '''Reads config file from path.''' 357 | 358 | for file in paths: 359 | parser = ConfigParser.RawConfigParser() 360 | parser.read(os.path.expanduser(file)) 361 | 362 | for key,val in parser.items('neap'): 363 | if not self.set(key, val): 364 | print ("{0}: {1}: {2} = {3}: invalid syntax".format(self.name, file, key, val)) 365 | return False 366 | 367 | return True 368 | 369 | 370 | class Neap(object): 371 | 372 | def __init__(self, config): 373 | '''Initialization.''' 374 | 375 | # program info 376 | self.name = __appname__ 377 | self.version = __version__ 378 | self.author = __author__ 379 | self.website = __website__ 380 | 381 | # default config 382 | self.conf = config 383 | self.grid = [] 384 | 385 | # X stuff 386 | self.display = display.Display() 387 | self.screen = self.display.screen() 388 | self.root = self.screen.root 389 | 390 | # select pager 391 | self.pager = PAGERS[self.conf['pager']](self.display, self.screen, self.root) 392 | 393 | # notify about and handle workspace switches 394 | screen = gtk.gdk.screen_get_default() 395 | root = screen.get_root_window() 396 | root.set_events(gtk.gdk.SUBSTRUCTURE_MASK) 397 | root.add_filter(self.event_filter) 398 | 399 | # initialize data 400 | self.old_active = -2 401 | self.count = -2 402 | self.layout = (-2, -2) 403 | self.colormap = gtk.gdk.Colormap(gtk.gdk.visual_get_best(), False) 404 | self.color_active = self.colormap.alloc_color(self.conf['color_active']) 405 | self.color_inactive = self.colormap.alloc_color(self.conf['color_inactive']) 406 | self.color_background = self.colormap.alloc_color(self.conf['color_background']) 407 | self.pixmap = gtk.gdk.Pixmap(None, self.conf['geometry'][0], self.conf['geometry'][1], 32) 408 | self.gc = self.pixmap.new_gc() 409 | 410 | self.pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, 411 | self.conf['geometry'][0], self.conf['geometry'][1]) 412 | self.icon = gtk.StatusIcon() 413 | self.icon.connect('activate', self.left) 414 | self.icon.connect('popup-menu', self.right) 415 | self.icon.connect('scroll-event', self.scroll) 416 | 417 | def event_filter(self, event, user_data=None): 418 | '''Handles incoming gtk.gdk.Events.''' 419 | 420 | # we only registered for SUBSTRUCTURE_MASK events, so 421 | # we just update the pixbuf 422 | self.update_pixbuf(self.pager.get_current_desktop()) 423 | return gtk.gdk.FILTER_CONTINUE 424 | 425 | def get_icon_desktop_layout(self): 426 | '''Returns a tupel containing the number of rows and cols, for the pager icon.''' 427 | 428 | if self.conf['rows'] > 0 and self.conf['columns'] > 0: 429 | # if .neaprc config variables are set, enforce them: 430 | rows = self.conf['rows'] 431 | cols = self.conf['columns'] 432 | return (rows, cols) 433 | else: 434 | # else icon layout = window manager's desktop layout 435 | return self.pager.get_desktop_layout() 436 | 437 | def update_grid(self): 438 | '''Updates internal grid structure. Returns False if the grid did not need to be updated.''' 439 | 440 | (rows, cols) = self.get_icon_desktop_layout() 441 | count = self.pager.get_desktop_count() 442 | if ((rows, cols) == self.layout and count == self.count): 443 | return False 444 | self.layout = (rows, cols) 445 | self.count = count 446 | 447 | (w, h) = (self.pixbuf.get_width(), self.pixbuf.get_height()) 448 | padding = self.conf['padding'] 449 | spacing = self.conf['spacing'] 450 | 451 | n = max(rows, cols) 452 | c = (w - 2 * padding - (n - 1) * spacing) / cols 453 | 454 | grid_width = cols * c + (cols - 1) * spacing 455 | grid_height = rows * c + (rows - 1) * spacing 456 | 457 | off_x = (w - 2 * padding - grid_width) / 2 458 | off_y = (h - 2 * padding - grid_height) / 2 459 | 460 | grid = [] 461 | for i in range(rows): 462 | for j in range(cols): 463 | x = off_x + padding + j * (c + spacing) 464 | y = off_y + padding + i * (c + spacing) 465 | 466 | if i * cols + j < count: 467 | grid.append((x, y, c, c)) 468 | 469 | self.grid = grid 470 | return True 471 | 472 | def update_pixbuf(self, active=-1): 473 | '''Updates internal icon pixbuf.''' 474 | 475 | if (self.update_grid() is False and self.old_active == active): 476 | return 477 | 478 | self.old_active = active 479 | 480 | (w, h) = (self.pixbuf.get_width(), self.pixbuf.get_height()) 481 | 482 | # ---ACTUAL DRAWING CODE--- 483 | self.gc.set_foreground(self.color_background) 484 | self.pixmap.draw_rectangle(self.gc, True, 0, 0, w, h) 485 | 486 | self.gc.set_foreground(self.color_inactive) 487 | 488 | i = 0 489 | for cell in self.grid: 490 | if i == active: 491 | self.gc.set_foreground(self.color_active) 492 | else: 493 | self.gc.set_foreground(self.color_inactive) 494 | 495 | i = i + 1 496 | 497 | (x, y, size_x, size_y) = cell 498 | self.pixmap.draw_rectangle(self.gc, True, x, y, size_x, size_y) 499 | # ------------------------- 500 | 501 | self.pixbuf.get_from_drawable(self.pixmap, self.colormap, 0, 0, 0, 0, w, h) 502 | self.pixbuf = self.pixbuf.add_alpha(True, chr(0x23), chr(0x57), 503 | chr(0xbd)) 504 | self.icon.set_from_pixbuf(self.pixbuf) 505 | 506 | def get_screen(self): 507 | '''Returns the clicked grid box number.''' 508 | 509 | (pointer_x, pointer_y, mods) = \ 510 | self.icon.get_screen().get_root_window().get_pointer() 511 | 512 | (screen, rectangle, orientation) = self.icon.get_geometry() 513 | (width, height, size_x, size_y) = rectangle 514 | (w, h) = self.conf['geometry'] 515 | 516 | pos_x = pointer_x - width - ( size_x - w ) / 2; 517 | pos_y = pointer_y - height - ( size_y - h ) / 2; 518 | 519 | i = 0 520 | for cell in self.grid: 521 | (x, y, size_x, size_y) = cell 522 | if pos_x >= x and pos_x <= x + size_x and pos_y >= y \ 523 | and pos_y <= y + size_y: 524 | return i 525 | i = i + 1 526 | 527 | return -1 528 | 529 | def left(self, status_icon): 530 | '''Callback for left-clicks.''' 531 | 532 | num = self.get_screen() 533 | 534 | if 0 <= num <= self.pager.get_desktop_count(): 535 | self.update_pixbuf(num) 536 | gobject.idle_add(self.pager.switch_desktop, num) 537 | 538 | def scroll(self, status_icon, event): 539 | '''Callback for scroll wheel actions.''' 540 | 541 | if event.direction == gtk.gdk.SCROLL_UP: 542 | target = self.pager.get_current_desktop() - 1 543 | elif event.direction == gtk.gdk.SCROLL_DOWN: 544 | target = self.pager.get_current_desktop() + 1 545 | else: 546 | return 547 | 548 | self.pager.switch_desktop(target % self.pager.get_desktop_count()) 549 | 550 | def desktop_callback(self, widget, data=None): 551 | '''Callback for the menu's desktop selector.''' 552 | 553 | if widget.get_active(): 554 | self.pager.switch_desktop(data) 555 | 556 | def get_menu_about(self): 557 | '''Returns a menu for the right-click action.''' 558 | 559 | menu = gtk.Menu() 560 | 561 | submenu = gtk.Menu() 562 | group = None 563 | i = 0 564 | for desktop in self.pager.get_desktop_names(): 565 | item = gtk.RadioMenuItem(group, desktop) 566 | 567 | group = item 568 | 569 | if i == self.pager.get_current_desktop(): 570 | item.set_active(True) 571 | 572 | item.connect('toggled', self.desktop_callback, i) 573 | i = i + 1 574 | submenu.add(item) 575 | 576 | item = gtk.MenuItem('Desktops') 577 | item.set_submenu(submenu) 578 | menu.add(item) 579 | 580 | sep = gtk.SeparatorMenuItem() 581 | menu.add(sep) 582 | 583 | item = gtk.ImageMenuItem(gtk.STOCK_ABOUT) 584 | item.connect('activate', self.about) 585 | menu.add(item) 586 | 587 | item = gtk.ImageMenuItem(gtk.STOCK_QUIT) 588 | item.connect("activate", gtk.main_quit) 589 | menu.add(item) 590 | 591 | self.menu = menu 592 | menu.show_all() 593 | return menu 594 | 595 | def right(self, status_icon, button, activate_time): 596 | '''Callback for right-clicks.''' 597 | 598 | self.get_menu_about().popup( 599 | None, 600 | None, 601 | gtk.status_icon_position_menu, 602 | 1, 603 | activate_time, 604 | self.icon, 605 | ) 606 | 607 | def about(self, menu_item): 608 | '''Shows an about dialog.''' 609 | 610 | about = gtk.AboutDialog() 611 | 612 | about.set_name(self.name) 613 | about.set_version(self.version) 614 | about.set_authors(self.author) 615 | about.set_comments('notification area / systray pager') 616 | about.set_copyright('Copyright (c) 2010 Philip Busch') 617 | about.set_website(self.website) 618 | about.set_logo(self.pixbuf) 619 | about.set_program_name(self.name) 620 | about.run() 621 | about.hide() 622 | 623 | def __setitem__(self, key, val): 624 | '''Sets config variable key to val.''' 625 | 626 | self.conf[key] = val 627 | 628 | def __getitem__(self, key): 629 | '''Returns the value of config variable key.''' 630 | 631 | return self.conf[key] 632 | 633 | def set(self, key, val): 634 | '''Sets config variable key to val.''' 635 | 636 | self.conf[key] = val 637 | 638 | # Obsolete. Use __getitem__() or corresponding syntax instead. 639 | def get(self, key): 640 | '''Returns the value of config variable key.''' 641 | 642 | return self.conf[key] 643 | 644 | def main(self): 645 | '''Starts the main GTK loop.''' 646 | 647 | num = self.pager.get_current_desktop() 648 | self.update_pixbuf(num) 649 | gtk.main() 650 | 651 | 652 | 653 | 654 | if __name__ == '__main__': 655 | 656 | def print_help(option, opt, value, parser): 657 | print("{0} is a lightweight pager running in the notification area".format(__appname__)) 658 | print("of freedesktop-compliant window managers\n") 659 | parser.print_help() 660 | print("\nRun \"man neap\" or see {0} for more information.".format(__website__)) 661 | print("\nReport bugs to ") 662 | sys.exit(0) 663 | 664 | parser = OptionParser() 665 | 666 | parser.add_option('-p', '--padding', dest='padding', 667 | help='number of pixels between outer edge and grid' 668 | , metavar='N') 669 | 670 | parser.add_option('-s', '--spacing', dest='spacing', 671 | help='number of pixels between rows/cols in the grid' 672 | , metavar='N') 673 | 674 | parser.add_option('-a', '--active-color', dest='color_active', 675 | help='color of active desktop', metavar='C') 676 | 677 | parser.add_option('-i', '--inactive-color', dest='color_inactive', 678 | help='color of inactive desktop(s)', metavar='C') 679 | 680 | parser.add_option('-b', '--background-color', 681 | dest='color_background', help='background color', 682 | metavar='C') 683 | 684 | parser.add_option('-r', '--rows', dest='rows', 685 | help='number of grid rows', metavar='N') 686 | 687 | parser.add_option('-c', '--columns', dest='columns', 688 | help='number of grid columns', metavar='N') 689 | 690 | parser.add_option('-P', '--pager', dest='pager', 691 | help='select a specific pager: virtual|viewport|auto', 692 | metavar='P') 693 | 694 | parser.add_option('-g', '--geometry', dest='geometry', 695 | help='set icon geometry', metavar='WxH') 696 | 697 | parser.add_option('-v', '--version', action='store_true', dest='version', 698 | default=False, help="print version information and exit") 699 | 700 | parser.remove_option('-h') 701 | parser.add_option('-h', '--help', action="callback", callback=print_help, 702 | help='print this help text and exit') 703 | 704 | (opts, args) = parser.parse_args() 705 | 706 | if opts.version: 707 | print("%s %s" % (__appname__, __version__)) 708 | exit() 709 | 710 | cfile = os.path.expanduser('~/.neaprc') 711 | if not os.path.exists(cfile): 712 | try: 713 | f = open(cfile, 'w') 714 | f.write(DEFAULT_CONFIG.format(__appname__, __version__, 715 | datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) 716 | 717 | f.close() 718 | except IOError (errno, strerror): 719 | print ('{0}: {1}: {2}'.format(__appname__, cfile, strerror)) 720 | sys.exit(-1) 721 | 722 | config = NeapConfig() 723 | 724 | if not config.read_configfiles([cfile]): 725 | sys.exit(-1) 726 | 727 | config['padding'] = opts.padding 728 | config['spacing'] = opts.spacing 729 | config['color_active'] = opts.color_active 730 | config['color_inactive'] = opts.color_inactive 731 | config['color_background'] = opts.color_background 732 | config['rows'] = opts.rows 733 | config['columns'] = opts.columns 734 | config['pager'] = opts.pager 735 | config['geometry'] = opts.geometry 736 | 737 | neap = Neap(config) 738 | 739 | neap.main() 740 | -------------------------------------------------------------------------------- /neap.1: -------------------------------------------------------------------------------- 1 | .TH NEAP "1" "December 2011" "neap" "User Commands" 2 | .SH NAME 3 | neap \- manual page for neap 4 | .SH SYNOPSIS 5 | .B neap 6 | [\fIoptions\fR] 7 | .SH DESCRIPTION 8 | Neap is a lightweight pager running in the notification area 9 | of freedesktop\-compliant window managers. 10 | .SH OPTIONS 11 | .TP 12 | \fB\-p\fR N, \fB\-\-padding\fR=\fIN\fR 13 | number of pixels between outer edge and grid 14 | .TP 15 | \fB\-s\fR N, \fB\-\-spacing\fR=\fIN\fR 16 | number of pixels between rows/cols in the grid 17 | .TP 18 | \fB\-a\fR C, \fB\-\-active\-color\fR=\fIC\fR 19 | color of active desktop 20 | .TP 21 | \fB\-i\fR C, \fB\-\-inactive\-color\fR=\fIC\fR 22 | color of inactive desktop(s) 23 | .TP 24 | \fB\-b\fR C, \fB\-\-background\-color\fR=\fIC\fR 25 | background color 26 | .TP 27 | \fB\-r\fR N, \fB\-\-rows\fR=\fIN\fR 28 | number of grid rows 29 | .TP 30 | \fB\-c\fR N, \fB\-\-columns\fR=\fIN\fR 31 | number of grid columns 32 | .TP 33 | \fB\-P\fR P, \fB\-\-pager\fR=\fIP\fR 34 | select a specific pager: virtual|viewport|auto 35 | .TP 36 | \fB\-g\fR WxH, \fB\-\-geometry\fR=\fIWxH\fR 37 | set icon geometry 38 | .TP 39 | \fB\-v\fR, \fB\-\-version\fR 40 | print version information and exit 41 | .TP 42 | \fB\-h\fR, \fB\-\-help\fR 43 | print this help text and exit 44 | .SH "FILES" 45 | .TP 46 | .B $HOME/.neaprc 47 | neap configuration file 48 | .SH "AUTHOR" 49 | Neap was written by Philip Busch 50 | with the help of many volunteers. 51 | .SH "REPORTING BUGS" 52 | Report bugs to 53 | .SH "SEE ALSO" 54 | Visit us at 55 | .B http://code.google.com/p/neap/ 56 | for more information about neap. 57 | -------------------------------------------------------------------------------- /neap.desktop: -------------------------------------------------------------------------------- 1 | # This file needs to be placed in /usr/share/applications/ to make neap 2 | # listed in the application menus. 3 | [Desktop Entry] 4 | Version=1.0 5 | Type=Application 6 | Name=Neap 7 | Name[fr]=Neap 8 | Comment=A simple pager/desktop switcher 9 | Comment[fr]=Un pager/changeur de bureau simple 10 | Comment[de]=Ein einfacher Pager / Desktop Switcher 11 | Exec=neap 12 | TryExec=neap 13 | Icon=workspace-switcher 14 | StartupNotify=true 15 | Categories=GTK;Utility; 16 | Terminal=false 17 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vzxwco/neap/0645ffb825176f481ba997e9e9eb937490475df6/screenshot.png --------------------------------------------------------------------------------